mirror of
https://github.com/block/buzz.git
synced 2026-11-02 07:22:55 +00:00
Signed-off-by: npub1cl47vfhsqpqy9pwndphpm36vcp7vvz5h2js4qpqm5yewzj7nutkq7xyw8c <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> Co-authored-by: npub1cl47vfhsqpqy9pwndphpm36vcp7vvz5h2js4qpqm5yewzj7nutkq7xyw8c <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
194 lines
5.2 KiB
TypeScript
194 lines
5.2 KiB
TypeScript
import { relayClient } from "@/shared/api/relayClient";
|
|
import { invokeTauri, signRelayEvent } from "@/shared/api/tauri";
|
|
import { getIdentity } from "@/shared/api/tauriIdentity";
|
|
import type {
|
|
RelayEvent,
|
|
RelayMember,
|
|
RelayMemberRole,
|
|
} from "@/shared/api/types";
|
|
|
|
const KIND_NIP43_MEMBERSHIP_LIST = 13534;
|
|
const KIND_RELAY_ADMIN_ADD_MEMBER = 9030;
|
|
const KIND_RELAY_ADMIN_REMOVE_MEMBER = 9031;
|
|
const KIND_RELAY_ADMIN_CHANGE_ROLE = 9032;
|
|
|
|
function isRelayMemberRole(
|
|
value: string | undefined,
|
|
): value is RelayMemberRole {
|
|
return value === "owner" || value === "admin" || value === "member";
|
|
}
|
|
|
|
function normalizePubkey(pubkey: string): string {
|
|
return pubkey.trim().toLowerCase();
|
|
}
|
|
|
|
function eventCreatedAtIso(event: RelayEvent): string {
|
|
return new Date(event.created_at * 1_000).toISOString();
|
|
}
|
|
|
|
export type RelayMembershipLookup = {
|
|
/**
|
|
* True when the relay returned a NIP-43 membership snapshot.
|
|
*
|
|
* Open relays do not publish kind:13534, so absence of this snapshot must not
|
|
* be treated as a denial by onboarding.
|
|
*/
|
|
snapshotFound: boolean;
|
|
membershipRequired: boolean;
|
|
membership: RelayMember | null;
|
|
};
|
|
|
|
export function canEditCommunityProfile(
|
|
lookup: RelayMembershipLookup | undefined,
|
|
): boolean {
|
|
const role = lookup?.membership?.role;
|
|
return (
|
|
lookup?.membershipRequired === false || role === "owner" || role === "admin"
|
|
);
|
|
}
|
|
|
|
export function shouldWarnMissingMembershipSnapshot(
|
|
lookup: RelayMembershipLookup | undefined,
|
|
): boolean {
|
|
return lookup?.membershipRequired === true && !lookup.snapshotFound;
|
|
}
|
|
|
|
export function relayMembersFromEvent(event: RelayEvent): RelayMember[] {
|
|
const seen = new Set<string>();
|
|
const members: RelayMember[] = [];
|
|
const createdAt = eventCreatedAtIso(event);
|
|
|
|
for (const tag of event.tags) {
|
|
const [name, rawPubkey, maybeRoleOrRelay, maybePTagRole] = tag;
|
|
if (name !== "member" && name !== "p") continue;
|
|
if (!rawPubkey) continue;
|
|
|
|
const pubkey = normalizePubkey(rawPubkey);
|
|
if (!/^[0-9a-f]{64}$/.test(pubkey) || seen.has(pubkey)) continue;
|
|
seen.add(pubkey);
|
|
|
|
const rawRole = name === "member" ? maybeRoleOrRelay : maybePTagRole;
|
|
const role = isRelayMemberRole(rawRole) ? rawRole : "member";
|
|
|
|
members.push({
|
|
pubkey,
|
|
role,
|
|
addedBy: null,
|
|
createdAt,
|
|
});
|
|
}
|
|
|
|
return members;
|
|
}
|
|
|
|
export function relayMembershipLookupFromEvent(
|
|
event: RelayEvent | null,
|
|
pubkey: string,
|
|
membershipRequired = event !== null,
|
|
): RelayMembershipLookup {
|
|
if (!event) {
|
|
return {
|
|
snapshotFound: false,
|
|
membershipRequired,
|
|
membership: null,
|
|
};
|
|
}
|
|
|
|
const normalizedPubkey = normalizePubkey(pubkey);
|
|
return {
|
|
snapshotFound: true,
|
|
membershipRequired,
|
|
membership:
|
|
relayMembersFromEvent(event).find(
|
|
(member) => normalizePubkey(member.pubkey) === normalizedPubkey,
|
|
) ?? null,
|
|
};
|
|
}
|
|
|
|
async function fetchMembershipListEvent(): Promise<RelayEvent | null> {
|
|
const events = await relayClient.fetchEvents({
|
|
kinds: [KIND_NIP43_MEMBERSHIP_LIST],
|
|
limit: 1,
|
|
});
|
|
|
|
return events[events.length - 1] ?? null;
|
|
}
|
|
|
|
/** Loads the NIP-43 snapshot only when the relay advertises membership support. */
|
|
export async function loadRelayMembershipLookup(
|
|
pubkey: string,
|
|
membershipRequired: boolean,
|
|
fetchSnapshot: () => Promise<RelayEvent | null> = fetchMembershipListEvent,
|
|
): Promise<RelayMembershipLookup> {
|
|
if (!membershipRequired) {
|
|
return relayMembershipLookupFromEvent(null, pubkey, false);
|
|
}
|
|
return relayMembershipLookupFromEvent(await fetchSnapshot(), pubkey, true);
|
|
}
|
|
|
|
export async function listRelayMembers(): Promise<RelayMember[]> {
|
|
const event = await fetchMembershipListEvent();
|
|
return event ? relayMembersFromEvent(event) : [];
|
|
}
|
|
|
|
async function relayRequiresMembership(): Promise<boolean> {
|
|
return invokeTauri<boolean>("relay_requires_membership");
|
|
}
|
|
|
|
export async function getMyRelayMembershipLookup(): Promise<RelayMembershipLookup> {
|
|
const [{ pubkey }, membershipRequired] = await Promise.all([
|
|
getIdentity(),
|
|
relayRequiresMembership(),
|
|
]);
|
|
return loadRelayMembershipLookup(pubkey, membershipRequired);
|
|
}
|
|
|
|
export async function getMyRelayMembership(): Promise<RelayMember | null> {
|
|
return (await getMyRelayMembershipLookup()).membership;
|
|
}
|
|
|
|
async function publishRelayAdminEvent(
|
|
kind: number,
|
|
targetPubkey: string,
|
|
role?: string,
|
|
): Promise<void> {
|
|
const tags = [["p", normalizePubkey(targetPubkey)]];
|
|
if (role) {
|
|
tags.push(["role", role]);
|
|
}
|
|
|
|
const event = await signRelayEvent({
|
|
kind,
|
|
content: "",
|
|
tags,
|
|
});
|
|
|
|
await relayClient.publishEvent(
|
|
event,
|
|
"Timed out while updating relay access.",
|
|
"Failed to update relay access.",
|
|
);
|
|
}
|
|
|
|
export async function addRelayMember(
|
|
targetPubkey: string,
|
|
role: string,
|
|
): Promise<void> {
|
|
await publishRelayAdminEvent(KIND_RELAY_ADMIN_ADD_MEMBER, targetPubkey, role);
|
|
}
|
|
|
|
export async function removeRelayMember(targetPubkey: string): Promise<void> {
|
|
await publishRelayAdminEvent(KIND_RELAY_ADMIN_REMOVE_MEMBER, targetPubkey);
|
|
}
|
|
|
|
export async function changeRelayMemberRole(
|
|
targetPubkey: string,
|
|
newRole: string,
|
|
): Promise<void> {
|
|
await publishRelayAdminEvent(
|
|
KIND_RELAY_ADMIN_CHANGE_ROLE,
|
|
targetPubkey,
|
|
newRole,
|
|
);
|
|
}
|