Skip to content

Commit a455b75

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 c6fb665 commit a455b75

File tree

14 files changed

+361
-10
lines changed

14 files changed

+361
-10
lines changed

_locales/en/messages.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,10 +265,13 @@
265265
"blockedSettingsTitle": "Blocked Contacts",
266266
"conversationsSettingsTitle": "Conversations",
267267
"unbanUser": "Unban User",
268+
"serverUnbanUser": "Unban User from Server",
268269
"userUnbanned": "User unbanned successfully",
269270
"userUnbanFailed": "Unban failed!",
270271
"banUser": "Ban User",
271272
"banUserAndDeleteAll": "Ban and Delete All",
273+
"serverBanUser": "Ban User from Server",
274+
"serverBanUserAndDeleteAll": "Ban from Server and Delete All",
272275
"userBanned": "Banned successfully",
273276
"userBanFailed": "Ban failed!",
274277
"leaveGroup": "Leave Group",

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,14 @@ export const MessageContextMenu = (props: Props) => {
216216
MessageInteraction.unbanUser(sender, convoId);
217217
}, [sender, convoId]);
218218

219+
const onServerBan = useCallback(() => {
220+
MessageInteraction.serverBanUser(sender, convoId);
221+
}, [sender, convoId]);
222+
223+
const onServerUnban = useCallback(() => {
224+
MessageInteraction.serverUnbanUser(sender, convoId);
225+
}, [sender, convoId]);
226+
219227
const onSelect = useCallback(() => {
220228
dispatch(toggleSelectedMessageId(messageId));
221229
}, [messageId]);
@@ -340,6 +348,8 @@ export const MessageContextMenu = (props: Props) => {
340348
<>
341349
<Item onClick={onBan}>{window.i18n('banUser')}</Item>
342350
<Item onClick={onUnban}>{window.i18n('unbanUser')}</Item>
351+
<Item onClick={onServerBan}>{window.i18n('serverBanUser')}</Item>
352+
<Item onClick={onServerUnban}>{window.i18n('serverUnbanUser')}</Item>
343353
{isSenderAdmin ? (
344354
<Item onClick={removeModerator}>{window.i18n('removeFromModerators')}</Item>
345355
) : (

ts/components/dialog/BanOrUnbanUserDialog.tsx

Lines changed: 127 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,113 @@ 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 serverUrl = convo.toOpenGroupV2().serverUrl.match(/[^/]+/g);
227+
const serverHost = serverUrl ? serverUrl[1] : 'server-wide';
228+
const title = `${isBan ? window.i18n('banUser') : window.i18n('unbanUser')} @ ${serverHost}`;
229+
230+
const onPubkeyBoxChanges = (e: React.ChangeEvent<HTMLInputElement>) => {
231+
setInputBoxValue(e.target.value?.trim() || '');
232+
};
233+
234+
/**
235+
* Starts procedure for banning/unbanning user and all their messages using dialog
236+
*/
237+
const startBanAndDeleteAllSequence = async () => {
238+
await banOrUnBanUser(true);
239+
};
240+
241+
const buttonText = isBan ? i18n('banUser') : i18n('unbanUser');
242+
243+
return (
244+
<SessionWrapperModal
245+
showExitIcon={true}
246+
title={title}
247+
onClose={() => {
248+
dispatch(updateServerBanOrUnbanUserModal(null));
249+
}}
250+
>
251+
<Flex container={true} flexDirection="column" alignItems="center">
252+
<SessionHeaderSearchInput
253+
ref={inputRef}
254+
type="text"
255+
darkMode={darkMode}
256+
placeholder={i18n('enterSessionID')}
257+
dir="auto"
258+
onChange={onPubkeyBoxChanges}
259+
disabled={inProgress || wasGivenAPubkey}
260+
value={wasGivenAPubkey ? inputTextToDisplay : inputBoxValue}
261+
/>
262+
<Flex container={true}>
263+
<SessionButton
264+
buttonType={SessionButtonType.Simple}
265+
onClick={banOrUnBanUser}
266+
text={buttonText}
267+
disabled={inProgress}
268+
/>
269+
{isBan && (
270+
<>
271+
<SpacerSM />
272+
<SessionButton
273+
buttonType={SessionButtonType.Simple}
274+
buttonColor={SessionButtonColor.Danger}
275+
onClick={startBanAndDeleteAllSequence}
276+
text={i18n('serverBanUserAndDeleteAll')}
277+
disabled={inProgress}
278+
/>
279+
</>
280+
)}
281+
</Flex>
282+
<SessionSpinner loading={inProgress} />
283+
</Flex>
284+
</SessionWrapperModal>
285+
);
286+
}

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
@@ -32,6 +32,8 @@ import {
3232
MarkAllReadMenuItem,
3333
NotificationForConvoMenuItem,
3434
RemoveModeratorsMenuItem,
35+
ServerBanMenuItem,
36+
ServerUnbanMenuItem,
3537
ShowUserDetailsMenuItem,
3638
UnbanMenuItem,
3739
UpdateGroupNameMenuItem,
@@ -76,6 +78,8 @@ export const ConversationHeaderMenu = (props: PropsConversationHeaderMenu) => {
7678
<RemoveModeratorsMenuItem />
7779
<BanMenuItem />
7880
<UnbanMenuItem />
81+
<ServerBanMenuItem />
82+
<ServerUnbanMenuItem />
7983
<UpdateGroupNameMenuItem />
8084
<LeaveGroupMenuItem />
8185
<InviteContactMenuItem />

ts/components/menu/ConversationListItemContextMenu.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import {
2828
UnbanMenuItem,
2929
DeletePrivateConversationMenuItem,
3030
NotificationForConvoMenuItem,
31+
ServerBanMenuItem,
32+
ServerUnbanMenuItem
3133
} from './Menu';
3234
import { isSearching } from '../../state/selectors/search';
3335

@@ -65,6 +67,8 @@ const ConversationListItemContextMenu = (props: PropsContextConversationItem) =>
6567
{/* Communities actions */}
6668
<BanMenuItem />
6769
<UnbanMenuItem />
70+
<ServerBanMenuItem />
71+
<ServerUnbanMenuItem />
6872
<InviteContactMenuItem />
6973
<DeleteMessagesMenuItem />
7074
<DeletePrivateConversationMenuItem />

ts/components/menu/Menu.tsx

Lines changed: 40 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,
@@ -347,6 +349,44 @@ export const BanMenuItem = (): JSX.Element | null => {
347349
return null;
348350
};
349351

352+
export const ServerUnbanMenuItem = (): JSX.Element | null => {
353+
const convoId = useConvoIdFromContext();
354+
const isPublic = useIsPublic(convoId);
355+
const weAreAdmin = useWeAreAdmin(convoId);
356+
357+
if (isPublic && weAreAdmin) {
358+
return (
359+
<Item
360+
onClick={() => {
361+
showServerUnbanUserByConvoId(convoId);
362+
}}
363+
>
364+
{window.i18n('serverUnbanUser')}
365+
</Item>
366+
);
367+
}
368+
return null;
369+
};
370+
371+
export const ServerBanMenuItem = (): JSX.Element | null => {
372+
const convoId = useConvoIdFromContext();
373+
const isPublic = useIsPublic(convoId);
374+
const weAreAdmin = useWeAreAdmin(convoId);
375+
376+
if (isPublic && weAreAdmin) {
377+
return (
378+
<Item
379+
onClick={() => {
380+
showServerBanUserByConvoId(convoId);
381+
}}
382+
>
383+
{window.i18n('serverBanUser')}
384+
</Item>
385+
);
386+
}
387+
return null;
388+
};
389+
350390
export const CopyMenuItem = (): JSX.Element | null => {
351391
const convoId = useConvoIdFromContext();
352392
const isPublic = useIsPublic(convoId);

ts/interactions/conversationInteractions.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
updateGroupNameModal,
3434
updateInviteContactModal,
3535
updateRemoveModeratorsModal,
36+
updateServerBanOrUnbanUserModal
3637
} from '../state/ducks/modalDialog';
3738
import { MIME } from '../types';
3839
import { IMAGE_JPEG } from '../types/MIME';
@@ -297,6 +298,18 @@ export function showUnbanUserByConvoId(conversationId: string, pubkey?: string)
297298
);
298299
}
299300

301+
export function showServerBanUserByConvoId(conversationId: string, pubkey?: string) {
302+
window.inboxStore?.dispatch(
303+
updateServerBanOrUnbanUserModal({ banType: 'ban', conversationId, pubkey })
304+
);
305+
}
306+
307+
export function showServerUnbanUserByConvoId(conversationId: string, pubkey?: string) {
308+
window.inboxStore?.dispatch(
309+
updateServerBanOrUnbanUserModal({ banType: 'unban', conversationId, pubkey })
310+
);
311+
}
312+
300313
export async function markAllReadByConvoId(conversationId: string) {
301314
const conversation = getConversationController().get(conversationId);
302315
perfStart(`markAllReadByConvoId-${conversationId}`);

0 commit comments

Comments
 (0)