diff --git a/keyserver/src/creators/role-creator.js b/keyserver/src/creators/role-creator.js index 285063db0..77c25444b 100644 --- a/keyserver/src/creators/role-creator.js +++ b/keyserver/src/creators/role-creator.js @@ -1,62 +1,143 @@ // @flow import { getRolePermissionBlobs } from 'lib/permissions/thread-permissions.js'; +import { + universalCommunityPermissions, + userSurfacedPermissionsSet, + configurableCommunityPermissions, + threadPermissions, +} from 'lib/types/thread-permission-types.js'; import type { ThreadType } from 'lib/types/thread-types-enum.js'; -import type { RoleInfo } from 'lib/types/thread-types.js'; +import { threadTypes } from 'lib/types/thread-types-enum.js'; +import type { + RoleInfo, + RoleModificationRequest, +} from 'lib/types/thread-types.js'; +import { ServerError } from 'lib/utils/errors.js'; import createIDs from './id-creator.js'; import { dbQuery, SQL } from '../database/database.js'; +import { fetchThreadInfos } from '../fetchers/thread-fetchers.js'; +import { checkThreadPermission } from '../fetchers/thread-permission-fetchers.js'; +import type { Viewer } from '../session/viewer.js'; type InitialRoles = { +default: RoleInfo, +creator: RoleInfo, }; async function createInitialRolesForNewThread( threadID: string, threadType: ThreadType, ): Promise { const rolePermissions = getRolePermissionBlobs(threadType); const ids = await createIDs('roles', Object.values(rolePermissions).length); const time = Date.now(); const newRows = []; const namesToIDs = {}; for (const name in rolePermissions) { const id = ids.shift(); namesToIDs[name] = id; const permissionsBlob = JSON.stringify(rolePermissions[name]); newRows.push([id, threadID, name, permissionsBlob, time]); } const query = SQL` INSERT INTO roles (id, thread, name, permissions, creation_time) VALUES ${newRows} `; await dbQuery(query); const defaultRoleInfo = { id: namesToIDs.Members, name: 'Members', permissions: rolePermissions.Members, isDefault: true, }; if (!rolePermissions.Admins) { return { default: defaultRoleInfo, creator: defaultRoleInfo, }; } const adminRoleInfo = { id: namesToIDs.Admins, name: 'Admins', permissions: rolePermissions.Admins, isDefault: false, }; return { default: defaultRoleInfo, creator: adminRoleInfo, }; } -export { createInitialRolesForNewThread }; +async function modifyRole( + viewer: Viewer, + request: RoleModificationRequest, +): Promise { + const hasPermission = await checkThreadPermission( + viewer, + request.community, + threadPermissions.CHANGE_ROLE, + ); + if (!hasPermission) { + throw new ServerError('invalid_credentials'); + } + + const { community, name, permissions, action } = request; + + for (const permission of permissions) { + if (!userSurfacedPermissionsSet.has(permission)) { + throw new ServerError('invalid_parameters'); + } + } + + const [id] = await createIDs('roles', 1); + const time = Date.now(); + + const configuredPermissions = permissions + .map(permission => [...configurableCommunityPermissions[permission]]) + .flat(); + + const rolePermissions = [ + ...universalCommunityPermissions, + ...configuredPermissions, + ]; + + // For communities of the type `COMMUNITY_ANNOUNCEMENT_ROOT`, the ability for + // the role to be voiced needs to be configured (i.e. the parameters should + // include the user-facing permission VOICED_IN_ANNOUNCEMENT_CHANNELS). This + // means we do not give 'voiced' permissions by default to all new roles. As + // a result, if the thread type is `COMMUNITY_ROOT`, we want to ensure that + // the role has the voiced permission. + const { threadInfos } = await fetchThreadInfos(viewer, { + threadID: community, + }); + const threadInfo = threadInfos[community]; + + if (threadInfo.type === threadTypes.COMMUNITY_ROOT) { + rolePermissions.push(threadPermissions.VOICED); + } + + const permissionsBlob = JSON.stringify( + Object.fromEntries(rolePermissions.map(permission => [permission, true])), + ); + + const row = [id, community, name, permissionsBlob, time]; + + let query = SQL``; + if (action === 'create_role') { + query = SQL` + INSERT INTO roles (id, thread, name, permissions, creation_time) + VALUES (${row}) + `; + } else if (action === 'edit_role') { + throw new ServerError("unimplemented: can't edit roles yet"); + } + + await dbQuery(query); +} + +export { createInitialRolesForNewThread, modifyRole }; diff --git a/keyserver/src/endpoints.js b/keyserver/src/endpoints.js index 428d4a2f1..7c267a190 100644 --- a/keyserver/src/endpoints.js +++ b/keyserver/src/endpoints.js @@ -1,320 +1,325 @@ // @flow import { baseLegalPolicies } from 'lib/facts/policies.js'; import type { Endpoint } from 'lib/types/endpoints.js'; import { updateActivityResponder, threadSetUnreadStatusResponder, } from './responders/activity-responders.js'; import { deviceTokenUpdateResponder } from './responders/device-responders.js'; import { entryFetchResponder, entryRevisionFetchResponder, entryCreationResponder, entryUpdateResponder, entryDeletionResponder, entryRestorationResponder, calendarQueryUpdateResponder, } from './responders/entry-responders.js'; import type { JSONResponder } from './responders/handlers.js'; import { getSessionPublicKeysResponder, getOlmSessionInitializationDataResponder, } from './responders/keys-responders.js'; import { createOrUpdatePublicLinkResponder, disableInviteLinkResponder, fetchPrimaryInviteLinksResponder, inviteLinkVerificationResponder, } from './responders/link-responders.js'; import { messageReportCreationResponder } from './responders/message-report-responder.js'; import { textMessageCreationResponder, messageFetchResponder, multimediaMessageCreationResponder, reactionMessageCreationResponder, editMessageCreationResponder, fetchPinnedMessagesResponder, searchMessagesResponder, } from './responders/message-responders.js'; import { updateRelationshipsResponder } from './responders/relationship-responders.js'; import { reportCreationResponder, reportMultiCreationResponder, errorReportFetchInfosResponder, } from './responders/report-responders.js'; import { userSearchResponder, exactUserSearchResponder, } from './responders/search-responders.js'; import { siweNonceResponder } from './responders/siwe-nonce-responders.js'; import { threadDeletionResponder, roleUpdateResponder, memberRemovalResponder, threadLeaveResponder, threadUpdateResponder, threadCreationResponder, threadFetchMediaResponder, threadJoinResponder, toggleMessagePinResponder, + roleModificationResponder, } from './responders/thread-responders.js'; import { userSubscriptionUpdateResponder, passwordUpdateResponder, sendVerificationEmailResponder, sendPasswordResetEmailResponder, logOutResponder, accountDeletionResponder, accountCreationResponder, logInResponder, siweAuthResponder, oldPasswordUpdateResponder, updateUserSettingsResponder, policyAcknowledgmentResponder, updateUserAvatarResponder, } from './responders/user-responders.js'; import { codeVerificationResponder } from './responders/verification-responders.js'; import { versionResponder } from './responders/version-responders.js'; import { uploadMediaMetadataResponder, uploadDeletionResponder, } from './uploads/uploads.js'; const jsonEndpoints: { [id: Endpoint]: JSONResponder } = { create_account: { responder: accountCreationResponder, requiredPolicies: [], }, create_entry: { responder: entryCreationResponder, requiredPolicies: baseLegalPolicies, }, create_error_report: { responder: reportCreationResponder, requiredPolicies: [], }, create_message_report: { responder: messageReportCreationResponder, requiredPolicies: baseLegalPolicies, }, create_multimedia_message: { responder: multimediaMessageCreationResponder, requiredPolicies: baseLegalPolicies, }, create_or_update_public_link: { responder: createOrUpdatePublicLinkResponder, requiredPolicies: baseLegalPolicies, }, create_reaction_message: { responder: reactionMessageCreationResponder, requiredPolicies: baseLegalPolicies, }, disable_invite_link: { responder: disableInviteLinkResponder, requiredPolicies: baseLegalPolicies, }, edit_message: { responder: editMessageCreationResponder, requiredPolicies: baseLegalPolicies, }, create_report: { responder: reportCreationResponder, requiredPolicies: [], }, create_reports: { responder: reportMultiCreationResponder, requiredPolicies: [], }, create_text_message: { responder: textMessageCreationResponder, requiredPolicies: baseLegalPolicies, }, create_thread: { responder: threadCreationResponder, requiredPolicies: baseLegalPolicies, }, delete_account: { responder: accountDeletionResponder, requiredPolicies: [], }, delete_entry: { responder: entryDeletionResponder, requiredPolicies: baseLegalPolicies, }, delete_thread: { responder: threadDeletionResponder, requiredPolicies: baseLegalPolicies, }, delete_upload: { responder: uploadDeletionResponder, requiredPolicies: baseLegalPolicies, }, exact_search_user: { responder: exactUserSearchResponder, requiredPolicies: [], }, fetch_entries: { responder: entryFetchResponder, requiredPolicies: baseLegalPolicies, }, fetch_entry_revisions: { responder: entryRevisionFetchResponder, requiredPolicies: baseLegalPolicies, }, fetch_error_report_infos: { responder: errorReportFetchInfosResponder, requiredPolicies: baseLegalPolicies, }, fetch_messages: { responder: messageFetchResponder, requiredPolicies: baseLegalPolicies, }, fetch_pinned_messages: { responder: fetchPinnedMessagesResponder, requiredPolicies: baseLegalPolicies, }, fetch_primary_invite_links: { responder: fetchPrimaryInviteLinksResponder, requiredPolicies: baseLegalPolicies, }, fetch_thread_media: { responder: threadFetchMediaResponder, requiredPolicies: baseLegalPolicies, }, get_session_public_keys: { responder: getSessionPublicKeysResponder, requiredPolicies: baseLegalPolicies, }, join_thread: { responder: threadJoinResponder, requiredPolicies: baseLegalPolicies, }, leave_thread: { responder: threadLeaveResponder, requiredPolicies: baseLegalPolicies, }, log_in: { responder: logInResponder, requiredPolicies: [], }, log_out: { responder: logOutResponder, requiredPolicies: [], }, + modify_community_role: { + responder: roleModificationResponder, + requiredPolicies: baseLegalPolicies, + }, policy_acknowledgment: { responder: policyAcknowledgmentResponder, requiredPolicies: [], }, remove_members: { responder: memberRemovalResponder, requiredPolicies: baseLegalPolicies, }, restore_entry: { responder: entryRestorationResponder, requiredPolicies: baseLegalPolicies, }, search_messages: { responder: searchMessagesResponder, requiredPolicies: baseLegalPolicies, }, search_users: { responder: userSearchResponder, requiredPolicies: baseLegalPolicies, }, send_password_reset_email: { responder: sendPasswordResetEmailResponder, requiredPolicies: [], }, send_verification_email: { responder: sendVerificationEmailResponder, requiredPolicies: [], }, set_thread_unread_status: { responder: threadSetUnreadStatusResponder, requiredPolicies: baseLegalPolicies, }, toggle_message_pin: { responder: toggleMessagePinResponder, requiredPolicies: baseLegalPolicies, }, update_account: { responder: passwordUpdateResponder, requiredPolicies: baseLegalPolicies, }, update_activity: { responder: updateActivityResponder, requiredPolicies: baseLegalPolicies, }, update_calendar_query: { responder: calendarQueryUpdateResponder, requiredPolicies: baseLegalPolicies, }, update_user_settings: { responder: updateUserSettingsResponder, requiredPolicies: baseLegalPolicies, }, update_device_token: { responder: deviceTokenUpdateResponder, requiredPolicies: [], }, update_entry: { responder: entryUpdateResponder, requiredPolicies: baseLegalPolicies, }, update_password: { responder: oldPasswordUpdateResponder, requiredPolicies: baseLegalPolicies, }, update_relationships: { responder: updateRelationshipsResponder, requiredPolicies: baseLegalPolicies, }, update_role: { responder: roleUpdateResponder, requiredPolicies: baseLegalPolicies, }, update_thread: { responder: threadUpdateResponder, requiredPolicies: baseLegalPolicies, }, update_user_subscription: { responder: userSubscriptionUpdateResponder, requiredPolicies: baseLegalPolicies, }, verify_code: { responder: codeVerificationResponder, requiredPolicies: baseLegalPolicies, }, verify_invite_link: { responder: inviteLinkVerificationResponder, requiredPolicies: baseLegalPolicies, }, siwe_nonce: { responder: siweNonceResponder, requiredPolicies: [], }, siwe_auth: { responder: siweAuthResponder, requiredPolicies: [], }, update_user_avatar: { responder: updateUserAvatarResponder, requiredPolicies: baseLegalPolicies, }, upload_media_metadata: { responder: uploadMediaMetadataResponder, requiredPolicies: baseLegalPolicies, }, get_olm_session_initialization_data: { responder: getOlmSessionInitializationDataResponder, requiredPolicies: [], }, version: { responder: versionResponder, requiredPolicies: [], }, }; export { jsonEndpoints }; diff --git a/keyserver/src/responders/thread-responders.js b/keyserver/src/responders/thread-responders.js index 7e8664219..feec17317 100644 --- a/keyserver/src/responders/thread-responders.js +++ b/keyserver/src/responders/thread-responders.js @@ -1,360 +1,383 @@ // @flow import t from 'tcomb'; import type { TInterface, TUnion } from 'tcomb'; import { mediaValidator } from 'lib/types/media-types.js'; import { rawMessageInfoValidator, messageTruncationStatusesValidator, } from 'lib/types/message-types.js'; +import { userSurfacedPermissionValidator } from 'lib/types/thread-permission-types.js'; import { threadTypes } from 'lib/types/thread-types-enum.js'; import { type ThreadDeletionRequest, type RoleChangeRequest, type ChangeThreadSettingsResult, type RemoveMembersRequest, type LeaveThreadRequest, type LeaveThreadResult, type UpdateThreadRequest, type ServerNewThreadRequest, type NewThreadResponse, type ServerThreadJoinRequest, type ThreadJoinResult, type ThreadFetchMediaResult, type ThreadFetchMediaRequest, type ToggleMessagePinRequest, type ToggleMessagePinResult, + type RoleModificationRequest, } from 'lib/types/thread-types.js'; import { serverUpdateInfoValidator } from 'lib/types/update-types.js'; import { userInfosValidator } from 'lib/types/user-types.js'; import { updateUserAvatarRequestValidator } from 'lib/utils/avatar-utils.js'; import { values } from 'lib/utils/objects.js'; import { tShape, tNumEnum, tColor, tPassword, tID, } from 'lib/utils/validation-utils.js'; import { entryQueryInputValidator, verifyCalendarQueryThreadIDs, } from './entry-responders.js'; +import { modifyRole } from '../creators/role-creator.js'; import { createThread } from '../creators/thread-creator.js'; import { deleteThread } from '../deleters/thread-deleters.js'; import { fetchMediaForThread } from '../fetchers/upload-fetchers.js'; import type { Viewer } from '../session/viewer.js'; import { updateRole, removeMembers, leaveThread, updateThread, joinThread, toggleMessagePinForThread, } from '../updaters/thread-updaters.js'; import { validateInput, validateOutput } from '../utils/validation-utils.js'; const threadDeletionRequestInputValidator = tShape({ threadID: tID, accountPassword: t.maybe(tPassword), }); export const leaveThreadResultValidator: TInterface = tShape({ updatesResult: tShape({ newUpdates: t.list(serverUpdateInfoValidator), }), }); async function threadDeletionResponder( viewer: Viewer, input: mixed, ): Promise { const request = await validateInput( viewer, threadDeletionRequestInputValidator, input, ); const result = await deleteThread(viewer, request); return validateOutput( viewer.platformDetails, leaveThreadResultValidator, result, ); } export const roleChangeRequestInputValidator: TInterface = tShape({ threadID: tID, memberIDs: t.list(t.String), role: t.refinement(tID, str => { if (str.indexOf('|') !== -1) { str = str.split('|')[1]; } const int = parseInt(str, 10); return String(int) === str && int > 0; }), }); export const changeThreadSettingsResultValidator: TInterface = tShape({ newMessageInfos: t.list(rawMessageInfoValidator), updatesResult: tShape({ newUpdates: t.list(serverUpdateInfoValidator), }), }); async function roleUpdateResponder( viewer: Viewer, input: mixed, ): Promise { const request = await validateInput( viewer, roleChangeRequestInputValidator, input, ); const result = await updateRole(viewer, request); return validateOutput( viewer.platformDetails, changeThreadSettingsResultValidator, result, ); } const removeMembersRequestInputValidator = tShape({ threadID: tID, memberIDs: t.list(t.String), }); async function memberRemovalResponder( viewer: Viewer, input: mixed, ): Promise { const request = await validateInput( viewer, removeMembersRequestInputValidator, input, ); const result = await removeMembers(viewer, request); return validateOutput( viewer.platformDetails, changeThreadSettingsResultValidator, result, ); } const leaveThreadRequestInputValidator = tShape({ threadID: tID, }); async function threadLeaveResponder( viewer: Viewer, input: mixed, ): Promise { const request = await validateInput( viewer, leaveThreadRequestInputValidator, input, ); const result = await leaveThread(viewer, request); return validateOutput( viewer.platformDetails, leaveThreadResultValidator, result, ); } const updateThreadRequestInputValidator = tShape({ threadID: tID, changes: tShape({ type: t.maybe(tNumEnum(values(threadTypes))), name: t.maybe(t.String), description: t.maybe(t.String), color: t.maybe(tColor), parentThreadID: t.maybe(tID), newMemberIDs: t.maybe(t.list(t.String)), avatar: t.maybe(updateUserAvatarRequestValidator), }), accountPassword: t.maybe(tPassword), }); async function threadUpdateResponder( viewer: Viewer, input: mixed, ): Promise { const request = await validateInput( viewer, updateThreadRequestInputValidator, input, ); const result = await updateThread(viewer, request); return validateOutput( viewer.platformDetails, changeThreadSettingsResultValidator, result, ); } const threadRequestValidationShape = { name: t.maybe(t.String), description: t.maybe(t.String), color: t.maybe(tColor), parentThreadID: t.maybe(tID), initialMemberIDs: t.maybe(t.list(t.String)), calendarQuery: t.maybe(entryQueryInputValidator), }; const newThreadRequestInputValidator: TUnion = t.union([ tShape({ type: tNumEnum([threadTypes.SIDEBAR]), sourceMessageID: tID, ...threadRequestValidationShape, }), tShape({ type: tNumEnum([ threadTypes.COMMUNITY_OPEN_SUBTHREAD, threadTypes.COMMUNITY_SECRET_SUBTHREAD, threadTypes.PERSONAL, threadTypes.LOCAL, threadTypes.COMMUNITY_ROOT, threadTypes.COMMUNITY_ANNOUNCEMENT_ROOT, ]), ...threadRequestValidationShape, }), ]); export const newThreadResponseValidator: TInterface = tShape({ updatesResult: tShape({ newUpdates: t.list(serverUpdateInfoValidator), }), newMessageInfos: t.list(rawMessageInfoValidator), userInfos: userInfosValidator, newThreadID: tID, }); async function threadCreationResponder( viewer: Viewer, input: mixed, ): Promise { const request = await validateInput( viewer, newThreadRequestInputValidator, input, ); const result = await createThread(viewer, request, { silentlyFailMembers: request.type === threadTypes.SIDEBAR, }); return validateOutput( viewer.platformDetails, newThreadResponseValidator, result, ); } const joinThreadRequestInputValidator = tShape({ threadID: tID, calendarQuery: t.maybe(entryQueryInputValidator), inviteLinkSecret: t.maybe(t.String), }); export const threadJoinResultValidator: TInterface = tShape({ updatesResult: tShape({ newUpdates: t.list(serverUpdateInfoValidator), }), rawMessageInfos: t.list(rawMessageInfoValidator), truncationStatuses: messageTruncationStatusesValidator, userInfos: userInfosValidator, }); async function threadJoinResponder( viewer: Viewer, input: mixed, ): Promise { const request = await validateInput( viewer, joinThreadRequestInputValidator, input, ); if (request.calendarQuery) { await verifyCalendarQueryThreadIDs(request.calendarQuery); } const result = await joinThread(viewer, request); return validateOutput( viewer.platformDetails, threadJoinResultValidator, result, ); } const threadFetchMediaRequestInputValidator = tShape({ threadID: tID, limit: t.Number, offset: t.Number, }); export const threadFetchMediaResultValidator: TInterface = tShape({ media: t.list(mediaValidator) }); async function threadFetchMediaResponder( viewer: Viewer, input: mixed, ): Promise { const request = await validateInput( viewer, threadFetchMediaRequestInputValidator, input, ); const result = await fetchMediaForThread(viewer, request); return validateOutput( viewer.platformDetails, threadFetchMediaResultValidator, result, ); } const toggleMessagePinRequestInputValidator = tShape({ messageID: tID, action: t.enums.of(['pin', 'unpin']), }); export const toggleMessagePinResultValidator: TInterface = tShape({ newMessageInfos: t.list(rawMessageInfoValidator), threadID: tID, }); async function toggleMessagePinResponder( viewer: Viewer, input: mixed, ): Promise { const request = await validateInput( viewer, toggleMessagePinRequestInputValidator, input, ); const result = await toggleMessagePinForThread(viewer, request); return validateOutput( viewer.platformDetails, toggleMessagePinResultValidator, result, ); } +const roleModificationRequestInputValidator = tShape({ + community: tID, + name: t.String, + permissions: t.list(userSurfacedPermissionValidator), + action: t.enums.of(['create_role', 'edit_role']), +}); + +async function roleModificationResponder( + viewer: Viewer, + input: mixed, +): Promise { + const request = await validateInput( + viewer, + roleModificationRequestInputValidator, + input, + ); + await modifyRole(viewer, request); +} + export { threadDeletionResponder, roleUpdateResponder, memberRemovalResponder, threadLeaveResponder, threadUpdateResponder, threadCreationResponder, threadJoinResponder, threadFetchMediaResponder, newThreadRequestInputValidator, toggleMessagePinResponder, + roleModificationResponder, }; diff --git a/lib/actions/thread-actions.js b/lib/actions/thread-actions.js index bc74d740b..24ef058cd 100644 --- a/lib/actions/thread-actions.js +++ b/lib/actions/thread-actions.js @@ -1,214 +1,224 @@ // @flow import invariant from 'invariant'; import type { ChangeThreadSettingsPayload, LeaveThreadPayload, UpdateThreadRequest, ClientNewThreadRequest, NewThreadResult, ClientThreadJoinRequest, ThreadJoinPayload, ThreadFetchMediaRequest, ThreadFetchMediaResult, ToggleMessagePinRequest, ToggleMessagePinResult, + RoleModificationRequest, } from '../types/thread-types.js'; import type { CallServerEndpoint } from '../utils/call-server-endpoint.js'; import { values } from '../utils/objects.js'; const deleteThreadActionTypes = Object.freeze({ started: 'DELETE_THREAD_STARTED', success: 'DELETE_THREAD_SUCCESS', failed: 'DELETE_THREAD_FAILED', }); const deleteThread = ( callServerEndpoint: CallServerEndpoint, ): (( threadID: string, currentAccountPassword: ?string, ) => Promise) => async (threadID, currentAccountPassword) => { const response = await callServerEndpoint('delete_thread', { threadID, accountPassword: currentAccountPassword, }); return { updatesResult: response.updatesResult, }; }; const changeThreadSettingsActionTypes = Object.freeze({ started: 'CHANGE_THREAD_SETTINGS_STARTED', success: 'CHANGE_THREAD_SETTINGS_SUCCESS', failed: 'CHANGE_THREAD_SETTINGS_FAILED', }); const changeThreadSettings = ( callServerEndpoint: CallServerEndpoint, ): ((request: UpdateThreadRequest) => Promise) => async request => { invariant( Object.keys(request.changes).length > 0, 'No changes provided to changeThreadSettings!', ); const response = await callServerEndpoint('update_thread', request); return { threadID: request.threadID, updatesResult: response.updatesResult, newMessageInfos: response.newMessageInfos, }; }; const removeUsersFromThreadActionTypes = Object.freeze({ started: 'REMOVE_USERS_FROM_THREAD_STARTED', success: 'REMOVE_USERS_FROM_THREAD_SUCCESS', failed: 'REMOVE_USERS_FROM_THREAD_FAILED', }); const removeUsersFromThread = ( callServerEndpoint: CallServerEndpoint, ): (( threadID: string, memberIDs: $ReadOnlyArray, ) => Promise) => async (threadID, memberIDs) => { const response = await callServerEndpoint('remove_members', { threadID, memberIDs, }); return { threadID, updatesResult: response.updatesResult, newMessageInfos: response.newMessageInfos, }; }; const changeThreadMemberRolesActionTypes = Object.freeze({ started: 'CHANGE_THREAD_MEMBER_ROLES_STARTED', success: 'CHANGE_THREAD_MEMBER_ROLES_SUCCESS', failed: 'CHANGE_THREAD_MEMBER_ROLES_FAILED', }); const changeThreadMemberRoles = ( callServerEndpoint: CallServerEndpoint, ): (( threadID: string, memberIDs: $ReadOnlyArray, newRole: string, ) => Promise) => async (threadID, memberIDs, newRole) => { const response = await callServerEndpoint('update_role', { threadID, memberIDs, role: newRole, }); return { threadID, updatesResult: response.updatesResult, newMessageInfos: response.newMessageInfos, }; }; const newThreadActionTypes = Object.freeze({ started: 'NEW_THREAD_STARTED', success: 'NEW_THREAD_SUCCESS', failed: 'NEW_THREAD_FAILED', }); const newThread = ( callServerEndpoint: CallServerEndpoint, ): ((request: ClientNewThreadRequest) => Promise) => async request => { const response = await callServerEndpoint('create_thread', request); return { newThreadID: response.newThreadID, updatesResult: response.updatesResult, newMessageInfos: response.newMessageInfos, userInfos: response.userInfos, }; }; const joinThreadActionTypes = Object.freeze({ started: 'JOIN_THREAD_STARTED', success: 'JOIN_THREAD_SUCCESS', failed: 'JOIN_THREAD_FAILED', }); const joinThread = ( callServerEndpoint: CallServerEndpoint, ): ((request: ClientThreadJoinRequest) => Promise) => async request => { const response = await callServerEndpoint('join_thread', request); const userInfos = values(response.userInfos); return { updatesResult: response.updatesResult, rawMessageInfos: response.rawMessageInfos, truncationStatuses: response.truncationStatuses, userInfos, }; }; const leaveThreadActionTypes = Object.freeze({ started: 'LEAVE_THREAD_STARTED', success: 'LEAVE_THREAD_SUCCESS', failed: 'LEAVE_THREAD_FAILED', }); const leaveThread = ( callServerEndpoint: CallServerEndpoint, ): ((threadID: string) => Promise) => async threadID => { const response = await callServerEndpoint('leave_thread', { threadID }); return { updatesResult: response.updatesResult, }; }; const fetchThreadMedia = ( callServerEndpoint: CallServerEndpoint, ): ((request: ThreadFetchMediaRequest) => Promise) => async request => { const response = await callServerEndpoint('fetch_thread_media', request); return response; }; const toggleMessagePinActionTypes = Object.freeze({ started: 'TOGGLE_MESSAGE_PIN_STARTED', success: 'TOGGLE_MESSAGE_PIN_SUCCESS', failed: 'TOGGLE_MESSAGE_PIN_FAILED', }); const toggleMessagePin = ( callServerEndpoint: CallServerEndpoint, ): ((request: ToggleMessagePinRequest) => Promise) => async request => { const response = await callServerEndpoint('toggle_message_pin', request); return { newMessageInfos: response.newMessageInfos, threadID: response.threadID, }; }; +const modifyCommunityRole = + ( + callServerEndpoint: CallServerEndpoint, + ): ((request: RoleModificationRequest) => Promise) => + async request => { + await callServerEndpoint('modify_community_role', request); + }; + export { deleteThreadActionTypes, deleteThread, changeThreadSettingsActionTypes, changeThreadSettings, removeUsersFromThreadActionTypes, removeUsersFromThread, changeThreadMemberRolesActionTypes, changeThreadMemberRoles, newThreadActionTypes, newThread, joinThreadActionTypes, joinThread, leaveThreadActionTypes, leaveThread, fetchThreadMedia, toggleMessagePinActionTypes, toggleMessagePin, + modifyCommunityRole, }; diff --git a/lib/types/endpoints.js b/lib/types/endpoints.js index b9d9f7d93..9d6d990ee 100644 --- a/lib/types/endpoints.js +++ b/lib/types/endpoints.js @@ -1,128 +1,129 @@ // @flow export type APIRequest = { endpoint: Endpoint, input?: Object, }; export type SocketAPIHandler = (request: APIRequest) => Promise; export type Endpoint = | HTTPOnlyEndpoint | SocketOnlyEndpoint | HTTPPreferredEndpoint | SocketPreferredEndpoint; // Endpoints that can cause session changes should occur over HTTP, since the // socket code does not currently support changing sessions. In the future they // could be made to work for native, but cookie changes on web require HTTP // since websockets aren't able to Set-Cookie. Note that technically any // endpoint can cause a sessionChange, and in that case the server will close // the socket with a specific error code, and the client will proceed via HTTP. const sessionChangingEndpoints = Object.freeze({ LOG_OUT: 'log_out', DELETE_ACCOUNT: 'delete_account', CREATE_ACCOUNT: 'create_account', LOG_IN: 'log_in', UPDATE_PASSWORD: 'update_password', POLICY_ACKNOWLEDGMENT: 'policy_acknowledgment', }); type SessionChangingEndpoint = $Values; // We do uploads over HTTP as well. This is because Websockets use TCP, which // guarantees ordering. That means that if we start an upload, any messages we // try to send the server after the upload starts will have to wait until the // upload ends. To avoid blocking other messages we upload using HTTP // multipart/form-data. const uploadEndpoints = Object.freeze({ UPLOAD_MULTIMEDIA: 'upload_multimedia', }); type UploadEndpoint = $Values; type HTTPOnlyEndpoint = SessionChangingEndpoint | UploadEndpoint; const socketOnlyEndpoints = Object.freeze({ UPDATE_ACTIVITY: 'update_activity', UPDATE_CALENDAR_QUERY: 'update_calendar_query', }); type SocketOnlyEndpoint = $Values; const socketPreferredEndpoints = Object.freeze({ CREATE_ENTRY: 'create_entry', CREATE_ERROR_REPORT: 'create_error_report', CREATE_MESSAGE_REPORT: 'create_message_report', CREATE_MULTIMEDIA_MESSAGE: 'create_multimedia_message', CREATE_OR_UPDATE_PUBLIC_LINK: 'create_or_update_public_link', CREATE_REACTION_MESSAGE: 'create_reaction_message', EDIT_MESSAGE: 'edit_message', CREATE_TEXT_MESSAGE: 'create_text_message', CREATE_THREAD: 'create_thread', DELETE_ENTRY: 'delete_entry', DELETE_THREAD: 'delete_thread', DELETE_UPLOAD: 'delete_upload', DISABLE_INVITE_LINK: 'disable_invite_link', EXACT_SEARCH_USER: 'exact_search_user', FETCH_ENTRIES: 'fetch_entries', FETCH_ENTRY_REVISIONS: 'fetch_entry_revisions', FETCH_ERROR_REPORT_INFOS: 'fetch_error_report_infos', FETCH_MESSAGES: 'fetch_messages', FETCH_PINNED_MESSAGES: 'fetch_pinned_messages', FETCH_PRIMARY_INVITE_LINKS: 'fetch_primary_invite_links', FETCH_THREAD_MEDIA: 'fetch_thread_media', GET_SESSION_PUBLIC_KEYS: 'get_session_public_keys', JOIN_THREAD: 'join_thread', LEAVE_THREAD: 'leave_thread', + MODIFY_COMMUNITY_ROLE: 'modify_community_role', REMOVE_MEMBERS: 'remove_members', REQUEST_ACCESS: 'request_access', RESTORE_ENTRY: 'restore_entry', SEARCH_USERS: 'search_users', SEND_PASSWORD_RESET_EMAIL: 'send_password_reset_email', SEND_VERIFICATION_EMAIL: 'send_verification_email', SET_THREAD_UNREAD_STATUS: 'set_thread_unread_status', TOGGLE_MESSAGE_PIN: 'toggle_message_pin', UPDATE_ACCOUNT: 'update_account', UPDATE_USER_SETTINGS: 'update_user_settings', UPDATE_DEVICE_TOKEN: 'update_device_token', UPDATE_ENTRY: 'update_entry', UPDATE_RELATIONSHIPS: 'update_relationships', UPDATE_ROLE: 'update_role', UPDATE_THREAD: 'update_thread', UPDATE_USER_SUBSCRIPTION: 'update_user_subscription', VERIFY_CODE: 'verify_code', VERIFY_INVITE_LINK: 'verify_invite_link', SIWE_NONCE: 'siwe_nonce', SIWE_AUTH: 'siwe_auth', UPDATE_USER_AVATAR: 'update_user_avatar', UPLOAD_MEDIA_METADATA: 'upload_media_metadata', SEARCH_MESSAGES: 'search_messages', GET_OLM_SESSION_INITIALIZATION_DATA: 'get_olm_session_initialization_data', VERSION: 'version', }); type SocketPreferredEndpoint = $Values; const httpPreferredEndpoints = Object.freeze({ CREATE_REPORT: 'create_report', CREATE_REPORTS: 'create_reports', }); type HTTPPreferredEndpoint = $Values; const socketPreferredEndpointSet = new Set([ ...Object.values(socketOnlyEndpoints), ...Object.values(socketPreferredEndpoints), ]); export function endpointIsSocketPreferred(endpoint: Endpoint): boolean { return socketPreferredEndpointSet.has(endpoint); } const socketSafeEndpointSet = new Set([ ...Object.values(socketOnlyEndpoints), ...Object.values(socketPreferredEndpoints), ...Object.values(httpPreferredEndpoints), ]); export function endpointIsSocketSafe(endpoint: Endpoint): boolean { return socketSafeEndpointSet.has(endpoint); } const socketOnlyEndpointSet = new Set(Object.values(socketOnlyEndpoints)); export function endpointIsSocketOnly(endpoint: Endpoint): boolean { return socketOnlyEndpointSet.has(endpoint); } diff --git a/lib/types/thread-permission-types.js b/lib/types/thread-permission-types.js index 3212c48d9..6c2bd764d 100644 --- a/lib/types/thread-permission-types.js +++ b/lib/types/thread-permission-types.js @@ -1,393 +1,429 @@ // @flow import invariant from 'invariant'; -import t, { type TDict, type TUnion } from 'tcomb'; +import t, { type TDict, type TUnion, type TEnums } from 'tcomb'; import { values } from '../utils/objects.js'; import { tBool, tShape, tID } from '../utils/validation-utils.js'; // When a new permission is added, if it should be configurable for a role, it // should be either added to an existing set or a new set alongside a // new user-facing permission. If it is a permission that should be ensured // across all roles, it should be added to `universalCommunityPermissions`. export const threadPermissions = Object.freeze({ KNOW_OF: 'know_of', MEMBERSHIP_DEPRECATED: 'membership', VISIBLE: 'visible', VOICED: 'voiced', EDIT_ENTRIES: 'edit_entries', EDIT_THREAD_NAME: 'edit_thread', EDIT_THREAD_DESCRIPTION: 'edit_thread_description', EDIT_THREAD_COLOR: 'edit_thread_color', DELETE_THREAD: 'delete_thread', CREATE_SUBCHANNELS: 'create_subthreads', CREATE_SIDEBARS: 'create_sidebars', JOIN_THREAD: 'join_thread', EDIT_PERMISSIONS: 'edit_permissions', ADD_MEMBERS: 'add_members', REMOVE_MEMBERS: 'remove_members', CHANGE_ROLE: 'change_role', LEAVE_THREAD: 'leave_thread', REACT_TO_MESSAGE: 'react_to_message', EDIT_MESSAGE: 'edit_message', EDIT_THREAD_AVATAR: 'edit_thread_avatar', MANAGE_PINS: 'manage_pins', MANAGE_INVITE_LINKS: 'manage_invite_links', }); export type ThreadPermission = $Values; export function assertThreadPermissions( ourThreadPermissions: string, ): ThreadPermission { invariant( ourThreadPermissions === 'know_of' || ourThreadPermissions === 'membership' || ourThreadPermissions === 'visible' || ourThreadPermissions === 'voiced' || ourThreadPermissions === 'edit_entries' || ourThreadPermissions === 'edit_thread' || ourThreadPermissions === 'edit_thread_description' || ourThreadPermissions === 'edit_thread_color' || ourThreadPermissions === 'delete_thread' || ourThreadPermissions === 'create_subthreads' || ourThreadPermissions === 'create_sidebars' || ourThreadPermissions === 'join_thread' || ourThreadPermissions === 'edit_permissions' || ourThreadPermissions === 'add_members' || ourThreadPermissions === 'remove_members' || ourThreadPermissions === 'change_role' || ourThreadPermissions === 'leave_thread' || ourThreadPermissions === 'react_to_message' || ourThreadPermissions === 'edit_message' || ourThreadPermissions === 'edit_thread_avatar' || ourThreadPermissions === 'manage_pins' || ourThreadPermissions === 'manage_invite_links', 'string is not threadPermissions enum', ); return ourThreadPermissions; } const threadPermissionValidator = t.enums.of(values(threadPermissions)); export const threadPermissionPropagationPrefixes = Object.freeze({ DESCENDANT: 'descendant_', CHILD: 'child_', }); export type ThreadPermissionPropagationPrefix = $Values< typeof threadPermissionPropagationPrefixes, >; export const threadPermissionFilterPrefixes = Object.freeze({ // includes only SIDEBAR, COMMUNITY_OPEN_SUBTHREAD, // COMMUNITY_OPEN_ANNOUNCEMENT_SUBTHREAD OPEN: 'open_', // excludes only SIDEBAR TOP_LEVEL: 'toplevel_', // includes only COMMUNITY_OPEN_SUBTHREAD, // COMMUNITY_OPEN_ANNOUNCEMENT_SUBTHREAD OPEN_TOP_LEVEL: 'opentoplevel_', }); export type ThreadPermissionFilterPrefix = $Values< typeof threadPermissionFilterPrefixes, >; // These are the set of user-facing permissions that we display as configurable // to the user when they are creating a custom role for their given community. // They are per-community rather than per-thread, so when configured they are // to be expected to be propagated across the community. Also notably, // `threadPermissions` is used on the keyserver for permission checks to // validate actions, but these `userSurfacedPermissions` are only used // on the client for the UI and propagated to the server. The // `configurableCommunityPermissions` mapping below is the association between // each userSurfacedPermission and a set of threadPermissions. export const userSurfacedPermissions = Object.freeze({ EDIT_CALENDAR: 'edit_calendar', KNOW_OF_SECRET_CHANNELS: 'know_of_secret_channels', VOICED_IN_ANNOUNCEMENT_CHANNELS: 'voiced_in_announcement_channels', CREATE_AND_EDIT_CHANNELS: 'create_and_edit_channels', DELETE_CHANNELS: 'delete_channels', ADD_MEMBERS: 'add_members', REMOVE_MEMBERS: 'remove_members', CHANGE_ROLES: 'change_roles', EDIT_VISIBILITY: 'edit_visibility', MANAGE_PINS: 'manage_pins', REACT_TO_MESSAGES: 'react_to_messages', EDIT_MESSAGES: 'edit_messages', MANAGE_INVITE_LINKS: 'manage_invite_links', }); export type UserSurfacedPermission = $Values; +export const userSurfacedPermissionsSet: $ReadOnlySet = + new Set(values(userSurfacedPermissions)); +export const userSurfacedPermissionValidator: TEnums = t.enums.of( + values(userSurfacedPermissions), +); const editCalendarPermission = { title: 'Edit calendar', description: 'Allows members to edit the community calendar', userSurfacedPermission: userSurfacedPermissions.EDIT_CALENDAR, }; const editEntries = threadPermissions.EDIT_ENTRIES; const descendantEditEntries = threadPermissionPropagationPrefixes.DESCENDANT + threadPermissions.EDIT_ENTRIES; const editCalendarPermissions = new Set([editEntries, descendantEditEntries]); const knowOfSecretChannelsPermission = { title: 'Know of secret channels', description: 'Allows members to know of all secret channels', userSurfacedPermission: userSurfacedPermissions.KNOW_OF_SECRET_CHANNELS, }; const descendantKnowOf = threadPermissionPropagationPrefixes.DESCENDANT + threadPermissions.KNOW_OF; const descendantVisible = threadPermissionPropagationPrefixes.DESCENDANT + threadPermissions.VISIBLE; const descendantTopLevelJoinThread = threadPermissionPropagationPrefixes.DESCENDANT + threadPermissionFilterPrefixes.TOP_LEVEL + threadPermissions.JOIN_THREAD; const descendantJoinThread = threadPermissionPropagationPrefixes.DESCENDANT + threadPermissions.JOIN_THREAD; const childJoinThread = threadPermissionPropagationPrefixes.CHILD + threadPermissions.JOIN_THREAD; const knowOfSecretChannelsPermissions = new Set([ descendantKnowOf, descendantVisible, descendantTopLevelJoinThread, descendantJoinThread, childJoinThread, ]); const voicedPermission = { title: 'Voiced in announcement channels', description: 'Allows members to send messages in announcement channels', userSurfacedPermission: userSurfacedPermissions.VOICED_IN_ANNOUNCEMENT_CHANNELS, }; const voiced = threadPermissions.VOICED; const voicedPermissions = new Set([voiced]); const createAndEditChannelsPermission = { title: 'Create and edit channels', description: 'Allows members to create new and edit existing channels', userSurfacedPermission: userSurfacedPermissions.CREATE_AND_EDIT_CHANNELS, }; const editThreadName = threadPermissions.EDIT_THREAD_NAME; const descendantEditThreadName = threadPermissionPropagationPrefixes.DESCENDANT + threadPermissions.EDIT_THREAD_NAME; const editThreadDescription = threadPermissions.EDIT_THREAD_DESCRIPTION; const descendantEditThreadDescription = threadPermissionPropagationPrefixes.DESCENDANT + threadPermissions.EDIT_THREAD_DESCRIPTION; const editThreadColor = threadPermissions.EDIT_THREAD_COLOR; const descendantEditThreadColor = threadPermissionPropagationPrefixes.DESCENDANT + threadPermissions.EDIT_THREAD_COLOR; const createSubchannels = threadPermissions.CREATE_SUBCHANNELS; const descendantCreateSubchannels = threadPermissionPropagationPrefixes.DESCENDANT + threadPermissionFilterPrefixes.TOP_LEVEL + threadPermissions.CREATE_SUBCHANNELS; const editThreadAvatar = threadPermissions.EDIT_THREAD_AVATAR; const descendantEditThreadAvatar = threadPermissionPropagationPrefixes.DESCENDANT + threadPermissions.EDIT_THREAD_AVATAR; const descendantTopLevelCreateSidebars = threadPermissionPropagationPrefixes.DESCENDANT + threadPermissionFilterPrefixes.TOP_LEVEL + threadPermissions.CREATE_SIDEBARS; const createAndEditChannelsPermissions = new Set([ editThreadName, descendantEditThreadName, editThreadDescription, descendantEditThreadDescription, editThreadColor, descendantEditThreadColor, createSubchannels, descendantCreateSubchannels, editThreadAvatar, descendantEditThreadAvatar, descendantTopLevelCreateSidebars, ]); const deleteChannelsPermission = { title: 'Delete channels', description: 'Allows members to delete channels', userSurfacedPermission: userSurfacedPermissions.DELETE_CHANNELS, }; const deleteThread = threadPermissions.DELETE_THREAD; const descendantDeleteThread = threadPermissionPropagationPrefixes.DESCENDANT + threadPermissions.DELETE_THREAD; const deleteChannelsPermissions = new Set([ deleteThread, descendantDeleteThread, ]); const addMembersPermission = { title: 'Add members', description: 'Allows members to add other members to channels', userSurfacedPermission: userSurfacedPermissions.ADD_MEMBERS, }; const addMembers = threadPermissions.ADD_MEMBERS; const descendantAddMembers = threadPermissionPropagationPrefixes.DESCENDANT + threadPermissions.ADD_MEMBERS; const childOpenAddMembers = threadPermissionPropagationPrefixes.CHILD + threadPermissionFilterPrefixes.OPEN + threadPermissions.ADD_MEMBERS; const addMembersPermissions = new Set([ addMembers, descendantAddMembers, childOpenAddMembers, ]); const removeMembersPermission = { title: 'Remove members', description: 'Allows members to remove anybody they can demote from channels', userSurfacedPermission: userSurfacedPermissions.REMOVE_MEMBERS, }; const removeMembers = threadPermissions.REMOVE_MEMBERS; const descendantRemoveMembers = threadPermissionPropagationPrefixes.DESCENDANT + threadPermissions.REMOVE_MEMBERS; const removeMembersPermissions = new Set([ removeMembers, descendantRemoveMembers, ]); const changeRolePermission = { title: 'Change roles', description: 'Allows members to promote and demote other members', userSurfacedPermission: userSurfacedPermissions.CHANGE_ROLES, }; const changeRole = threadPermissions.CHANGE_ROLE; const descendantChangeRole = threadPermissionPropagationPrefixes.DESCENDANT + threadPermissions.CHANGE_ROLE; const changeRolePermissions = new Set([changeRole, descendantChangeRole]); const editVisibilityPermission = { title: 'Edit visibility', description: 'Allows members to edit visibility permissions of channels', userSurfacedPermission: userSurfacedPermissions.EDIT_VISIBILITY, }; const editPermissions = threadPermissions.EDIT_PERMISSIONS; const descendantEditPermissions = threadPermissionPropagationPrefixes.DESCENDANT + threadPermissions.EDIT_PERMISSIONS; const editVisibilityPermissions = new Set([ editPermissions, descendantEditPermissions, ]); const managePinsPermission = { title: 'Manage pins', description: 'Allows members to pin or unpin messages in channels', userSurfacedPermission: userSurfacedPermissions.MANAGE_PINS, }; const managePins = threadPermissions.MANAGE_PINS; const descendantManagePins = threadPermissionPropagationPrefixes.DESCENDANT + threadPermissions.MANAGE_PINS; const managePinsPermissions = new Set([managePins, descendantManagePins]); const reactToMessagePermission = { title: 'React to messages', description: 'Allows members to add reactions to messages', userSurfacedPermission: userSurfacedPermissions.REACT_TO_MESSAGES, }; const reactToMessage = threadPermissions.REACT_TO_MESSAGE; const descendantReactToMessage = threadPermissionPropagationPrefixes.DESCENDANT + threadPermissions.REACT_TO_MESSAGE; const reactToMessagePermissions = new Set([ reactToMessage, descendantReactToMessage, ]); const editMessagePermission = { title: 'Edit messages', description: 'Allows members to edit their sent messages', userSurfacedPermission: userSurfacedPermissions.EDIT_MESSAGES, }; const editMessage = threadPermissions.EDIT_MESSAGE; const descendantEditMessage = threadPermissionPropagationPrefixes.DESCENDANT + threadPermissions.EDIT_MESSAGE; const editMessagePermissions = new Set([editMessage, descendantEditMessage]); const manageInviteLinksPermission = { title: 'Manage invite links', description: 'Allows members to create and delete invite links', userSurfacedPermission: userSurfacedPermissions.MANAGE_INVITE_LINKS, }; const manageInviteLinks = threadPermissions.MANAGE_INVITE_LINKS; const descendantManageInviteLinks = threadPermissionPropagationPrefixes.DESCENDANT + threadPermissions.MANAGE_INVITE_LINKS; const manageInviteLinksPermissions = new Set([ manageInviteLinks, descendantManageInviteLinks, ]); export type UserSurfacedPermissionOption = { +title: string, +description: string, +userSurfacedPermission: UserSurfacedPermission, }; export const userSurfacedPermissionOptions: $ReadOnlySet = new Set([ editCalendarPermission, knowOfSecretChannelsPermission, voicedPermission, createAndEditChannelsPermission, deleteChannelsPermission, addMembersPermission, removeMembersPermission, changeRolePermission, editVisibilityPermission, managePinsPermission, reactToMessagePermission, editMessagePermission, manageInviteLinksPermission, ]); type ConfigurableCommunityPermission = { +[permission: UserSurfacedPermission]: $ReadOnlySet, }; export const configurableCommunityPermissions: ConfigurableCommunityPermission = Object.freeze({ [userSurfacedPermissions.EDIT_CALENDAR]: editCalendarPermissions, [userSurfacedPermissions.KNOW_OF_SECRET_CHANNELS]: knowOfSecretChannelsPermissions, [userSurfacedPermissions.VOICED_IN_ANNOUNCEMENT_CHANNELS]: voicedPermissions, [userSurfacedPermissions.CREATE_AND_EDIT_CHANNELS]: createAndEditChannelsPermissions, [userSurfacedPermissions.DELETE_CHANNELS]: deleteChannelsPermissions, [userSurfacedPermissions.ADD_MEMBERS]: addMembersPermissions, [userSurfacedPermissions.REMOVE_MEMBERS]: removeMembersPermissions, [userSurfacedPermissions.CHANGE_ROLES]: changeRolePermissions, [userSurfacedPermissions.EDIT_VISIBILITY]: editVisibilityPermissions, [userSurfacedPermissions.MANAGE_PINS]: managePinsPermissions, [userSurfacedPermissions.REACT_TO_MESSAGES]: reactToMessagePermissions, [userSurfacedPermissions.EDIT_MESSAGES]: editMessagePermissions, [userSurfacedPermissions.MANAGE_INVITE_LINKS]: manageInviteLinksPermissions, }); +export const universalCommunityPermissions: $ReadOnlyArray = [ + // know_of | descendant_open_know_of + threadPermissions.KNOW_OF, + threadPermissionPropagationPrefixes.DESCENDANT + + threadPermissionFilterPrefixes.OPEN + + threadPermissions.KNOW_OF, + + // descendant_open_voiced + threadPermissionPropagationPrefixes.DESCENDANT + + threadPermissionFilterPrefixes.OPEN + + threadPermissions.VOICED, + + // visible | descendant_open_visible + threadPermissions.VISIBLE, + threadPermissionPropagationPrefixes.DESCENDANT + + threadPermissionFilterPrefixes.OPEN + + threadPermissions.VISIBLE, + + // join_thread | child_open_join_thread | descendant_opentoplevel_join_thread + threadPermissions.JOIN_THREAD, + threadPermissionPropagationPrefixes.CHILD + + threadPermissionFilterPrefixes.OPEN + + threadPermissions.JOIN_THREAD, + threadPermissionPropagationPrefixes.DESCENDANT + + threadPermissionFilterPrefixes.OPEN_TOP_LEVEL + + threadPermissions.JOIN_THREAD, + + threadPermissions.CREATE_SIDEBARS, + threadPermissions.LEAVE_THREAD, +]; + export type ThreadPermissionInfo = | { +value: true, +source: string } | { +value: false, +source: null }; export const threadPermissionInfoValidator: TUnion = t.union([ tShape({ value: tBool(true), source: tID }), tShape({ value: tBool(false), source: t.Nil }), ]); export type ThreadPermissionsBlob = { +[permission: string]: ThreadPermissionInfo, }; export type ThreadRolePermissionsBlob = { +[permission: string]: boolean }; export const threadRolePermissionsBlobValidator: TDict = t.dict(t.String, t.Boolean); export type ThreadPermissionsInfo = { +[permission: ThreadPermission]: ThreadPermissionInfo, }; export const threadPermissionsInfoValidator: TDict = t.dict(threadPermissionValidator, threadPermissionInfoValidator); diff --git a/lib/types/thread-types.js b/lib/types/thread-types.js index e48dc7375..54aae9160 100644 --- a/lib/types/thread-types.js +++ b/lib/types/thread-types.js @@ -1,407 +1,415 @@ // @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, }; +export type RoleModificationRequest = { + +community: string, + +name: string, + +permissions: $ReadOnlyArray, + +action: 'create_role' | 'edit_role', +}; + // 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 };