diff --git a/lexicons/tools/ozone/server/describeServer.json b/lexicons/tools/ozone/server/describeServer.json new file mode 100644 index 00000000000..1d688207062 --- /dev/null +++ b/lexicons/tools/ozone/server/describeServer.json @@ -0,0 +1,36 @@ +{ + "lexicon": 1, + "id": "tools.ozone.server.describeServer", + "defs": { + "main": { + "type": "query", + "description": "Describes the server's configuration.", + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["did", "moderators"], + "properties": { + "moderators": { + "type": "array", + "items": { "type": "ref", "ref": "#moderator" }, + "description": "Users that have access to the service and their levels of access." + }, + "did": { + "type": "string", + "format": "did" + } + } + } + } + }, + "moderator": { + "type": "object", + "properties": { + "did": { "type": "string", "format": "did" }, + "handle": { "type": "string", "format": "handle" }, + "role": { "type": "string", "enum": ["admin", "moderator", "triage"] } + } + } + } +} diff --git a/packages/api/src/client/index.ts b/packages/api/src/client/index.ts index 6ef6428b3ea..295465a9254 100644 --- a/packages/api/src/client/index.ts +++ b/packages/api/src/client/index.ts @@ -167,6 +167,7 @@ import * as ToolsOzoneModerationGetRepo from './types/tools/ozone/moderation/get import * as ToolsOzoneModerationQueryEvents from './types/tools/ozone/moderation/queryEvents' import * as ToolsOzoneModerationQueryStatuses from './types/tools/ozone/moderation/queryStatuses' import * as ToolsOzoneModerationSearchRepos from './types/tools/ozone/moderation/searchRepos' +import * as ToolsOzoneServerDescribeServer from './types/tools/ozone/server/describeServer' export * as ComAtprotoAdminDefs from './types/com/atproto/admin/defs' export * as ComAtprotoAdminDeleteAccount from './types/com/atproto/admin/deleteAccount' @@ -328,6 +329,7 @@ export * as ToolsOzoneModerationGetRepo from './types/tools/ozone/moderation/get export * as ToolsOzoneModerationQueryEvents from './types/tools/ozone/moderation/queryEvents' export * as ToolsOzoneModerationQueryStatuses from './types/tools/ozone/moderation/queryStatuses' export * as ToolsOzoneModerationSearchRepos from './types/tools/ozone/moderation/searchRepos' +export * as ToolsOzoneServerDescribeServer from './types/tools/ozone/server/describeServer' export const COM_ATPROTO_MODERATION = { DefsReasonSpam: 'com.atproto.moderation.defs#reasonSpam', @@ -2701,11 +2703,13 @@ export class ToolsOzoneNS { _service: AtpServiceClient communication: ToolsOzoneCommunicationNS moderation: ToolsOzoneModerationNS + server: ToolsOzoneServerNS constructor(service: AtpServiceClient) { this._service = service this.communication = new ToolsOzoneCommunicationNS(service) this.moderation = new ToolsOzoneModerationNS(service) + this.server = new ToolsOzoneServerNS(service) } } @@ -2845,3 +2849,22 @@ export class ToolsOzoneModerationNS { }) } } + +export class ToolsOzoneServerNS { + _service: AtpServiceClient + + constructor(service: AtpServiceClient) { + this._service = service + } + + describeServer( + params?: ToolsOzoneServerDescribeServer.QueryParams, + opts?: ToolsOzoneServerDescribeServer.CallOptions, + ): Promise { + return this._service.xrpc + .call('tools.ozone.server.describeServer', params, undefined, opts) + .catch((e) => { + throw ToolsOzoneServerDescribeServer.toKnownErr(e) + }) + } +} diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index f2886955a36..249a625bbe9 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -9606,6 +9606,65 @@ export const schemaDict = { }, }, }, + ToolsOzoneServerDescribeServer: { + lexicon: 1, + id: 'tools.ozone.server.describeServer', + defs: { + main: { + type: 'query', + description: "Describes the server's configuration.", + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['did', 'moderators'], + properties: { + moderators: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:tools.ozone.server.describeServer#moderator', + }, + description: + 'Users that have access to the service and their levels of access.', + }, + did: { + type: 'string', + format: 'did', + }, + plcUrl: { + type: 'string', + format: 'uri', + description: 'The URL of the PLC server.', + }, + queueConfig: { + type: 'unknown', + description: + 'Configuration used to split subjects in multiple queues.', + }, + }, + }, + }, + }, + moderator: { + type: 'object', + properties: { + did: { + type: 'string', + format: 'did', + }, + handle: { + type: 'string', + format: 'handle', + }, + role: { + type: 'string', + enum: ['admin', 'moderator', 'triage'], + }, + }, + }, + }, + }, } export const schemas: LexiconDoc[] = Object.values(schemaDict) as LexiconDoc[] export const lexicons: Lexicons = new Lexicons(schemas) @@ -9790,4 +9849,5 @@ export const ids = { ToolsOzoneModerationQueryEvents: 'tools.ozone.moderation.queryEvents', ToolsOzoneModerationQueryStatuses: 'tools.ozone.moderation.queryStatuses', ToolsOzoneModerationSearchRepos: 'tools.ozone.moderation.searchRepos', + ToolsOzoneServerDescribeServer: 'tools.ozone.server.describeServer', } diff --git a/packages/api/src/client/types/tools/ozone/server/describeServer.ts b/packages/api/src/client/types/tools/ozone/server/describeServer.ts new file mode 100644 index 00000000000..e348479b84a --- /dev/null +++ b/packages/api/src/client/types/tools/ozone/server/describeServer.ts @@ -0,0 +1,58 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { Headers, XRPCError } from '@atproto/xrpc' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' + +export interface QueryParams {} + +export type InputSchema = undefined + +export interface OutputSchema { + /** Users that have access to the service and their levels of access. */ + moderators: Moderator[] + did: string + /** The URL of the PLC server. */ + plcUrl?: string + /** Configuration used to split subjects in multiple queues. */ + queueConfig?: {} + [k: string]: unknown +} + +export interface CallOptions { + headers?: Headers +} + +export interface Response { + success: boolean + headers: Headers + data: OutputSchema +} + +export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + } + return e +} + +export interface Moderator { + did?: string + handle?: string + role?: 'admin' | 'moderator' | 'triage' + [k: string]: unknown +} + +export function isModerator(v: unknown): v is Moderator { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'tools.ozone.server.describeServer#moderator' + ) +} + +export function validateModerator(v: unknown): ValidationResult { + return lexicons.validate('tools.ozone.server.describeServer#moderator', v) +} diff --git a/packages/ozone/src/api/index.ts b/packages/ozone/src/api/index.ts index d251c021e2a..bc74b57c218 100644 --- a/packages/ozone/src/api/index.ts +++ b/packages/ozone/src/api/index.ts @@ -15,6 +15,7 @@ import createTemplate from './communication/createTemplate' import updateTemplate from './communication/updateTemplate' import deleteTemplate from './communication/deleteTemplate' import listTemplates from './communication/listTemplates' +import describeServer from './server/describeServer' import proxied from './proxied' export * as health from './health' @@ -37,6 +38,7 @@ export default function (server: Server, ctx: AppContext) { createTemplate(server, ctx) updateTemplate(server, ctx) deleteTemplate(server, ctx) + describeServer(server, ctx) proxied(server, ctx) return server } diff --git a/packages/ozone/src/api/server/describeServer.ts b/packages/ozone/src/api/server/describeServer.ts new file mode 100644 index 00000000000..2ff4aa008fb --- /dev/null +++ b/packages/ozone/src/api/server/describeServer.ts @@ -0,0 +1,51 @@ +import { Server } from '../../lexicon' +import AppContext from '../../context' +import { ToolsOzoneServerDescribeServer } from '@atproto/api' +import { getHandle } from '@atproto/identity' + +const moderatorsCache: Map = + new Map() + +export default function (server: Server, ctx: AppContext) { + server.tools.ozone.server.describeServer({ + auth: ctx.authVerifier.modOrAdminToken, + handler: async () => { + const getModeratorDetails = async ( + did: string, + role: ToolsOzoneServerDescribeServer.Moderator['role'], + ) => { + const fromCache = moderatorsCache.get(did) + if (fromCache) { + return fromCache + } + + const didDoc = await ctx.idResolver.did.resolve(did) + + const handle = didDoc ? getHandle(didDoc) : undefined + const details = { handle, did, role } + moderatorsCache.set(did, details) + return details + } + const moderators: ToolsOzoneServerDescribeServer.Moderator[] = + await Promise.all([ + ...ctx.cfg.access.admins.map((did) => + getModeratorDetails(did, 'admin'), + ), + ...ctx.cfg.access.moderators.map((did) => + getModeratorDetails(did, 'moderator'), + ), + ...ctx.cfg.access.triage.map((did) => + getModeratorDetails(did, 'triage'), + ), + ]) + + return { + encoding: 'application/json', + body: { + moderators, + did: ctx.cfg.service.did, + }, + } + }, + }) +} diff --git a/packages/ozone/src/lexicon/index.ts b/packages/ozone/src/lexicon/index.ts index 08568551580..d7c13883c1c 100644 --- a/packages/ozone/src/lexicon/index.ts +++ b/packages/ozone/src/lexicon/index.ts @@ -140,6 +140,7 @@ import * as ToolsOzoneModerationGetRepo from './types/tools/ozone/moderation/get import * as ToolsOzoneModerationQueryEvents from './types/tools/ozone/moderation/queryEvents' import * as ToolsOzoneModerationQueryStatuses from './types/tools/ozone/moderation/queryStatuses' import * as ToolsOzoneModerationSearchRepos from './types/tools/ozone/moderation/searchRepos' +import * as ToolsOzoneServerDescribeServer from './types/tools/ozone/server/describeServer' export const COM_ATPROTO_MODERATION = { DefsReasonSpam: 'com.atproto.moderation.defs#reasonSpam', @@ -1727,11 +1728,13 @@ export class ToolsOzoneNS { _server: Server communication: ToolsOzoneCommunicationNS moderation: ToolsOzoneModerationNS + server: ToolsOzoneServerNS constructor(server: Server) { this._server = server this.communication = new ToolsOzoneCommunicationNS(server) this.moderation = new ToolsOzoneModerationNS(server) + this.server = new ToolsOzoneServerNS(server) } } @@ -1872,6 +1875,25 @@ export class ToolsOzoneModerationNS { } } +export class ToolsOzoneServerNS { + _server: Server + + constructor(server: Server) { + this._server = server + } + + describeServer( + cfg: ConfigOf< + AV, + ToolsOzoneServerDescribeServer.Handler>, + ToolsOzoneServerDescribeServer.HandlerReqCtx> + >, + ) { + const nsid = 'tools.ozone.server.describeServer' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } +} + type SharedRateLimitOpts = { name: string calcKey?: (ctx: T) => string diff --git a/packages/ozone/src/lexicon/lexicons.ts b/packages/ozone/src/lexicon/lexicons.ts index f2886955a36..249a625bbe9 100644 --- a/packages/ozone/src/lexicon/lexicons.ts +++ b/packages/ozone/src/lexicon/lexicons.ts @@ -9606,6 +9606,65 @@ export const schemaDict = { }, }, }, + ToolsOzoneServerDescribeServer: { + lexicon: 1, + id: 'tools.ozone.server.describeServer', + defs: { + main: { + type: 'query', + description: "Describes the server's configuration.", + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['did', 'moderators'], + properties: { + moderators: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:tools.ozone.server.describeServer#moderator', + }, + description: + 'Users that have access to the service and their levels of access.', + }, + did: { + type: 'string', + format: 'did', + }, + plcUrl: { + type: 'string', + format: 'uri', + description: 'The URL of the PLC server.', + }, + queueConfig: { + type: 'unknown', + description: + 'Configuration used to split subjects in multiple queues.', + }, + }, + }, + }, + }, + moderator: { + type: 'object', + properties: { + did: { + type: 'string', + format: 'did', + }, + handle: { + type: 'string', + format: 'handle', + }, + role: { + type: 'string', + enum: ['admin', 'moderator', 'triage'], + }, + }, + }, + }, + }, } export const schemas: LexiconDoc[] = Object.values(schemaDict) as LexiconDoc[] export const lexicons: Lexicons = new Lexicons(schemas) @@ -9790,4 +9849,5 @@ export const ids = { ToolsOzoneModerationQueryEvents: 'tools.ozone.moderation.queryEvents', ToolsOzoneModerationQueryStatuses: 'tools.ozone.moderation.queryStatuses', ToolsOzoneModerationSearchRepos: 'tools.ozone.moderation.searchRepos', + ToolsOzoneServerDescribeServer: 'tools.ozone.server.describeServer', } diff --git a/packages/ozone/src/lexicon/types/tools/ozone/server/describeServer.ts b/packages/ozone/src/lexicon/types/tools/ozone/server/describeServer.ts new file mode 100644 index 00000000000..59b9090e30c --- /dev/null +++ b/packages/ozone/src/lexicon/types/tools/ozone/server/describeServer.ts @@ -0,0 +1,68 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export type InputSchema = undefined + +export interface OutputSchema { + /** Users that have access to the service and their levels of access. */ + moderators: Moderator[] + did: string + /** The URL of the PLC server. */ + plcUrl?: string + /** Configuration used to split subjects in multiple queues. */ + queueConfig?: {} + [k: string]: unknown +} + +export type HandlerInput = undefined + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string +} + +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough +export type HandlerReqCtx = { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput + +export interface Moderator { + did?: string + handle?: string + role?: 'admin' | 'moderator' | 'triage' + [k: string]: unknown +} + +export function isModerator(v: unknown): v is Moderator { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'tools.ozone.server.describeServer#moderator' + ) +} + +export function validateModerator(v: unknown): ValidationResult { + return lexicons.validate('tools.ozone.server.describeServer#moderator', v) +} diff --git a/packages/ozone/tests/__snapshots__/describeServer.test.ts.snap b/packages/ozone/tests/__snapshots__/describeServer.test.ts.snap new file mode 100644 index 00000000000..70abc44bd0e --- /dev/null +++ b/packages/ozone/tests/__snapshots__/describeServer.test.ts.snap @@ -0,0 +1,29 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`moderation status language tagging gets server details with moderators 1`] = ` +Object { + "did": "user(4)", + "moderators": Array [ + Object { + "did": "user(0)", + "handle": "admin.ozone", + "role": "admin", + }, + Object { + "did": "user(1)", + "handle": "bsky.test", + "role": "moderator", + }, + Object { + "did": "user(2)", + "handle": "moderator.ozone", + "role": "moderator", + }, + Object { + "did": "user(3)", + "handle": "triage.ozone", + "role": "triage", + }, + ], +} +`; diff --git a/packages/ozone/tests/describeServer.test.ts b/packages/ozone/tests/describeServer.test.ts new file mode 100644 index 00000000000..1447e4ba7d1 --- /dev/null +++ b/packages/ozone/tests/describeServer.test.ts @@ -0,0 +1,40 @@ +import { + ModeratorClient, + SeedClient, + TestNetwork, + basicSeed, +} from '@atproto/dev-env' +import AtpAgent from '@atproto/api' +import { REASONSPAM } from '../src/lexicon/types/com/atproto/moderation/defs' +import { forSnapshot } from './_util' + +describe('moderation status language tagging', () => { + let network: TestNetwork + let agent: AtpAgent + let sc: SeedClient + let ozoneClient: AtpAgent + + beforeAll(async () => { + network = await TestNetwork.create({ + dbPostgresSchema: 'ozone_describe_server_test', + }) + agent = network.pds.getClient() + sc = network.getSeedClient() + ozoneClient = network.ozone.getClient() + await basicSeed(sc) + await network.processAll() + }) + + afterAll(async () => { + await network.close() + }) + + it('gets server details with moderators', async () => { + const { data } = await ozoneClient.api.tools.ozone.server.describeServer( + {}, + { headers: await network.ozone.modHeaders() }, + ) + + expect(forSnapshot(data)).toMatchSnapshot() + }) +}) diff --git a/packages/pds/src/api/tools/ozone/index.ts b/packages/pds/src/api/tools/ozone/index.ts index 99ba163e298..3737259665a 100644 --- a/packages/pds/src/api/tools/ozone/index.ts +++ b/packages/pds/src/api/tools/ozone/index.ts @@ -2,8 +2,10 @@ import AppContext from '../../../context' import { Server } from '../../../lexicon' import communication from './communication' import moderation from './moderation' +import serverRoutes from './server' export default function (server: Server, ctx: AppContext) { communication(server, ctx) moderation(server, ctx) + serverRoutes(server, ctx) } diff --git a/packages/pds/src/api/tools/ozone/server/describeServer.ts b/packages/pds/src/api/tools/ozone/server/describeServer.ts new file mode 100644 index 00000000000..4302c532d22 --- /dev/null +++ b/packages/pds/src/api/tools/ozone/server/describeServer.ts @@ -0,0 +1,13 @@ +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' +import { pipethrough } from '../../../../pipethrough' + +export default function (server: Server, ctx: AppContext) { + server.tools.ozone.server.describeServer({ + auth: ctx.authVerifier.access, + handler: async ({ req, auth }) => { + const requester = auth.credentials.did + return pipethrough(ctx, req, requester) + }, + }) +} diff --git a/packages/pds/src/api/tools/ozone/server/index.ts b/packages/pds/src/api/tools/ozone/server/index.ts new file mode 100644 index 00000000000..39309749d9b --- /dev/null +++ b/packages/pds/src/api/tools/ozone/server/index.ts @@ -0,0 +1,7 @@ +import AppContext from '../../../../context' +import { Server } from '../../../../lexicon' +import describeServer from './describeServer' + +export default function (server: Server, ctx: AppContext) { + describeServer(server, ctx) +} diff --git a/packages/pds/src/lexicon/index.ts b/packages/pds/src/lexicon/index.ts index 08568551580..d7c13883c1c 100644 --- a/packages/pds/src/lexicon/index.ts +++ b/packages/pds/src/lexicon/index.ts @@ -140,6 +140,7 @@ import * as ToolsOzoneModerationGetRepo from './types/tools/ozone/moderation/get import * as ToolsOzoneModerationQueryEvents from './types/tools/ozone/moderation/queryEvents' import * as ToolsOzoneModerationQueryStatuses from './types/tools/ozone/moderation/queryStatuses' import * as ToolsOzoneModerationSearchRepos from './types/tools/ozone/moderation/searchRepos' +import * as ToolsOzoneServerDescribeServer from './types/tools/ozone/server/describeServer' export const COM_ATPROTO_MODERATION = { DefsReasonSpam: 'com.atproto.moderation.defs#reasonSpam', @@ -1727,11 +1728,13 @@ export class ToolsOzoneNS { _server: Server communication: ToolsOzoneCommunicationNS moderation: ToolsOzoneModerationNS + server: ToolsOzoneServerNS constructor(server: Server) { this._server = server this.communication = new ToolsOzoneCommunicationNS(server) this.moderation = new ToolsOzoneModerationNS(server) + this.server = new ToolsOzoneServerNS(server) } } @@ -1872,6 +1875,25 @@ export class ToolsOzoneModerationNS { } } +export class ToolsOzoneServerNS { + _server: Server + + constructor(server: Server) { + this._server = server + } + + describeServer( + cfg: ConfigOf< + AV, + ToolsOzoneServerDescribeServer.Handler>, + ToolsOzoneServerDescribeServer.HandlerReqCtx> + >, + ) { + const nsid = 'tools.ozone.server.describeServer' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } +} + type SharedRateLimitOpts = { name: string calcKey?: (ctx: T) => string diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index f2886955a36..249a625bbe9 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -9606,6 +9606,65 @@ export const schemaDict = { }, }, }, + ToolsOzoneServerDescribeServer: { + lexicon: 1, + id: 'tools.ozone.server.describeServer', + defs: { + main: { + type: 'query', + description: "Describes the server's configuration.", + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['did', 'moderators'], + properties: { + moderators: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:tools.ozone.server.describeServer#moderator', + }, + description: + 'Users that have access to the service and their levels of access.', + }, + did: { + type: 'string', + format: 'did', + }, + plcUrl: { + type: 'string', + format: 'uri', + description: 'The URL of the PLC server.', + }, + queueConfig: { + type: 'unknown', + description: + 'Configuration used to split subjects in multiple queues.', + }, + }, + }, + }, + }, + moderator: { + type: 'object', + properties: { + did: { + type: 'string', + format: 'did', + }, + handle: { + type: 'string', + format: 'handle', + }, + role: { + type: 'string', + enum: ['admin', 'moderator', 'triage'], + }, + }, + }, + }, + }, } export const schemas: LexiconDoc[] = Object.values(schemaDict) as LexiconDoc[] export const lexicons: Lexicons = new Lexicons(schemas) @@ -9790,4 +9849,5 @@ export const ids = { ToolsOzoneModerationQueryEvents: 'tools.ozone.moderation.queryEvents', ToolsOzoneModerationQueryStatuses: 'tools.ozone.moderation.queryStatuses', ToolsOzoneModerationSearchRepos: 'tools.ozone.moderation.searchRepos', + ToolsOzoneServerDescribeServer: 'tools.ozone.server.describeServer', } diff --git a/packages/pds/src/lexicon/types/tools/ozone/server/describeServer.ts b/packages/pds/src/lexicon/types/tools/ozone/server/describeServer.ts new file mode 100644 index 00000000000..59b9090e30c --- /dev/null +++ b/packages/pds/src/lexicon/types/tools/ozone/server/describeServer.ts @@ -0,0 +1,68 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export type InputSchema = undefined + +export interface OutputSchema { + /** Users that have access to the service and their levels of access. */ + moderators: Moderator[] + did: string + /** The URL of the PLC server. */ + plcUrl?: string + /** Configuration used to split subjects in multiple queues. */ + queueConfig?: {} + [k: string]: unknown +} + +export type HandlerInput = undefined + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string +} + +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough +export type HandlerReqCtx = { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput + +export interface Moderator { + did?: string + handle?: string + role?: 'admin' | 'moderator' | 'triage' + [k: string]: unknown +} + +export function isModerator(v: unknown): v is Moderator { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'tools.ozone.server.describeServer#moderator' + ) +} + +export function validateModerator(v: unknown): ValidationResult { + return lexicons.validate('tools.ozone.server.describeServer#moderator', v) +}