Keep NIP-86 state on a per-app SpaceManagement plugin and gate controls on named space permissions
This commit is contained in:
parent
f130af4edd
commit
f10dc96c43
23 changed files with 243 additions and 243 deletions
|
|
@ -166,7 +166,7 @@ relay's URL, each call signed with a fresh NIP-98 event. Every method resolves t
|
|||
|
||||
| Methods | Called from |
|
||||
|---|---|
|
||||
| `supportedMethods` | `deriveSpaceSupportedMethods` (`src/app/management.ts`), `Access.prepareInvite` |
|
||||
| `supportedMethods`, `listMethodAssignees`, `listBannedPubkeys` (cached) | `SpaceManagement` (`src/app/management.ts`) |
|
||||
| `banPubkey`, `unbanPubkey`, `allowPubkey`, `unallowPubkey`, `listBannedPubkeys` | `ProfileDetail`, `SpaceMemberMenu`, `SpaceMemberBannedMenu`, `SpaceInvite`, `ReportMenuList`, `addRoomMembers` |
|
||||
| `banEvent` | `RoomItemMenu`, `EventMenu`, `ReportMenuList`, `RoomJoinItem` (dismissing a join request) |
|
||||
| `createRole`, `editRole`, `deleteRole`, `assignRole`, `unassignRole` | `RoleCreate`, `RoleEdit`, `SpaceRoleMenu`, `SpaceMemberRoles`, `RoleAddMembers` |
|
||||
|
|
@ -174,9 +174,14 @@ relay's URL, each call signed with a fresh NIP-98 event. Every method resolves t
|
|||
| `changeRelayName`, `changeRelayDescription`, `changeRelayIcon` | `SpaceEdit` |
|
||||
|
||||
A relay answers `supportedmethods` with what the authenticated pubkey may call, so every control
|
||||
is gated on the method behind it: `deriveSpaceSupportedMethods(url)` (re-checked at most every
|
||||
five minutes per pubkey and URL) and `$supportedMethods.includes("banpubkey")`. Still handle an
|
||||
error from the call, since a listed method can be refused for a particular event or target.
|
||||
is gated on the method behind it through `deriveSpacePermissions(url)`, a store of named booleans
|
||||
(`$permissions.ban`, `$permissions.editRoles`). The composite gates a parent uses to decide whether
|
||||
to render a menu (`memberMenu`, `roleMenu`, `bannedMenu`, `directoryMenu`) are defined there too,
|
||||
so parent and menu can't disagree. The `SpaceManagement` plugin (`spaceManagement`) caches the
|
||||
methods per app, re-checked at most every five minutes per URL, and `Access.prepareInvite` reads
|
||||
them through `loadSupportedMethods`. It also holds the admin and ban lists, which
|
||||
`loadMethodAssignees`/`loadBannedPubkeys` refresh after a change. Still handle an error from the
|
||||
call, since a listed method can be refused for a particular event or target.
|
||||
`deriveUserIsSpaceStaff(url)` is only "the list came back non-empty", which is all there is to go
|
||||
on for the room permissions NIP-86 has no method for — `deriveUserIsRoomAdmin` and
|
||||
`deriveUserCanCreateRoom`, which also takes `ROOM_CREATE_PERMISSION` grants.
|
||||
|
|
|
|||
|
|
@ -117,6 +117,7 @@ they cache is itself a rebinding store. `commandsByUrl` holds `fromApp` stores.
|
|||
| `Statuses` (`statuses.ts`) | `DerivedPlugin` | NIP-38 general status, keyed by pubkey |
|
||||
| `Commands` (`commands.ts`) | `RelayScopedDerivedPlugin` | slash-command definitions, keyed per relay |
|
||||
| `HealthChecks` (`healthChecks.ts`) | none | a plain class over `IApp` exposing `Projection`s |
|
||||
| `SpaceManagement` (`management.ts`) | none | NIP-86 supported methods (5-minute TTL), admins and bans per URL |
|
||||
|
||||
Each is exposed with `usePlugin`. `Statuses` is the minimal shape:
|
||||
|
||||
|
|
@ -197,12 +198,12 @@ export const deriveSpaceActionItems = (url: string) =>
|
|||
[
|
||||
deriveEventsForUrl(url, [{kinds: [REPORT]}]),
|
||||
rooms.get().pendingJoins(url).$,
|
||||
deriveSpaceSupportedMethods(url),
|
||||
deriveSpacePermissions(url),
|
||||
],
|
||||
([$reports, $pendingJoins, $methods]) =>
|
||||
([$reports, $pendingJoins, $permissions]) =>
|
||||
sortEventsDesc([
|
||||
...($methods.includes("banevent") ? $reports : []),
|
||||
...($methods.includes("allowpubkey") ? $pendingJoins : []),
|
||||
...($permissions.deleteContent ? $reports : []),
|
||||
...($permissions.addMembers ? $pendingJoins : []),
|
||||
]),
|
||||
)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -12,13 +12,13 @@ import {
|
|||
MESSAGING_RELAYS,
|
||||
PROFILE,
|
||||
RELAYS,
|
||||
type ManagementResponse,
|
||||
} from "@welshman/util"
|
||||
import {RelayJoin, RelayLeave, RoomJoin, RoomLeave} from "@welshman/domain"
|
||||
import {Sync, User, publish} from "@welshman/app"
|
||||
import {stripPrefix} from "@lib/util"
|
||||
import {app, command, relayManagement, roomLists, thunks, writer} from "@app/core"
|
||||
import {PLATFORM_URL} from "@app/env"
|
||||
import {spaceManagement} from "@app/management"
|
||||
import {relaysMostlyRestricted} from "@app/policies"
|
||||
import {Push} from "@app/push"
|
||||
import {deriveSocket} from "@app/relays"
|
||||
|
|
@ -389,18 +389,15 @@ export class Access {
|
|||
try {
|
||||
const management = relayManagement.get().forUrl(this.url)
|
||||
|
||||
const [{result: methods}, roomInviteResult] = await Promise.all([
|
||||
management.supportedMethods().catch((error): ManagementResponse => {
|
||||
console.error(error)
|
||||
return {}
|
||||
}),
|
||||
const [methods, roomInviteResult] = await Promise.all([
|
||||
spaceManagement.get().loadSupportedMethods(this.url),
|
||||
h ? publishRoomInvite(this.url, h) : Promise.resolve({code: undefined, error: undefined}),
|
||||
// Keep the spinner up long enough that a fast relay doesn't make it flash
|
||||
sleep(300),
|
||||
])
|
||||
|
||||
// A relay reporting methods relay-wide can still come back "blocked" for this user.
|
||||
if (methods?.includes("createclaim")) {
|
||||
if (methods.includes("createclaim")) {
|
||||
const {result: claims} = await management.listClaims()
|
||||
|
||||
if (claims?.[0]) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import {derived} from "svelte/store"
|
||||
import {REPORT, sortEventsDesc} from "@welshman/util"
|
||||
import {rooms} from "@app/core"
|
||||
import {deriveSpaceSupportedMethods} from "@app/management"
|
||||
import {deriveSpacePermissions} from "@app/management"
|
||||
import {deriveEventsForUrl} from "@app/repository"
|
||||
|
||||
// Action items (admin review queue)
|
||||
|
|
@ -12,11 +12,11 @@ export const deriveSpaceActionItems = (url: string) =>
|
|||
[
|
||||
deriveEventsForUrl(url, [{kinds: [REPORT]}]),
|
||||
rooms.get().pendingJoins(url).$,
|
||||
deriveSpaceSupportedMethods(url),
|
||||
deriveSpacePermissions(url),
|
||||
],
|
||||
([$reports, $pendingJoins, $methods]) =>
|
||||
([$reports, $pendingJoins, $permissions]) =>
|
||||
sortEventsDesc([
|
||||
...($methods.includes("banevent") ? $reports : []),
|
||||
...($methods.includes("allowpubkey") ? $pendingJoins : []),
|
||||
...($permissions.deleteContent ? $reports : []),
|
||||
...($permissions.addMembers ? $pendingJoins : []),
|
||||
]),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import {readable} from "svelte/store"
|
||||
import {removeUndefined, spec} from "@welshman/lib"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import UserCircle from "@assets/icons/user-circle.svg?dataurl"
|
||||
|
|
@ -20,7 +21,8 @@
|
|||
import ProfilePinnedNote from "@app/components/ProfilePinnedNote.svelte"
|
||||
import ProfileStatus from "@app/components/ProfileStatus.svelte"
|
||||
import {messagingRelayLists, profiles, relayManagement, user} from "@app/core"
|
||||
import {deriveSpaceSupportedMethods} from "@app/management"
|
||||
import {deriveSpacePermissions, spaceManagement} from "@app/management"
|
||||
import type {BannedPubkeyItem} from "@app/management"
|
||||
import {navigate, popModal, pushModal} from "@app/modal"
|
||||
import {pushToast} from "@app/toast"
|
||||
import {goToChat, makeProfilePath} from "@app/routes"
|
||||
|
|
@ -32,11 +34,9 @@
|
|||
|
||||
const {pubkey, url}: Props = $props()
|
||||
|
||||
const supportedMethods = deriveSpaceSupportedMethods(url)
|
||||
const canBan = $derived($supportedMethods.includes("banpubkey"))
|
||||
const canUnallow = $derived($supportedMethods.includes("unallowpubkey"))
|
||||
const canAllow = $derived($supportedMethods.includes("allowpubkey"))
|
||||
const canListBans = $derived($supportedMethods.includes("listbannedpubkeys"))
|
||||
const permissions = deriveSpacePermissions(url)
|
||||
const bans = url ? $spaceManagement.bannedPubkeys(url) : readable<BannedPubkeyItem[]>([])
|
||||
const isBanned = $derived($bans.some(spec({pubkey})))
|
||||
|
||||
const isSelf = $derived($user.pubkey === pubkey)
|
||||
|
||||
|
|
@ -49,11 +49,18 @@
|
|||
goToChat([pubkey])
|
||||
}
|
||||
|
||||
const loadBans = () => {
|
||||
if (url && $permissions.listBans) {
|
||||
$spaceManagement.loadBannedPubkeys(url)
|
||||
}
|
||||
}
|
||||
|
||||
const report = (error: string | undefined, message: string) => {
|
||||
if (error) {
|
||||
pushToast({theme: "error", message: error})
|
||||
} else {
|
||||
pushToast({message})
|
||||
loadBans()
|
||||
back()
|
||||
}
|
||||
}
|
||||
|
|
@ -81,18 +88,7 @@
|
|||
report(error, "User has successfully been restored!")
|
||||
}
|
||||
|
||||
let isBanned = $state(false)
|
||||
|
||||
$effect(() => {
|
||||
if (url && canListBans) {
|
||||
$relayManagement
|
||||
.forUrl(url)
|
||||
.listBannedPubkeys()
|
||||
.then(({result = []}) => {
|
||||
isBanned = result.some(spec({pubkey}))
|
||||
})
|
||||
}
|
||||
})
|
||||
$effect(loadBans)
|
||||
|
||||
onMount(() => {
|
||||
$messagingRelayLists.load(pubkey)
|
||||
|
|
@ -115,7 +111,7 @@
|
|||
</li>
|
||||
{/if}
|
||||
{#if isBanned}
|
||||
{#if canAllow}
|
||||
{#if $permissions.addMembers}
|
||||
<li>
|
||||
<Button onclick={restoreMember}>
|
||||
<Icon size={4} icon={Restart} />
|
||||
|
|
@ -124,7 +120,7 @@
|
|||
</li>
|
||||
{/if}
|
||||
{:else}
|
||||
{#if canUnallow}
|
||||
{#if $permissions.removeMembers}
|
||||
<li>
|
||||
<Button onclick={removeMember}>
|
||||
<Icon size={4} icon={UserMinus} />
|
||||
|
|
@ -132,7 +128,7 @@
|
|||
</Button>
|
||||
</li>
|
||||
{/if}
|
||||
{#if canBan}
|
||||
{#if $permissions.ban}
|
||||
<li>
|
||||
<Button class="text-error" onclick={banMember}>
|
||||
<Icon size={4} icon={MinusCircle} />
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@
|
|||
import ZapModal from "@app/components/Zap.svelte"
|
||||
import {app, user} from "@app/core"
|
||||
import type {FeedContext} from "@app/feeds"
|
||||
import {deriveSpaceSupportedMethods} from "@app/management"
|
||||
import {deriveSpacePermissions} from "@app/management"
|
||||
import {pushModal} from "@app/modal"
|
||||
import {deriveDisplaysByPubkey} from "@app/social"
|
||||
|
||||
|
|
@ -128,8 +128,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
const supportedMethods = deriveSpaceSupportedMethods(url)
|
||||
const canBanEvent = $derived($supportedMethods.includes("banevent"))
|
||||
const permissions = deriveSpacePermissions(url)
|
||||
|
||||
const onReportClick = () => pushModal(ReportDetails, {url, event})
|
||||
|
||||
|
|
@ -156,7 +155,7 @@
|
|||
|
||||
{#if $reactions.length > 0 || (!hideZaps && $zaps.length > 0) || $reports.length > 0 || children}
|
||||
<div class="flex min-w-0 flex-wrap gap-2">
|
||||
{#if url && $reports.length > 0 && canBanEvent}
|
||||
{#if url && $reports.length > 0 && $permissions.deleteContent}
|
||||
<Button
|
||||
data-tip={`This content has been reported as "${displayList(reportReasons)}".`}
|
||||
class={cx(
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
import Button from "@lib/components/Button.svelte"
|
||||
import Confirm from "@lib/components/Confirm.svelte"
|
||||
import {app, deletes, profiles, relayManagement, relays, user} from "@app/core"
|
||||
import {deriveSpaceSupportedMethods} from "@app/management"
|
||||
import {deriveSpacePermissions} from "@app/management"
|
||||
import {pushToast} from "@app/toast"
|
||||
import {pushModal} from "@app/modal"
|
||||
|
||||
|
|
@ -23,9 +23,7 @@
|
|||
|
||||
const {url, event, onResolved, onClick}: Props = $props()
|
||||
|
||||
const supportedMethods = deriveSpaceSupportedMethods(url)
|
||||
const canBanEvent = $derived($supportedMethods.includes("banevent"))
|
||||
const canBanPubkey = $derived($supportedMethods.includes("banpubkey"))
|
||||
const permissions = deriveSpacePermissions(url)
|
||||
const etag = matchTag(tagSpec("e"), event.tags)
|
||||
const ptag = matchTag(tagSpec("p"), event.tags)
|
||||
|
||||
|
|
@ -108,7 +106,7 @@
|
|||
</Button>
|
||||
</li>
|
||||
{/if}
|
||||
{#if canBanEvent}
|
||||
{#if $permissions.deleteContent}
|
||||
<li>
|
||||
<Button onclick={dismissReport}>
|
||||
<Icon icon={InboxOut} />
|
||||
|
|
@ -124,7 +122,7 @@
|
|||
</li>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if ptag && canBanPubkey}
|
||||
{#if ptag && $permissions.ban}
|
||||
<li>
|
||||
<Button class="text-error" onclick={banMember}>
|
||||
<Icon icon={MinusCircle} />
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
||||
import RoomName from "@app/components/RoomName.svelte"
|
||||
import {app, relayManagement} from "@app/core"
|
||||
import {deriveSpaceSupportedMethods} from "@app/management"
|
||||
import {deriveSpacePermissions} from "@app/management"
|
||||
import {pushModal} from "@app/modal"
|
||||
import {pushToast} from "@app/toast"
|
||||
import {addRoomMembers} from "@app/rooms"
|
||||
|
|
@ -22,8 +22,7 @@
|
|||
|
||||
const h = tagValue(tagSpec("h"), event.tags) || ""
|
||||
|
||||
const supportedMethods = deriveSpaceSupportedMethods(url)
|
||||
const canDismiss = $derived($supportedMethods.includes("banevent"))
|
||||
const permissions = deriveSpacePermissions(url)
|
||||
|
||||
const showProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey, url})
|
||||
|
||||
|
|
@ -78,7 +77,7 @@
|
|||
</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
{#if canDismiss}
|
||||
{#if $permissions.deleteContent}
|
||||
<Button class="button button-neutral button-sm" onclick={dismiss} disabled={loading}
|
||||
>Dismiss</Button>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -17,12 +17,7 @@
|
|||
import RelayName from "@app/components/RelayName.svelte"
|
||||
import SpaceMemberMethods from "@app/components/SpaceMemberMethods.svelte"
|
||||
import {fromApp} from "@app/core"
|
||||
import {
|
||||
deriveSpaceMethodAssignees,
|
||||
deriveSpaceSupportedMethods,
|
||||
displayManagementMethod,
|
||||
loadSpaceMethodAssignees,
|
||||
} from "@app/management"
|
||||
import {deriveSpacePermissions, displayManagementMethod, spaceManagement} from "@app/management"
|
||||
import {pushModal} from "@app/modal"
|
||||
import {pushToast} from "@app/toast"
|
||||
|
||||
|
|
@ -33,12 +28,9 @@
|
|||
const {url}: Props = $props()
|
||||
|
||||
const relay = fromApp($app => $app.use(Relays).one(url))
|
||||
const assignees = deriveSpaceMethodAssignees(url)
|
||||
const assignees = $spaceManagement.methodAssignees(url)
|
||||
const others = $derived($assignees.filter(({pubkey}) => pubkey !== $relay?.pubkey))
|
||||
const supportedMethods = deriveSpaceSupportedMethods(url)
|
||||
const canEdit = $derived(
|
||||
["assignmethod", "unassignmethod"].some(method => $supportedMethods.includes(method)),
|
||||
)
|
||||
const permissions = deriveSpacePermissions(url)
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
|
|
@ -47,7 +39,7 @@
|
|||
let loading = $state(true)
|
||||
|
||||
onMount(async () => {
|
||||
const error = await loadSpaceMethodAssignees(url)
|
||||
const error = await $spaceManagement.loadMethodAssignees(url)
|
||||
|
||||
if (error) {
|
||||
pushToast({theme: "error", message: error})
|
||||
|
|
@ -85,7 +77,7 @@
|
|||
<div class="min-w-0 flex-1">
|
||||
<Profile {pubkey} {url} />
|
||||
</div>
|
||||
{#if canEdit}
|
||||
{#if $permissions.editAdmins}
|
||||
<Button
|
||||
class="button button-ghost button-sm button-square"
|
||||
aria-label="Edit permissions"
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import ProfileMultiSelect from "@app/components/ProfileMultiSelect.svelte"
|
||||
import {deriveSpaceSupportedMethods} from "@app/management"
|
||||
import {deriveSpacePermissions} from "@app/management"
|
||||
import QRCode from "@app/components/QRCode.svelte"
|
||||
import {Access, makeInviteLink} from "@app/access"
|
||||
import {relayManagement, relayMemberLists} from "@app/core"
|
||||
|
|
@ -73,9 +73,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
const supportedMethods = deriveSpaceSupportedMethods(url)
|
||||
|
||||
const canAddMembers = $derived($supportedMethods.includes("allowpubkey"))
|
||||
const permissions = deriveSpacePermissions(url)
|
||||
|
||||
let canShare = $state(false)
|
||||
let invite = $derived(makeInviteLink({url, claim: $claim}))
|
||||
|
|
@ -158,7 +156,7 @@
|
|||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if canAddMembers}
|
||||
{#if $permissions.addMembers}
|
||||
<Divider>or</Divider>
|
||||
<Field>
|
||||
{#snippet label()}
|
||||
|
|
@ -171,7 +169,7 @@
|
|||
{/if}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
{#if canAddMembers}
|
||||
{#if $permissions.addMembers}
|
||||
<Button class="button button-link" onclick={back}>
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
||||
import SpaceMemberMenu from "@app/components/SpaceMemberMenu.svelte"
|
||||
import RoleBadge from "@app/components/RoleBadge.svelte"
|
||||
import {deriveSpaceSupportedMethods} from "@app/management"
|
||||
import {deriveSpacePermissions} from "@app/management"
|
||||
import {pushModal} from "@app/modal"
|
||||
import {profiles} from "@app/core"
|
||||
|
||||
|
|
@ -19,15 +19,7 @@
|
|||
const {url, pubkey, roles = []}: Props = $props()
|
||||
|
||||
const profileDisplay = $profiles.display(pubkey, [url]).$
|
||||
const supportedMethods = deriveSpaceSupportedMethods(url)
|
||||
const canUnallow = $derived($supportedMethods.includes("unallowpubkey"))
|
||||
const canBan = $derived($supportedMethods.includes("banpubkey"))
|
||||
const canAssign = $derived($supportedMethods.includes("assignrole"))
|
||||
const canUnassign = $derived($supportedMethods.includes("unassignrole"))
|
||||
const canEditMethods = $derived(
|
||||
$supportedMethods.includes("listmethodassignees") &&
|
||||
["assignmethod", "unassignmethod"].some(method => $supportedMethods.includes(method)),
|
||||
)
|
||||
const permissions = deriveSpacePermissions(url)
|
||||
|
||||
const openProfile = () => pushModal(ProfileDetail, {pubkey, url})
|
||||
</script>
|
||||
|
|
@ -53,7 +45,7 @@
|
|||
<ProfileAbout {pubkey} {url} singleLine />
|
||||
</div>
|
||||
</div>
|
||||
{#if canAssign || canUnassign || canUnallow || canBan || canEditMethods}
|
||||
{#if $permissions.memberMenu}
|
||||
<div class="pointer-events-auto shrink-0">
|
||||
<MenuButton
|
||||
class="button button-ghost button-sm button-square"
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import {deriveSpaceSupportedMethods} from "@app/management"
|
||||
import {deriveSpacePermissions, spaceManagement} from "@app/management"
|
||||
import {pushToast} from "@app/toast"
|
||||
import {relayManagement} from "@app/core"
|
||||
|
||||
|
|
@ -16,11 +16,7 @@
|
|||
|
||||
const {url, pubkey, onClick}: Props = $props()
|
||||
|
||||
const supportedMethods = deriveSpaceSupportedMethods(url)
|
||||
const canUnban = $derived($supportedMethods.includes("unbanpubkey"))
|
||||
const canRestore = $derived($supportedMethods.includes("allowpubkey"))
|
||||
|
||||
const back = () => history.back()
|
||||
const permissions = deriveSpacePermissions(url)
|
||||
|
||||
const unbanMember = async () => {
|
||||
const {error} = await $relayManagement.forUrl(url).unbanPubkey(pubkey)
|
||||
|
|
@ -29,7 +25,7 @@
|
|||
pushToast({theme: "error", message: error})
|
||||
} else {
|
||||
pushToast({message: "User has successfully been removed from the ban list!"})
|
||||
back()
|
||||
$spaceManagement.loadBannedPubkeys(url)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -40,7 +36,7 @@
|
|||
pushToast({theme: "error", message: error})
|
||||
} else {
|
||||
pushToast({message: "User has successfully been restored to membership!"})
|
||||
back()
|
||||
$spaceManagement.loadBannedPubkeys(url)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -52,7 +48,7 @@
|
|||
</script>
|
||||
|
||||
<ul class="menu whitespace-nowrap rounded-2xl bg-surface p-2" bind:this={ul}>
|
||||
{#if canUnban}
|
||||
{#if $permissions.unban}
|
||||
<li>
|
||||
<Button onclick={unbanMember}>
|
||||
<Icon icon={CloseCircle} />
|
||||
|
|
@ -60,7 +56,7 @@
|
|||
</Button>
|
||||
</li>
|
||||
{/if}
|
||||
{#if canRestore}
|
||||
{#if $permissions.addMembers}
|
||||
<li>
|
||||
<Button onclick={restoreMember}>
|
||||
<Icon icon={Restart} />
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
import SpaceMemberMethods from "@app/components/SpaceMemberMethods.svelte"
|
||||
import SpaceMemberRoles from "@app/components/SpaceMemberRoles.svelte"
|
||||
import {profiles, relayManagement} from "@app/core"
|
||||
import {deriveSpaceSupportedMethods} from "@app/management"
|
||||
import {deriveSpacePermissions} from "@app/management"
|
||||
import {pushModal} from "@app/modal"
|
||||
import {pushToast} from "@app/toast"
|
||||
|
||||
|
|
@ -22,16 +22,7 @@
|
|||
|
||||
const {url, pubkey, onClick}: Props = $props()
|
||||
|
||||
const supportedMethods = deriveSpaceSupportedMethods(url)
|
||||
|
||||
const canUnallow = $derived($supportedMethods.includes("unallowpubkey"))
|
||||
const canBan = $derived($supportedMethods.includes("banpubkey"))
|
||||
const canAssign = $derived($supportedMethods.includes("assignrole"))
|
||||
const canUnassign = $derived($supportedMethods.includes("unassignrole"))
|
||||
const canEditMethods = $derived(
|
||||
$supportedMethods.includes("listmethodassignees") &&
|
||||
["assignmethod", "unassignmethod"].some(method => $supportedMethods.includes(method)),
|
||||
)
|
||||
const permissions = deriveSpacePermissions(url)
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
|
|
@ -79,7 +70,7 @@
|
|||
</script>
|
||||
|
||||
<ul class="menu whitespace-nowrap rounded-2xl bg-surface p-2" bind:this={ul}>
|
||||
{#if canAssign || canUnassign}
|
||||
{#if $permissions.assignRoles || $permissions.unassignRoles}
|
||||
<li>
|
||||
<Button onclick={editRoles}>
|
||||
<Icon icon={Pen} />
|
||||
|
|
@ -87,7 +78,7 @@
|
|||
</Button>
|
||||
</li>
|
||||
{/if}
|
||||
{#if canEditMethods}
|
||||
{#if $permissions.editMethods}
|
||||
<li>
|
||||
<Button onclick={editMethods}>
|
||||
<Icon icon={ShieldUser} />
|
||||
|
|
@ -95,7 +86,7 @@
|
|||
</Button>
|
||||
</li>
|
||||
{/if}
|
||||
{#if canUnallow}
|
||||
{#if $permissions.removeMembers}
|
||||
<li>
|
||||
<Button onclick={removeMember}>
|
||||
<Icon icon={UserMinus} />
|
||||
|
|
@ -103,7 +94,7 @@
|
|||
</Button>
|
||||
</li>
|
||||
{/if}
|
||||
{#if canBan}
|
||||
{#if $permissions.ban}
|
||||
<li>
|
||||
<Button class="text-error" onclick={banMember}>
|
||||
<Icon icon={MinusCircle} />
|
||||
|
|
|
|||
|
|
@ -13,12 +13,7 @@
|
|||
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import {profiles, relayManagement} from "@app/core"
|
||||
import {
|
||||
MANAGEMENT_METHOD_GROUPS,
|
||||
deriveSpaceMethodAssignees,
|
||||
deriveSpaceSupportedMethods,
|
||||
loadSpaceMethodAssignees,
|
||||
} from "@app/management"
|
||||
import {MANAGEMENT_METHOD_GROUPS, deriveSpacePermissions, spaceManagement} from "@app/management"
|
||||
import {pushToast} from "@app/toast"
|
||||
|
||||
type Props = {
|
||||
|
|
@ -29,9 +24,7 @@
|
|||
const {url, pubkey}: Props = $props()
|
||||
|
||||
const profileDisplay = $profiles.display(pubkey, [url]).$
|
||||
const supportedMethods = deriveSpaceSupportedMethods(url)
|
||||
const canAssign = $derived($supportedMethods.includes("assignmethod"))
|
||||
const canUnassign = $derived($supportedMethods.includes("unassignmethod"))
|
||||
const permissions = deriveSpacePermissions(url)
|
||||
|
||||
// A relay can grant methods this client doesn't know, and leaving them out would revoke them.
|
||||
const groups = $derived.by(() => {
|
||||
|
|
@ -90,7 +83,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
await loadSpaceMethodAssignees(url)
|
||||
await $spaceManagement.loadMethodAssignees(url)
|
||||
|
||||
pushToast({message: "Permissions updated!"})
|
||||
back()
|
||||
|
|
@ -105,13 +98,15 @@
|
|||
let saving = $state(false)
|
||||
|
||||
onMount(async () => {
|
||||
const error = await loadSpaceMethodAssignees(url)
|
||||
const error = await $spaceManagement.loadMethodAssignees(url)
|
||||
|
||||
if (error) {
|
||||
pushToast({theme: "error", message: error})
|
||||
}
|
||||
|
||||
assigned = new Set(get(deriveSpaceMethodAssignees(url)).find(spec({pubkey}))?.methods ?? [])
|
||||
assigned = new Set(
|
||||
get($spaceManagement.methodAssignees(url)).find(spec({pubkey}))?.methods ?? [],
|
||||
)
|
||||
selected = new Set(assigned)
|
||||
loading = false
|
||||
})
|
||||
|
|
@ -139,7 +134,9 @@
|
|||
type="checkbox"
|
||||
class="checkbox"
|
||||
checked={selected.has(method)}
|
||||
disabled={selected.has(method) ? !canUnassign : !canAssign}
|
||||
disabled={selected.has(method)
|
||||
? !$permissions.unassignMethods
|
||||
: !$permissions.assignMethods}
|
||||
onchange={() => toggle(method)} />
|
||||
</label>
|
||||
{/each}
|
||||
|
|
@ -153,7 +150,7 @@
|
|||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</Button>
|
||||
{#if canAssign || canUnassign}
|
||||
{#if $permissions.editAdmins}
|
||||
<Button class="button button-primary" onclick={submit} disabled={saving}>
|
||||
<Spinner loading={saving}>Save changes</Spinner>
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
import RoleItem from "@app/components/RoleItem.svelte"
|
||||
import SpaceRoles from "@app/components/SpaceRoles.svelte"
|
||||
import {app, profiles, relayManagement} from "@app/core"
|
||||
import {deriveUserCanManageRoles} from "@app/management"
|
||||
import {deriveSpacePermissions} from "@app/management"
|
||||
import {deriveSpaceMemberRoles} from "@app/roles"
|
||||
import {pushModal} from "@app/modal"
|
||||
import {pushToast} from "@app/toast"
|
||||
|
|
@ -34,7 +34,7 @@
|
|||
const profileDisplay = $profiles.display(pubkey, [url]).$
|
||||
const memberRoles = deriveSpaceMemberRoles(url)
|
||||
const initial = new Set(get(memberRoles).get(pubkey) ?? [])
|
||||
const canManageRoles = deriveUserCanManageRoles(url)
|
||||
const permissions = deriveSpacePermissions(url)
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
|
|
@ -125,7 +125,7 @@
|
|||
<Button class="button button-primary" onclick={submit} disabled={loading}>
|
||||
<Spinner {loading}>Save changes</Spinner>
|
||||
</Button>
|
||||
{:else if $canManageRoles}
|
||||
{:else if $permissions.manageRoles}
|
||||
<Button class="button button-primary" onclick={manageRoles}>
|
||||
<Icon icon={UsersGroup} />
|
||||
Manage Roles
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import {displayRelayUrl} from "@welshman/util"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
|
|
@ -12,7 +13,7 @@
|
|||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import Profile from "@app/components/Profile.svelte"
|
||||
import SpaceMemberBannedMenu from "@app/components/SpaceMemberBannedMenu.svelte"
|
||||
import {deriveSpaceBannedPubkeyItems, deriveSpaceSupportedMethods} from "@app/management"
|
||||
import {deriveSpacePermissions, spaceManagement} from "@app/management"
|
||||
|
||||
interface Props {
|
||||
url: string
|
||||
|
|
@ -20,12 +21,14 @@
|
|||
|
||||
const {url}: Props = $props()
|
||||
|
||||
const bans = deriveSpaceBannedPubkeyItems(url)
|
||||
const supportedMethods = deriveSpaceSupportedMethods(url)
|
||||
const canUnban = $derived($supportedMethods.includes("unbanpubkey"))
|
||||
const canRestore = $derived($supportedMethods.includes("allowpubkey"))
|
||||
const bans = $spaceManagement.bannedPubkeys(url)
|
||||
const permissions = deriveSpacePermissions(url)
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
onMount(() => {
|
||||
$spaceManagement.loadBannedPubkeys(url)
|
||||
})
|
||||
</script>
|
||||
|
||||
<Modal>
|
||||
|
|
@ -44,7 +47,7 @@
|
|||
<div class="min-w-0 flex-1">
|
||||
<Profile {pubkey} {url} />
|
||||
</div>
|
||||
{#if canUnban || canRestore}
|
||||
{#if $permissions.bannedMenu}
|
||||
<MenuButton component={SpaceMemberBannedMenu} componentProps={{url, pubkey}} />
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
import SpaceActionItems from "@app/components/SpaceActionItems.svelte"
|
||||
import {relays, user} from "@app/core"
|
||||
import {deriveHostedRelay, HOSTING_ENABLED} from "@app/hosting"
|
||||
import {deriveSpaceSupportedMethods} from "@app/management"
|
||||
import {deriveSpacePermissions} from "@app/management"
|
||||
import {userSpaceUrls} from "@app/rooms"
|
||||
import {deriveSpaceActionItems} from "@app/actionItems"
|
||||
import {notificationSettings, deriveShouldNotify, setSpaceNotifications} from "@app/settings"
|
||||
|
|
@ -33,15 +33,7 @@
|
|||
const {url}: Props = $props()
|
||||
|
||||
const relay = $relays.one(url)
|
||||
const supportedMethods = deriveSpaceSupportedMethods(url)
|
||||
const canReview = $derived(
|
||||
["banevent", "allowpubkey"].some(method => $supportedMethods.includes(method)),
|
||||
)
|
||||
const canEditSpace = $derived(
|
||||
["changerelayname", "changerelaydescription", "changerelayicon"].some(method =>
|
||||
$supportedMethods.includes(method),
|
||||
),
|
||||
)
|
||||
const permissions = deriveSpacePermissions(url)
|
||||
const hostedRelay = deriveHostedRelay(url)
|
||||
const actionItems = deriveSpaceActionItems(url)
|
||||
const shouldNotify = deriveShouldNotify(url)
|
||||
|
|
@ -85,7 +77,7 @@
|
|||
{/snippet}
|
||||
|
||||
{@render actionButton(createInvite, LinkRound, "Create Invite")}
|
||||
{#if canReview}
|
||||
{#if $permissions.review}
|
||||
<li>
|
||||
<Button onclick={showActionItems}>
|
||||
<Icon icon={Danger} />
|
||||
|
|
@ -120,7 +112,7 @@
|
|||
Hosting settings
|
||||
</Link>
|
||||
</li>
|
||||
{:else if canEditSpace}
|
||||
{:else if $permissions.editSpace}
|
||||
{@render actionButton(startEdit, Pen, "Edit Space")}
|
||||
{/if}
|
||||
{#if $userSpaceUrls.includes(url)}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
import SpaceActionItems from "@app/components/SpaceActionItems.svelte"
|
||||
import {relays, user} from "@app/core"
|
||||
import {deriveHostedRelay, HOSTING_ENABLED} from "@app/hosting"
|
||||
import {deriveSpaceSupportedMethods} from "@app/management"
|
||||
import {deriveSpacePermissions} from "@app/management"
|
||||
import {userSpaceUrls} from "@app/rooms"
|
||||
import {deriveSpaceActionItems} from "@app/actionItems"
|
||||
import {notificationSettings, deriveShouldNotify, setSpaceNotifications} from "@app/settings"
|
||||
|
|
@ -33,15 +33,7 @@
|
|||
const {url}: Props = $props()
|
||||
|
||||
const relay = $relays.one(url)
|
||||
const supportedMethods = deriveSpaceSupportedMethods(url)
|
||||
const canReview = $derived(
|
||||
["banevent", "allowpubkey"].some(method => $supportedMethods.includes(method)),
|
||||
)
|
||||
const canEditSpace = $derived(
|
||||
["changerelayname", "changerelaydescription", "changerelayicon"].some(method =>
|
||||
$supportedMethods.includes(method),
|
||||
),
|
||||
)
|
||||
const permissions = deriveSpacePermissions(url)
|
||||
const hostedRelay = deriveHostedRelay(url)
|
||||
const actionItems = deriveSpaceActionItems(url)
|
||||
const shouldNotify = deriveShouldNotify(url)
|
||||
|
|
@ -84,7 +76,7 @@
|
|||
{/snippet}
|
||||
|
||||
{@render actionButton(createInvite, LinkRound, "Create Invite")}
|
||||
{#if canReview}
|
||||
{#if $permissions.review}
|
||||
<Button class="button button-neutral w-full justify-start" onclick={showActionItems}>
|
||||
<Icon size={4} icon={Danger} />
|
||||
Action Items ({$actionItems.length})
|
||||
|
|
@ -113,7 +105,7 @@
|
|||
<Icon size={4} icon={ServerPath} />
|
||||
Hosting settings
|
||||
</Link>
|
||||
{:else if canEditSpace}
|
||||
{:else if $permissions.editSpace}
|
||||
{@render actionButton(startEdit, Pen, "Edit Space")}
|
||||
{/if}
|
||||
{#if $userSpaceUrls.includes(url)}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
import RoleEdit from "@app/components/RoleEdit.svelte"
|
||||
import RoleAddMembers from "@app/components/RoleAddMembers.svelte"
|
||||
import {relayManagement} from "@app/core"
|
||||
import {deriveSpaceSupportedMethods} from "@app/management"
|
||||
import {deriveSpacePermissions} from "@app/management"
|
||||
import {pushModal} from "@app/modal"
|
||||
import {pushToast} from "@app/toast"
|
||||
|
||||
|
|
@ -22,10 +22,7 @@
|
|||
|
||||
const {url, role, onClick}: Props = $props()
|
||||
|
||||
const supportedMethods = deriveSpaceSupportedMethods(url)
|
||||
const canEdit = $derived($supportedMethods.includes("editrole"))
|
||||
const canDelete = $derived($supportedMethods.includes("deleterole"))
|
||||
const canAssign = $derived($supportedMethods.includes("assignrole"))
|
||||
const permissions = deriveSpacePermissions(url)
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
|
|
@ -59,7 +56,7 @@
|
|||
</script>
|
||||
|
||||
<ul class="menu whitespace-nowrap rounded-2xl bg-surface p-2" bind:this={ul}>
|
||||
{#if canAssign}
|
||||
{#if $permissions.assignRoles}
|
||||
<li>
|
||||
<Button onclick={addMembers}>
|
||||
<Icon icon={AddCircle} />
|
||||
|
|
@ -67,7 +64,7 @@
|
|||
</Button>
|
||||
</li>
|
||||
{/if}
|
||||
{#if canEdit}
|
||||
{#if $permissions.editRoles}
|
||||
<li>
|
||||
<Button onclick={editRole}>
|
||||
<Icon icon={Pen} />
|
||||
|
|
@ -75,7 +72,7 @@
|
|||
</Button>
|
||||
</li>
|
||||
{/if}
|
||||
{#if canDelete}
|
||||
{#if $permissions.deleteRoles}
|
||||
<li>
|
||||
<Button class="text-error" onclick={confirmDelete}>
|
||||
<Icon icon={TrashBin} />
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
import RoleItem from "@app/components/RoleItem.svelte"
|
||||
import SpaceRoleMenu from "@app/components/SpaceRoleMenu.svelte"
|
||||
import {app} from "@app/core"
|
||||
import {deriveSpaceSupportedMethods} from "@app/management"
|
||||
import {deriveSpacePermissions} from "@app/management"
|
||||
import {pushModal} from "@app/modal"
|
||||
|
||||
type Props = {
|
||||
|
|
@ -29,11 +29,7 @@
|
|||
const relayRoles = $app.use(RelayRoles).forUrl(url).$
|
||||
const roles = $derived(sortBy(role => [role.order(), role.label() ?? ""], $relayRoles))
|
||||
|
||||
const supportedMethods = deriveSpaceSupportedMethods(url)
|
||||
const canCreate = $derived($supportedMethods.includes("createrole"))
|
||||
const canEdit = $derived($supportedMethods.includes("editrole"))
|
||||
const canDelete = $derived($supportedMethods.includes("deleterole"))
|
||||
const canAssign = $derived($supportedMethods.includes("assignrole"))
|
||||
const permissions = deriveSpacePermissions(url)
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
|
|
@ -55,7 +51,7 @@
|
|||
{#each roles as role (role.identifier())}
|
||||
<div class="card card-sm flex justify-between gap-2">
|
||||
<RoleItem {role} />
|
||||
{#if canEdit || canDelete || canAssign}
|
||||
{#if $permissions.roleMenu}
|
||||
<div class="shrink-0">
|
||||
<MenuButton
|
||||
class="button button-ghost button-sm button-square"
|
||||
|
|
@ -73,7 +69,7 @@
|
|||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</Button>
|
||||
{#if canCreate}
|
||||
{#if $permissions.createRoles}
|
||||
<Button class="button button-primary" onclick={createRole}>
|
||||
<Icon icon={AddCircle} />
|
||||
Create Role
|
||||
|
|
|
|||
|
|
@ -1,27 +1,11 @@
|
|||
import {derived, readable, writable} from "svelte/store"
|
||||
import {ago, MINUTE, now, simpleCache} from "@welshman/lib"
|
||||
import {ROOM_CREATE_PERMISSION, hexTags, tagValues} from "@welshman/util"
|
||||
import {Relays} from "@welshman/app"
|
||||
import {fromApp, relayManagement, user} from "@app/core"
|
||||
import {RelayManagement, Relays} from "@welshman/app"
|
||||
import type {IApp} from "@welshman/app"
|
||||
import {fromApp, usePlugin, user} from "@app/core"
|
||||
import {deriveEventsForUrl} from "@app/repository"
|
||||
|
||||
export type BannedPubkeyItem = {
|
||||
pubkey: string
|
||||
reason: string
|
||||
}
|
||||
|
||||
export const deriveSpaceBannedPubkeyItems = (url: string) => {
|
||||
const store = writable<BannedPubkeyItem[]>([])
|
||||
|
||||
relayManagement
|
||||
.get()
|
||||
.forUrl(url)
|
||||
.listBannedPubkeys()
|
||||
.then(({result = []}) => store.set(result))
|
||||
|
||||
return store
|
||||
}
|
||||
|
||||
// `supportedmethods` answers for the pubkey that signed the request, so this catalog is the client's.
|
||||
export const MANAGEMENT_METHOD_GROUPS = [
|
||||
{
|
||||
|
|
@ -92,51 +76,134 @@ export type MethodAssigneeItem = {
|
|||
methods: string[]
|
||||
}
|
||||
|
||||
// The relay answers the same assignments to anyone allowed to ask, so one store per space.
|
||||
export const deriveSpaceMethodAssignees = simpleCache(([url]: [url: string]) =>
|
||||
writable<MethodAssigneeItem[]>([]),
|
||||
)
|
||||
|
||||
export const loadSpaceMethodAssignees = async (url: string) => {
|
||||
const {result, error} = await relayManagement.get().forUrl(url).listMethodAssignees()
|
||||
|
||||
if (result) {
|
||||
deriveSpaceMethodAssignees(url).set(result)
|
||||
}
|
||||
|
||||
return error
|
||||
export type BannedPubkeyItem = {
|
||||
pubkey: string
|
||||
reason: string
|
||||
}
|
||||
|
||||
const deriveSupportedMethodsForPubkey = simpleCache(([, url]: [pubkey: string, url: string]) => {
|
||||
let checkedAt = 0
|
||||
// `supportedmethods` answers for the pubkey that signed the request, so the cache lives on the app.
|
||||
export class SpaceManagement {
|
||||
private methodRequests = new Map<string, {checkedAt: number; request: Promise<string[]>}>()
|
||||
|
||||
return readable<string[]>([], set => {
|
||||
if (checkedAt < ago(5, MINUTE)) {
|
||||
checkedAt = now()
|
||||
constructor(private readonly app: IApp) {}
|
||||
|
||||
relayManagement
|
||||
.get()
|
||||
.forUrl(url)
|
||||
.supportedMethods()
|
||||
.then(({result = []}) => set(result))
|
||||
.catch(error => {
|
||||
checkedAt = 0
|
||||
console.error(error)
|
||||
})
|
||||
loadSupportedMethods = (url: string) => {
|
||||
const cached = this.methodRequests.get(url)
|
||||
|
||||
if (cached && cached.checkedAt >= ago(5, MINUTE)) {
|
||||
return cached.request
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// The request is answered for the pubkey that signed it, so logging in swaps the methods out.
|
||||
export const deriveSpaceSupportedMethods = (url?: string) =>
|
||||
const request = this.app
|
||||
.use(RelayManagement)
|
||||
.forUrl(url)
|
||||
.supportedMethods()
|
||||
.then(({result = []}): string[] => result)
|
||||
.catch((error): string[] => {
|
||||
this.methodRequests.delete(url)
|
||||
console.error(error)
|
||||
return []
|
||||
})
|
||||
|
||||
this.methodRequests.set(url, {checkedAt: now(), request})
|
||||
|
||||
return request
|
||||
}
|
||||
|
||||
supportedMethods = simpleCache(([url]: [url: string]) =>
|
||||
readable<string[]>([], set => {
|
||||
this.loadSupportedMethods(url).then(set)
|
||||
}),
|
||||
)
|
||||
|
||||
methodAssignees = simpleCache(([url]: [url: string]) => writable<MethodAssigneeItem[]>([]))
|
||||
|
||||
loadMethodAssignees = async (url: string) => {
|
||||
const {result, error} = await this.app.use(RelayManagement).forUrl(url).listMethodAssignees()
|
||||
|
||||
if (result) {
|
||||
this.methodAssignees(url).set(result)
|
||||
}
|
||||
|
||||
return error
|
||||
}
|
||||
|
||||
bannedPubkeys = simpleCache(([url]: [url: string]) => writable<BannedPubkeyItem[]>([]))
|
||||
|
||||
loadBannedPubkeys = async (url: string) => {
|
||||
const {result, error} = await this.app.use(RelayManagement).forUrl(url).listBannedPubkeys()
|
||||
|
||||
if (result) {
|
||||
this.bannedPubkeys(url).set(result)
|
||||
}
|
||||
|
||||
return error
|
||||
}
|
||||
}
|
||||
|
||||
export const spaceManagement = usePlugin(SpaceManagement)
|
||||
|
||||
const deriveSpaceSupportedMethods = (url?: string) =>
|
||||
fromApp($app => {
|
||||
if (url && $app.user) {
|
||||
return deriveSupportedMethodsForPubkey($app.user.pubkey, url)
|
||||
return $app.use(SpaceManagement).supportedMethods(url)
|
||||
}
|
||||
|
||||
return readable<string[]>([])
|
||||
})
|
||||
|
||||
const getSpacePermissions = (methods: string[]) => {
|
||||
const can = (method: string) => methods.includes(method)
|
||||
|
||||
const addMembers = can("allowpubkey")
|
||||
const removeMembers = can("unallowpubkey")
|
||||
const ban = can("banpubkey")
|
||||
const unban = can("unbanpubkey")
|
||||
const listBans = can("listbannedpubkeys")
|
||||
const deleteContent = can("banevent")
|
||||
const createRoles = can("createrole")
|
||||
const editRoles = can("editrole")
|
||||
const deleteRoles = can("deleterole")
|
||||
const assignRoles = can("assignrole")
|
||||
const unassignRoles = can("unassignrole")
|
||||
const assignMethods = can("assignmethod")
|
||||
const unassignMethods = can("unassignmethod")
|
||||
const listAdmins = can("listmethodassignees")
|
||||
const editAdmins = assignMethods || unassignMethods
|
||||
const editMethods = listAdmins && editAdmins
|
||||
const manageRoles = createRoles || editRoles || deleteRoles
|
||||
|
||||
return {
|
||||
addMembers,
|
||||
removeMembers,
|
||||
ban,
|
||||
unban,
|
||||
listBans,
|
||||
deleteContent,
|
||||
createRoles,
|
||||
editRoles,
|
||||
deleteRoles,
|
||||
assignRoles,
|
||||
unassignRoles,
|
||||
assignMethods,
|
||||
unassignMethods,
|
||||
listAdmins,
|
||||
editAdmins,
|
||||
editMethods,
|
||||
manageRoles,
|
||||
editSpace: ["changerelayname", "changerelaydescription", "changerelayicon"].some(can),
|
||||
review: deleteContent || addMembers,
|
||||
memberMenu: assignRoles || unassignRoles || removeMembers || ban || editMethods,
|
||||
roleMenu: editRoles || deleteRoles || assignRoles,
|
||||
bannedMenu: unban || addMembers,
|
||||
directoryMenu: manageRoles || listBans || listAdmins,
|
||||
}
|
||||
}
|
||||
|
||||
// Each control is gated on the NIP-86 method behind it, as reported for the signed-in user.
|
||||
export const deriveSpacePermissions = (url?: string) =>
|
||||
derived(deriveSpaceSupportedMethods(url), getSpacePermissions)
|
||||
|
||||
// User
|
||||
|
||||
// Holding any management method makes someone staff, for the room permissions NIP-86 has no method for.
|
||||
|
|
@ -150,11 +217,6 @@ export const deriveUserIsSpaceOwner = (url: string) =>
|
|||
([$user, $relay]) => $user.pubkey === $relay?.pubkey,
|
||||
)
|
||||
|
||||
export const deriveUserCanManageRoles = (url: string) =>
|
||||
derived(deriveSpaceSupportedMethods(url), $methods =>
|
||||
["createrole", "editrole", "deleterole"].some(method => $methods.includes(method)),
|
||||
)
|
||||
|
||||
export const deriveUserCanCreateRoom = (url: string) =>
|
||||
derived(
|
||||
[
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ import {
|
|||
user,
|
||||
writer,
|
||||
} from "@app/core"
|
||||
import {deriveSpaceSupportedMethods, deriveUserIsSpaceStaff} from "@app/management"
|
||||
import {deriveSpacePermissions, deriveUserIsSpaceStaff} from "@app/management"
|
||||
import {makeRoomPath} from "@app/routes"
|
||||
|
||||
export const PROTECTED = ["-"]
|
||||
|
|
@ -224,13 +224,13 @@ export const deriveUserAdminDelete = (url: string, event: TrustedEvent) => {
|
|||
const h = tagValue(tagSpec("h"), event.tags) ?? ""
|
||||
|
||||
return derived(
|
||||
[deriveUserRoomPermissions(url, h), deriveSpaceSupportedMethods(url)],
|
||||
([$permissions, $methods]): Maybe<AdminDelete> => {
|
||||
if (h && $permissions.includes(ROOM_DELETE_EVENT)) {
|
||||
[deriveUserRoomPermissions(url, h), deriveSpacePermissions(url)],
|
||||
([$roomPermissions, $spacePermissions]): Maybe<AdminDelete> => {
|
||||
if (h && $roomPermissions.includes(ROOM_DELETE_EVENT)) {
|
||||
return AdminDelete.Room
|
||||
}
|
||||
|
||||
if ($methods.includes("banevent")) {
|
||||
if ($spacePermissions.deleteContent) {
|
||||
return AdminDelete.Space
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
import SpaceAdmins from "@app/components/SpaceAdmins.svelte"
|
||||
import SpaceRoles from "@app/components/SpaceRoles.svelte"
|
||||
import SpaceMembersBanned from "@app/components/SpaceMembersBanned.svelte"
|
||||
import {deriveSpaceSupportedMethods, deriveUserCanManageRoles} from "@app/management"
|
||||
import {deriveSpacePermissions} from "@app/management"
|
||||
import {deriveSpaceMemberRoles} from "@app/roles"
|
||||
import {relayMemberLists, relayRoles} from "@app/core"
|
||||
import {deriveDisplaysByPubkey} from "@app/social"
|
||||
|
|
@ -32,10 +32,7 @@
|
|||
const roles = $relayRoles.forUrl(url).$
|
||||
const members = $relayMemberLists.forUrl(url)
|
||||
const memberRoles = deriveSpaceMemberRoles(url)
|
||||
const supportedMethods = deriveSpaceSupportedMethods(url)
|
||||
const canManageRoles = deriveUserCanManageRoles(url)
|
||||
const canListBans = $derived($supportedMethods.includes("listbannedpubkeys"))
|
||||
const canListAdmins = $derived($supportedMethods.includes("listmethodassignees"))
|
||||
const permissions = deriveSpacePermissions(url)
|
||||
|
||||
// Each member with their resolved roles (sorted by order).
|
||||
const memberList = derived([members, memberRoles, roles], ([$members, $memberRoles, $roles]) => {
|
||||
|
|
@ -114,7 +111,7 @@
|
|||
<Icon icon={AddCircle} />
|
||||
Invite people
|
||||
</Button>
|
||||
{#if $canManageRoles || canListBans || canListAdmins}
|
||||
{#if $permissions.directoryMenu}
|
||||
<div class="relative">
|
||||
<Button
|
||||
class="button button-neutral button-sm button-square"
|
||||
|
|
@ -127,7 +124,7 @@
|
|||
<ul
|
||||
transition:fly
|
||||
class="menu bg-surface absolute right-0 z-popover mt-2 w-48 gap-1 rounded-2xl p-2">
|
||||
{#if $canManageRoles}
|
||||
{#if $permissions.manageRoles}
|
||||
<li>
|
||||
<Button onclick={manageRoles}>
|
||||
<Icon icon={UsersGroup} />
|
||||
|
|
@ -135,7 +132,7 @@
|
|||
</Button>
|
||||
</li>
|
||||
{/if}
|
||||
{#if canListAdmins}
|
||||
{#if $permissions.listAdmins}
|
||||
<li>
|
||||
<Button onclick={spaceAdmins}>
|
||||
<Icon icon={ShieldUser} />
|
||||
|
|
@ -143,7 +140,7 @@
|
|||
</Button>
|
||||
</li>
|
||||
{/if}
|
||||
{#if canListBans}
|
||||
{#if $permissions.listBans}
|
||||
<li>
|
||||
<Button onclick={bannedMembers}>
|
||||
<Icon icon={MinusCircle} />
|
||||
|
|
|
|||
Loading…
Reference in a new issue