Skip to content

Commit d85a0bb

Browse files
committed
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@2c8e4f1. 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.
1 parent b1b82f2 commit d85a0bb

File tree

14 files changed

+369
-9
lines changed

14 files changed

+369
-9
lines changed

_locales/en/messages.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,10 +247,13 @@
247247
"blockedSettingsTitle": "Blocked Contacts",
248248
"conversationsSettingsTitle": "Conversations",
249249
"unbanUser": "Unban User",
250+
"serverUnbanUser": "Unban User from Server",
250251
"userUnbanned": "User unbanned successfully",
251252
"userUnbanFailed": "Unban failed!",
252253
"banUser": "Ban User",
253254
"banUserAndDeleteAll": "Ban and Delete All",
255+
"serverBanUser": "Ban User from Server",
256+
"serverBanUserAndDeleteAll": "Ban from Server and Delete All",
254257
"userBanned": "Banned successfully",
255258
"userBanFailed": "Ban failed!",
256259
"leaveGroup": "Leave Group",

ts/components/conversation/message/message-content/MessageContextMenu.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,14 @@ export const MessageContextMenu = (props: Props) => {
210210
MessageInteraction.unbanUser(sender, convoId);
211211
}, [sender, convoId]);
212212

213+
const onServerBan = useCallback(() => {
214+
MessageInteraction.serverBanUser(sender, convoId);
215+
}, [sender, convoId]);
216+
217+
const onServerUnban = useCallback(() => {
218+
MessageInteraction.serverUnbanUser(sender, convoId);
219+
}, [sender, convoId]);
220+
213221
const onSelect = useCallback(() => {
214222
dispatch(toggleSelectedMessageId(messageId));
215223
}, [messageId]);
@@ -334,6 +342,10 @@ export const MessageContextMenu = (props: Props) => {
334342
{weAreAdmin && isPublic ? (
335343
<Item onClick={onUnban}>{window.i18n('unbanUser')}</Item>
336344
) : null}
345+
{weAreAdmin && isPublic ? <Item onClick={onServerBan}>{window.i18n('serverBanUser')}</Item> : null}
346+
{weAreAdmin && isPublic ? (
347+
<Item onClick={onServerUnban}>{window.i18n('serverUnbanUser')}</Item>
348+
) : null}
337349
{weAreAdmin && isPublic && !isSenderAdmin ? (
338350
<Item onClick={addModerator}>{window.i18n('addAsModerator')}</Item>
339351
) : null}

ts/components/dialog/BanOrUnbanUserDialog.tsx

Lines changed: 126 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ import { PubKey } from '../../session/types';
33
import { ToastUtils } from '../../session/utils';
44
import { Flex } from '../basic/Flex';
55
import { useDispatch, useSelector } from 'react-redux';
6-
import { BanType, updateBanOrUnbanUserModal } from '../../state/ducks/modalDialog';
6+
import {
7+
BanType,
8+
updateBanOrUnbanUserModal,
9+
updateServerBanOrUnbanUserModal
10+
} from '../../state/ducks/modalDialog';
711
import { SpacerSM } from '../basic/Text';
812
import { getConversationController } from '../../session/conversations/ConversationController';
913
import { SessionWrapperModal } from '../SessionWrapperModal';
@@ -14,7 +18,9 @@ import { useFocusMount } from '../../hooks/useFocusMount';
1418
import { useConversationPropsById } from '../../hooks/useParamSelector';
1519
import {
1620
sogsV3BanUser,
17-
sogsV3UnbanUser,
21+
sogsV3ServerBanUser,
22+
sogsV3ServerUnbanUser,
23+
sogsV3UnbanUser
1824
} from '../../session/apis/open_group_api/sogsv3/sogsV3BanUnban';
1925
import { SessionHeaderSearchInput } from '../SessionHeaderSearchInput';
2026
import { isDarkTheme } from '../../state/selectors/theme';
@@ -25,7 +31,8 @@ async function banOrUnBanUserCall(
2531
convo: ConversationModel,
2632
textValue: string,
2733
banType: BanType,
28-
deleteAll: boolean
34+
deleteAll: boolean,
35+
isGlobal: boolean
2936
) {
3037
// if we don't have valid data entered by the user
3138
const pubkey = PubKey.from(textValue);
@@ -39,8 +46,12 @@ async function banOrUnBanUserCall(
3946
const roomInfos = convo.toOpenGroupV2();
4047
const isChangeApplied =
4148
banType === 'ban'
42-
? await sogsV3BanUser(pubkey, roomInfos, deleteAll)
43-
: await sogsV3UnbanUser(pubkey, roomInfos);
49+
? isGlobal
50+
? await sogsV3ServerBanUser(pubkey, roomInfos, deleteAll)
51+
: await sogsV3BanUser(pubkey, roomInfos, deleteAll)
52+
: isGlobal
53+
? await sogsV3ServerUnbanUser(pubkey, roomInfos)
54+
: await sogsV3UnbanUser(pubkey, roomInfos);
4455

4556
if (!isChangeApplied) {
4657
window?.log?.warn(`failed to ${banType} user: ${isChangeApplied}`);
@@ -92,7 +103,7 @@ export const BanOrUnBanUserDialog = (props: {
92103

93104
window?.log?.info(`asked to ${banType} user: ${castedPubkey}, banAndDeleteAll:${deleteAll}`);
94105
setInProgress(true);
95-
const isBanned = await banOrUnBanUserCall(convo, castedPubkey, banType, deleteAll);
106+
const isBanned = await banOrUnBanUserCall(convo, castedPubkey, banType, deleteAll, false);
96107
if (isBanned) {
97108
// clear input box
98109
setInputBoxValue('');
@@ -163,4 +174,112 @@ export const BanOrUnBanUserDialog = (props: {
163174
</Flex>
164175
</SessionWrapperModal>
165176
);
166-
};
177+
}
178+
179+
// FIXME: Refactor with BanOrUnBanUserDialog().
180+
export const ServerBanOrUnBanUserDialog = (props: {
181+
conversationId: string;
182+
banType: BanType;
183+
pubkey?: string;
184+
}) => {
185+
const { conversationId, banType, pubkey } = props;
186+
const { i18n } = window;
187+
const isBan = banType === 'ban';
188+
const dispatch = useDispatch();
189+
const darkMode = useSelector(isDarkTheme);
190+
const convo = getConversationController().get(conversationId);
191+
const inputRef = useRef(null);
192+
193+
useFocusMount(inputRef, true);
194+
const wasGivenAPubkey = Boolean(pubkey?.length);
195+
const [inputBoxValue, setInputBoxValue] = useState('');
196+
const [inProgress, setInProgress] = useState(false);
197+
198+
const sourceConvoProps = useConversationPropsById(pubkey);
199+
200+
const inputTextToDisplay =
201+
wasGivenAPubkey && sourceConvoProps
202+
? `${sourceConvoProps.displayNameInProfile} ${PubKey.shorten(sourceConvoProps.id)}`
203+
: undefined;
204+
205+
/**
206+
* Ban or Unban a user from an open group
207+
* @param deleteAll Delete all messages for that user in the group (only works with ban)
208+
*/
209+
const banOrUnBanUser = async (deleteAll: boolean = false) => {
210+
const castedPubkey = pubkey?.length ? pubkey : inputBoxValue;
211+
212+
window?.log?.info(`asked to ${banType} user server-wide: ${castedPubkey}, banAndDeleteAll:${deleteAll}`);
213+
setInProgress(true);
214+
const isBanned = await banOrUnBanUserCall(convo, castedPubkey, banType, deleteAll, true);
215+
if (isBanned) {
216+
// clear input box
217+
setInputBoxValue('');
218+
if (wasGivenAPubkey) {
219+
dispatch(updateServerBanOrUnbanUserModal(null));
220+
}
221+
}
222+
223+
setInProgress(false);
224+
};
225+
226+
const serverHost = new window.URL(convo.toOpenGroupV2().serverUrl).host;
227+
const title = `${isBan ? window.i18n('banUser') : window.i18n('unbanUser')} @ ${serverHost}`;
228+
229+
const onPubkeyBoxChanges = (e: React.ChangeEvent<HTMLInputElement>) => {
230+
setInputBoxValue(e.target.value?.trim() || '');
231+
};
232+
233+
/**
234+
* Starts procedure for banning/unbanning user and all their messages using dialog
235+
*/
236+
const startBanAndDeleteAllSequence = async () => {
237+
await banOrUnBanUser(true);
238+
};
239+
240+
const buttonText = isBan ? i18n('banUser') : i18n('unbanUser');
241+
242+
return (
243+
<SessionWrapperModal
244+
showExitIcon={true}
245+
title={title}
246+
onClose={() => {
247+
dispatch(updateServerBanOrUnbanUserModal(null));
248+
}}
249+
>
250+
<Flex container={true} flexDirection="column" alignItems="center">
251+
<SessionHeaderSearchInput
252+
ref={inputRef}
253+
type="text"
254+
darkMode={darkMode}
255+
placeholder={i18n('enterSessionID')}
256+
dir="auto"
257+
onChange={onPubkeyBoxChanges}
258+
disabled={inProgress || wasGivenAPubkey}
259+
value={wasGivenAPubkey ? inputTextToDisplay : inputBoxValue}
260+
/>
261+
<Flex container={true}>
262+
<SessionButton
263+
buttonType={SessionButtonType.Simple}
264+
onClick={banOrUnBanUser}
265+
text={buttonText}
266+
disabled={inProgress}
267+
/>
268+
{isBan && (
269+
<>
270+
<SpacerSM />
271+
<SessionButton
272+
buttonType={SessionButtonType.Simple}
273+
buttonColor={SessionButtonColor.Danger}
274+
onClick={startBanAndDeleteAllSequence}
275+
text={i18n('serverBanUserAndDeleteAll')}
276+
disabled={inProgress}
277+
/>
278+
</>
279+
)}
280+
</Flex>
281+
<SessionSpinner loading={inProgress} />
282+
</Flex>
283+
</SessionWrapperModal>
284+
);
285+
}

ts/components/dialog/ModalContainer.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
getReactListDialog,
1515
getRecoveryPhraseDialog,
1616
getRemoveModeratorsModal,
17+
getServerBanOrUnbanUserModalState,
1718
getSessionPasswordDialog,
1819
getUpdateGroupMembersModal,
1920
getUpdateGroupNameModal,
@@ -33,7 +34,7 @@ import { RemoveModeratorsDialog } from './ModeratorsRemoveDialog';
3334
import { UpdateGroupMembersDialog } from './UpdateGroupMembersDialog';
3435
import { UpdateGroupNameDialog } from './UpdateGroupNameDialog';
3536
import { SessionNicknameDialog } from './SessionNicknameDialog';
36-
import { BanOrUnBanUserDialog } from './BanOrUnbanUserDialog';
37+
import { BanOrUnBanUserDialog, ServerBanOrUnBanUserDialog } from './BanOrUnbanUserDialog';
3738
import { ReactListModal } from './ReactListModal';
3839
import { ReactClearAllModal } from './ReactClearAllModal';
3940

@@ -53,12 +54,14 @@ export const ModalContainer = () => {
5354
const sessionPasswordModalState = useSelector(getSessionPasswordDialog);
5455
const deleteAccountModalState = useSelector(getDeleteAccountModalState);
5556
const banOrUnbanUserModalState = useSelector(getBanOrUnbanUserModalState);
57+
const serverBanOrUnbanUserModalState = useSelector(getServerBanOrUnbanUserModalState);
5658
const reactListModalState = useSelector(getReactListDialog);
5759
const reactClearAllModalState = useSelector(getReactClearAllDialog);
5860

5961
return (
6062
<>
6163
{banOrUnbanUserModalState && <BanOrUnBanUserDialog {...banOrUnbanUserModalState} />}
64+
{serverBanOrUnbanUserModalState && <ServerBanOrUnBanUserDialog {...serverBanOrUnbanUserModalState} />}
6265
{inviteModalState && <InviteContactsDialog {...inviteModalState} />}
6366
{addModeratorsModalState && <AddModeratorsDialog {...addModeratorsModalState} />}
6467
{removeModeratorsModalState && <RemoveModeratorsDialog {...removeModeratorsModalState} />}

ts/components/menu/ConversationHeaderMenu.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import {
1818
NotificationForConvoMenuItem,
1919
PinConversationMenuItem,
2020
RemoveModeratorsMenuItem,
21+
ServerBanMenuItem,
22+
ServerUnbanMenuItem,
2123
ShowUserDetailsMenuItem,
2224
UnbanMenuItem,
2325
UpdateGroupNameMenuItem,
@@ -60,6 +62,8 @@ export const ConversationHeaderMenu = (props: PropsConversationHeaderMenu) => {
6062
<RemoveModeratorsMenuItem />
6163
<BanMenuItem />
6264
<UnbanMenuItem />
65+
<ServerBanMenuItem />
66+
<ServerUnbanMenuItem />
6367
<UpdateGroupNameMenuItem />
6468
<LeaveGroupMenuItem />
6569
<InviteContactMenuItem />

ts/components/menu/ConversationListItemContextMenu.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import {
1717
MarkAllReadMenuItem,
1818
NotificationForConvoMenuItem,
1919
PinConversationMenuItem,
20+
ServerBanMenuItem,
21+
ServerUnbanMenuItem,
2022
ShowUserDetailsMenuItem,
2123
UnbanMenuItem,
2224
} from './Menu';
@@ -44,6 +46,8 @@ const ConversationListItemContextMenu = (props: PropsContextConversationItem) =>
4446
<DeleteMessagesMenuItem />
4547
<BanMenuItem />
4648
<UnbanMenuItem />
49+
<ServerBanMenuItem />
50+
<ServerUnbanMenuItem />
4751
<InviteContactMenuItem />
4852
<DeleteContactMenuItem />
4953
<LeaveGroupMenuItem />

ts/components/menu/Menu.tsx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ import {
3333
showInviteContactByConvoId,
3434
showLeaveGroupByConvoId,
3535
showRemoveModeratorsByConvoId,
36+
showServerBanUserByConvoId,
37+
showServerUnbanUserByConvoId,
3638
showUnbanUserByConvoId,
3739
showUpdateGroupNameByConvoId,
3840
unblockConvoById,
@@ -116,6 +118,14 @@ const showBanUser = (weAreAdmin: boolean, isPublic: boolean, isKickedFromGroup:
116118
return !isKickedFromGroup && weAreAdmin && isPublic;
117119
};
118120

121+
const showServerUnbanUser = (weAreAdmin: boolean, isPublic: boolean) => {
122+
return weAreAdmin && isPublic;
123+
};
124+
125+
const showServerBanUser = (weAreAdmin: boolean, isPublic: boolean) => {
126+
return weAreAdmin && isPublic;
127+
};
128+
119129
function showAddModerators(
120130
weAreAdmin: boolean,
121131
isPublic: boolean,
@@ -387,6 +397,44 @@ export const BanMenuItem = (): JSX.Element | null => {
387397
return null;
388398
};
389399

400+
export const ServerUnbanMenuItem = (): JSX.Element | null => {
401+
const convoId = useContext(ContextConversationId);
402+
const isPublic = useIsPublic(convoId);
403+
const weAreAdmin = useWeAreAdmin(convoId);
404+
405+
if (showServerUnbanUser(weAreAdmin, isPublic)) {
406+
return (
407+
<Item
408+
onClick={() => {
409+
showServerUnbanUserByConvoId(convoId);
410+
}}
411+
>
412+
{window.i18n('serverUnbanUser')}
413+
</Item>
414+
);
415+
}
416+
return null;
417+
};
418+
419+
export const ServerBanMenuItem = (): JSX.Element | null => {
420+
const convoId = useContext(ContextConversationId);
421+
const isPublic = useIsPublic(convoId);
422+
const weAreAdmin = useWeAreAdmin(convoId);
423+
424+
if (showServerBanUser(weAreAdmin, isPublic)) {
425+
return (
426+
<Item
427+
onClick={() => {
428+
showServerBanUserByConvoId(convoId);
429+
}}
430+
>
431+
{window.i18n('serverBanUser')}
432+
</Item>
433+
);
434+
}
435+
return null;
436+
};
437+
390438
export const CopyMenuItem = (): JSX.Element | null => {
391439
const convoId = useContext(ContextConversationId);
392440
const isPublic = useIsPublic(convoId);

ts/interactions/conversationInteractions.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
updateGroupNameModal,
2424
updateInviteContactModal,
2525
updateRemoveModeratorsModal,
26+
updateServerBanOrUnbanUserModal
2627
} from '../state/ducks/modalDialog';
2728
import { Data, hasLinkPreviewPopupBeenDisplayed, lastAvatarUploadTimestamp } from '../data/data';
2829
import { quoteMessage, resetConversationExternal } from '../state/ducks/conversations';
@@ -275,6 +276,18 @@ export function showUnbanUserByConvoId(conversationId: string, pubkey?: string)
275276
);
276277
}
277278

279+
export function showServerBanUserByConvoId(conversationId: string, pubkey?: string) {
280+
window.inboxStore?.dispatch(
281+
updateServerBanOrUnbanUserModal({ banType: 'ban', conversationId, pubkey })
282+
);
283+
}
284+
285+
export function showServerUnbanUserByConvoId(conversationId: string, pubkey?: string) {
286+
window.inboxStore?.dispatch(
287+
updateServerBanOrUnbanUserModal({ banType: 'unban', conversationId, pubkey })
288+
);
289+
}
290+
278291
export async function markAllReadByConvoId(conversationId: string) {
279292
const conversation = getConversationController().get(conversationId);
280293
perfStart(`markAllReadByConvoId-${conversationId}`);

0 commit comments

Comments
 (0)