mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 05:25:40 +09:00
242 lines
9.6 KiB
TypeScript
242 lines
9.6 KiB
TypeScript
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
|
|
import {ALL_PERMISSIONS, ChannelTypes, Permissions} from '@fluxer/constants/src/ChannelConstants';
|
|
import {THREAD_CHANNEL_TYPES} from '@fluxer/constants/src/ThreadConstants';
|
|
|
|
export const ThreadPermissionFlags = {
|
|
MANAGE_THREADS: 1n << 34n,
|
|
CREATE_PUBLIC_THREADS: 1n << 35n,
|
|
CREATE_PRIVATE_THREADS: 1n << 36n,
|
|
SEND_MESSAGES_IN_THREADS: 1n << 38n,
|
|
} as const;
|
|
|
|
export const THREAD_PERMISSIONS = Object.values(ThreadPermissionFlags).reduce((acc, p) => acc | p, 0n);
|
|
export const DEFAULT_THREAD_PERMISSIONS =
|
|
ThreadPermissionFlags.CREATE_PUBLIC_THREADS |
|
|
ThreadPermissionFlags.CREATE_PRIVATE_THREADS |
|
|
ThreadPermissionFlags.SEND_MESSAGES_IN_THREADS;
|
|
export const THREAD_AWARE_ALL_PERMISSIONS = ALL_PERMISSIONS | THREAD_PERMISSIONS;
|
|
|
|
export type ThreadDenial =
|
|
| typeof APIErrorCodes.MISSING_ACCESS
|
|
| typeof APIErrorCodes.MISSING_PERMISSIONS
|
|
| typeof APIErrorCodes.COMMUNICATION_DISABLED
|
|
| typeof APIErrorCodes.THREAD_ARCHIVED
|
|
| typeof APIErrorCodes.THREAD_LOCKED
|
|
| typeof APIErrorCodes.UNKNOWN_THREAD_MEMBER;
|
|
|
|
export interface ThreadActor {
|
|
permissions: bigint;
|
|
isOwner: boolean;
|
|
timedOut: boolean;
|
|
}
|
|
|
|
export interface ThreadFacts {
|
|
type: number;
|
|
archived: boolean;
|
|
locked: boolean;
|
|
invitable: boolean;
|
|
}
|
|
|
|
export interface ThreadActorContext extends ThreadActor {
|
|
thread: ThreadFacts;
|
|
isThreadOwner: boolean;
|
|
isMember: boolean;
|
|
}
|
|
|
|
export type ThreadWriteAction = 'send' | 'react' | 'pin' | 'edit' | 'delete';
|
|
|
|
export interface ThreadPatch {
|
|
name?: unknown;
|
|
archived?: boolean;
|
|
auto_archive_duration?: unknown;
|
|
locked?: boolean;
|
|
invitable?: unknown;
|
|
rate_limit_per_user?: unknown;
|
|
flags?: number;
|
|
applied_tags?: unknown;
|
|
}
|
|
|
|
export type ThreadCreateKind = 'from_message' | 'public' | 'private' | 'forum_post';
|
|
|
|
function has(perms: bigint, bit: bigint): boolean {
|
|
return (perms & bit) === bit;
|
|
}
|
|
|
|
export function withImplicitThreadBits(perms: bigint): bigint {
|
|
return has(perms, Permissions.ADMINISTRATOR) ? perms | THREAD_AWARE_ALL_PERMISSIONS : perms;
|
|
}
|
|
|
|
export function isThreadModerator(perms: bigint, {isOwner, timedOut}: {isOwner: boolean; timedOut: boolean}): boolean {
|
|
if (isOwner || has(perms, Permissions.ADMINISTRATOR)) return true;
|
|
return !timedOut && has(perms, ThreadPermissionFlags.MANAGE_THREADS);
|
|
}
|
|
|
|
function isCommunicationBlocked(actor: ThreadActor): boolean {
|
|
return actor.timedOut && !actor.isOwner && !has(actor.permissions, Permissions.ADMINISTRATOR);
|
|
}
|
|
|
|
function isModerator(actor: ThreadActor): boolean {
|
|
return isThreadModerator(withImplicitThreadBits(actor.permissions), actor);
|
|
}
|
|
|
|
export function threadViewPermissions(parentPerms: bigint): bigint {
|
|
const perms = withImplicitThreadBits(parentPerms);
|
|
const withoutSend = perms & ~Permissions.SEND_MESSAGES;
|
|
return has(perms, ThreadPermissionFlags.SEND_MESSAGES_IN_THREADS)
|
|
? withoutSend | Permissions.SEND_MESSAGES
|
|
: withoutSend;
|
|
}
|
|
|
|
export function messageSendPermissionFor(type: number): bigint {
|
|
return THREAD_CHANNEL_TYPES.has(type) ? ThreadPermissionFlags.SEND_MESSAGES_IN_THREADS : Permissions.SEND_MESSAGES;
|
|
}
|
|
|
|
export function canViewThread(ctx: ThreadActorContext): ThreadDenial | null {
|
|
const perms = withImplicitThreadBits(ctx.permissions);
|
|
if (!ctx.isOwner && !has(perms, Permissions.VIEW_CHANNEL)) return APIErrorCodes.MISSING_ACCESS;
|
|
if (ctx.thread.type === ChannelTypes.PRIVATE_THREAD && !ctx.isMember && !isModerator(ctx)) {
|
|
return APIErrorCodes.MISSING_ACCESS;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function threadWriteBlock(action: ThreadWriteAction, ctx: ThreadActorContext): ThreadDenial | null {
|
|
if (action === 'delete') return null;
|
|
if (isCommunicationBlocked(ctx)) return APIErrorCodes.COMMUNICATION_DISABLED;
|
|
const moderator = isModerator(ctx);
|
|
if (action === 'send') {
|
|
return ctx.thread.locked && !moderator ? APIErrorCodes.THREAD_LOCKED : null;
|
|
}
|
|
if (ctx.thread.archived) return APIErrorCodes.THREAD_ARCHIVED;
|
|
if (ctx.thread.locked && !moderator) return APIErrorCodes.THREAD_LOCKED;
|
|
return null;
|
|
}
|
|
|
|
export function canUnarchive(ctx: ThreadActorContext): ThreadDenial | null {
|
|
if (isCommunicationBlocked(ctx)) return APIErrorCodes.COMMUNICATION_DISABLED;
|
|
if (isModerator(ctx)) return null;
|
|
if (ctx.thread.locked) return APIErrorCodes.THREAD_LOCKED;
|
|
return ctx.isMember || ctx.isThreadOwner ? null : APIErrorCodes.MISSING_PERMISSIONS;
|
|
}
|
|
|
|
export function threadPatchRequirement(
|
|
patch: ThreadPatch,
|
|
currentFlags: number,
|
|
ctx: ThreadActorContext,
|
|
): ThreadDenial | null {
|
|
if (isCommunicationBlocked(ctx)) return APIErrorCodes.COMMUNICATION_DISABLED;
|
|
const moderator = isModerator(ctx);
|
|
const ownerOrModerator = moderator || ctx.isThreadOwner;
|
|
if (patch.archived === false && ctx.thread.archived) {
|
|
const unarchive = canUnarchive(ctx);
|
|
if (unarchive !== null) return unarchive;
|
|
}
|
|
const staysArchived = ctx.thread.archived && patch.archived !== false;
|
|
const touchesOtherFields =
|
|
patch.name !== undefined ||
|
|
patch.auto_archive_duration !== undefined ||
|
|
patch.locked !== undefined ||
|
|
patch.invitable !== undefined ||
|
|
patch.rate_limit_per_user !== undefined ||
|
|
(patch.flags !== undefined && patch.flags !== currentFlags) ||
|
|
patch.applied_tags !== undefined;
|
|
if (staysArchived && touchesOtherFields) return APIErrorCodes.THREAD_ARCHIVED;
|
|
if (patch.locked !== undefined && patch.locked !== ctx.thread.locked && !moderator) {
|
|
if (!(patch.locked && ctx.isThreadOwner)) return APIErrorCodes.MISSING_PERMISSIONS;
|
|
}
|
|
if (ctx.thread.locked && !moderator) return APIErrorCodes.THREAD_LOCKED;
|
|
if ((patch.name !== undefined || patch.auto_archive_duration !== undefined) && !ownerOrModerator) {
|
|
return APIErrorCodes.MISSING_PERMISSIONS;
|
|
}
|
|
if (patch.archived === true && !ctx.thread.archived && !ownerOrModerator) return APIErrorCodes.MISSING_PERMISSIONS;
|
|
if (patch.invitable !== undefined && !ownerOrModerator) return APIErrorCodes.MISSING_PERMISSIONS;
|
|
if (patch.rate_limit_per_user !== undefined && !moderator) return APIErrorCodes.MISSING_PERMISSIONS;
|
|
if (patch.flags !== undefined && patch.flags !== currentFlags && !moderator) return APIErrorCodes.MISSING_PERMISSIONS;
|
|
return null;
|
|
}
|
|
|
|
export function canSetTags(
|
|
ctx: ThreadActorContext,
|
|
{touchesModeratedTag}: {touchesModeratedTag: boolean},
|
|
): ThreadDenial | null {
|
|
if (isCommunicationBlocked(ctx)) return APIErrorCodes.COMMUNICATION_DISABLED;
|
|
const moderator = isModerator(ctx);
|
|
if (!moderator && !ctx.isThreadOwner) return APIErrorCodes.MISSING_PERMISSIONS;
|
|
if (touchesModeratedTag && !moderator) return APIErrorCodes.MISSING_PERMISSIONS;
|
|
return null;
|
|
}
|
|
|
|
export function canJoinThread(ctx: ThreadActorContext): ThreadDenial | null {
|
|
const view = canViewThread(ctx);
|
|
if (view !== null) return view;
|
|
if (ctx.thread.archived) return APIErrorCodes.THREAD_ARCHIVED;
|
|
if (ctx.thread.locked && !isModerator(ctx)) return APIErrorCodes.THREAD_LOCKED;
|
|
return null;
|
|
}
|
|
|
|
export function canAddThreadMember(
|
|
ctx: ThreadActorContext,
|
|
target: {canViewParent: boolean; isModerator: boolean},
|
|
): ThreadDenial | null {
|
|
if (isCommunicationBlocked(ctx)) return APIErrorCodes.COMMUNICATION_DISABLED;
|
|
if (ctx.thread.archived) return APIErrorCodes.THREAD_ARCHIVED;
|
|
const moderator = isModerator(ctx);
|
|
if (ctx.thread.locked && !moderator) return APIErrorCodes.THREAD_LOCKED;
|
|
if (!has(threadViewPermissions(ctx.permissions), Permissions.SEND_MESSAGES)) return APIErrorCodes.MISSING_PERMISSIONS;
|
|
if (ctx.thread.type === ChannelTypes.PRIVATE_THREAD && !ctx.thread.invitable && !moderator && !target.isModerator) {
|
|
return APIErrorCodes.MISSING_PERMISSIONS;
|
|
}
|
|
if (!target.canViewParent) return APIErrorCodes.MISSING_ACCESS;
|
|
return null;
|
|
}
|
|
|
|
export function canRemoveThreadMember(ctx: ThreadActorContext): ThreadDenial | null {
|
|
if (isCommunicationBlocked(ctx)) return APIErrorCodes.COMMUNICATION_DISABLED;
|
|
if (ctx.thread.archived) return APIErrorCodes.THREAD_ARCHIVED;
|
|
if (isModerator(ctx)) return null;
|
|
if (ctx.thread.type === ChannelTypes.PRIVATE_THREAD && ctx.isThreadOwner) return null;
|
|
return APIErrorCodes.MISSING_PERMISSIONS;
|
|
}
|
|
|
|
export function canLeave(ctx: ThreadActorContext): ThreadDenial | null {
|
|
if (!ctx.isMember) return APIErrorCodes.UNKNOWN_THREAD_MEMBER;
|
|
if (ctx.thread.archived) return APIErrorCodes.THREAD_ARCHIVED;
|
|
return null;
|
|
}
|
|
|
|
export function canDeleteThread(ctx: ThreadActor): ThreadDenial | null {
|
|
if (isCommunicationBlocked(ctx)) return APIErrorCodes.COMMUNICATION_DISABLED;
|
|
return isModerator(ctx) ? null : APIErrorCodes.MISSING_PERMISSIONS;
|
|
}
|
|
|
|
export function canCreateThread(actor: ThreadActor, kind: ThreadCreateKind): ThreadDenial | null {
|
|
const perms = withImplicitThreadBits(actor.permissions);
|
|
if (actor.isOwner) return null;
|
|
if (!has(perms, Permissions.VIEW_CHANNEL)) return APIErrorCodes.MISSING_ACCESS;
|
|
if (isCommunicationBlocked(actor)) return APIErrorCodes.COMMUNICATION_DISABLED;
|
|
switch (kind) {
|
|
case 'forum_post':
|
|
return has(perms, Permissions.SEND_MESSAGES) ? null : APIErrorCodes.MISSING_PERMISSIONS;
|
|
case 'from_message':
|
|
return has(perms, ThreadPermissionFlags.CREATE_PUBLIC_THREADS) && has(perms, Permissions.READ_MESSAGE_HISTORY)
|
|
? null
|
|
: APIErrorCodes.MISSING_PERMISSIONS;
|
|
case 'public':
|
|
return has(perms, ThreadPermissionFlags.CREATE_PUBLIC_THREADS) ? null : APIErrorCodes.MISSING_PERMISSIONS;
|
|
case 'private':
|
|
return has(perms, ThreadPermissionFlags.CREATE_PRIVATE_THREADS) ? null : APIErrorCodes.MISSING_PERMISSIONS;
|
|
}
|
|
}
|
|
|
|
export function canListArchivedThreads(
|
|
actor: ThreadActor,
|
|
{privateThreads}: {privateThreads: boolean},
|
|
): ThreadDenial | null {
|
|
const perms = withImplicitThreadBits(actor.permissions);
|
|
if (!actor.isOwner && !has(perms, Permissions.READ_MESSAGE_HISTORY)) return APIErrorCodes.MISSING_PERMISSIONS;
|
|
if (privateThreads && !isModerator(actor)) return APIErrorCodes.MISSING_PERMISSIONS;
|
|
return null;
|
|
}
|