From d85a0bb0629cbf36e633e75ae1bc959274f2855d Mon Sep 17 00:00:00 2001 From: Ian Macdonald Date: Tue, 17 Jan 2023 19:04:09 +0100 Subject: [PATCH 1/4] Implement server-wide open group user bans and unbans. This adds to Session the ability to ban a user not just from a single group, but from an entire SOGS. To successfully ban (or unban) a user across a whole server, the executor must be a global moderator of the SOGS instance. When banning a user, the global moderator may opt to also remove all of the user's messages from the server. This requires PySOGS > 0.3.7 to allow the simultaneous deletion of messages from multiple groups. See oxen-io/session-pysogs@2c8e4f1535bbd2cc676fa46914c691d2332cb41f. This has been tested with Session 1.10.4 in combination with `open.getsession.org` and `sog.caliban.org`, both of which have been updated to support server-wide banning. --- _locales/en/messages.json | 3 + .../message-content/MessageContextMenu.tsx | 12 ++ ts/components/dialog/BanOrUnbanUserDialog.tsx | 133 +++++++++++++++++- ts/components/dialog/ModalContainer.tsx | 5 +- ts/components/menu/ConversationHeaderMenu.tsx | 4 + .../menu/ConversationListItemContextMenu.tsx | 4 + ts/components/menu/Menu.tsx | 48 +++++++ ts/interactions/conversationInteractions.ts | 13 ++ ts/interactions/messageInteractions.ts | 47 ++++++- .../open_group_api/sogsv3/sogsV3BanUnban.ts | 60 ++++++++ .../open_group_api/sogsv3/sogsV3BatchPoll.ts | 29 ++++ ts/state/ducks/modalDialog.tsx | 11 ++ ts/state/selectors/modal.ts | 6 + ts/types/LocalizerKeys.ts | 3 + 14 files changed, 369 insertions(+), 9 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index beeefcc030..48024e3ae6 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -247,10 +247,13 @@ "blockedSettingsTitle": "Blocked Contacts", "conversationsSettingsTitle": "Conversations", "unbanUser": "Unban User", + "serverUnbanUser": "Unban User from Server", "userUnbanned": "User unbanned successfully", "userUnbanFailed": "Unban failed!", "banUser": "Ban User", "banUserAndDeleteAll": "Ban and Delete All", + "serverBanUser": "Ban User from Server", + "serverBanUserAndDeleteAll": "Ban from Server and Delete All", "userBanned": "Banned successfully", "userBanFailed": "Ban failed!", "leaveGroup": "Leave Group", diff --git a/ts/components/conversation/message/message-content/MessageContextMenu.tsx b/ts/components/conversation/message/message-content/MessageContextMenu.tsx index 8a1c31248e..8fb3f2cf8a 100644 --- a/ts/components/conversation/message/message-content/MessageContextMenu.tsx +++ b/ts/components/conversation/message/message-content/MessageContextMenu.tsx @@ -210,6 +210,14 @@ export const MessageContextMenu = (props: Props) => { MessageInteraction.unbanUser(sender, convoId); }, [sender, convoId]); + const onServerBan = useCallback(() => { + MessageInteraction.serverBanUser(sender, convoId); + }, [sender, convoId]); + + const onServerUnban = useCallback(() => { + MessageInteraction.serverUnbanUser(sender, convoId); + }, [sender, convoId]); + const onSelect = useCallback(() => { dispatch(toggleSelectedMessageId(messageId)); }, [messageId]); @@ -334,6 +342,10 @@ export const MessageContextMenu = (props: Props) => { {weAreAdmin && isPublic ? ( {window.i18n('unbanUser')} ) : null} + {weAreAdmin && isPublic ? {window.i18n('serverBanUser')} : null} + {weAreAdmin && isPublic ? ( + {window.i18n('serverUnbanUser')} + ) : null} {weAreAdmin && isPublic && !isSenderAdmin ? ( {window.i18n('addAsModerator')} ) : null} diff --git a/ts/components/dialog/BanOrUnbanUserDialog.tsx b/ts/components/dialog/BanOrUnbanUserDialog.tsx index 02ed38b939..745a44380a 100644 --- a/ts/components/dialog/BanOrUnbanUserDialog.tsx +++ b/ts/components/dialog/BanOrUnbanUserDialog.tsx @@ -3,7 +3,11 @@ import { PubKey } from '../../session/types'; import { ToastUtils } from '../../session/utils'; import { Flex } from '../basic/Flex'; import { useDispatch, useSelector } from 'react-redux'; -import { BanType, updateBanOrUnbanUserModal } from '../../state/ducks/modalDialog'; +import { + BanType, + updateBanOrUnbanUserModal, + updateServerBanOrUnbanUserModal +} from '../../state/ducks/modalDialog'; import { SpacerSM } from '../basic/Text'; import { getConversationController } from '../../session/conversations/ConversationController'; import { SessionWrapperModal } from '../SessionWrapperModal'; @@ -14,7 +18,9 @@ import { useFocusMount } from '../../hooks/useFocusMount'; import { useConversationPropsById } from '../../hooks/useParamSelector'; import { sogsV3BanUser, - sogsV3UnbanUser, + sogsV3ServerBanUser, + sogsV3ServerUnbanUser, + sogsV3UnbanUser } from '../../session/apis/open_group_api/sogsv3/sogsV3BanUnban'; import { SessionHeaderSearchInput } from '../SessionHeaderSearchInput'; import { isDarkTheme } from '../../state/selectors/theme'; @@ -25,7 +31,8 @@ async function banOrUnBanUserCall( convo: ConversationModel, textValue: string, banType: BanType, - deleteAll: boolean + deleteAll: boolean, + isGlobal: boolean ) { // if we don't have valid data entered by the user const pubkey = PubKey.from(textValue); @@ -39,8 +46,12 @@ async function banOrUnBanUserCall( const roomInfos = convo.toOpenGroupV2(); const isChangeApplied = banType === 'ban' - ? await sogsV3BanUser(pubkey, roomInfos, deleteAll) - : await sogsV3UnbanUser(pubkey, roomInfos); + ? isGlobal + ? await sogsV3ServerBanUser(pubkey, roomInfos, deleteAll) + : await sogsV3BanUser(pubkey, roomInfos, deleteAll) + : isGlobal + ? await sogsV3ServerUnbanUser(pubkey, roomInfos) + : await sogsV3UnbanUser(pubkey, roomInfos); if (!isChangeApplied) { window?.log?.warn(`failed to ${banType} user: ${isChangeApplied}`); @@ -92,7 +103,7 @@ export const BanOrUnBanUserDialog = (props: { window?.log?.info(`asked to ${banType} user: ${castedPubkey}, banAndDeleteAll:${deleteAll}`); setInProgress(true); - const isBanned = await banOrUnBanUserCall(convo, castedPubkey, banType, deleteAll); + const isBanned = await banOrUnBanUserCall(convo, castedPubkey, banType, deleteAll, false); if (isBanned) { // clear input box setInputBoxValue(''); @@ -163,4 +174,112 @@ export const BanOrUnBanUserDialog = (props: { ); -}; +} + +// FIXME: Refactor with BanOrUnBanUserDialog(). +export const ServerBanOrUnBanUserDialog = (props: { + conversationId: string; + banType: BanType; + pubkey?: string; +}) => { + const { conversationId, banType, pubkey } = props; + const { i18n } = window; + const isBan = banType === 'ban'; + const dispatch = useDispatch(); + const darkMode = useSelector(isDarkTheme); + const convo = getConversationController().get(conversationId); + const inputRef = useRef(null); + + useFocusMount(inputRef, true); + const wasGivenAPubkey = Boolean(pubkey?.length); + const [inputBoxValue, setInputBoxValue] = useState(''); + const [inProgress, setInProgress] = useState(false); + + const sourceConvoProps = useConversationPropsById(pubkey); + + const inputTextToDisplay = + wasGivenAPubkey && sourceConvoProps + ? `${sourceConvoProps.displayNameInProfile} ${PubKey.shorten(sourceConvoProps.id)}` + : undefined; + + /** + * Ban or Unban a user from an open group + * @param deleteAll Delete all messages for that user in the group (only works with ban) + */ + const banOrUnBanUser = async (deleteAll: boolean = false) => { + const castedPubkey = pubkey?.length ? pubkey : inputBoxValue; + + window?.log?.info(`asked to ${banType} user server-wide: ${castedPubkey}, banAndDeleteAll:${deleteAll}`); + setInProgress(true); + const isBanned = await banOrUnBanUserCall(convo, castedPubkey, banType, deleteAll, true); + if (isBanned) { + // clear input box + setInputBoxValue(''); + if (wasGivenAPubkey) { + dispatch(updateServerBanOrUnbanUserModal(null)); + } + } + + setInProgress(false); + }; + + const serverHost = new window.URL(convo.toOpenGroupV2().serverUrl).host; + const title = `${isBan ? window.i18n('banUser') : window.i18n('unbanUser')} @ ${serverHost}`; + + const onPubkeyBoxChanges = (e: React.ChangeEvent) => { + setInputBoxValue(e.target.value?.trim() || ''); + }; + + /** + * Starts procedure for banning/unbanning user and all their messages using dialog + */ + const startBanAndDeleteAllSequence = async () => { + await banOrUnBanUser(true); + }; + + const buttonText = isBan ? i18n('banUser') : i18n('unbanUser'); + + return ( + { + dispatch(updateServerBanOrUnbanUserModal(null)); + }} + > + + + + + {isBan && ( + <> + + + + )} + + + + + ); +} diff --git a/ts/components/dialog/ModalContainer.tsx b/ts/components/dialog/ModalContainer.tsx index d54bbe7e81..8d5ce620f3 100644 --- a/ts/components/dialog/ModalContainer.tsx +++ b/ts/components/dialog/ModalContainer.tsx @@ -14,6 +14,7 @@ import { getReactListDialog, getRecoveryPhraseDialog, getRemoveModeratorsModal, + getServerBanOrUnbanUserModalState, getSessionPasswordDialog, getUpdateGroupMembersModal, getUpdateGroupNameModal, @@ -33,7 +34,7 @@ import { RemoveModeratorsDialog } from './ModeratorsRemoveDialog'; import { UpdateGroupMembersDialog } from './UpdateGroupMembersDialog'; import { UpdateGroupNameDialog } from './UpdateGroupNameDialog'; import { SessionNicknameDialog } from './SessionNicknameDialog'; -import { BanOrUnBanUserDialog } from './BanOrUnbanUserDialog'; +import { BanOrUnBanUserDialog, ServerBanOrUnBanUserDialog } from './BanOrUnbanUserDialog'; import { ReactListModal } from './ReactListModal'; import { ReactClearAllModal } from './ReactClearAllModal'; @@ -53,12 +54,14 @@ export const ModalContainer = () => { const sessionPasswordModalState = useSelector(getSessionPasswordDialog); const deleteAccountModalState = useSelector(getDeleteAccountModalState); const banOrUnbanUserModalState = useSelector(getBanOrUnbanUserModalState); + const serverBanOrUnbanUserModalState = useSelector(getServerBanOrUnbanUserModalState); const reactListModalState = useSelector(getReactListDialog); const reactClearAllModalState = useSelector(getReactClearAllDialog); return ( <> {banOrUnbanUserModalState && } + {serverBanOrUnbanUserModalState && } {inviteModalState && } {addModeratorsModalState && } {removeModeratorsModalState && } diff --git a/ts/components/menu/ConversationHeaderMenu.tsx b/ts/components/menu/ConversationHeaderMenu.tsx index 6ee07b4b3e..25e8730cea 100644 --- a/ts/components/menu/ConversationHeaderMenu.tsx +++ b/ts/components/menu/ConversationHeaderMenu.tsx @@ -18,6 +18,8 @@ import { NotificationForConvoMenuItem, PinConversationMenuItem, RemoveModeratorsMenuItem, + ServerBanMenuItem, + ServerUnbanMenuItem, ShowUserDetailsMenuItem, UnbanMenuItem, UpdateGroupNameMenuItem, @@ -60,6 +62,8 @@ export const ConversationHeaderMenu = (props: PropsConversationHeaderMenu) => { + + diff --git a/ts/components/menu/ConversationListItemContextMenu.tsx b/ts/components/menu/ConversationListItemContextMenu.tsx index 7f91d0b629..5cad968085 100644 --- a/ts/components/menu/ConversationListItemContextMenu.tsx +++ b/ts/components/menu/ConversationListItemContextMenu.tsx @@ -17,6 +17,8 @@ import { MarkAllReadMenuItem, NotificationForConvoMenuItem, PinConversationMenuItem, + ServerBanMenuItem, + ServerUnbanMenuItem, ShowUserDetailsMenuItem, UnbanMenuItem, } from './Menu'; @@ -44,6 +46,8 @@ const ConversationListItemContextMenu = (props: PropsContextConversationItem) => + + diff --git a/ts/components/menu/Menu.tsx b/ts/components/menu/Menu.tsx index 6c0fc05cc7..77c652982c 100644 --- a/ts/components/menu/Menu.tsx +++ b/ts/components/menu/Menu.tsx @@ -33,6 +33,8 @@ import { showInviteContactByConvoId, showLeaveGroupByConvoId, showRemoveModeratorsByConvoId, + showServerBanUserByConvoId, + showServerUnbanUserByConvoId, showUnbanUserByConvoId, showUpdateGroupNameByConvoId, unblockConvoById, @@ -116,6 +118,14 @@ const showBanUser = (weAreAdmin: boolean, isPublic: boolean, isKickedFromGroup: return !isKickedFromGroup && weAreAdmin && isPublic; }; +const showServerUnbanUser = (weAreAdmin: boolean, isPublic: boolean) => { + return weAreAdmin && isPublic; +}; + +const showServerBanUser = (weAreAdmin: boolean, isPublic: boolean) => { + return weAreAdmin && isPublic; +}; + function showAddModerators( weAreAdmin: boolean, isPublic: boolean, @@ -387,6 +397,44 @@ export const BanMenuItem = (): JSX.Element | null => { return null; }; +export const ServerUnbanMenuItem = (): JSX.Element | null => { + const convoId = useContext(ContextConversationId); + const isPublic = useIsPublic(convoId); + const weAreAdmin = useWeAreAdmin(convoId); + + if (showServerUnbanUser(weAreAdmin, isPublic)) { + return ( + { + showServerUnbanUserByConvoId(convoId); + }} + > + {window.i18n('serverUnbanUser')} + + ); + } + return null; +}; + +export const ServerBanMenuItem = (): JSX.Element | null => { + const convoId = useContext(ContextConversationId); + const isPublic = useIsPublic(convoId); + const weAreAdmin = useWeAreAdmin(convoId); + + if (showServerBanUser(weAreAdmin, isPublic)) { + return ( + { + showServerBanUserByConvoId(convoId); + }} + > + {window.i18n('serverBanUser')} + + ); + } + return null; +}; + export const CopyMenuItem = (): JSX.Element | null => { const convoId = useContext(ContextConversationId); const isPublic = useIsPublic(convoId); diff --git a/ts/interactions/conversationInteractions.ts b/ts/interactions/conversationInteractions.ts index 6b4cd28983..e09782e5ac 100644 --- a/ts/interactions/conversationInteractions.ts +++ b/ts/interactions/conversationInteractions.ts @@ -23,6 +23,7 @@ import { updateGroupNameModal, updateInviteContactModal, updateRemoveModeratorsModal, + updateServerBanOrUnbanUserModal } from '../state/ducks/modalDialog'; import { Data, hasLinkPreviewPopupBeenDisplayed, lastAvatarUploadTimestamp } from '../data/data'; import { quoteMessage, resetConversationExternal } from '../state/ducks/conversations'; @@ -275,6 +276,18 @@ export function showUnbanUserByConvoId(conversationId: string, pubkey?: string) ); } +export function showServerBanUserByConvoId(conversationId: string, pubkey?: string) { + window.inboxStore?.dispatch( + updateServerBanOrUnbanUserModal({ banType: 'ban', conversationId, pubkey }) + ); +} + +export function showServerUnbanUserByConvoId(conversationId: string, pubkey?: string) { + window.inboxStore?.dispatch( + updateServerBanOrUnbanUserModal({ banType: 'unban', conversationId, pubkey }) + ); +} + export async function markAllReadByConvoId(conversationId: string) { const conversation = getConversationController().get(conversationId); perfStart(`markAllReadByConvoId-${conversationId}`); diff --git a/ts/interactions/messageInteractions.ts b/ts/interactions/messageInteractions.ts index 76cfccec90..ecc121351c 100644 --- a/ts/interactions/messageInteractions.ts +++ b/ts/interactions/messageInteractions.ts @@ -12,7 +12,11 @@ import { getConversationController } from '../session/conversations'; import { PubKey } from '../session/types'; import { ToastUtils } from '../session/utils'; -import { updateBanOrUnbanUserModal, updateConfirmModal } from '../state/ducks/modalDialog'; +import { + updateBanOrUnbanUserModal, + updateConfirmModal, + updateServerBanOrUnbanUserModal +} from '../state/ducks/modalDialog'; export function banUser(userToBan: string, conversationId: string) { let pubKeyToBan: PubKey; @@ -59,6 +63,47 @@ export function unbanUser(userToUnBan: string, conversationId: string) { ); } +export function serverBanUser(userToBan: string, conversationId: string) { + let pubKeyToBan: PubKey; + try { + pubKeyToBan = PubKey.cast(userToBan); + } catch (e) { + window?.log?.warn(e); + ToastUtils.pushUserBanFailure(); + return; + } + if (!isOpenGroupV2(conversationId)) { + window.log.warn(`Conversation ${conversationId} is not an open group`); + ToastUtils.pushUserBanFailure(); + + return; + } + + window.inboxStore?.dispatch( + updateServerBanOrUnbanUserModal({ banType: 'ban', conversationId, pubkey: pubKeyToBan.key }) + ); +} + +export function serverUnbanUser(userToUnBan: string, conversationId: string) { + let pubKeyToUnban: PubKey; + try { + pubKeyToUnban = PubKey.cast(userToUnBan); + } catch (e) { + window?.log?.warn(e); + ToastUtils.pushUserBanFailure(); + return; + } + if (!isOpenGroupV2(conversationId)) { + window.log.warn(`Conversation ${conversationId} is not an open group`); + ToastUtils.pushUserUnbanFailure(); + + return; + } + window.inboxStore?.dispatch( + updateServerBanOrUnbanUserModal({ banType: 'unban', conversationId, pubkey: pubKeyToUnban.key }) + ); +} + export function copyBodyToClipboard(body?: string | null) { window.clipboard.writeText(body); diff --git a/ts/session/apis/open_group_api/sogsv3/sogsV3BanUnban.ts b/ts/session/apis/open_group_api/sogsv3/sogsV3BanUnban.ts index 680d583dae..295407d555 100644 --- a/ts/session/apis/open_group_api/sogsv3/sogsV3BanUnban.ts +++ b/ts/session/apis/open_group_api/sogsv3/sogsV3BanUnban.ts @@ -15,6 +15,7 @@ export const sogsV3BanUser = async ( sessionId: userToBan.key, roomId: roomInfos.roomId, type: 'ban', + isGlobal: false }, }, ]; @@ -51,10 +52,69 @@ export const sogsV3UnbanUser = async ( sessionId: userToBan.key, roomId: roomInfos.roomId, type: 'unban', + isGlobal: false }, }, ], 'batch' ); return batchFirstSubIsSuccess(batchSendResponse); +} + +export const sogsV3ServerBanUser = async ( + userToBan: PubKey, + roomInfos: OpenGroupRequestCommonType, + deleteAllMessages: boolean, +): Promise => { + const sequence: Array = [ + { + type: 'banUnbanUser', + banUnbanUser: { + sessionId: userToBan.key, + roomId: roomInfos.roomId, + type: 'ban', + isGlobal: true + }, + }, + ]; + + if (deleteAllMessages) { + sequence.push({ + type: 'deleteAllUserPosts', + deleteAllUserPosts: { sessionId: userToBan.key }, + }); + } + + const batchSendResponse = await sogsBatchSend( + roomInfos.serverUrl, + new Set([roomInfos.roomId]), + new AbortController().signal, + sequence, + 'sequence' + ); + return batchFirstSubIsSuccess(batchSendResponse); }; + +export const sogsV3ServerUnbanUser = async ( + userToBan: PubKey, + roomInfos: OpenGroupRequestCommonType +): Promise => { + const batchSendResponse = await sogsBatchSend( + roomInfos.serverUrl, + new Set([roomInfos.roomId]), + new AbortController().signal, + [ + { + type: 'banUnbanUser', + banUnbanUser: { + sessionId: userToBan.key, + roomId: roomInfos.roomId, + type: 'unban', + isGlobal: true + }, + }, + ], + 'batch' + ); + return batchFirstSubIsSuccess(batchSendResponse); +} diff --git a/ts/session/apis/open_group_api/sogsv3/sogsV3BatchPoll.ts b/ts/session/apis/open_group_api/sogsv3/sogsV3BatchPoll.ts index 7461b16f4e..69d77a43bf 100644 --- a/ts/session/apis/open_group_api/sogsv3/sogsV3BatchPoll.ts +++ b/ts/session/apis/open_group_api/sogsv3/sogsV3BatchPoll.ts @@ -179,6 +179,7 @@ export type SubRequestBanUnbanUserType = { type: 'ban' | 'unban'; sessionId: string; // can be blinded id or not roomId: string; + isGlobal: boolean; }; }; @@ -190,6 +191,13 @@ export type SubRequestDeleteAllUserPostsType = { }; }; +export type SubRequestDeleteAllUserServerPostsType = { + type: 'deleteAllUserPosts'; + deleteAllUserPosts: { + sessionId: string; // can be blinded id or not + }; +}; + export type SubRequestUpdateRoomType = { type: 'updateRoom'; updateRoom: { @@ -218,6 +226,7 @@ export type OpenGroupBatchRow = | SubRequestAddRemoveModeratorType | SubRequestBanUnbanUserType | SubRequestDeleteAllUserPostsType + | SubRequestDeleteAllUserServerPostsType | SubRequestUpdateRoomType | SubRequestDeleteReactionType; @@ -225,6 +234,7 @@ export type OpenGroupBatchRow = * * @param options Array of subrequest options to be made. */ +// tslint:disable-next-line: cyclomatic-complexity const makeBatchRequestPayload = ( options: OpenGroupBatchRow ): BatchSubRequest | Array | null => { @@ -295,6 +305,20 @@ const makeBatchRequestPayload = ( })); case 'banUnbanUser': const isBan = Boolean(options.banUnbanUser.type === 'ban'); + const isGlobal = options.banUnbanUser.isGlobal; + window?.log?.info(`BAN: ${options.banUnbanUser.sessionId}, global: ${options.banUnbanUser.isGlobal}`); + if (isGlobal) { + // Issue server-wide (un)ban. + return { + method: 'POST', + path: `/user/${options.banUnbanUser.sessionId}/${isBan ? 'ban' : 'unban'}`, + json: { + global: true, + // timeout: null, // for now we do not support the timeout argument + }, + } + } + // Issue room-wide (un)ban. return { method: 'POST', path: `/user/${options.banUnbanUser.sessionId}/${isBan ? 'ban' : 'unban'}`, @@ -311,6 +335,11 @@ const makeBatchRequestPayload = ( method: 'DELETE', path: `/room/${options.deleteAllPosts.roomId}/all/${options.deleteAllPosts.sessionId}`, }; + case 'deleteAllUserPosts': + return { + method: 'DELETE', + path: `/rooms/all/${options.deleteAllUserPosts.sessionId}`, + }; case 'updateRoom': return { method: 'PUT', diff --git a/ts/state/ducks/modalDialog.tsx b/ts/state/ducks/modalDialog.tsx index ea8428b703..4a9213b9b5 100644 --- a/ts/state/ducks/modalDialog.tsx +++ b/ts/state/ducks/modalDialog.tsx @@ -10,6 +10,11 @@ export type BanOrUnbanUserModalState = { banType: BanType; pubkey?: string; } | null; +export type ServerBanOrUnbanUserModalState = { + conversationId: string; + banType: BanType; + pubkey?: string; +} | null; export type AddModeratorsModalState = InviteContactModalState; export type RemoveModeratorsModalState = InviteContactModalState; export type UpdateGroupMembersModalState = InviteContactModalState; @@ -38,6 +43,7 @@ export type ModalState = { confirmModal: ConfirmModalState; inviteContactModal: InviteContactModalState; banOrUnbanUserModal: BanOrUnbanUserModalState; + serverBanOrUnbanUserModal: ServerBanOrUnbanUserModalState; removeModeratorsModal: RemoveModeratorsModalState; addModeratorsModal: AddModeratorsModalState; groupNameModal: UpdateGroupNameModalState; @@ -60,6 +66,7 @@ export const initialModalState: ModalState = { addModeratorsModal: null, removeModeratorsModal: null, banOrUnbanUserModal: null, + serverBanOrUnbanUserModal: null, groupNameModal: null, groupMembersModal: null, userDetailsModal: null, @@ -87,6 +94,9 @@ const ModalSlice = createSlice({ updateBanOrUnbanUserModal(state, action: PayloadAction) { return { ...state, banOrUnbanUserModal: action.payload }; }, + updateServerBanOrUnbanUserModal(state, action: PayloadAction) { + return { ...state, serverBanOrUnbanUserModal: action.payload }; + }, updateAddModeratorsModal(state, action: PayloadAction) { return { ...state, addModeratorsModal: action.payload }; }, @@ -149,6 +159,7 @@ export const { sessionPassword, updateDeleteAccountModal, updateBanOrUnbanUserModal, + updateServerBanOrUnbanUserModal, updateReactListModal, updateReactClearAllModal, } = actions; diff --git a/ts/state/selectors/modal.ts b/ts/state/selectors/modal.ts index f959bc243c..55d3e159a6 100644 --- a/ts/state/selectors/modal.ts +++ b/ts/state/selectors/modal.ts @@ -15,6 +15,7 @@ import { ReactModalsState, RecoveryPhraseModalState, RemoveModeratorsModalState, + ServerBanOrUnbanUserModalState, SessionPasswordModalState, UpdateGroupMembersModalState, UpdateGroupNameModalState, @@ -50,6 +51,11 @@ export const getBanOrUnbanUserModalState = createSelector( (state: ModalState): BanOrUnbanUserModalState => state.banOrUnbanUserModal ); +export const getServerBanOrUnbanUserModalState = createSelector( + getModal, + (state: ModalState): ServerBanOrUnbanUserModalState => state.serverBanOrUnbanUserModal +); + export const getUpdateGroupNameModal = createSelector( getModal, (state: ModalState): UpdateGroupNameModalState => state.groupNameModal diff --git a/ts/types/LocalizerKeys.ts b/ts/types/LocalizerKeys.ts index 2aa7aff76b..8c8b8b3382 100644 --- a/ts/types/LocalizerKeys.ts +++ b/ts/types/LocalizerKeys.ts @@ -147,12 +147,14 @@ export type LocalizerKeys = | 'reactionNotification' | 'leaveGroupConfirmation' | 'banUserAndDeleteAll' + | 'serverBanUserAndDeleteAll' | 'joinOpenGroupAfterInvitationConfirmationDesc' | 'invalidNumberError' | 'contextMenuNoSuggestions' | 'callMediaPermissionsDialogTitle' | 'recoveryPhraseRevealButtonText' | 'banUser' + | 'serverBanUser' | 'primaryColorBlue' | 'sendMessage' | 'recoveryPhraseRevealMessage' @@ -491,6 +493,7 @@ export type LocalizerKeys = | 'you' | 'pruneSettingTitle' | 'unbanUser' + | 'serverUnbanUser' | 'notificationForConvo_mentions_only' | 'trustThisContactDialogDescription' | 'unknownCountry' From 3f8327c0c43a8979ec41f07fe1f55ff995503911 Mon Sep 17 00:00:00 2001 From: Ian Macdonald Date: Fri, 27 Jan 2023 11:42:19 +0100 Subject: [PATCH 2/4] Fix failed user unban being reported as success. --- ts/components/dialog/BanOrUnbanUserDialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts/components/dialog/BanOrUnbanUserDialog.tsx b/ts/components/dialog/BanOrUnbanUserDialog.tsx index 745a44380a..6faba0081d 100644 --- a/ts/components/dialog/BanOrUnbanUserDialog.tsx +++ b/ts/components/dialog/BanOrUnbanUserDialog.tsx @@ -56,7 +56,7 @@ async function banOrUnBanUserCall( if (!isChangeApplied) { window?.log?.warn(`failed to ${banType} user: ${isChangeApplied}`); - banType === 'ban' ? ToastUtils.pushUserBanFailure() : ToastUtils.pushUserUnbanSuccess(); + banType === 'ban' ? ToastUtils.pushUserBanFailure() : ToastUtils.pushUserUnbanFailure(); return false; } window?.log?.info(`${pubkey.key} user ${banType}ned successfully...`); From 93c6b06fc0ac82b1104eec5a4b91a89237bea652 Mon Sep 17 00:00:00 2001 From: Ian Macdonald Date: Fri, 27 Jan 2023 11:44:54 +0100 Subject: [PATCH 3/4] Toast a hint of the likely reason for a failed global (un)ban. --- _locales/en/messages.json | 2 ++ ts/components/dialog/BanOrUnbanUserDialog.tsx | 8 +++++++- ts/session/utils/Toast.tsx | 8 ++++++++ ts/types/LocalizerKeys.ts | 2 ++ 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 48024e3ae6..12381a5e81 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -250,12 +250,14 @@ "serverUnbanUser": "Unban User from Server", "userUnbanned": "User unbanned successfully", "userUnbanFailed": "Unban failed!", + "globalUserUnbanFailed": "Unban failed! Are you a global admin/mod?", "banUser": "Ban User", "banUserAndDeleteAll": "Ban and Delete All", "serverBanUser": "Ban User from Server", "serverBanUserAndDeleteAll": "Ban from Server and Delete All", "userBanned": "Banned successfully", "userBanFailed": "Ban failed!", + "GlobalUserBanFailed": "Ban failed! Are you a global admin/mod?", "leaveGroup": "Leave Group", "leaveAndRemoveForEveryone": "Leave Group and Remove for Everyone", "leaveGroupConfirmation": "Are you sure you want to leave this group?", diff --git a/ts/components/dialog/BanOrUnbanUserDialog.tsx b/ts/components/dialog/BanOrUnbanUserDialog.tsx index 6faba0081d..a7120a1263 100644 --- a/ts/components/dialog/BanOrUnbanUserDialog.tsx +++ b/ts/components/dialog/BanOrUnbanUserDialog.tsx @@ -56,7 +56,13 @@ async function banOrUnBanUserCall( if (!isChangeApplied) { window?.log?.warn(`failed to ${banType} user: ${isChangeApplied}`); - banType === 'ban' ? ToastUtils.pushUserBanFailure() : ToastUtils.pushUserUnbanFailure(); + banType === 'ban' + ? isGlobal + ? ToastUtils.pushGlobalUserBanFailure() + : ToastUtils.pushUserBanFailure() + : isGlobal + ? ToastUtils.pushGlobalUserUnbanFailure() + : ToastUtils.pushUserUnbanFailure(); return false; } window?.log?.info(`${pubkey.key} user ${banType}ned successfully...`); diff --git a/ts/session/utils/Toast.tsx b/ts/session/utils/Toast.tsx index 4170dc64d2..4c3918fc83 100644 --- a/ts/session/utils/Toast.tsx +++ b/ts/session/utils/Toast.tsx @@ -117,6 +117,10 @@ export function pushUserBanFailure() { pushToastError('userBanFailed', window.i18n('userBanFailed')); } +export function pushGlobalUserBanFailure() { + pushToastError('globalUserBanFailed', window.i18n('globalUserBanFailed')); +} + export function pushUserUnbanSuccess() { pushToastSuccess('userUnbanned', window.i18n('userUnbanned')); } @@ -125,6 +129,10 @@ export function pushUserUnbanFailure() { pushToastError('userUnbanFailed', window.i18n('userUnbanFailed')); } +export function pushGlobalUserUnbanFailure() { + pushToastError('globalUserUnbanFailed', window.i18n('globalUserUnbanFailed')); +} + export function pushMessageDeleteForbidden() { pushToastError('messageDeletionForbidden', window.i18n('messageDeletionForbidden')); } diff --git a/ts/types/LocalizerKeys.ts b/ts/types/LocalizerKeys.ts index 8c8b8b3382..a94576707d 100644 --- a/ts/types/LocalizerKeys.ts +++ b/ts/types/LocalizerKeys.ts @@ -2,6 +2,7 @@ export type LocalizerKeys = | 'removePassword' | 'classicDarkThemeTitle' | 'userUnbanFailed' + | 'globalUserUnbanFailed' | 'changePassword' | 'saved' | 'startedACall' @@ -39,6 +40,7 @@ export type LocalizerKeys = | 'video' | 'readReceiptSettingDescription' | 'userBanFailed' + | 'globalUserBanFailed' | 'autoUpdateLaterButtonLabel' | 'maximumAttachments' | 'deviceOnly' From 8d6051787f584f58c1820890145084b1b4592cf7 Mon Sep 17 00:00:00 2001 From: Ian Macdonald Date: Tue, 24 Jan 2023 12:17:22 +0100 Subject: [PATCH 4/4] When purging while banning a user, also remove their emoji reactions. This expands on #2653 and ensures that when a user is banned from a room or an entire server, and **Delete All** has been selected, not just his messages but also his emoji reactions will be removed. This pull-request depends on oxen-io/session-pysogs#169. Verified working on `sog.caliban.org`. --- .../open_group_api/sogsv3/sogsV3BanUnban.ts | 8 +++++ .../open_group_api/sogsv3/sogsV3BatchPoll.ts | 29 ++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/ts/session/apis/open_group_api/sogsv3/sogsV3BanUnban.ts b/ts/session/apis/open_group_api/sogsv3/sogsV3BanUnban.ts index 295407d555..bcdb3b40ce 100644 --- a/ts/session/apis/open_group_api/sogsv3/sogsV3BanUnban.ts +++ b/ts/session/apis/open_group_api/sogsv3/sogsV3BanUnban.ts @@ -25,6 +25,10 @@ export const sogsV3BanUser = async ( type: 'deleteAllPosts', deleteAllPosts: { sessionId: userToBan.key, roomId: roomInfos.roomId }, }); + sequence.push({ + type: 'deleteAllReactions', + deleteAllReactions: { sessionId: userToBan.key, roomId: roomInfos.roomId }, + }); } const batchSendResponse = await sogsBatchSend( @@ -83,6 +87,10 @@ export const sogsV3ServerBanUser = async ( type: 'deleteAllUserPosts', deleteAllUserPosts: { sessionId: userToBan.key }, }); + sequence.push({ + type: 'deleteAllUserReactions', + deleteAllUserReactions: { sessionId: userToBan.key }, + }); } const batchSendResponse = await sogsBatchSend( diff --git a/ts/session/apis/open_group_api/sogsv3/sogsV3BatchPoll.ts b/ts/session/apis/open_group_api/sogsv3/sogsV3BatchPoll.ts index 69d77a43bf..f413362bf0 100644 --- a/ts/session/apis/open_group_api/sogsv3/sogsV3BatchPoll.ts +++ b/ts/session/apis/open_group_api/sogsv3/sogsV3BatchPoll.ts @@ -198,6 +198,21 @@ export type SubRequestDeleteAllUserServerPostsType = { }; }; +export type SubRequestDeleteAllUserReactionsType = { + type: 'deleteAllReactions'; + deleteAllReactions: { + sessionId: string; // can be blinded id or not + roomId: string; + }; +}; + +export type SubRequestDeleteAllUserServerReactionsType = { + type: 'deleteAllUserReactions'; + deleteAllUserReactions: { + sessionId: string; // can be blinded id or not + }; +}; + export type SubRequestUpdateRoomType = { type: 'updateRoom'; updateRoom: { @@ -226,7 +241,9 @@ export type OpenGroupBatchRow = | SubRequestAddRemoveModeratorType | SubRequestBanUnbanUserType | SubRequestDeleteAllUserPostsType + | SubRequestDeleteAllUserReactionsType | SubRequestDeleteAllUserServerPostsType + | SubRequestDeleteAllUserServerReactionsType | SubRequestUpdateRoomType | SubRequestDeleteReactionType; @@ -340,7 +357,17 @@ const makeBatchRequestPayload = ( method: 'DELETE', path: `/rooms/all/${options.deleteAllUserPosts.sessionId}`, }; - case 'updateRoom': + case 'deleteAllReactions': + return { + method: 'DELETE', + path: `/room/${options.deleteAllReactions.roomId}/all/reactions/${options.deleteAllReactions.sessionId}`, + }; + case 'deleteAllUserReactions': + return { + method: 'DELETE', + path: `/rooms/all/reactions/${options.deleteAllUserReactions.sessionId}`, + }; + case 'updateRoom': return { method: 'PUT', path: `/room/${options.updateRoom.roomId}`,