Skip to content

Commit

Permalink
Implement server-wide open group user bans and unbans.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ianmacd committed Jan 31, 2023
1 parent b1b82f2 commit d85a0bb
Show file tree
Hide file tree
Showing 14 changed files with 369 additions and 9 deletions.
3 changes: 3 additions & 0 deletions _locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand Down Expand Up @@ -334,6 +342,10 @@ export const MessageContextMenu = (props: Props) => {
{weAreAdmin && isPublic ? (
<Item onClick={onUnban}>{window.i18n('unbanUser')}</Item>
) : null}
{weAreAdmin && isPublic ? <Item onClick={onServerBan}>{window.i18n('serverBanUser')}</Item> : null}
{weAreAdmin && isPublic ? (
<Item onClick={onServerUnban}>{window.i18n('serverUnbanUser')}</Item>
) : null}
{weAreAdmin && isPublic && !isSenderAdmin ? (
<Item onClick={addModerator}>{window.i18n('addAsModerator')}</Item>
) : null}
Expand Down
133 changes: 126 additions & 7 deletions ts/components/dialog/BanOrUnbanUserDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand All @@ -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);
Expand All @@ -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}`);
Expand Down Expand Up @@ -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('');
Expand Down Expand Up @@ -163,4 +174,112 @@ export const BanOrUnBanUserDialog = (props: {
</Flex>
</SessionWrapperModal>
);
};
}

// 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<HTMLInputElement>) => {
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 (
<SessionWrapperModal
showExitIcon={true}
title={title}
onClose={() => {
dispatch(updateServerBanOrUnbanUserModal(null));
}}
>
<Flex container={true} flexDirection="column" alignItems="center">
<SessionHeaderSearchInput
ref={inputRef}
type="text"
darkMode={darkMode}
placeholder={i18n('enterSessionID')}
dir="auto"
onChange={onPubkeyBoxChanges}
disabled={inProgress || wasGivenAPubkey}
value={wasGivenAPubkey ? inputTextToDisplay : inputBoxValue}
/>
<Flex container={true}>
<SessionButton
buttonType={SessionButtonType.Simple}
onClick={banOrUnBanUser}
text={buttonText}
disabled={inProgress}
/>
{isBan && (
<>
<SpacerSM />
<SessionButton
buttonType={SessionButtonType.Simple}
buttonColor={SessionButtonColor.Danger}
onClick={startBanAndDeleteAllSequence}
text={i18n('serverBanUserAndDeleteAll')}
disabled={inProgress}
/>
</>
)}
</Flex>
<SessionSpinner loading={inProgress} />
</Flex>
</SessionWrapperModal>
);
}
5 changes: 4 additions & 1 deletion ts/components/dialog/ModalContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
getReactListDialog,
getRecoveryPhraseDialog,
getRemoveModeratorsModal,
getServerBanOrUnbanUserModalState,
getSessionPasswordDialog,
getUpdateGroupMembersModal,
getUpdateGroupNameModal,
Expand All @@ -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';

Expand All @@ -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 && <BanOrUnBanUserDialog {...banOrUnbanUserModalState} />}
{serverBanOrUnbanUserModalState && <ServerBanOrUnBanUserDialog {...serverBanOrUnbanUserModalState} />}
{inviteModalState && <InviteContactsDialog {...inviteModalState} />}
{addModeratorsModalState && <AddModeratorsDialog {...addModeratorsModalState} />}
{removeModeratorsModalState && <RemoveModeratorsDialog {...removeModeratorsModalState} />}
Expand Down
4 changes: 4 additions & 0 deletions ts/components/menu/ConversationHeaderMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
NotificationForConvoMenuItem,
PinConversationMenuItem,
RemoveModeratorsMenuItem,
ServerBanMenuItem,
ServerUnbanMenuItem,
ShowUserDetailsMenuItem,
UnbanMenuItem,
UpdateGroupNameMenuItem,
Expand Down Expand Up @@ -60,6 +62,8 @@ export const ConversationHeaderMenu = (props: PropsConversationHeaderMenu) => {
<RemoveModeratorsMenuItem />
<BanMenuItem />
<UnbanMenuItem />
<ServerBanMenuItem />
<ServerUnbanMenuItem />
<UpdateGroupNameMenuItem />
<LeaveGroupMenuItem />
<InviteContactMenuItem />
Expand Down
4 changes: 4 additions & 0 deletions ts/components/menu/ConversationListItemContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
MarkAllReadMenuItem,
NotificationForConvoMenuItem,
PinConversationMenuItem,
ServerBanMenuItem,
ServerUnbanMenuItem,
ShowUserDetailsMenuItem,
UnbanMenuItem,
} from './Menu';
Expand Down Expand Up @@ -44,6 +46,8 @@ const ConversationListItemContextMenu = (props: PropsContextConversationItem) =>
<DeleteMessagesMenuItem />
<BanMenuItem />
<UnbanMenuItem />
<ServerBanMenuItem />
<ServerUnbanMenuItem />
<InviteContactMenuItem />
<DeleteContactMenuItem />
<LeaveGroupMenuItem />
Expand Down
48 changes: 48 additions & 0 deletions ts/components/menu/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import {
showInviteContactByConvoId,
showLeaveGroupByConvoId,
showRemoveModeratorsByConvoId,
showServerBanUserByConvoId,
showServerUnbanUserByConvoId,
showUnbanUserByConvoId,
showUpdateGroupNameByConvoId,
unblockConvoById,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 (
<Item
onClick={() => {
showServerUnbanUserByConvoId(convoId);
}}
>
{window.i18n('serverUnbanUser')}
</Item>
);
}
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 (
<Item
onClick={() => {
showServerBanUserByConvoId(convoId);
}}
>
{window.i18n('serverBanUser')}
</Item>
);
}
return null;
};

export const CopyMenuItem = (): JSX.Element | null => {
const convoId = useContext(ContextConversationId);
const isPublic = useIsPublic(convoId);
Expand Down
13 changes: 13 additions & 0 deletions ts/interactions/conversationInteractions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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}`);
Expand Down
Loading

0 comments on commit d85a0bb

Please sign in to comment.