diff --git a/lib/permissions/thread-permissions.js b/lib/permissions/thread-permissions.js index 2f29b18e3..876066a89 100644 --- a/lib/permissions/thread-permissions.js +++ b/lib/permissions/thread-permissions.js @@ -1,528 +1,506 @@ // @flow import invariant from 'invariant'; import { parseThreadPermissionString, constructThreadPermissionString, includeThreadPermissionForThreadType, } from './prefixes.js'; import { configurableCommunityPermissions, threadPermissionFilterPrefixes, threadPermissionPropagationPrefixes, threadPermissions, userSurfacedPermissions, + threadPermissionsRequiringVoicedInAnnouncementChannels, + threadPermissionsRemovedForGenesisMembers, } from '../types/thread-permission-types.js'; import type { ThreadPermission, ThreadPermissionInfo, ThreadPermissionsBlob, ThreadPermissionsInfo, ThreadRolePermissionsBlob, UserSurfacedPermission, } from '../types/thread-permission-types.js'; import { type ThreadType, type ThinThreadType, type ThickThreadType, threadTypes, threadTypeIsAnnouncementThread, threadTypeIsThick, assertThickThreadType, assertThinThreadType, + threadTypeIsCommunityRoot, } from '../types/thread-types-enum.js'; function permissionLookup( permissions: ?ThreadPermissionsBlob | ?ThreadPermissionsInfo, permission: ThreadPermission, ): boolean { return !!( permissions && permissions[permission] && permissions[permission].value && permissions[threadPermissions.KNOW_OF] && permissions[threadPermissions.KNOW_OF].value ); } function getAllThreadPermissions( permissions: ?ThreadPermissionsBlob, threadID: string, ): ThreadPermissionsInfo { const result: { [permission: ThreadPermission]: ThreadPermissionInfo } = {}; for (const permissionName in threadPermissions) { const permissionKey = threadPermissions[permissionName]; const permission = permissionLookup(permissions, permissionKey); let entry: ThreadPermissionInfo = { value: false, source: null }; if (permission) { const blobEntry = permissions ? permissions[permissionKey] : null; if (blobEntry) { invariant( blobEntry.value, 'permissionLookup returned true but blob had false permission!', ); entry = { value: true, source: blobEntry.source }; } else { entry = { value: true, source: threadID }; } } result[permissionKey] = entry; } return result; } // - rolePermissions can be null if role <= 0, ie. not a member // - permissionsFromParent can be null if there are no permissions from the // parent // - return can be null if no permissions exist function makePermissionsBlob( rolePermissions: ?ThreadRolePermissionsBlob, permissionsFromParent: ?ThreadPermissionsBlob, threadID: string, threadType: ThreadType, ): ?ThreadPermissionsBlob { let permissions: { [permission: string]: ThreadPermissionInfo } = {}; const isMember = !!rolePermissions; if (permissionsFromParent) { for (const permissionKey in permissionsFromParent) { const permissionValue = permissionsFromParent[permissionKey]; const parsed = parseThreadPermissionString(permissionKey); if (!includeThreadPermissionForThreadType(parsed, threadType, isMember)) { continue; } if (parsed.propagationPrefix) { permissions[permissionKey] = permissionValue; } else { permissions[parsed.permission] = permissionValue; } } } const combinedPermissions: { [permission: string]: ThreadPermissionInfo, } = { ...permissions }; if (rolePermissions) { for (const permissionKey in rolePermissions) { const permissionValue = rolePermissions[permissionKey]; const currentValue = combinedPermissions[permissionKey]; if (permissionValue) { combinedPermissions[permissionKey] = { value: true, source: threadID, }; } else if (!currentValue || !currentValue.value) { combinedPermissions[permissionKey] = { value: false, source: null, }; } } } if (permissionLookup(combinedPermissions, threadPermissions.KNOW_OF)) { permissions = combinedPermissions; } const threadIsAnnouncementThread = threadTypeIsAnnouncementThread(threadType); const hasVoicedInAnnouncementChannelsPermission = permissionLookup( (permissions: ThreadPermissionsBlob), threadPermissions.VOICED_IN_ANNOUNCEMENT_CHANNELS, ); - if ( threadIsAnnouncementThread && - hasVoicedInAnnouncementChannelsPermission && - isMember + !hasVoicedInAnnouncementChannelsPermission ) { - permissions[threadPermissions.VOICED] = { - value: true, - source: threadID, - }; + for (const permission of threadPermissionsRequiringVoicedInAnnouncementChannels) { + delete permissions[permission]; + } + } + if ( + threadType === threadTypes.GENESIS && + !hasVoicedInAnnouncementChannelsPermission + ) { + for (const permission of threadPermissionsRemovedForGenesisMembers) { + delete permissions[permission]; + } } if (Object.keys(permissions).length === 0) { return null; } return permissions; } function makePermissionsForChildrenBlob( permissions: ?ThreadPermissionsBlob, ): ?ThreadPermissionsBlob { if (!permissions) { return null; } const permissionsForChildren: { [permission: string]: ThreadPermissionInfo } = {}; for (const permissionKey in permissions) { const permissionValue = permissions[permissionKey]; const parsed = parseThreadPermissionString(permissionKey); if (!parsed.propagationPrefix) { continue; } if ( parsed.propagationPrefix === threadPermissionPropagationPrefixes.DESCENDANT ) { permissionsForChildren[permissionKey] = permissionValue; } const withoutPropagationPrefix = constructThreadPermissionString({ ...parsed, propagationPrefix: null, }); permissionsForChildren[withoutPropagationPrefix] = permissionValue; } if (Object.keys(permissionsForChildren).length === 0) { return null; } return permissionsForChildren; } function getRoleForPermissions( inputRole: string, permissions: ?ThreadPermissionsBlob, ): string { if (!permissionLookup(permissions, threadPermissions.KNOW_OF)) { return '-1'; } else if (Number(inputRole) <= 0) { return '0'; } else { return inputRole; } } function getThreadPermissionBlobFromUserSurfacedPermissions( communityUserSurfacedPermissions: $ReadOnlyArray, threadType: ThinThreadType, ): ThreadRolePermissionsBlob { const mappedUserSurfacedPermissions = communityUserSurfacedPermissions .map(permission => [...configurableCommunityPermissions[permission]]) .flat(); const userSurfacedPermissionsObj = Object.fromEntries( mappedUserSurfacedPermissions.map(p => [p, true]), ); const universalCommunityPermissions = getUniversalCommunityRootPermissionsBlob(threadType); return { ...universalCommunityPermissions, ...userSurfacedPermissionsObj, }; } export type RolePermissionBlobs = { +Members: ThreadRolePermissionsBlob, +Admins?: ThreadRolePermissionsBlob, }; const { CHILD, DESCENDANT } = threadPermissionPropagationPrefixes; const { OPEN, TOP_LEVEL, OPEN_TOP_LEVEL } = threadPermissionFilterPrefixes; const OPEN_CHILD = CHILD + OPEN; const OPEN_DESCENDANT = DESCENDANT + OPEN; const TOP_LEVEL_DESCENDANT = DESCENDANT + TOP_LEVEL; const OPEN_TOP_LEVEL_DESCENDANT = DESCENDANT + OPEN_TOP_LEVEL; -const baseMemberUserSurfacedPermissions = [ +const defaultUserSurfacedPermissions = [ userSurfacedPermissions.REACT_TO_MESSAGES, userSurfacedPermissions.EDIT_MESSAGES, userSurfacedPermissions.ADD_MEMBERS, -]; -const baseVoicedUserSurfacedPermissions = [ userSurfacedPermissions.EDIT_CALENDAR, userSurfacedPermissions.CREATE_AND_EDIT_CHANNELS, ]; function getRolePermissionBlobsForCommunityRoot( threadType: ThinThreadType, ): RolePermissionBlobs { - let memberUserSurfacedPermissions; - if (threadType === threadTypes.GENESIS) { - memberUserSurfacedPermissions = []; - } else if (threadType === threadTypes.COMMUNITY_ANNOUNCEMENT_ROOT) { - memberUserSurfacedPermissions = baseMemberUserSurfacedPermissions; - } else { - memberUserSurfacedPermissions = [ - ...baseMemberUserSurfacedPermissions, - ...baseVoicedUserSurfacedPermissions, - ]; - } - const memberPermissions = getThreadPermissionBlobFromUserSurfacedPermissions( - memberUserSurfacedPermissions, + defaultUserSurfacedPermissions, threadType, ); const descendantKnowOf = DESCENDANT + threadPermissions.KNOW_OF; const descendantVisible = DESCENDANT + threadPermissions.VISIBLE; const topLevelDescendantJoinThread = TOP_LEVEL_DESCENDANT + threadPermissions.JOIN_THREAD; const childJoinThread = CHILD + threadPermissions.JOIN_THREAD; const descendantVoiced = DESCENDANT + threadPermissions.VOICED; const descendantEditEntries = DESCENDANT + threadPermissions.EDIT_ENTRIES; const descendantEditThreadName = DESCENDANT + threadPermissions.EDIT_THREAD_NAME; const descendantEditThreadColor = DESCENDANT + threadPermissions.EDIT_THREAD_COLOR; const descendantEditThreadDescription = DESCENDANT + threadPermissions.EDIT_THREAD_DESCRIPTION; const descendantEditThreadAvatar = DESCENDANT + threadPermissions.EDIT_THREAD_AVATAR; const topLevelDescendantCreateSubchannels = TOP_LEVEL_DESCENDANT + threadPermissions.CREATE_SUBCHANNELS; const topLevelDescendantCreateSidebars = TOP_LEVEL_DESCENDANT + threadPermissions.CREATE_SIDEBARS; const descendantAddMembers = DESCENDANT + threadPermissions.ADD_MEMBERS; const descendantDeleteThread = DESCENDANT + threadPermissions.DELETE_THREAD; const descendantEditPermissions = DESCENDANT + threadPermissions.EDIT_PERMISSIONS; const descendantRemoveMembers = DESCENDANT + threadPermissions.REMOVE_MEMBERS; const descendantChangeRole = DESCENDANT + threadPermissions.CHANGE_ROLE; const descendantManagePins = DESCENDANT + threadPermissions.MANAGE_PINS; const topLevelDescendantVoicedInAnnouncementChannels = TOP_LEVEL_DESCENDANT + threadPermissions.VOICED_IN_ANNOUNCEMENT_CHANNELS; const descendantReactToMessage = TOP_LEVEL_DESCENDANT + threadPermissions.REACT_TO_MESSAGE; const descendantEditMessage = TOP_LEVEL_DESCENDANT + threadPermissions.EDIT_MESSAGE; const baseAdminPermissions = { [threadPermissions.KNOW_OF]: true, [threadPermissions.VISIBLE]: true, [threadPermissions.VOICED]: true, [threadPermissions.REACT_TO_MESSAGE]: true, [threadPermissions.EDIT_MESSAGE]: true, [threadPermissions.EDIT_ENTRIES]: true, [threadPermissions.EDIT_THREAD_NAME]: true, [threadPermissions.EDIT_THREAD_COLOR]: true, [threadPermissions.EDIT_THREAD_DESCRIPTION]: true, [threadPermissions.EDIT_THREAD_AVATAR]: true, [threadPermissions.CREATE_SUBCHANNELS]: true, [threadPermissions.CREATE_SIDEBARS]: true, [threadPermissions.DELETE_THREAD]: true, [threadPermissions.REMOVE_MEMBERS]: true, [threadPermissions.CHANGE_ROLE]: true, [threadPermissions.MANAGE_PINS]: true, [threadPermissions.MANAGE_INVITE_LINKS]: true, [threadPermissions.VOICED_IN_ANNOUNCEMENT_CHANNELS]: true, [threadPermissions.MANAGE_FARCASTER_CHANNEL_TAGS]: true, [descendantKnowOf]: true, [descendantVisible]: true, [topLevelDescendantJoinThread]: true, [childJoinThread]: true, [descendantVoiced]: true, [descendantEditEntries]: true, [descendantEditThreadName]: true, [descendantEditThreadColor]: true, [descendantEditThreadDescription]: true, [descendantEditThreadAvatar]: true, [topLevelDescendantCreateSubchannels]: true, [topLevelDescendantCreateSidebars]: true, [descendantAddMembers]: true, [descendantDeleteThread]: true, [descendantEditPermissions]: true, [descendantRemoveMembers]: true, [descendantChangeRole]: true, [descendantManagePins]: true, [topLevelDescendantVoicedInAnnouncementChannels]: true, [descendantReactToMessage]: true, [descendantEditMessage]: true, }; let adminPermissions; if (threadType === threadTypes.GENESIS) { adminPermissions = { ...baseAdminPermissions, [threadPermissions.ADD_MEMBERS]: true, }; } else { adminPermissions = { ...baseAdminPermissions, [threadPermissions.LEAVE_THREAD]: true, }; } return { Members: memberPermissions, Admins: adminPermissions, }; } function getRolePermissionBlobs(threadType: ThreadType): RolePermissionBlobs { if (threadTypeIsThick(threadType)) { const thickThreadType = assertThickThreadType(threadType); const memberPermissions = getThickThreadRolePermissionsBlob(thickThreadType); return { Members: memberPermissions, }; } const thinThreadType = assertThinThreadType(threadType); if (thinThreadType === threadTypes.SIDEBAR) { const memberPermissions = { [threadPermissions.VOICED]: true, [threadPermissions.LEAVE_THREAD]: true, }; return { Members: memberPermissions, }; } const openDescendantKnowOf = OPEN_DESCENDANT + threadPermissions.KNOW_OF; const openDescendantVisible = OPEN_DESCENDANT + threadPermissions.VISIBLE; const openChildJoinThread = OPEN_CHILD + threadPermissions.JOIN_THREAD; if (thinThreadType === threadTypes.GENESIS_PRIVATE) { const memberPermissions = { [threadPermissions.KNOW_OF]: true, [threadPermissions.VISIBLE]: true, [threadPermissions.VOICED]: true, [threadPermissions.CREATE_SIDEBARS]: true, [openDescendantKnowOf]: true, [openDescendantVisible]: true, [openChildJoinThread]: true, }; return { Members: memberPermissions, }; } if (thinThreadType === threadTypes.GENESIS_PERSONAL) { return { Members: { [threadPermissions.KNOW_OF]: true, [threadPermissions.VISIBLE]: true, [threadPermissions.VOICED]: true, [threadPermissions.CREATE_SIDEBARS]: true, [openDescendantKnowOf]: true, [openDescendantVisible]: true, [openChildJoinThread]: true, }, }; } const openTopLevelDescendantJoinThread = OPEN_TOP_LEVEL_DESCENDANT + threadPermissions.JOIN_THREAD; - const subthreadBasePermissions = { - [threadPermissions.KNOW_OF]: true, - [threadPermissions.VISIBLE]: true, - [threadPermissions.CREATE_SIDEBARS]: true, - [threadPermissions.LEAVE_THREAD]: true, - [openDescendantKnowOf]: true, - [openDescendantVisible]: true, - [openTopLevelDescendantJoinThread]: true, - [openChildJoinThread]: true, - }; - - if ( - thinThreadType === threadTypes.COMMUNITY_OPEN_SUBTHREAD || - thinThreadType === threadTypes.COMMUNITY_SECRET_SUBTHREAD - ) { + if (!threadTypeIsCommunityRoot(thinThreadType)) { const memberPermissions = { - ...subthreadBasePermissions, [threadPermissions.VOICED]: true, + [threadPermissions.KNOW_OF]: true, + [threadPermissions.VISIBLE]: true, + [threadPermissions.CREATE_SIDEBARS]: true, + [threadPermissions.LEAVE_THREAD]: true, + [openDescendantKnowOf]: true, + [openDescendantVisible]: true, + [openTopLevelDescendantJoinThread]: true, + [openChildJoinThread]: true, }; return { Members: memberPermissions, }; } - if ( - thinThreadType === threadTypes.COMMUNITY_OPEN_ANNOUNCEMENT_SUBTHREAD || - thinThreadType === threadTypes.COMMUNITY_SECRET_ANNOUNCEMENT_SUBTHREAD - ) { - return { - Members: subthreadBasePermissions, - }; - } - return getRolePermissionBlobsForCommunityRoot(thinThreadType); } // ESLint doesn't recognize that invariant always throws // eslint-disable-next-line consistent-return function getUniversalCommunityRootPermissionsBlob( threadType: ThinThreadType, ): ThreadRolePermissionsBlob { const openDescendantKnowOf = OPEN_DESCENDANT + threadPermissions.KNOW_OF; const openDescendantVisible = OPEN_DESCENDANT + threadPermissions.VISIBLE; const openChildJoinThread = OPEN_CHILD + threadPermissions.JOIN_THREAD; const openTopLevelDescendantJoinThread = OPEN_TOP_LEVEL_DESCENDANT + threadPermissions.JOIN_THREAD; const genesisUniversalCommunityPermissions = { [threadPermissions.KNOW_OF]: true, [threadPermissions.VISIBLE]: true, [openDescendantKnowOf]: true, [openDescendantVisible]: true, [openTopLevelDescendantJoinThread]: true, }; const baseUniversalCommunityPermissions = { ...genesisUniversalCommunityPermissions, [threadPermissions.CREATE_SIDEBARS]: true, [threadPermissions.LEAVE_THREAD]: true, [openChildJoinThread]: true, }; if (threadType === threadTypes.GENESIS) { return genesisUniversalCommunityPermissions; } else if (threadType === threadTypes.COMMUNITY_ANNOUNCEMENT_ROOT) { return baseUniversalCommunityPermissions; } else if (threadType === threadTypes.COMMUNITY_ROOT) { return { ...baseUniversalCommunityPermissions, [threadPermissions.VOICED]: true, }; } invariant(false, 'invalid threadType parameter'); } function getThickThreadRolePermissionsBlob( threadType: ThickThreadType, ): ThreadRolePermissionsBlob { invariant(threadTypeIsThick(threadType), 'ThreadType should be thick'); const basePermissions = { [threadPermissions.KNOW_OF]: true, [threadPermissions.VISIBLE]: true, [threadPermissions.VOICED]: true, [threadPermissions.REACT_TO_MESSAGE]: true, [threadPermissions.EDIT_MESSAGE]: true, [threadPermissions.EDIT_THREAD_NAME]: true, [threadPermissions.EDIT_THREAD_COLOR]: true, [threadPermissions.EDIT_THREAD_DESCRIPTION]: true, [threadPermissions.EDIT_THREAD_AVATAR]: true, [threadPermissions.ADD_MEMBERS]: true, [threadPermissions.LEAVE_THREAD]: true, }; if (threadType === threadTypes.THICK_SIDEBAR) { return { ...basePermissions, [threadPermissions.JOIN_THREAD]: true, }; } return { ...basePermissions, [threadPermissions.EDIT_ENTRIES]: true, [threadPermissions.CREATE_SIDEBARS]: true, }; } export { permissionLookup, getAllThreadPermissions, makePermissionsBlob, makePermissionsForChildrenBlob, getRoleForPermissions, getThreadPermissionBlobFromUserSurfacedPermissions, getRolePermissionBlobs, getUniversalCommunityRootPermissionsBlob, getThickThreadRolePermissionsBlob, }; diff --git a/lib/types/thread-permission-types.js b/lib/types/thread-permission-types.js index f3bfe9748..383ff65ba 100644 --- a/lib/types/thread-permission-types.js +++ b/lib/types/thread-permission-types.js @@ -1,468 +1,491 @@ // @flow import invariant from 'invariant'; 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'; export const threadPermissionsDisabledByBlock = Object.freeze({ VOICED: 'voiced', EDIT_ENTRIES: 'edit_entries', EDIT_THREAD_NAME: 'edit_thread', EDIT_THREAD_DESCRIPTION: 'edit_thread_description', EDIT_THREAD_COLOR: 'edit_thread_color', EDIT_THREAD_AVATAR: 'edit_thread_avatar', CREATE_SUBCHANNELS: 'create_subthreads', CREATE_SIDEBARS: 'create_sidebars', JOIN_THREAD: 'join_thread', EDIT_PERMISSIONS: 'edit_permissions', ADD_MEMBERS: 'add_members', REMOVE_MEMBERS: 'remove_members', REACT_TO_MESSAGE: 'react_to_message', EDIT_MESSAGE: 'edit_message', }); export const threadPermissionsNotAffectedByBlock = Object.freeze({ KNOW_OF: 'know_of', VISIBLE: 'visible', DELETE_THREAD: 'delete_thread', CHANGE_ROLE: 'change_role', LEAVE_THREAD: 'leave_thread', MANAGE_PINS: 'manage_pins', MANAGE_INVITE_LINKS: 'manage_invite_links', VOICED_IN_ANNOUNCEMENT_CHANNELS: 'voiced_in_announcement_channels', MANAGE_FARCASTER_CHANNEL_TAGS: 'manage_farcaster_channel_tags', }); export type ThreadPermissionNotAffectedByBlock = $Values< typeof threadPermissionsNotAffectedByBlock, >; // 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({ ...threadPermissionsDisabledByBlock, ...threadPermissionsNotAffectedByBlock, }); export type ThreadPermission = $Values; export function assertThreadPermission( ourThreadPermission: string, ): ThreadPermission { invariant( ourThreadPermission === 'know_of' || ourThreadPermission === 'visible' || ourThreadPermission === 'voiced' || ourThreadPermission === 'edit_entries' || ourThreadPermission === 'edit_thread' || ourThreadPermission === 'edit_thread_description' || ourThreadPermission === 'edit_thread_color' || ourThreadPermission === 'delete_thread' || ourThreadPermission === 'create_subthreads' || ourThreadPermission === 'create_sidebars' || ourThreadPermission === 'join_thread' || ourThreadPermission === 'edit_permissions' || ourThreadPermission === 'add_members' || ourThreadPermission === 'remove_members' || ourThreadPermission === 'change_role' || ourThreadPermission === 'leave_thread' || ourThreadPermission === 'react_to_message' || ourThreadPermission === 'edit_message' || ourThreadPermission === 'edit_thread_avatar' || ourThreadPermission === 'manage_pins' || ourThreadPermission === 'manage_invite_links' || ourThreadPermission === 'voiced_in_announcement_channels' || ourThreadPermission === 'manage_farcaster_channel_tags', 'string is not threadPermissions enum', ); return ourThreadPermission; } const threadPermissionValidator = t.enums.of(values(threadPermissions)); export const threadPermissionPropagationPrefixes = Object.freeze({ DESCENDANT: 'descendant_', CHILD: 'child_', }); export type ThreadPermissionPropagationPrefix = $Values< typeof threadPermissionPropagationPrefixes, >; export function assertThreadPermissionPropagationPrefix( ourPrefix: string, ): ThreadPermissionPropagationPrefix { invariant( ourPrefix === 'descendant_' || ourPrefix === 'child_', 'string is not ThreadPermissionPropagationPrefix', ); return ourPrefix; } 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, >; export function assertThreadPermissionFilterPrefix( ourPrefix: string, ): ThreadPermissionFilterPrefix { invariant( ourPrefix === 'open_' || ourPrefix === 'toplevel_' || ourPrefix === 'opentoplevel_', 'string is not ThreadPermissionFilterPrefix', ); return ourPrefix; } export const threadPermissionMembershipPrefixes = Object.freeze({ MEMBER: 'member_', }); export type ThreadPermissionMembershipPrefix = $Values< typeof threadPermissionMembershipPrefixes, >; export function assertThreadPermissionMembershipPrefix( ourPrefix: string, ): ThreadPermissionMembershipPrefix { invariant( ourPrefix === 'member_', 'string is not ThreadPermissionMembershipPrefix', ); return ourPrefix; } +export const threadPermissionsRequiringVoicedInAnnouncementChannels: $ReadOnlyArray = + [ + threadPermissions.VOICED, + threadPermissions.EDIT_ENTRIES, + threadPermissions.EDIT_THREAD_NAME, + threadPermissions.EDIT_THREAD_DESCRIPTION, + threadPermissions.EDIT_THREAD_COLOR, + threadPermissions.EDIT_THREAD_AVATAR, + threadPermissions.CREATE_SUBCHANNELS, + threadPermissions.EDIT_PERMISSIONS, + threadPermissions.DELETE_THREAD, + threadPermissions.CHANGE_ROLE, + threadPermissions.MANAGE_PINS, + ]; + +export const threadPermissionsRemovedForGenesisMembers: $ReadOnlyArray = + [ + threadPermissions.REACT_TO_MESSAGE, + threadPermissions.EDIT_MESSAGE, + threadPermissions.ADD_MEMBERS, + ]; + // 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', MANAGE_FARCASTER_CHANNEL_TAGS: 'manage_farcaster_channel_tags', }); 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 + threadPermissionMembershipPrefixes.MEMBER + 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 childJoinThread = threadPermissionPropagationPrefixes.CHILD + threadPermissions.JOIN_THREAD; const knowOfSecretChannelsPermissions = new Set([ descendantKnowOf, descendantVisible, descendantTopLevelJoinThread, childJoinThread, ]); const voicedPermission = { title: 'Voiced in announcement channels', description: 'Allows members to send messages in announcement channels', userSurfacedPermission: userSurfacedPermissions.VOICED_IN_ANNOUNCEMENT_CHANNELS, }; const voicedInAnnouncementChannels = threadPermissions.VOICED_IN_ANNOUNCEMENT_CHANNELS; const descendantTopLevelVoicedInAnnouncementChannels = threadPermissionPropagationPrefixes.DESCENDANT + threadPermissionFilterPrefixes.TOP_LEVEL + + threadPermissionMembershipPrefixes.MEMBER + threadPermissions.VOICED_IN_ANNOUNCEMENT_CHANNELS; const voicedPermissions = new Set([ voicedInAnnouncementChannels, descendantTopLevelVoicedInAnnouncementChannels, ]); 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 + threadPermissionMembershipPrefixes.MEMBER + threadPermissions.EDIT_THREAD_NAME; const editThreadDescription = threadPermissions.EDIT_THREAD_DESCRIPTION; const descendantEditThreadDescription = threadPermissionPropagationPrefixes.DESCENDANT + threadPermissionMembershipPrefixes.MEMBER + threadPermissions.EDIT_THREAD_DESCRIPTION; const editThreadColor = threadPermissions.EDIT_THREAD_COLOR; const descendantEditThreadColor = threadPermissionPropagationPrefixes.DESCENDANT + threadPermissionMembershipPrefixes.MEMBER + threadPermissions.EDIT_THREAD_COLOR; const createSubchannels = threadPermissions.CREATE_SUBCHANNELS; const descendantCreateSubchannels = threadPermissionPropagationPrefixes.DESCENDANT + threadPermissionFilterPrefixes.TOP_LEVEL + threadPermissionMembershipPrefixes.MEMBER + threadPermissions.CREATE_SUBCHANNELS; const editThreadAvatar = threadPermissions.EDIT_THREAD_AVATAR; const descendantEditThreadAvatar = threadPermissionPropagationPrefixes.DESCENDANT + threadPermissionMembershipPrefixes.MEMBER + threadPermissions.EDIT_THREAD_AVATAR; const createAndEditChannelsPermissions = new Set([ editThreadName, descendantEditThreadName, editThreadDescription, descendantEditThreadDescription, editThreadColor, descendantEditThreadColor, createSubchannels, descendantCreateSubchannels, editThreadAvatar, descendantEditThreadAvatar, ]); const deleteChannelsPermission = { title: 'Delete channels', description: 'Allows members to delete channels', userSurfacedPermission: userSurfacedPermissions.DELETE_CHANNELS, }; const deleteThread = threadPermissions.DELETE_THREAD; const descendantDeleteThread = threadPermissionPropagationPrefixes.DESCENDANT + threadPermissionMembershipPrefixes.MEMBER + 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 descendantAddMembers = threadPermissionPropagationPrefixes.DESCENDANT + threadPermissionMembershipPrefixes.MEMBER + threadPermissions.ADD_MEMBERS; const addMembersPermissions = new Set([descendantAddMembers]); 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 + threadPermissionMembershipPrefixes.MEMBER + 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 changeRolePermissions = new Set([changeRole]); 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 + threadPermissionMembershipPrefixes.MEMBER + 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 + threadPermissionMembershipPrefixes.MEMBER + 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 + threadPermissionMembershipPrefixes.MEMBER + 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 + threadPermissionMembershipPrefixes.MEMBER + 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 + threadPermissionMembershipPrefixes.MEMBER + threadPermissions.MANAGE_INVITE_LINKS; const manageInviteLinksPermissions = new Set([ manageInviteLinks, descendantManageInviteLinks, ]); const manageFarcasterChannelTagsPermission = { title: 'Manage Farcaster channel tags', description: 'Allows members to associate your community with a Farcaster channel,' + ' or to delete the association', userSurfacedPermission: userSurfacedPermissions.MANAGE_FARCASTER_CHANNEL_TAGS, }; const manageFarcasterChannelTags = threadPermissions.MANAGE_FARCASTER_CHANNEL_TAGS; const manageFarcasterChannelTagsPermissions = new Set([ manageFarcasterChannelTags, ]); 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, manageFarcasterChannelTagsPermission, ]); 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, [userSurfacedPermissions.MANAGE_FARCASTER_CHANNEL_TAGS]: manageFarcasterChannelTagsPermissions, }); 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-enum.js b/lib/types/thread-types-enum.js index 6490b1d6d..8fccd2c5f 100644 --- a/lib/types/thread-types-enum.js +++ b/lib/types/thread-types-enum.js @@ -1,173 +1,174 @@ // @flow import invariant from 'invariant'; import type { TRefinement } from 'tcomb'; import { values } from '../utils/objects.js'; import { tNumEnum } from '../utils/validation-utils.js'; export const thinThreadTypes = Object.freeze({ //OPEN: 0, (DEPRECATED) //CLOSED: 1, (DEPRECATED) //SECRET: 2, (DEPRECATED) // has parent, not top-level (appears under parent in inbox), and visible to // all members of parent SIDEBAR: 5, // canonical thread for each pair of users. represents the friendship // created under GENESIS. being deprecated in favor of PERSONAL GENESIS_PERSONAL: 6, // canonical thread for each single user // created under GENESIS. being deprecated in favor of PRIVATE GENESIS_PRIVATE: 7, // aka "org". no parent, top-level, has admin COMMUNITY_ROOT: 8, // like COMMUNITY_ROOT, but members aren't voiced COMMUNITY_ANNOUNCEMENT_ROOT: 9, // an open subthread. has parent, top-level (not sidebar), and visible to all // members of parent. root ancestor is a COMMUNITY_ROOT COMMUNITY_OPEN_SUBTHREAD: 3, // like COMMUNITY_SECRET_SUBTHREAD, but members aren't voiced COMMUNITY_OPEN_ANNOUNCEMENT_SUBTHREAD: 10, // a secret subthread. optional parent, top-level (not sidebar), visible only // to its members. root ancestor is a COMMUNITY_ROOT COMMUNITY_SECRET_SUBTHREAD: 4, // like COMMUNITY_SECRET_SUBTHREAD, but members aren't voiced COMMUNITY_SECRET_ANNOUNCEMENT_SUBTHREAD: 11, // like COMMUNITY_SECRET_ANNOUNCEMENT_SUBTHREAD, but you can't leave GENESIS: 12, }); export type ThinThreadType = $Values; export const nonSidebarThickThreadTypes = Object.freeze({ // local "thick" thread (outside of community). no parent, can only have // sidebar children LOCAL: 13, // canonical thread for each pair of users. represents the friendship PERSONAL: 14, // canonical thread for each single user PRIVATE: 15, }); export type NonSidebarThickThreadType = $Values< typeof nonSidebarThickThreadTypes, >; export const sidebarThickThreadTypes = Object.freeze({ // has parent, not top-level (appears under parent in inbox), and visible to // all members of parent THICK_SIDEBAR: 16, }); export type SidebarThickThreadType = $Values; export const thickThreadTypes = Object.freeze({ ...nonSidebarThickThreadTypes, ...sidebarThickThreadTypes, }); export type ThickThreadType = | NonSidebarThickThreadType | SidebarThickThreadType; export type ThreadType = ThinThreadType | ThickThreadType; export const threadTypes = Object.freeze({ ...thinThreadTypes, ...thickThreadTypes, }); const thickThreadTypesSet = new Set(Object.values(thickThreadTypes)); export function threadTypeIsThick(threadType: ThreadType): boolean { return thickThreadTypesSet.has(threadType); } export function assertThinThreadType(threadType: number): ThinThreadType { invariant( threadType === 3 || threadType === 4 || threadType === 5 || threadType === 6 || threadType === 7 || threadType === 8 || threadType === 9 || threadType === 10 || threadType === 11 || threadType === 12, 'number is not ThinThreadType enum', ); return threadType; } export function assertThickThreadType(threadType: number): ThickThreadType { invariant( threadType === 13 || threadType === 14 || threadType === 15 || threadType === 16, 'number is not ThickThreadType enum', ); return threadType; } export const thickThreadTypeValidator: TRefinement = tNumEnum( values(thickThreadTypes), ); export function assertThreadType(threadType: number): ThreadType { invariant( threadType === 3 || threadType === 4 || threadType === 5 || threadType === 6 || threadType === 7 || threadType === 8 || threadType === 9 || threadType === 10 || threadType === 11 || threadType === 12 || threadType === 13 || threadType === 14 || threadType === 15 || threadType === 16, 'number is not ThreadType enum', ); return threadType; } export const threadTypeValidator: TRefinement = tNumEnum( values(threadTypes), ); export const communityThreadTypes: $ReadOnlyArray = Object.freeze([ threadTypes.COMMUNITY_ROOT, threadTypes.COMMUNITY_ANNOUNCEMENT_ROOT, threadTypes.GENESIS, ]); export const announcementThreadTypes: $ReadOnlyArray = Object.freeze([ + threadTypes.GENESIS, threadTypes.COMMUNITY_ANNOUNCEMENT_ROOT, threadTypes.COMMUNITY_OPEN_ANNOUNCEMENT_SUBTHREAD, threadTypes.COMMUNITY_SECRET_ANNOUNCEMENT_SUBTHREAD, ]); export const communitySubthreads: $ReadOnlyArray = Object.freeze([ threadTypes.COMMUNITY_OPEN_SUBTHREAD, threadTypes.COMMUNITY_OPEN_ANNOUNCEMENT_SUBTHREAD, threadTypes.COMMUNITY_SECRET_SUBTHREAD, threadTypes.COMMUNITY_SECRET_ANNOUNCEMENT_SUBTHREAD, ]); export const sidebarThreadTypes: $ReadOnlyArray = Object.freeze([ threadTypes.SIDEBAR, threadTypes.THICK_SIDEBAR, ]); export function threadTypeIsCommunityRoot(threadType: ThreadType): boolean { return communityThreadTypes.includes(threadType); } export function threadTypeIsAnnouncementThread( threadType: ThreadType, ): boolean { return announcementThreadTypes.includes(threadType); } export function threadTypeIsSidebar(threadType: ThreadType): boolean { return sidebarThreadTypes.includes(threadType); }