759ae5e
/**
* Space-administration API: every space endpoint behind the community settings
* overlay (members, roles, invites, join rules, bans, channel overwrites,
* images, directory maintenance). The header menu's create flows live in
* `./api`; this module covers the read/manage surface they never reached.
*
* The generated client collapses all paged space lists into one untyped `Page`
* schema, so each row type is asserted here from its single-resource envelope
* (`AppResponseData_MemberDto` etc.) instead of being read off the list call.
*/
import Apis, { trackFetch } from '@/shared/api'
import { ApiFailure, unwrapApi, type ApiEnvelope } from '@/shared/lib'
import type {
AcceptInviteCmd,
AddMemberCmd,
AppResponseData_BanDto,
AppResponseData_GroupDto,
AppResponseData_InviteDto,
AppResponseData_JoinRequestDto,
AppResponseData_JoinRuleDto,
AppResponseData_MemberDto,
AppResponseData_SpaceImageDto,
AppResponseData_UserPublic,
AppResponseData_Vec_CleanupObjectDto,
AppResponseData_Vec_GroupOverwriteDto,
AppResponseData_Vec_UserOverwriteDto,
AssignGroupCmd,
BanUserCmd,
CreateGroupCmd,
CreateInviteCmd,
CreateJoinRuleCmd,
MemberStatus,
SpaceImageKind,
SpaceJoinRequestStatus,
UpdateBanCmd,
UpdateGroupCmd,
UpdateJoinRuleCmd,
UpdateMemberCmd,
} from '@/shared/api'
export type SpaceMember = NonNullable<AppResponseData_MemberDto['data']>
export type SpaceGroup = NonNullable<AppResponseData_GroupDto['data']>
export type SpaceInvite = NonNullable<AppResponseData_InviteDto['data']>
export type SpaceBan = NonNullable<AppResponseData_BanDto['data']>
export type SpaceJoinRule = NonNullable<AppResponseData_JoinRuleDto['data']>
export type SpaceImage = NonNullable<AppResponseData_SpaceImageDto['data']>
export type DirGroupOverwrite = NonNullable<AppResponseData_Vec_GroupOverwriteDto['data']>[number]
export type DirUserOverwrite = NonNullable<AppResponseData_Vec_UserOverwriteDto['data']>[number]
export type DirOverwrite = DirGroupOverwrite | DirUserOverwrite
export type PublicUser = NonNullable<AppResponseData_UserPublic['data']>
/* ---------------------------------------------------------------- cleanup */
/** 对象清理清单里的一行:目录节点或频道(见 webapi::cleanup)。 */
export type CleanupObject = NonNullable<AppResponseData_Vec_CleanupObjectDto['data']>[number]
export type { MemberStatus, SpaceImageKind }
const pageRows = async <T>(method: unknown): Promise<T[]> => {
const page = await unwrapApi<{ items?: T[] } | null>(
method as PromiseLike<ApiEnvelope<{ items?: T[] } | null>>,
)
return page?.items ?? []
}
const rows = async <T>(method: unknown): Promise<T[]> => {
const list = await unwrapApi<T[] | null>(method as PromiseLike<ApiEnvelope<T[] | null>>)
return list ?? []
}
/** Paged list that keeps `total`/`offset`: the join-request queue pages. */
const pageOf = async <T>(
method: unknown,
): Promise<{ items: T[]; total: number; limit: number; offset: number }> => {
const page = await unwrapApi<{ items?: T[]; total?: number; limit?: number; offset?: number } | null>(
method as PromiseLike<
ApiEnvelope<{ items?: T[]; total?: number; limit?: number; offset?: number } | null>
>,
)
return {
items: page?.items ?? [],
total: page?.total ?? 0,
limit: page?.limit ?? 0,
offset: page?.offset ?? 0,
}
}
/** Page size for every settings list; the overlay has no pagination controls. */
export const ADMIN_PAGE_LIMIT = 200
/* ---------------------------------------------------------------- identity */
/** Resolve a username to the ids the member/ban commands need. */
export const findUserByUsername = (username: string) =>
unwrapApi<PublicUser>(
Apis.users.get_user_by_username({ pathParams: { username: username.replace(/^@/, '') } }),
)
/* ----------------------------------------------------------------- members */
export const listSpaceMembers = (spaceName: string) =>
pageRows<SpaceMember>(
Apis.space.list_space_members({
pathParams: { space_name: spaceName },
params: { limit: ADMIN_PAGE_LIMIT },
}),
)
export const getSpaceMember = (spaceName: string, userId: number) =>
unwrapApi<SpaceMember>(
Apis.space.get_member({ pathParams: { space_name: spaceName, user_id: userId } }),
)
export const addSpaceMember = (spaceName: string, cmd: AddMemberCmd) =>
unwrapApi<SpaceMember>(Apis.space.add_member({ pathParams: { space_name: spaceName }, data: cmd }))
export const updateSpaceMember = (spaceName: string, userId: number, cmd: UpdateMemberCmd) =>
unwrapApi<SpaceMember>(
Apis.space.update_member({ pathParams: { space_name: spaceName, user_id: userId }, data: cmd }),
)
/** The viewer's own row: nickname alias and notification prefs need no rights. */
export const updateMySpaceMembership = (spaceName: string, cmd: UpdateMemberCmd) =>
unwrapApi<SpaceMember>(Apis.space.update_self_member({ pathParams: { space_name: spaceName }, data: cmd }))
export const removeSpaceMember = (spaceName: string, userId: number) =>
unwrapApi<null>(
Apis.space.remove_member({ pathParams: { space_name: spaceName, user_id: userId } }),
)
/* ------------------------------------------------------------ roles/groups */
export const listSpaceGroups = (spaceName: string) =>
pageRows<SpaceGroup>(
Apis.space.list_groups({
pathParams: { space_name: spaceName },
params: { limit: ADMIN_PAGE_LIMIT, sort: 'position' },
}),
)
export const createSpaceGroup = (spaceName: string, cmd: CreateGroupCmd) =>
unwrapApi<SpaceGroup>(Apis.space.create_group({ pathParams: { space_name: spaceName }, data: cmd }))
export const updateSpaceGroup = (spaceName: string, groupId: string, cmd: UpdateGroupCmd) =>
unwrapApi<SpaceGroup>(
Apis.space.update_group({ pathParams: { space_name: spaceName, group_id: groupId }, data: cmd }),
)
export const deleteSpaceGroup = (spaceName: string, groupId: string) =>
unwrapApi<null>(
Apis.space.delete_group({ pathParams: { space_name: spaceName, group_id: groupId } }),
)
/** Group membership is a set write, not a field on the member row. */
export const assignSpaceGroup = (spaceName: string, userId: number, groupId: string) =>
unwrapApi<null>(
Apis.space.assign_group({
pathParams: { space_name: spaceName, user_id: userId },
data: { group_id: groupId } satisfies AssignGroupCmd,
}),
)
export const unassignSpaceGroup = (spaceName: string, userId: number, groupId: string) =>
unwrapApi<null>(
Apis.space.unassign_group({
pathParams: { space_name: spaceName, user_id: userId, group_id: groupId },
}),
)
export const joinSpaceGroup = (spaceName: string, groupId: string) =>
unwrapApi<null>(
Apis.space.join_group_self({ pathParams: { space_name: spaceName, group_id: groupId } }),
)
export const leaveSpaceGroup = (spaceName: string, groupId: string) =>
unwrapApi<null>(
Apis.space.leave_group_self({ pathParams: { space_name: spaceName, group_id: groupId } }),
)
/* ----------------------------------------------------------------- invites */
export const listSpaceInvites = (spaceName: string) =>
pageRows<SpaceInvite>(
Apis.space.list_invites({
pathParams: { space_name: spaceName },
params: { limit: ADMIN_PAGE_LIMIT },
}),
)
export const revokeSpaceInvite = (spaceName: string, inviteId: string) =>
unwrapApi<SpaceInvite | null>(
Apis.space.revoke_invite({ pathParams: { space_name: spaceName, invite_id: inviteId } }),
)
export const createSpaceInvite = (spaceName: string, cmd: CreateInviteCmd) =>
unwrapApi<SpaceInvite>(
Apis.space.create_invite({ pathParams: { space_name: spaceName }, data: cmd }),
)
/** Redeem an invite code received out of band; works for a non-member too. */
export const acceptSpaceInvite = (cmd: AcceptInviteCmd) =>
unwrapApi<unknown>(Apis.space.accept_invite({ data: cmd }))
/* ------------------------------------------------------------ join rules */
export const listSpaceJoinRules = (spaceName: string) =>
rows<SpaceJoinRule>(Apis.space.list_join_rules({ pathParams: { space_name: spaceName } }))
export const createSpaceJoinRule = (spaceName: string, cmd: CreateJoinRuleCmd) =>
unwrapApi<SpaceJoinRule>(
Apis.space.create_join_rule({ pathParams: { space_name: spaceName }, data: cmd }),
)
export const updateSpaceJoinRule = (spaceName: string, ruleId: string, cmd: UpdateJoinRuleCmd) =>
unwrapApi<SpaceJoinRule>(
Apis.space.update_join_rule({ pathParams: { space_name: spaceName, rule_id: ruleId }, data: cmd }),
)
export const deleteSpaceJoinRule = (spaceName: string, ruleId: string) =>
unwrapApi<null>(
Apis.space.delete_join_rule({ pathParams: { space_name: spaceName, rule_id: ruleId } }),
)
/* ---------------------------------------------------------- join requests */
export type SpaceJoinRequest = NonNullable<AppResponseData_JoinRequestDto['data']>
export const listSpaceJoinRequests = (
spaceName: string,
query: { status?: SpaceJoinRequestStatus; limit?: number; offset?: number } = {},
) =>
pageOf<SpaceJoinRequest>(
Apis.space.list_join_requests({
pathParams: { space_name: spaceName },
params: {
...(query.status ? { status: query.status } : {}),
...(query.limit === undefined ? {} : { limit: query.limit }),
...(query.offset === undefined ? {} : { offset: query.offset }),
},
}),
)
export const approveSpaceJoinRequest = (spaceName: string, requestId: string) =>
unwrapApi<SpaceJoinRequest>(
Apis.space.approve_join_request({
pathParams: { space_name: spaceName, request_id: requestId },
}),
)
export const rejectSpaceJoinRequest = (spaceName: string, requestId: string) =>
unwrapApi<SpaceJoinRequest>(
Apis.space.reject_join_request({
pathParams: { space_name: spaceName, request_id: requestId },
}),
)
/* ------------------------------------------------------------------ bans */
export const listSpaceBans = (spaceName: string) =>
pageRows<SpaceBan>(
Apis.space.list_bans({
pathParams: { space_name: spaceName },
params: { limit: ADMIN_PAGE_LIMIT },
}),
)
export const banSpaceUser = (spaceName: string, cmd: BanUserCmd) =>
unwrapApi<SpaceBan>(Apis.space.ban_user({ pathParams: { space_name: spaceName }, data: cmd }))
export const updateSpaceBan = (spaceName: string, userId: number, cmd: UpdateBanCmd) =>
unwrapApi<SpaceBan>(
Apis.space.update_ban({ pathParams: { space_name: spaceName, user_id: userId }, data: cmd }),
)
export const unbanSpaceUser = (spaceName: string, userId: number) =>
unwrapApi<null>(Apis.space.unban_user({ pathParams: { space_name: spaceName, user_id: userId } }))
/* ----------------------------------------------- channel permission edits */
export const listDirGroupOverwrites = (spaceName: string, dirId: string) =>
rows<DirGroupOverwrite>(
Apis.space.list_dir_group_overwrites({ pathParams: { space_name: spaceName, dir_id: dirId } }),
)
export const listDirUserOverwrites = (spaceName: string, dirId: string) =>
rows<DirUserOverwrite>(
Apis.space.list_dir_user_overwrites({ pathParams: { space_name: spaceName, dir_id: dirId } }),
)
export const setDirGroupOverwrite = (
spaceName: string,
dirId: string,
groupId: string,
cmd: { allow_mask: number; deny_mask: number },
) =>
unwrapApi<DirGroupOverwrite>(
Apis.space.set_dir_group_overwrite({
pathParams: { space_name: spaceName, dir_id: dirId, group_id: groupId },
data: cmd,
}),
)
export const removeDirGroupOverwrite = (spaceName: string, dirId: string, groupId: string) =>
unwrapApi<null>(
Apis.space.remove_dir_group_overwrite({
pathParams: { space_name: spaceName, dir_id: dirId, group_id: groupId },
}),
)
export const setDirUserOverwrite = (
spaceName: string,
dirId: string,
userId: number,
cmd: { allow_mask: number; deny_mask: number },
) =>
unwrapApi<DirUserOverwrite>(
Apis.space.set_dir_user_overwrite({
pathParams: { space_name: spaceName, dir_id: dirId, user_id: userId },
data: cmd,
}),
)
export const removeDirUserOverwrite = (spaceName: string, dirId: string, userId: number) =>
unwrapApi<null>(
Apis.space.remove_dir_user_overwrite({
pathParams: { space_name: spaceName, dir_id: dirId, user_id: userId },
}),
)
export const renameSpaceDir = (spaceName: string, dirId: string, name: string) =>
unwrapApi<unknown>(
Apis.space.update_dir_node({
pathParams: { space_name: spaceName, dir_id: dirId },
data: { name },
}),
)
/** Owner-only maintenance: clears dangling parents and cycles in the tree. */
export const repairSpaceDirs = (spaceName: string) =>
unwrapApi<{ fixed: number; scanned: number }>(
Apis.space.repair_dir_parents({ pathParams: { space_name: spaceName } }),
)
/* ----------------------------------------------------------------- images */
export const getSpaceImage = (spaceName: string, kind: SpaceImageKind) =>
unwrapApi<SpaceImage | null>(
Apis.space.get_space_image({ pathParams: { space_name: spaceName, kind } }),
)
/** Binary upload, same contract as `PUT /me/avatar`: the raw file bytes are the
* request body, so the server never decodes base64, and the intrinsic size rides
* along as query params because it cannot live in a byte stream. */
export async function uploadSpaceImage(
spaceName: string,
kind: SpaceImageKind,
file: File,
size?: { width: number; height: number } | null,
): Promise<SpaceImage> {
const query = new URLSearchParams()
if (size?.width) query.set('width', String(size.width))
if (size?.height) query.set('height', String(size.height))
const params = query.toString()
const res = await trackFetch(
`/api/v1/spaces/${encodeURIComponent(spaceName)}/images/${kind}${params ? `?${params}` : ''}`,
{
method: 'PUT',
body: file,
headers: { 'content-type': file.type || 'application/octet-stream' },
},
)
const env = (await res.json()) as ApiEnvelope<SpaceImage>
if (env.code !== 200) {
throw new ApiFailure(env.error || env.message || 'space image upload failed', env.code, env.error_code)
}
return env.data as SpaceImage
}
export const deleteSpaceImage = (spaceName: string, kind: SpaceImageKind) =>
unwrapApi<null>(Apis.space.delete_space_image({ pathParams: { space_name: spaceName, kind } }))
/* ----------------------------------------------------------------- removal */
/** Owner only, and soft: the space stops being visible but keeps its rows. */
export const deleteSpace = (spaceName: string) =>
unwrapApi<null>(Apis.space.delete_space({ pathParams: { space_name: spaceName } }))
/* ----------------------------------------------------------------- cleanup */
export const listCleanupObjects = (spaceName: string) =>
rows<CleanupObject>(
Apis.cleanup.list_cleanup_objects({ pathParams: { space_name: spaceName } }),
)
export const setCleanupVisibility = (
spaceName: string,
kind: string,
objectId: string,
isPrivate: boolean,
) =>
unwrapApi<null>(
Apis.cleanup.set_cleanup_visibility({
pathParams: { space_name: spaceName, kind, object_id: objectId },
data: { is_private: isPrivate },
}),
)
export const setCleanupArchived = (
spaceName: string,
kind: string,
objectId: string,
archived: boolean,
) =>
unwrapApi<null>(
Apis.cleanup.set_cleanup_archive({
pathParams: { space_name: spaceName, kind, object_id: objectId },
data: { archived },
}),
)
export const deleteCleanupObject = (spaceName: string, kind: string, objectId: string) =>
unwrapApi<null>(
Apis.cleanup.delete_cleanup_object({
pathParams: { space_name: spaceName, kind, object_id: objectId },
}),
)
export const clearCleanupOverwrites = (spaceName: string, kind: string, objectId: string) =>
unwrapApi<null>(
Apis.cleanup.clear_cleanup_overwrites({
pathParams: { space_name: spaceName, kind, object_id: objectId },
}),
)