Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293import { handleWebSocketMessages, makeLivestreamStore,} from "@streamplace/core";import type { StreamplaceAgent } from "streamplace";import { describe, expect, it } from "vitest";import { listModerationPermissionRecords, moderationPermissionsFor, moderatorRecordsFromListRecords, permissionRecordsFromListRecords, removeModeratorDelegations, type ModerationPermissionRecord,} from "./moderation";
const STREAMER = "did:plc:streamer";const MODERATOR = "did:plc:moderator";
function permissionRecord( overrides: Record<string, unknown> = {},): Record<string, unknown> { return { $type: "place.stream.moderation.permission", moderator: MODERATOR, permissions: [], createdAt: "2024-01-01T00:00:00.000Z", ...overrides, };}
describe("permissionRecordsFromListRecords", () => { it("keeps permission records and drops everything else", () => { const records = permissionRecordsFromListRecords([ { value: permissionRecord() }, { value: { $type: "place.stream.chat.message", text: "hi" } }, { value: null }, ]); expect(records).toHaveLength(1); expect(records[0].moderator).toBe(MODERATOR); expect(records[0].uri).toBeUndefined();
const recordsWithURI = permissionRecordsFromListRecords([ { value: permissionRecord(), uri: "at://streamer/permission/3kq" }, ]); expect(recordsWithURI[0].uri).toBe("at://streamer/permission/3kq"); });});
describe("moderatorRecordsFromListRecords", () => { it("derives each record's rkey from its URI", () => { const records = moderatorRecordsFromListRecords([ { value: permissionRecord(), uri: "at://did:plc:streamer/place.stream.moderation.permission/3kq", }, ]); expect(records).toHaveLength(1); expect(records[0].rkey).toBe("3kq"); expect(records[0].moderator).toBe(MODERATOR); });
it("drops non-permission records and keeps the rest addressable", () => { const records = moderatorRecordsFromListRecords([ { value: permissionRecord(), uri: "at://x/permission/aaa" }, { value: { $type: "place.stream.chat.message", text: "hi" } }, { value: null }, ]); expect(records.map((r) => r.rkey)).toEqual(["aaa"]); });
it("falls back to an empty rkey when no URI is present", () => { const records = moderatorRecordsFromListRecords([ { value: permissionRecord() }, ]); expect(records[0].rkey).toBe(""); });});
describe("listModerationPermissionRecords", () => { it("continues through every page of permission records", async () => { const pages = Array.from({ length: 12 }, (_, index) => ({ cursor: index < 11 ? String(index + 1) : undefined, records: [ { uri: `at://${STREAMER}/place.stream.moderation.permission/${index}`, value: permissionRecord({ createdAt: `2024-01-${index + 1}T00:00:00Z`, }), }, ], })); const agent = { com: { atproto: { repo: { listRecords: async ({ cursor }: { cursor?: string }) => ({ data: pages[Number(cursor ?? 0)], }), }, }, }, } as unknown as StreamplaceAgent;
const records = await listModerationPermissionRecords(agent, STREAMER);
expect(records).toHaveLength(12); expect(records.at(-1)?.rkey).toBe("11"); });});
describe("removeModeratorDelegations", () => { it("deletes every record for the moderator, including records on later pages", async () => { const deletedRkeys: string[] = []; const pages = { first: { cursor: "second", records: [ { uri: `at://${STREAMER}/place.stream.moderation.permission/first`, value: permissionRecord(), }, { uri: `at://${STREAMER}/place.stream.moderation.permission/other`, value: permissionRecord({ moderator: "did:plc:other" }), }, ], }, second: { records: [ { uri: `at://${STREAMER}/place.stream.moderation.permission/second`, value: permissionRecord({ permissions: ["hide"], createdAt: "2024-01-02T00:00:00Z", }), }, ], }, }; const agent = { com: { atproto: { repo: { listRecords: async ({ cursor }: { cursor?: string }) => ({ data: cursor ? pages.second : pages.first, }), deleteRecord: async ({ rkey }: { rkey: string }) => { deletedRkeys.push(rkey); }, }, }, }, } as unknown as StreamplaceAgent;
await removeModeratorDelegations(agent, STREAMER, MODERATOR);
expect(deletedRkeys).toEqual(["first", "second"]); });});
describe("moderationPermissionsFor", () => { it("grants everything to the streamer", () => { const perms = moderationPermissionsFor([], STREAMER, STREAMER); expect(perms).toEqual({ canBan: true, canHide: true, canPin: true, canManageLivestream: true, isOwner: true, }); });
it("grants nothing to anonymous viewers", () => { const perms = moderationPermissionsFor([], null, STREAMER); expect(perms).toEqual({ canBan: false, canHide: false, canPin: false, canManageLivestream: false, isOwner: false, }); });
it("grants nothing without a streamer", () => { const perms = moderationPermissionsFor([], MODERATOR, null); expect(perms.isOwner).toBe(false); expect(perms.canBan).toBe(false); });
it("maps delegated permission strings to flags", () => { const records = [ permissionRecord({ permissions: ["ban", "message.pin"], }) as ModerationPermissionRecord, ]; const perms = moderationPermissionsFor(records, MODERATOR, STREAMER); expect(perms).toEqual({ canBan: true, canHide: false, canPin: true, canManageLivestream: false, isOwner: false, }); });
it("merges permissions across multiple delegation records", () => { const records = [ permissionRecord({ permissions: ["ban"] }), permissionRecord({ permissions: ["hide", "livestream.manage"], createdAt: "2024-01-02T00:00:00.000Z", }), ] as ModerationPermissionRecord[]; const perms = moderationPermissionsFor(records, MODERATOR, STREAMER); expect(perms.canBan).toBe(true); expect(perms.canHide).toBe(true); expect(perms.canManageLivestream).toBe(true); });
it("ignores delegations for other moderators", () => { const records = [ permissionRecord({ moderator: "did:plc:someoneelse", permissions: ["ban"], }), ] as ModerationPermissionRecord[]; const perms = moderationPermissionsFor(records, MODERATOR, STREAMER); expect(perms.canBan).toBe(false); });
it("ignores expired delegations", () => { const records = [ permissionRecord({ permissions: ["ban"], expirationTime: "2020-01-01T00:00:00.000Z", }), ] as ModerationPermissionRecord[]; const perms = moderationPermissionsFor(records, MODERATOR, STREAMER); expect(perms.canBan).toBe(false); });
it("honors unexpired delegations", () => { const future = new Date(Date.now() + 60 * 60 * 1000).toISOString(); const records = [ permissionRecord({ permissions: ["ban"], expirationTime: future }), ] as ModerationPermissionRecord[]; const perms = moderationPermissionsFor(records, MODERATOR, STREAMER); expect(perms.canBan).toBe(true); });
it("removes a moderator's controls when the websocket revokes their permission", () => { const store = makeLivestreamStore(); const permission = permissionRecord({ permissions: ["ban", "hide", "message.pin", "livestream.manage"], uri: "at://did:plc:streamer/place.stream.moderation.permission/3kq", }) as ModerationPermissionRecord; store.setState({ moderationPermissions: [permission] });
expect( moderationPermissionsFor( store.getState().moderationPermissions, MODERATOR, STREAMER, ).canBan, ).toBe(true);
store.setState((state) => handleWebSocketMessages(state, [ { $type: "place.stream.moderation.permission", deleted: true, uri: permission.uri, }, ]), );
expect(store.getState().moderationPermissions).toEqual([]); expect( moderationPermissionsFor( store.getState().moderationPermissions, MODERATOR, STREAMER, ), ).toMatchObject({ canBan: false, canHide: false, canPin: false, canManageLivestream: false, isOwner: false, }); });});