diff --git a/lib/permissions/thread-permissions.js b/lib/permissions/thread-permissions.js index 831829968..31faa7f56 100644 --- a/lib/permissions/thread-permissions.js +++ b/lib/permissions/thread-permissions.js @@ -1,557 +1,522 @@ // @flow import invariant from 'invariant'; import { parseThreadPermissionString, constructThreadPermissionString, includeThreadPermissionForThreadType, } from './prefixes.js'; import { configurableCommunityPermissions, threadPermissionFilterPrefixes, threadPermissionPropagationPrefixes, threadPermissions, userSurfacedPermissions, } 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, } 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 ) { permissions[threadPermissions.VOICED] = { value: true, source: threadID, }; } 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 = [ 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, 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 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, }; let adminPermissions; if (threadType === threadTypes.GENESIS) { adminPermissions = { ...baseAdminPermissions, [threadPermissions.ADD_MEMBERS]: true, }; } else { adminPermissions = { ...baseAdminPermissions, [threadPermissions.LEAVE_THREAD]: true, }; } return { Members: memberPermissions, Admins: adminPermissions, }; } -const nonCommunityVoicedPermissions = { - [threadPermissions.VOICED]: 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.ADD_MEMBERS]: true, -}; - 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.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.EDIT_PERMISSIONS]: true, - [threadPermissions.REMOVE_MEMBERS]: 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.REACT_TO_MESSAGE]: true, - [threadPermissions.EDIT_MESSAGE]: true, - [threadPermissions.EDIT_THREAD_COLOR]: true, - [threadPermissions.EDIT_THREAD_DESCRIPTION]: true, [threadPermissions.CREATE_SIDEBARS]: true, - [threadPermissions.EDIT_ENTRIES]: 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.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.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.REACT_TO_MESSAGE]: true, - [threadPermissions.EDIT_MESSAGE]: 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 ) { const memberPermissions = { - [threadPermissions.REMOVE_MEMBERS]: true, - [threadPermissions.EDIT_PERMISSIONS]: true, ...subthreadBasePermissions, - ...nonCommunityVoicedPermissions, + [threadPermissions.VOICED]: 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 e68016864..edcf29d81 100644 --- a/lib/types/thread-permission-types.js +++ b/lib/types/thread-permission-types.js @@ -1,370 +1,443 @@ // @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', CREATE_SUBCHANNELS: 'create_subthreads', CREATE_SIDEBARS: 'create_sidebars', JOIN_THREAD: 'join_thread', EDIT_PERMISSIONS: 'edit_permissions', ADD_MEMBERS: 'add_members', REMOVE_MEMBERS: 'remove_members', }); export const threadPermissionsNotAffectedByBlock = Object.freeze({ KNOW_OF: 'know_of', VISIBLE: 'visible', DELETE_THREAD: 'delete_thread', 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', 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 assertThreadPermissions( ourThreadPermissions: string, ): ThreadPermission { invariant( ourThreadPermissions === 'know_of' || 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' || ourThreadPermissions === 'voiced_in_announcement_channels' || ourThreadPermissions === 'manage_farcaster_channel_tags', '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, >; export const threadPermissionMembershipPrefixes = Object.freeze({ MEMBER: 'member_', }); export type ThreadPermissionMembershipPrefix = $Values< typeof threadPermissionMembershipPrefixes, >; // 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 editCalendarPermissions = new Set([editEntries]); +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 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 voicedInAnnouncementChannels = threadPermissions.VOICED_IN_ANNOUNCEMENT_CHANNELS; const descendantTopLevelVoicedInAnnouncementChannels = threadPermissionPropagationPrefixes.DESCENDANT + threadPermissionFilterPrefixes.TOP_LEVEL + 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 deleteChannelsPermissions = new Set([deleteThread]); +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 childOpenAddMembers = - threadPermissionPropagationPrefixes.CHILD + - threadPermissionFilterPrefixes.OPEN + +const descendantAddMembers = + threadPermissionPropagationPrefixes.DESCENDANT + + threadPermissionMembershipPrefixes.MEMBER + threadPermissions.ADD_MEMBERS; -const addMembersPermissions = new Set([childOpenAddMembers]); +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 removeMembersPermissions = new Set([removeMembers]); +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 editVisibilityPermissions = new Set([editPermissions]); +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 managePinsPermissions = new Set([managePins]); +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 reactToMessagePermissions = new Set([reactToMessage]); +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 editMessagePermissions = new Set([editMessage]); +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 manageInviteLinksPermissions = new Set([manageInviteLinks]); +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);