diff --git a/keyserver/src/deleters/role-deleters.js b/keyserver/src/deleters/role-deleters.js index 07016d37d..e66b9f7f0 100644 --- a/keyserver/src/deleters/role-deleters.js +++ b/keyserver/src/deleters/role-deleters.js @@ -1,15 +1,137 @@ // @flow +import { threadPermissions } from 'lib/types/thread-permission-types.js'; +import type { + RoleDeletionRequest, + RoleDeletionResult, +} from 'lib/types/thread-types.js'; +import { updateTypes } from 'lib/types/update-types-enum.js'; +import { ServerError } from 'lib/utils/errors.js'; + +import { createUpdates } from '../creators/update-creator.js'; import { dbQuery, SQL } from '../database/database.js'; +import { + fetchServerThreadInfos, + rawThreadInfosFromServerThreadInfos, + fetchThreadInfos, +} from '../fetchers/thread-fetchers.js'; +import { checkThreadPermission } from '../fetchers/thread-permission-fetchers.js'; +import type { Viewer } from '../session/viewer.js'; +import { updateRole } from '../updaters/thread-updaters.js'; async function deleteOrphanedRoles(): Promise { await dbQuery(SQL` DELETE r, i FROM roles r LEFT JOIN ids i ON i.id = r.id LEFT JOIN threads t ON t.id = r.thread WHERE t.id IS NULL `); } -export { deleteOrphanedRoles }; +async function deleteRole( + viewer: Viewer, + request: RoleDeletionRequest, +): Promise { + const hasPermission = await checkThreadPermission( + viewer, + request.community, + threadPermissions.CHANGE_ROLE, + ); + if (!hasPermission) { + throw new ServerError('invalid_credentials'); + } + + const { community, roleID } = request; + + const defaultRoleQuery = SQL` + SELECT default_role + FROM threads + WHERE id = ${community} + `; + + const membersWithRoleQuery = SQL` + SELECT user + FROM memberships + WHERE thread = ${community} + AND role = ${roleID} + `; + + const [[defaultRoleResult], [membersWithRoleResult], { threadInfos }] = + await Promise.all([ + dbQuery(defaultRoleQuery), + dbQuery(membersWithRoleQuery), + fetchThreadInfos(viewer, { + threadID: community, + }), + ]); + const threadInfo = threadInfos[community]; + + if (!threadInfo) { + throw new ServerError('invalid_parameters'); + } + + const defaultRoleID = defaultRoleResult[0].default_role.toString(); + const membersWithRole = membersWithRoleResult.map(result => result.user); + const adminRoleID = Object.keys(threadInfo.roles).find( + role => threadInfo.roles[role].name === 'Admins', + ); + + if (roleID === defaultRoleID || roleID === adminRoleID) { + throw new ServerError('invalid_parameters'); + } + + if (membersWithRole.length > 0) { + await updateRole(viewer, { + threadID: community, + memberIDs: membersWithRole, + role: defaultRoleID, + }); + } + + const deleteFromRolesQuery = SQL` + DELETE FROM roles + WHERE id = ${roleID} + AND thread = ${community} + `; + + await dbQuery(deleteFromRolesQuery); + + const fetchServerThreadInfosResult = await fetchServerThreadInfos({ + threadID: community, + }); + const { threadInfos: serverThreadInfos } = fetchServerThreadInfosResult; + const serverThreadInfo = serverThreadInfos[community]; + + const time = Date.now(); + + const updateDatas = []; + for (const memberInfo of serverThreadInfo.members) { + updateDatas.push({ + type: updateTypes.UPDATE_THREAD, + userID: memberInfo.id, + time, + threadID: community, + }); + } + + const { viewerUpdates } = await createUpdates(updateDatas, { + viewer, + updatesForCurrentSession: 'return', + }); + + const { threadInfos: rawThreadInfos } = rawThreadInfosFromServerThreadInfos( + viewer, + fetchServerThreadInfosResult, + ); + const rawThreadInfo = rawThreadInfos[community]; + + return { + threadInfo: rawThreadInfo, + updatesResult: { + newUpdates: viewerUpdates, + }, + }; +} + +export { deleteOrphanedRoles, deleteRole }; diff --git a/lib/types/thread-types.js b/lib/types/thread-types.js index 1ce68efe2..d398cd305 100644 --- a/lib/types/thread-types.js +++ b/lib/types/thread-types.js @@ -1,439 +1,458 @@ // @flow import t, { type TInterface } from 'tcomb'; import { type AvatarDBContent, type ClientAvatar, clientAvatarValidator, type UpdateUserAvatarRequest, } from './avatar-types.js'; import type { Shape } from './core.js'; import type { CalendarQuery } from './entry-types.js'; import type { Media } from './media-types.js'; import type { MessageTruncationStatuses, RawMessageInfo, } from './message-types.js'; import { type ThreadSubscription, threadSubscriptionValidator, } from './subscription-types.js'; import { type ThreadPermissionsInfo, threadPermissionsInfoValidator, type ThreadRolePermissionsBlob, threadRolePermissionsBlobValidator, type UserSurfacedPermission, } from './thread-permission-types.js'; import { type ThreadType, threadTypeValidator } from './thread-types-enum.js'; import type { ClientUpdateInfo, ServerUpdateInfo } from './update-types.js'; import type { UserInfo, UserInfos } from './user-types.js'; import { type ThreadEntity, threadEntityValidator, } from '../utils/entity-text.js'; import { tID, tShape } from '../utils/validation-utils.js'; export type MemberInfo = { +id: string, +role: ?string, +permissions: ThreadPermissionsInfo, +isSender: boolean, }; const memberInfoValidator = tShape({ id: t.String, role: t.maybe(tID), permissions: threadPermissionsInfoValidator, isSender: t.Boolean, }); export type RelativeMemberInfo = { ...MemberInfo, +username: ?string, +isViewer: boolean, }; const relativeMemberInfoValidator = tShape({ ...memberInfoValidator.meta.props, username: t.maybe(t.String), isViewer: t.Boolean, }); export type RoleInfo = { +id: string, +name: string, +permissions: ThreadRolePermissionsBlob, +isDefault: boolean, }; const roleInfoValidator = tShape({ id: tID, name: t.String, permissions: threadRolePermissionsBlobValidator, isDefault: t.Boolean, }); export type ThreadCurrentUserInfo = { +role: ?string, +permissions: ThreadPermissionsInfo, +subscription: ThreadSubscription, +unread: ?boolean, }; const threadCurrentUserInfoValidator = tShape({ role: t.maybe(tID), permissions: threadPermissionsInfoValidator, subscription: threadSubscriptionValidator, unread: t.maybe(t.Boolean), }); export type RawThreadInfo = { +id: string, +type: ThreadType, +name: ?string, +avatar?: ?ClientAvatar, +description: ?string, +color: string, // hex, without "#" or "0x" +creationTime: number, // millisecond timestamp +parentThreadID: ?string, +containingThreadID: ?string, +community: ?string, +members: $ReadOnlyArray, +roles: { +[id: string]: RoleInfo }, +currentUser: ThreadCurrentUserInfo, +sourceMessageID?: string, +repliesCount: number, +pinnedCount?: number, }; export const rawThreadInfoValidator: TInterface = tShape({ id: tID, type: threadTypeValidator, name: t.maybe(t.String), avatar: t.maybe(clientAvatarValidator), description: t.maybe(t.String), color: t.String, creationTime: t.Number, parentThreadID: t.maybe(tID), containingThreadID: t.maybe(tID), community: t.maybe(tID), members: t.list(memberInfoValidator), roles: t.dict(tID, roleInfoValidator), currentUser: threadCurrentUserInfoValidator, sourceMessageID: t.maybe(tID), repliesCount: t.Number, pinnedCount: t.maybe(t.Number), }); export type ThreadInfo = { +id: string, +type: ThreadType, +name: ?string, +uiName: string | ThreadEntity, +avatar?: ?ClientAvatar, +description: ?string, +color: string, // hex, without "#" or "0x" +creationTime: number, // millisecond timestamp +parentThreadID: ?string, +containingThreadID: ?string, +community: ?string, +members: $ReadOnlyArray, +roles: { +[id: string]: RoleInfo }, +currentUser: ThreadCurrentUserInfo, +sourceMessageID?: string, +repliesCount: number, +pinnedCount?: number, }; export const threadInfoValidator: TInterface = tShape({ id: tID, type: threadTypeValidator, name: t.maybe(t.String), uiName: t.union([t.String, threadEntityValidator]), avatar: t.maybe(clientAvatarValidator), description: t.maybe(t.String), color: t.String, creationTime: t.Number, parentThreadID: t.maybe(tID), containingThreadID: t.maybe(tID), community: t.maybe(tID), members: t.list(relativeMemberInfoValidator), roles: t.dict(tID, roleInfoValidator), currentUser: threadCurrentUserInfoValidator, sourceMessageID: t.maybe(tID), repliesCount: t.Number, pinnedCount: t.maybe(t.Number), }); export type ResolvedThreadInfo = { +id: string, +type: ThreadType, +name: ?string, +uiName: string, +avatar?: ?ClientAvatar, +description: ?string, +color: string, // hex, without "#" or "0x" +creationTime: number, // millisecond timestamp +parentThreadID: ?string, +containingThreadID: ?string, +community: ?string, +members: $ReadOnlyArray, +roles: { +[id: string]: RoleInfo }, +currentUser: ThreadCurrentUserInfo, +sourceMessageID?: string, +repliesCount: number, +pinnedCount?: number, }; export type ServerMemberInfo = { +id: string, +role: ?string, +permissions: ThreadPermissionsInfo, +subscription: ThreadSubscription, +unread: ?boolean, +isSender: boolean, }; export type ServerThreadInfo = { +id: string, +type: ThreadType, +name: ?string, +avatar?: AvatarDBContent, +description: ?string, +color: string, // hex, without "#" or "0x" +creationTime: number, // millisecond timestamp +parentThreadID: ?string, +containingThreadID: ?string, +community: ?string, +depth: number, +members: $ReadOnlyArray, +roles: { +[id: string]: RoleInfo }, +sourceMessageID?: string, +repliesCount: number, +pinnedCount: number, }; export type ThreadStore = { +threadInfos: { +[id: string]: RawThreadInfo }, }; export const threadStoreValidator: TInterface = tShape({ threadInfos: t.dict(tID, rawThreadInfoValidator), }); export type ClientDBThreadInfo = { +id: string, +type: number, +name: ?string, +avatar?: ?string, +description: ?string, +color: string, +creationTime: string, +parentThreadID: ?string, +containingThreadID: ?string, +community: ?string, +members: string, +roles: string, +currentUser: string, +sourceMessageID?: string, +repliesCount: number, +pinnedCount?: number, }; export type ThreadDeletionRequest = { +threadID: string, +accountPassword: ?string, }; export type RemoveMembersRequest = { +threadID: string, +memberIDs: $ReadOnlyArray, }; export type RoleChangeRequest = { +threadID: string, +memberIDs: $ReadOnlyArray, +role: string, }; export type ChangeThreadSettingsResult = { +updatesResult: { +newUpdates: $ReadOnlyArray, }, +newMessageInfos: $ReadOnlyArray, }; export type ChangeThreadSettingsPayload = { +threadID: string, +updatesResult: { +newUpdates: $ReadOnlyArray, }, +newMessageInfos: $ReadOnlyArray, }; export type LeaveThreadRequest = { +threadID: string, }; export type LeaveThreadResult = { +updatesResult: { +newUpdates: $ReadOnlyArray, }, }; export type LeaveThreadPayload = { +updatesResult: { +newUpdates: $ReadOnlyArray, }, }; export type ThreadChanges = Shape<{ +type: ThreadType, +name: string, +description: string, +color: string, +parentThreadID: ?string, +newMemberIDs: $ReadOnlyArray, +avatar: UpdateUserAvatarRequest, }>; export type UpdateThreadRequest = { +threadID: string, +changes: ThreadChanges, +accountPassword?: empty, }; export type BaseNewThreadRequest = { +id?: ?string, +name?: ?string, +description?: ?string, +color?: ?string, +parentThreadID?: ?string, +initialMemberIDs?: ?$ReadOnlyArray, +ghostMemberIDs?: ?$ReadOnlyArray, }; type NewThreadRequest = | { +type: 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12, ...BaseNewThreadRequest, } | { +type: 5, +sourceMessageID: string, ...BaseNewThreadRequest, }; export type ClientNewThreadRequest = { ...NewThreadRequest, +calendarQuery: CalendarQuery, }; export type ServerNewThreadRequest = { ...NewThreadRequest, +calendarQuery?: ?CalendarQuery, }; export type NewThreadResponse = { +updatesResult: { +newUpdates: $ReadOnlyArray, }, +newMessageInfos: $ReadOnlyArray, +userInfos: UserInfos, +newThreadID: string, }; export type NewThreadResult = { +updatesResult: { +newUpdates: $ReadOnlyArray, }, +newMessageInfos: $ReadOnlyArray, +userInfos: UserInfos, +newThreadID: string, }; export type ServerThreadJoinRequest = { +threadID: string, +calendarQuery?: ?CalendarQuery, +inviteLinkSecret?: string, }; export type ClientThreadJoinRequest = { +threadID: string, +calendarQuery: CalendarQuery, +inviteLinkSecret?: string, }; export type ThreadJoinResult = { +updatesResult: { +newUpdates: $ReadOnlyArray, }, +rawMessageInfos: $ReadOnlyArray, +truncationStatuses: MessageTruncationStatuses, +userInfos: UserInfos, }; export type ThreadJoinPayload = { +updatesResult: { newUpdates: $ReadOnlyArray, }, +rawMessageInfos: $ReadOnlyArray, +truncationStatuses: MessageTruncationStatuses, +userInfos: $ReadOnlyArray, }; export type ThreadFetchMediaResult = { +media: $ReadOnlyArray, }; export type ThreadFetchMediaRequest = { +threadID: string, +limit: number, +offset: number, }; export type SidebarInfo = { +threadInfo: ThreadInfo, +lastUpdatedTime: number, +mostRecentNonLocalMessage: ?string, }; export type ToggleMessagePinRequest = { +messageID: string, +action: 'pin' | 'unpin', }; export type ToggleMessagePinResult = { +newMessageInfos: $ReadOnlyArray, +threadID: string, }; type CreateRoleAction = { +community: string, +name: string, +permissions: $ReadOnlyArray, +action: 'create_role', }; type EditRoleAction = { +community: string, +existingRoleID: string, +name: string, +permissions: $ReadOnlyArray, +action: 'edit_role', }; export type RoleModificationRequest = CreateRoleAction | EditRoleAction; export type RoleModificationResult = { +threadInfo: RawThreadInfo, +updatesResult: { +newUpdates: $ReadOnlyArray, }, }; export type RoleModificationPayload = { +threadInfo: RawThreadInfo, +updatesResult: { +newUpdates: $ReadOnlyArray, }, }; +export type RoleDeletionRequest = { + +community: string, + +roleID: string, +}; + +export type RoleDeletionResult = { + +threadInfo: RawThreadInfo, + +updatesResult: { + +newUpdates: $ReadOnlyArray, + }, +}; + +export type RoleDeletionPayload = { + +threadInfo: RawThreadInfo, + +updatesResult: { + +newUpdates: $ReadOnlyArray, + }, +}; + // We can show a max of 3 sidebars inline underneath their parent in the chat // tab. If there are more, we show a button that opens a modal to see the rest export const maxReadSidebars = 3; // We can show a max of 5 sidebars inline underneath their parent // in the chat tab if every one of the displayed sidebars is unread export const maxUnreadSidebars = 5; export type ThreadStoreThreadInfos = { +[id: string]: RawThreadInfo };