2026-06-08 22:33:38 +00:00
|
|
|
import {derived, writable} from "svelte/store"
|
|
|
|
|
import {
|
|
|
|
|
ManagementMethod,
|
|
|
|
|
RELAY_ADD_MEMBER,
|
|
|
|
|
RELAY_JOIN,
|
|
|
|
|
RELAY_LEAVE,
|
|
|
|
|
RELAY_MEMBERS,
|
|
|
|
|
RELAY_REMOVE_MEMBER,
|
|
|
|
|
ROOM_ADD_MEMBER,
|
|
|
|
|
ROOM_ADMINS,
|
|
|
|
|
ROOM_CREATE_PERMISSION,
|
|
|
|
|
ROOM_JOIN,
|
|
|
|
|
ROOM_LEAVE,
|
|
|
|
|
ROOM_MEMBERS,
|
|
|
|
|
ROOM_REMOVE_MEMBER,
|
|
|
|
|
getPubkeyTagValues,
|
2026-06-22 20:36:57 +00:00
|
|
|
getTags,
|
2026-06-08 22:33:38 +00:00
|
|
|
getTagValue,
|
|
|
|
|
getTagValues,
|
|
|
|
|
sortEventsAsc,
|
|
|
|
|
} from "@welshman/util"
|
|
|
|
|
import type {Filter, PublishedRoomMeta, TrustedEvent} from "@welshman/util"
|
|
|
|
|
import {first, memoize, sortBy, spec, uniq} from "@welshman/lib"
|
|
|
|
|
import {addRoomMember, manageRelay, pubkey, waitForThunkError} from "@welshman/app"
|
2026-07-08 17:56:22 +00:00
|
|
|
import {load} from "@welshman/net"
|
2026-06-08 22:33:38 +00:00
|
|
|
import {get} from "svelte/store"
|
|
|
|
|
import {deriveEventsForUrl, deriveRelaySignedEvents} from "@app/repository"
|
2026-06-22 20:36:57 +00:00
|
|
|
|
2026-06-08 22:33:38 +00:00
|
|
|
export const deriveSpaceMembers = (url: string) =>
|
|
|
|
|
derived(deriveRelaySignedEvents(url, [{kinds: [RELAY_MEMBERS]}]), ([event]) =>
|
|
|
|
|
uniq(getTagValues("member", event?.tags ?? [])),
|
|
|
|
|
)
|
|
|
|
|
|
2026-06-22 20:36:57 +00:00
|
|
|
export const RELAY_ROLE = 33534
|
|
|
|
|
|
|
|
|
|
export type SpaceRole = {
|
|
|
|
|
id: string
|
|
|
|
|
label: string
|
|
|
|
|
description: string
|
2026-07-08 17:56:22 +00:00
|
|
|
color: number // hue, 0 to 360
|
2026-06-22 20:36:57 +00:00
|
|
|
order: number
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-08 17:56:22 +00:00
|
|
|
// Defaults for the components the client supplies, chosen to read on both themes.
|
2026-07-01 17:23:23 +00:00
|
|
|
const DEFAULT_SATURATION = 0.7
|
|
|
|
|
const DEFAULT_LIGHTNESS = 0.5
|
2026-06-22 20:36:57 +00:00
|
|
|
|
2026-07-08 17:56:22 +00:00
|
|
|
// Parse the hue from a ["color", hue] tag, falling back to 0.
|
|
|
|
|
export const parseRoleColor = (tags: string[][]): number => {
|
|
|
|
|
const hue = parseFloat(first(getTags("color", tags))?.[1] ?? "")
|
2026-07-01 17:23:23 +00:00
|
|
|
|
2026-07-08 17:56:22 +00:00
|
|
|
return isNaN(hue) ? 0 : hue
|
2026-07-01 17:23:23 +00:00
|
|
|
}
|
|
|
|
|
|
2026-07-08 17:56:22 +00:00
|
|
|
// Build an hsl() string from a role's hue, using our default saturation and lightness.
|
|
|
|
|
export const roleColor = (hue: number) =>
|
|
|
|
|
`hsl(${hue}, ${DEFAULT_SATURATION * 100}%, ${DEFAULT_LIGHTNESS * 100}%)`
|
2026-07-01 17:23:23 +00:00
|
|
|
|
|
|
|
|
// A translucent tint of the role color for use as a background fill.
|
2026-07-08 17:56:22 +00:00
|
|
|
export const roleColorSoft = (hue: number) =>
|
|
|
|
|
`hsl(${hue}, ${DEFAULT_SATURATION * 100}%, ${DEFAULT_LIGHTNESS * 100}%, 0.15)`
|
2026-06-22 20:36:57 +00:00
|
|
|
|
|
|
|
|
export const deriveSpaceRoles = (url: string) =>
|
|
|
|
|
derived(deriveRelaySignedEvents(url, [{kinds: [RELAY_ROLE]}]), $events => {
|
|
|
|
|
const roles: SpaceRole[] = []
|
|
|
|
|
|
|
|
|
|
for (const event of $events) {
|
|
|
|
|
const id = getTagValue("d", event.tags)
|
|
|
|
|
|
|
|
|
|
if (id) {
|
|
|
|
|
roles.push({
|
|
|
|
|
id,
|
|
|
|
|
label: getTagValue("label", event.tags) ?? "",
|
|
|
|
|
description: getTagValue("description", event.tags) ?? "",
|
2026-07-01 17:23:23 +00:00
|
|
|
color: parseRoleColor(event.tags),
|
2026-07-08 17:56:22 +00:00
|
|
|
order: Math.max(1, parseInt(getTagValue("order", event.tags) ?? "1", 10) || 1),
|
2026-06-22 20:36:57 +00:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return sortBy(r => [r.order, r.label] as [number, string], roles)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Map<pubkey, roleId[]> parsed from EXTRA values on ["member", pubkey, ...roleIds] tags
|
|
|
|
|
export const deriveSpaceMemberRoles = (url: string) =>
|
|
|
|
|
derived(deriveRelaySignedEvents(url, [{kinds: [RELAY_MEMBERS]}]), ([event]) => {
|
|
|
|
|
const memberRoles = new Map<string, string[]>()
|
|
|
|
|
|
|
|
|
|
if (event) {
|
|
|
|
|
for (const tag of getTags("member", event.tags)) {
|
|
|
|
|
const pubkey = tag[1]
|
|
|
|
|
const roleIds = tag.slice(2)
|
|
|
|
|
|
|
|
|
|
if (pubkey) {
|
|
|
|
|
memberRoles.set(pubkey, roleIds)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return memberRoles
|
|
|
|
|
})
|
|
|
|
|
|
2026-07-08 17:56:22 +00:00
|
|
|
// Pull the relay's freshly-signed role event so the UI reflects the change immediately,
|
|
|
|
|
// rather than waiting for the live subscription to catch up.
|
|
|
|
|
const reloadRole = (url: string, id: string) =>
|
|
|
|
|
load({relays: [url], filters: [{kinds: [RELAY_ROLE], "#d": [id]}]})
|
2026-07-01 17:23:23 +00:00
|
|
|
|
2026-06-22 20:36:57 +00:00
|
|
|
export const createRole = async (
|
|
|
|
|
url: string,
|
|
|
|
|
id: string,
|
|
|
|
|
label: string,
|
|
|
|
|
description: string,
|
2026-07-08 17:56:22 +00:00
|
|
|
color: number,
|
2026-06-22 20:36:57 +00:00
|
|
|
order: number,
|
|
|
|
|
): Promise<string | undefined> => {
|
|
|
|
|
const {error} = await manageRelay(url, {
|
|
|
|
|
method: "createrole" as ManagementMethod,
|
2026-07-08 17:56:22 +00:00
|
|
|
// hue and order go over the wire as JSON numbers, not strings, per the relay's NIP-86 impl.
|
|
|
|
|
params: [id, label, description, color, order] as unknown as string[],
|
2026-06-22 20:36:57 +00:00
|
|
|
})
|
|
|
|
|
|
2026-07-08 17:56:22 +00:00
|
|
|
if (!error) {
|
|
|
|
|
await reloadRole(url, id)
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-22 20:36:57 +00:00
|
|
|
return error
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const editRole = async (
|
|
|
|
|
url: string,
|
|
|
|
|
id: string,
|
|
|
|
|
label: string,
|
|
|
|
|
description: string,
|
2026-07-08 17:56:22 +00:00
|
|
|
color: number,
|
2026-06-22 20:36:57 +00:00
|
|
|
order: number,
|
|
|
|
|
): Promise<string | undefined> => {
|
|
|
|
|
const {error} = await manageRelay(url, {
|
|
|
|
|
method: "editrole" as ManagementMethod,
|
2026-07-08 17:56:22 +00:00
|
|
|
// hue and order go over the wire as JSON numbers, not strings, per the relay's NIP-86 impl.
|
|
|
|
|
params: [id, label, description, color, order] as unknown as string[],
|
2026-06-22 20:36:57 +00:00
|
|
|
})
|
|
|
|
|
|
2026-07-08 17:56:22 +00:00
|
|
|
if (!error) {
|
|
|
|
|
await reloadRole(url, id)
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-22 20:36:57 +00:00
|
|
|
return error
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const deleteRole = async (url: string, id: string): Promise<string | undefined> => {
|
|
|
|
|
const {error} = await manageRelay(url, {
|
|
|
|
|
method: "deleterole" as ManagementMethod,
|
|
|
|
|
params: [id],
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return error
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const assignRole = async (
|
|
|
|
|
url: string,
|
|
|
|
|
pubkey: string,
|
|
|
|
|
roleId: string,
|
|
|
|
|
): Promise<string | undefined> => {
|
|
|
|
|
const {error} = await manageRelay(url, {
|
|
|
|
|
method: "assignrole" as ManagementMethod,
|
|
|
|
|
params: [pubkey, roleId],
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return error
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const unassignRole = async (
|
|
|
|
|
url: string,
|
|
|
|
|
pubkey: string,
|
|
|
|
|
roleId: string,
|
|
|
|
|
): Promise<string | undefined> => {
|
|
|
|
|
const {error} = await manageRelay(url, {
|
|
|
|
|
method: "unassignrole" as ManagementMethod,
|
|
|
|
|
params: [pubkey, roleId],
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return error
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-08 22:33:38 +00:00
|
|
|
export const deriveRoomMembers = (url: string, h: string) => {
|
|
|
|
|
const filters: Filter[] = [{kinds: [ROOM_MEMBERS], "#d": [h]}]
|
|
|
|
|
|
|
|
|
|
return derived(deriveEventsForUrl(url, filters), ([event]) =>
|
|
|
|
|
uniq(getPubkeyTagValues(event?.tags ?? [])),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type BannedPubkeyItem = {
|
|
|
|
|
pubkey: string
|
|
|
|
|
reason: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const spaceBannedPubkeyItems = new Map<string, BannedPubkeyItem[]>()
|
|
|
|
|
|
|
|
|
|
export const deriveSpaceBannedPubkeyItems = (url: string) => {
|
|
|
|
|
const store = writable(spaceBannedPubkeyItems.get(url) || [])
|
|
|
|
|
|
|
|
|
|
manageRelay(url, {method: ManagementMethod.ListBannedPubkeys, params: []}).then(res => {
|
|
|
|
|
spaceBannedPubkeyItems.set(url, res.result)
|
|
|
|
|
store.set(res.result)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return store
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const deriveRoomAdmins = (url: string, h: string) => {
|
|
|
|
|
const filters: Filter[] = [{kinds: [ROOM_ADMINS], "#d": [h]}]
|
|
|
|
|
|
|
|
|
|
return derived(deriveEventsForUrl(url, filters), $events => {
|
|
|
|
|
const adminsEvent = first($events)
|
|
|
|
|
|
|
|
|
|
if (adminsEvent) {
|
|
|
|
|
return getPubkeyTagValues(adminsEvent.tags)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return []
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const getRoomMembers = (_url: string, h: string, events: TrustedEvent[]) => {
|
|
|
|
|
const members = new Set<string>()
|
|
|
|
|
|
|
|
|
|
for (const event of sortEventsAsc(events)) {
|
|
|
|
|
if (event.kind === ROOM_MEMBERS && getTagValue("d", event.tags) === h) {
|
|
|
|
|
members.clear()
|
|
|
|
|
|
|
|
|
|
for (const pubkey of uniq(getPubkeyTagValues(event.tags))) {
|
|
|
|
|
members.add(pubkey)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (getTagValue("h", event.tags) !== h) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const pubkeys = getPubkeyTagValues(event.tags)
|
|
|
|
|
|
|
|
|
|
if (event.kind === ROOM_ADD_MEMBER) {
|
|
|
|
|
for (const pubkey of pubkeys) {
|
|
|
|
|
members.add(pubkey)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (event.kind === ROOM_REMOVE_MEMBER) {
|
|
|
|
|
for (const pubkey of pubkeys) {
|
|
|
|
|
members.delete(pubkey)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Array.from(members)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// User membership status
|
|
|
|
|
|
|
|
|
|
export enum MembershipStatus {
|
|
|
|
|
Initial,
|
|
|
|
|
Pending,
|
|
|
|
|
Granted,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const deriveUserIsSpaceAdmin = memoize((url?: string) => {
|
|
|
|
|
const store = writable(false)
|
|
|
|
|
|
|
|
|
|
if (url) {
|
|
|
|
|
manageRelay(url, {method: ManagementMethod.SupportedMethods, params: []}).then(res =>
|
|
|
|
|
store.set(Boolean(res.result?.length)),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return store
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
export const deriveUserSpaceMembershipStatus = (url: string) => {
|
|
|
|
|
// Fetch member list and user add/remove events directly in this derivation.
|
|
|
|
|
const memberListFilters: Filter[] = [{kinds: [RELAY_MEMBERS]}]
|
|
|
|
|
const userEventFilters: Filter[] = [{kinds: [RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER]}]
|
|
|
|
|
|
|
|
|
|
return derived(
|
|
|
|
|
[
|
|
|
|
|
pubkey,
|
|
|
|
|
deriveRelaySignedEvents(url, memberListFilters),
|
|
|
|
|
deriveRelaySignedEvents(url, userEventFilters),
|
|
|
|
|
deriveEventsForUrl(url, [{kinds: [RELAY_JOIN, RELAY_LEAVE]}]),
|
|
|
|
|
deriveUserIsSpaceAdmin(url),
|
|
|
|
|
],
|
|
|
|
|
([$pubkey, $memberListEvents, $userAddRemoveEvents, $joinLeaveEvents, $isAdmin]) => {
|
|
|
|
|
// If admin, always granted.
|
|
|
|
|
if ($isAdmin) {
|
|
|
|
|
return MembershipStatus.Granted
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const membersEvent = $memberListEvents.find(spec({kind: RELAY_MEMBERS}))
|
|
|
|
|
const memberList = membersEvent ? uniq(getTagValues("member", membersEvent.tags)) : undefined
|
|
|
|
|
|
|
|
|
|
let isMember = false
|
|
|
|
|
|
|
|
|
|
if (memberList) {
|
|
|
|
|
// Member list exists - check if user is in it.
|
|
|
|
|
isMember = memberList.includes($pubkey!)
|
|
|
|
|
} else {
|
|
|
|
|
// No member list available - replay the user's add/remove history.
|
|
|
|
|
for (const event of sortBy(e => e.created_at, $userAddRemoveEvents)) {
|
|
|
|
|
if (event.pubkey !== $pubkey) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (event.kind === RELAY_ADD_MEMBER) {
|
|
|
|
|
isMember = true
|
|
|
|
|
} else if (event.kind === RELAY_REMOVE_MEMBER) {
|
|
|
|
|
isMember = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const event of $joinLeaveEvents) {
|
|
|
|
|
// Join events indicate pending or granted status, leave resets to initial.
|
|
|
|
|
if (event.pubkey !== $pubkey) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (event.kind === RELAY_JOIN) {
|
|
|
|
|
return isMember ? MembershipStatus.Granted : MembershipStatus.Pending
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (event.kind === RELAY_LEAVE) {
|
|
|
|
|
return MembershipStatus.Initial
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return isMember ? MembershipStatus.Granted : MembershipStatus.Initial
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const deriveUserIsRoomAdmin = (url: string, h: string) =>
|
|
|
|
|
derived(
|
|
|
|
|
[pubkey, deriveRoomAdmins(url, h), deriveUserIsSpaceAdmin(url)],
|
|
|
|
|
([$pubkey, $admins, $isSpaceAdmin]) => $isSpaceAdmin || $admins.includes($pubkey!),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
export const deriveUserRoomMembershipStatus = (url: string, h: string) => {
|
|
|
|
|
// Fetch the room member list and the current user's add/remove events.
|
|
|
|
|
const userEventFilters: Filter[] = [{kinds: [ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER], "#h": [h]}]
|
|
|
|
|
const joinLeaveFilters: Filter[] = [{kinds: [ROOM_JOIN, ROOM_LEAVE], "#h": [h]}]
|
|
|
|
|
|
|
|
|
|
return derived(
|
|
|
|
|
[
|
|
|
|
|
pubkey,
|
|
|
|
|
deriveRoomMembers(url, h),
|
|
|
|
|
deriveEventsForUrl(url, userEventFilters),
|
|
|
|
|
deriveEventsForUrl(url, joinLeaveFilters),
|
|
|
|
|
deriveUserIsRoomAdmin(url, h),
|
|
|
|
|
],
|
|
|
|
|
([$pubkey, $memberList, $userAddRemoveEvents, $joinLeaveEvents, $isAdmin]) => {
|
|
|
|
|
// If admin of this room's space, always granted.
|
|
|
|
|
if ($isAdmin) {
|
|
|
|
|
return MembershipStatus.Granted
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let isMember = false
|
|
|
|
|
|
|
|
|
|
if ($memberList) {
|
|
|
|
|
// Member list exists - check if user is in it.
|
|
|
|
|
isMember = $memberList.includes($pubkey!)
|
|
|
|
|
} else {
|
|
|
|
|
// No member list available - replay the user's add/remove history.
|
|
|
|
|
for (const event of sortEventsAsc($userAddRemoveEvents)) {
|
|
|
|
|
if (event.pubkey !== $pubkey) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (event.kind === ROOM_ADD_MEMBER) {
|
|
|
|
|
isMember = true
|
|
|
|
|
} else if (event.kind === ROOM_REMOVE_MEMBER) {
|
|
|
|
|
isMember = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const event of $joinLeaveEvents) {
|
|
|
|
|
// Join events indicate pending or granted status, leave resets to initial.
|
|
|
|
|
if (event.pubkey !== $pubkey) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (event.kind === ROOM_JOIN) {
|
|
|
|
|
return isMember ? MembershipStatus.Granted : MembershipStatus.Pending
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (event.kind === ROOM_LEAVE) {
|
|
|
|
|
return MembershipStatus.Initial
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return isMember ? MembershipStatus.Granted : MembershipStatus.Initial
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const deriveUserCanCreateRoom = (url: string) => {
|
|
|
|
|
const filters: Filter[] = [{kinds: [ROOM_CREATE_PERMISSION]}]
|
|
|
|
|
|
|
|
|
|
return derived(
|
|
|
|
|
[pubkey, deriveEventsForUrl(url, filters), deriveUserIsSpaceAdmin(url)],
|
|
|
|
|
([$pubkey, $events, $isAdmin]) => {
|
|
|
|
|
for (const event of $events) {
|
|
|
|
|
if (getPubkeyTagValues(event.tags).includes($pubkey!)) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $isAdmin
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const addSpaceMembers = async (
|
|
|
|
|
url: string,
|
|
|
|
|
pubkeys: string[],
|
|
|
|
|
): Promise<string | undefined> => {
|
|
|
|
|
const spaceMembers = get(deriveSpaceMembers(url))
|
|
|
|
|
|
|
|
|
|
const results = await Promise.all(
|
|
|
|
|
pubkeys
|
|
|
|
|
.filter(pubkey => !spaceMembers || !spaceMembers.includes(pubkey))
|
|
|
|
|
.map(pubkey =>
|
|
|
|
|
manageRelay(url, {
|
|
|
|
|
method: ManagementMethod.AllowPubkey,
|
|
|
|
|
params: [pubkey],
|
|
|
|
|
}),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
for (const {error} of results) {
|
|
|
|
|
if (error) {
|
|
|
|
|
return error
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-22 20:36:57 +00:00
|
|
|
export const removeSpaceMembers = async (
|
|
|
|
|
url: string,
|
|
|
|
|
pubkeys: string[],
|
|
|
|
|
): Promise<string | undefined> => {
|
|
|
|
|
const results = await Promise.all(
|
|
|
|
|
pubkeys.map(pubkey =>
|
|
|
|
|
manageRelay(url, {
|
|
|
|
|
method: ManagementMethod.UnallowPubkey,
|
|
|
|
|
params: [pubkey],
|
|
|
|
|
}),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
for (const {error} of results) {
|
|
|
|
|
if (error) {
|
|
|
|
|
return error
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const banSpaceMembers = async (
|
|
|
|
|
url: string,
|
|
|
|
|
pubkeys: string[],
|
|
|
|
|
reason = "",
|
|
|
|
|
): Promise<string | undefined> => {
|
|
|
|
|
const results = await Promise.all(
|
|
|
|
|
pubkeys.map(pubkey =>
|
|
|
|
|
manageRelay(url, {
|
|
|
|
|
method: ManagementMethod.BanPubkey,
|
|
|
|
|
params: reason ? [pubkey, reason] : [pubkey],
|
|
|
|
|
}),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
for (const {error} of results) {
|
|
|
|
|
if (error) {
|
|
|
|
|
return error
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-08 22:33:38 +00:00
|
|
|
export const addRoomMembers = async (
|
|
|
|
|
url: string,
|
|
|
|
|
room: PublishedRoomMeta,
|
|
|
|
|
pubkeys: string[],
|
|
|
|
|
): Promise<string | undefined> => {
|
|
|
|
|
const error = await addSpaceMembers(url, pubkeys)
|
|
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
|
return error
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const errors = await Promise.all(
|
|
|
|
|
pubkeys.map(pubkey => waitForThunkError(addRoomMember(url, room, pubkey))),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
for (const error of errors) {
|
|
|
|
|
if (error) {
|
|
|
|
|
return error
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|