diff --git a/lib/permissions/minimally-encoded-thread-permissions.js b/lib/permissions/minimally-encoded-thread-permissions.js index 25d2c4e1e..a81791cbf 100644 --- a/lib/permissions/minimally-encoded-thread-permissions.js +++ b/lib/permissions/minimally-encoded-thread-permissions.js @@ -1,244 +1,257 @@ // @flow import invariant from 'invariant'; -import { parseThreadPermissionString } from './prefixes.js'; -import type { - ThreadPermission, - ThreadPermissionInfo, - ThreadPermissionsInfo, - ThreadRolePermissionsBlob, +import { + parseThreadPermissionString, + constructThreadPermissionString, +} from './prefixes.js'; +import { + type ThreadPermission, + type ThreadPermissionInfo, + type ThreadPermissionsInfo, + type ThreadRolePermissionsBlob, + threadPermissions, + assertThreadPermission, + assertThreadPermissionPropagationPrefix, + assertThreadPermissionFilterPrefix, + assertThreadPermissionMembershipPrefix, } from '../types/thread-permission-types.js'; -import { threadPermissions } from '../types/thread-permission-types.js'; import { entries, invertObjectToMap } from '../utils/objects.js'; import type { TRegex } from '../utils/validation-utils.js'; import { tRegex } from '../utils/validation-utils.js'; // `baseRolePermissionEncoding` maps permission names to indices. // These indices represent the 6-bit basePermission part of the 10-bit role // permission encoding created by `rolePermissionToBitmaskHex`. // The 6-bit basePermission allows for up to 2^6 = 64 different permissions. // If more than 64 permissions are needed, the encoding in // `rolePermissionToBitmaskHex` will need to be updated to accommodate this. const baseRolePermissionEncoding = Object.freeze({ know_of: BigInt(0), visible: BigInt(1), voiced: BigInt(2), edit_entries: BigInt(3), edit_thread: BigInt(4), // EDIT_THREAD_NAME edit_thread_description: BigInt(5), edit_thread_color: BigInt(6), delete_thread: BigInt(7), create_subthreads: BigInt(8), // CREATE_SUBCHANNELS create_sidebars: BigInt(9), join_thread: BigInt(10), edit_permissions: BigInt(11), add_members: BigInt(12), remove_members: BigInt(13), change_role: BigInt(14), leave_thread: BigInt(15), react_to_message: BigInt(16), edit_message: BigInt(17), edit_thread_avatar: BigInt(18), manage_pins: BigInt(19), manage_invite_links: BigInt(20), voiced_in_announcement_channels: BigInt(21), manage_farcaster_channel_tags: BigInt(22), }); // `minimallyEncodedThreadPermissions` is used to map each permission // to its respective bitmask where the index from `baseRolePermissionEncoding` // is used to set a specific bit in the bitmask. This is used in the // `permissionsToBitmaskHex` function where each permission is represented as a // single bit and the final bitmask is the union of all granted permissions. const minimallyEncodedThreadPermissions = Object.fromEntries( Object.keys(baseRolePermissionEncoding).map((key, idx) => [ key, BigInt(1) << BigInt(idx), ]), ); // This function converts a set of permissions to a hex-encoded bitmask. // Each permission is represented as a single bit in the bitmask. const permissionsToBitmaskHex = ( permissions: ThreadPermissionsInfo, ): string => { let bitmask = BigInt(0); for (const [key, permission] of entries(permissions)) { if (permission.value && key in minimallyEncodedThreadPermissions) { invariant( typeof minimallyEncodedThreadPermissions[key] === 'bigint', 'must be bigint', ); bitmask |= minimallyEncodedThreadPermissions[key]; } } return bitmask.toString(16); }; const tHexEncodedPermissionsBitmask: TRegex = tRegex(/^[0-9a-fA-F]+$/); const threadPermissionsFromBitmaskHex = ( permissionsBitmaskHex: string, ): ThreadPermissionsInfo => { invariant( tHexEncodedPermissionsBitmask.is(permissionsBitmaskHex), 'permissionsBitmaskHex must be valid hex string.', ); const permissionsBitmask = BigInt(`0x${permissionsBitmaskHex}`); const permissions: { [permission: ThreadPermission]: ThreadPermissionInfo } = {}; for (const [key, permissionBitmask] of entries( minimallyEncodedThreadPermissions, )) { if ((permissionsBitmask & permissionBitmask) !== BigInt(0)) { permissions[key] = { value: true, source: 'null' }; } else { permissions[key] = { value: false, source: null }; } } return permissions; }; const hasPermission = ( permissionsBitmaskHex: string, permission: ThreadPermission, ): boolean => { const permissionsBitmask = BigInt(`0x${permissionsBitmaskHex}`); if (!(permission in minimallyEncodedThreadPermissions)) { return false; } const permissionBitmask = minimallyEncodedThreadPermissions[permission]; const knowOfBitmask = minimallyEncodedThreadPermissions[threadPermissions.KNOW_OF]; invariant( typeof permissionBitmask === 'bigint', 'permissionBitmask must be of type bigint', ); return ( (permissionsBitmask & permissionBitmask) !== BigInt(0) && (permissionsBitmask & knowOfBitmask) !== BigInt(0) ); }; const propagationPrefixes = Object.freeze({ '': BigInt(0), 'descendant_': BigInt(1), 'child_': BigInt(2), }); const filterPrefixes = Object.freeze({ '': BigInt(0), 'open_': BigInt(1), 'toplevel_': BigInt(2), 'opentoplevel_': BigInt(3), }); const membershipPrefixes = Object.freeze({ '': BigInt(0), 'member_': BigInt(1), }); // Role Permission Bitmask Structure // [10 9 8 7 6 5 4 3 2 1 0] - bit positions // [m b b b b b b p p f f] - symbol representation // m = membershipPrefix (1 bit) // b = basePermission (6 bits) // p = propagationPrefix (2 bits) // f = filterPrefix (2 bits) // membershipPrefix appears at the start because it was added later, // and we wanted to maintain backwards compatibility const rolePermissionToBitmaskHex = (threadRolePermission: string): string => { const parsed = parseThreadPermissionString(threadRolePermission); const basePermissionBits = baseRolePermissionEncoding[parsed.permission] & BigInt(63); const propagationPrefixBits = propagationPrefixes[parsed.propagationPrefix ?? ''] & BigInt(3); const filterPrefixBits = filterPrefixes[parsed.filterPrefix ?? ''] & BigInt(3); const membershipPrefixesBits = membershipPrefixes[parsed.membershipPrefix ?? ''] & BigInt(1); const bitmask = (membershipPrefixesBits << BigInt(10)) | (basePermissionBits << BigInt(4)) | (propagationPrefixBits << BigInt(2)) | filterPrefixBits; return bitmask.toString(16).padStart(3, '0'); }; const inverseBaseRolePermissionEncoding = invertObjectToMap( baseRolePermissionEncoding, ); const inversePropagationPrefixes: Map = invertObjectToMap(propagationPrefixes); const inverseFilterPrefixes: Map = invertObjectToMap(filterPrefixes); const inverseMembershipPrefixes: Map = invertObjectToMap(membershipPrefixes); const tHexEncodedRolePermission: TRegex = tRegex(/^[0-9a-fA-F]{3,}$/); const decodeRolePermissionBitmask = (bitmask: string): string => { const bitmaskInt = BigInt(`0x${bitmask}`); - const basePermission = (bitmaskInt >> BigInt(4)) & BigInt(63); - const propagationPrefix = (bitmaskInt >> BigInt(2)) & BigInt(3); - const filterPrefix = bitmaskInt & BigInt(3); - const membershipPrefix = (bitmaskInt >> BigInt(10)) & BigInt(1); - - const basePermissionString = - inverseBaseRolePermissionEncoding.get(basePermission); - const propagationPrefixString = - inversePropagationPrefixes.get(propagationPrefix) ?? ''; - const filterPrefixString = inverseFilterPrefixes.get(filterPrefix) ?? ''; - const membershipPrefixString = - inverseMembershipPrefixes.get(membershipPrefix) ?? ''; + const basePermissionBits = (bitmaskInt >> BigInt(4)) & BigInt(63); + const permissionString = + inverseBaseRolePermissionEncoding.get(basePermissionBits); invariant( - basePermissionString !== null && - basePermissionString !== undefined && - propagationPrefixString !== null && - propagationPrefixString !== undefined && - filterPrefixString !== null && - filterPrefixString !== undefined && - membershipPrefixString !== null && - membershipPrefixString !== undefined, + permissionString !== null && permissionString !== undefined, 'invalid bitmask', ); + const permission = assertThreadPermission(permissionString); - return ( - propagationPrefixString + - filterPrefixString + - membershipPrefixString + - basePermissionString + const propagationPrefixBits = (bitmaskInt >> BigInt(2)) & BigInt(3); + const propagationPrefixString = inversePropagationPrefixes.get( + propagationPrefixBits, ); + const propagationPrefix = propagationPrefixString + ? assertThreadPermissionPropagationPrefix(propagationPrefixString) + : undefined; + + const filterPrefixBits = bitmaskInt & BigInt(3); + const filterPrefixString = inverseFilterPrefixes.get(filterPrefixBits); + const filterPrefix = filterPrefixString + ? assertThreadPermissionFilterPrefix(filterPrefixString) + : undefined; + + const membershipPrefixBits = (bitmaskInt >> BigInt(10)) & BigInt(1); + const membershipPrefixString = + inverseMembershipPrefixes.get(membershipPrefixBits); + const membershipPrefix = membershipPrefixString + ? assertThreadPermissionMembershipPrefix(membershipPrefixString) + : undefined; + + return constructThreadPermissionString({ + permission, + propagationPrefix, + filterPrefix, + membershipPrefix, + }); }; const threadRolePermissionsBlobToBitmaskArray = ( threadRolePermissionsBlob: ThreadRolePermissionsBlob, ): $ReadOnlyArray => Object.keys(threadRolePermissionsBlob).map(rolePermissionToBitmaskHex); const decodeThreadRolePermissionsBitmaskArray = ( threadRolePermissionsBitmaskArray: $ReadOnlyArray, ): ThreadRolePermissionsBlob => Object.fromEntries( threadRolePermissionsBitmaskArray.map(bitmask => [ decodeRolePermissionBitmask(bitmask), true, ]), ); export { permissionsToBitmaskHex, threadPermissionsFromBitmaskHex, hasPermission, rolePermissionToBitmaskHex, decodeRolePermissionBitmask, threadRolePermissionsBlobToBitmaskArray, decodeThreadRolePermissionsBitmaskArray, tHexEncodedRolePermission, tHexEncodedPermissionsBitmask, }; diff --git a/lib/permissions/prefixes.js b/lib/permissions/prefixes.js index bae7c3e57..f293362d7 100644 --- a/lib/permissions/prefixes.js +++ b/lib/permissions/prefixes.js @@ -1,111 +1,111 @@ // @flow import { type ThreadPermission, type ThreadPermissionFilterPrefix, type ThreadPermissionPropagationPrefix, type ThreadPermissionMembershipPrefix, - assertThreadPermissions, + assertThreadPermission, threadPermissionFilterPrefixes, threadPermissionPropagationPrefixes, threadPermissionMembershipPrefixes, } from '../types/thread-permission-types.js'; import { threadTypes, type ThreadType, threadTypeIsSidebar, } from '../types/thread-types-enum.js'; type ParsedThreadPermissionString = { +permission: ThreadPermission, +propagationPrefix: ?ThreadPermissionPropagationPrefix, +filterPrefix: ?ThreadPermissionFilterPrefix, +membershipPrefix: ?ThreadPermissionMembershipPrefix, }; function parseThreadPermissionString( threadPermissionString: string, ): ParsedThreadPermissionString { let remainingString = threadPermissionString; let propagationPrefix; for (const key in threadPermissionPropagationPrefixes) { const prefix = threadPermissionPropagationPrefixes[key]; if (!remainingString.startsWith(prefix)) { continue; } propagationPrefix = prefix; remainingString = remainingString.substr(prefix.length); break; } let filterPrefix; for (const key in threadPermissionFilterPrefixes) { const prefix = threadPermissionFilterPrefixes[key]; if (!remainingString.startsWith(prefix)) { continue; } filterPrefix = prefix; remainingString = remainingString.substr(prefix.length); break; } let membershipPrefix; for (const key in threadPermissionMembershipPrefixes) { const prefix = threadPermissionMembershipPrefixes[key]; if (!remainingString.startsWith(prefix)) { continue; } membershipPrefix = prefix; remainingString = remainingString.substr(prefix.length); break; } - const permission = assertThreadPermissions(remainingString); + const permission = assertThreadPermission(remainingString); return { permission, propagationPrefix, filterPrefix, membershipPrefix }; } function constructThreadPermissionString( parsed: ParsedThreadPermissionString, ): string { const propagationPrefix = parsed.propagationPrefix ?? ''; const filterPrefix = parsed.filterPrefix ?? ''; const membershipPrefix = parsed.membershipPrefix ?? ''; return ( propagationPrefix + filterPrefix + membershipPrefix + parsed.permission ); } function includeThreadPermissionForThreadType( parsed: ParsedThreadPermissionString, threadType: ThreadType, isMember: boolean, ): boolean { if ( threadType !== threadTypes.COMMUNITY_OPEN_SUBTHREAD && threadType !== threadTypes.COMMUNITY_OPEN_ANNOUNCEMENT_SUBTHREAD && !threadTypeIsSidebar(threadType) && (parsed.filterPrefix === threadPermissionFilterPrefixes.OPEN || parsed.filterPrefix === threadPermissionFilterPrefixes.OPEN_TOP_LEVEL) ) { return false; } else if ( threadTypeIsSidebar(threadType) && (parsed.filterPrefix === threadPermissionFilterPrefixes.TOP_LEVEL || parsed.filterPrefix === threadPermissionFilterPrefixes.OPEN_TOP_LEVEL) ) { return false; } else if ( !isMember && parsed.membershipPrefix === threadPermissionMembershipPrefixes.MEMBER ) { return false; } return true; } export { parseThreadPermissionString, constructThreadPermissionString, includeThreadPermissionForThreadType, }; diff --git a/lib/types/thread-permission-types.js b/lib/types/thread-permission-types.js index d019600d5..4f6e5ed46 100644 --- a/lib/types/thread-permission-types.js +++ b/lib/types/thread-permission-types.js @@ -1,439 +1,468 @@ // @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, +export function assertThreadPermission( + ourThreadPermission: 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', + 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 ourThreadPermissions; + 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; +} // 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 + 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);