From c4b5e53957463c37dd16fdd1b897d4ab02ab8e84 Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Thu, 7 Nov 2024 22:43:30 +0100 Subject: [PATCH] :sparkles: Ozone instance-wide and user-specific settings (#2905) * :sparkles: Settings endpoints are working * :broom: Rename file * :sparkles: Replace ad-hoc manage roles to match team member roles * :recycle: Refactor role names * :sparkles: Polish up * :sparkles: Move to using id for pagination * :memo: Add changeset * :white_check_mark: Update snapshots * :zap: Change column order in setting table index and add did in all queries --- .changeset/tender-needles-ring.md | 6 + lexicons/tools/ozone/setting/defs.json | 63 ++++ lexicons/tools/ozone/setting/listOptions.json | 61 ++++ .../tools/ozone/setting/removeOptions.json | 39 +++ .../tools/ozone/setting/upsertOption.json | 55 ++++ packages/api/src/client/index.ts | 54 +++ packages/api/src/client/lexicons.ts | 223 +++++++++++++ .../client/types/tools/ozone/setting/defs.ts | 37 +++ .../types/tools/ozone/setting/listOptions.ts | 42 +++ .../tools/ozone/setting/removeOptions.ts | 37 +++ .../types/tools/ozone/setting/upsertOption.ts | 46 +++ packages/ozone/src/api/index.ts | 6 + packages/ozone/src/api/setting/listOptions.ts | 44 +++ .../ozone/src/api/setting/removeOptions.ts | 63 ++++ .../ozone/src/api/setting/upsertOption.ts | 142 ++++++++ packages/ozone/src/context.ts | 8 + .../migrations/20241018T205730722Z-setting.ts | 27 ++ packages/ozone/src/db/migrations/index.ts | 1 + packages/ozone/src/db/schema/index.ts | 4 +- packages/ozone/src/db/schema/setting.ts | 24 ++ packages/ozone/src/lexicon/index.ts | 46 +++ packages/ozone/src/lexicon/lexicons.ts | 223 +++++++++++++ .../lexicon/types/tools/ozone/setting/defs.ts | 37 +++ .../types/tools/ozone/setting/listOptions.ts | 53 +++ .../tools/ozone/setting/removeOptions.ts | 49 +++ .../types/tools/ozone/setting/upsertOption.ts | 58 ++++ packages/ozone/src/setting/service.ts | 148 +++++++++ .../tests/__snapshots__/settings.test.ts.snap | 52 +++ packages/ozone/tests/settings.test.ts | 310 ++++++++++++++++++ packages/pds/src/lexicon/index.ts | 46 +++ packages/pds/src/lexicon/lexicons.ts | 223 +++++++++++++ .../lexicon/types/tools/ozone/setting/defs.ts | 37 +++ .../types/tools/ozone/setting/listOptions.ts | 53 +++ .../tools/ozone/setting/removeOptions.ts | 49 +++ .../types/tools/ozone/setting/upsertOption.ts | 58 ++++ 35 files changed, 2423 insertions(+), 1 deletion(-) create mode 100644 .changeset/tender-needles-ring.md create mode 100644 lexicons/tools/ozone/setting/defs.json create mode 100644 lexicons/tools/ozone/setting/listOptions.json create mode 100644 lexicons/tools/ozone/setting/removeOptions.json create mode 100644 lexicons/tools/ozone/setting/upsertOption.json create mode 100644 packages/api/src/client/types/tools/ozone/setting/defs.ts create mode 100644 packages/api/src/client/types/tools/ozone/setting/listOptions.ts create mode 100644 packages/api/src/client/types/tools/ozone/setting/removeOptions.ts create mode 100644 packages/api/src/client/types/tools/ozone/setting/upsertOption.ts create mode 100644 packages/ozone/src/api/setting/listOptions.ts create mode 100644 packages/ozone/src/api/setting/removeOptions.ts create mode 100644 packages/ozone/src/api/setting/upsertOption.ts create mode 100644 packages/ozone/src/db/migrations/20241018T205730722Z-setting.ts create mode 100644 packages/ozone/src/db/schema/setting.ts create mode 100644 packages/ozone/src/lexicon/types/tools/ozone/setting/defs.ts create mode 100644 packages/ozone/src/lexicon/types/tools/ozone/setting/listOptions.ts create mode 100644 packages/ozone/src/lexicon/types/tools/ozone/setting/removeOptions.ts create mode 100644 packages/ozone/src/lexicon/types/tools/ozone/setting/upsertOption.ts create mode 100644 packages/ozone/src/setting/service.ts create mode 100644 packages/ozone/tests/__snapshots__/settings.test.ts.snap create mode 100644 packages/ozone/tests/settings.test.ts create mode 100644 packages/pds/src/lexicon/types/tools/ozone/setting/defs.ts create mode 100644 packages/pds/src/lexicon/types/tools/ozone/setting/listOptions.ts create mode 100644 packages/pds/src/lexicon/types/tools/ozone/setting/removeOptions.ts create mode 100644 packages/pds/src/lexicon/types/tools/ozone/setting/upsertOption.ts diff --git a/.changeset/tender-needles-ring.md b/.changeset/tender-needles-ring.md new file mode 100644 index 00000000000..6852a1dba5c --- /dev/null +++ b/.changeset/tender-needles-ring.md @@ -0,0 +1,6 @@ +--- +"@atproto/ozone": patch +"@atproto/api": patch +--- + +Add user specific and instance-wide settings api for ozone diff --git a/lexicons/tools/ozone/setting/defs.json b/lexicons/tools/ozone/setting/defs.json new file mode 100644 index 00000000000..3694a1a1ee9 --- /dev/null +++ b/lexicons/tools/ozone/setting/defs.json @@ -0,0 +1,63 @@ +{ + "lexicon": 1, + "id": "tools.ozone.setting.defs", + "defs": { + "option": { + "type": "object", + "required": [ + "key", + "value", + "did", + "scope", + "createdBy", + "lastUpdatedBy" + ], + "properties": { + "key": { + "type": "string", + "format": "nsid" + }, + "did": { + "type": "string", + "format": "did" + }, + "value": { + "type": "unknown" + }, + "description": { + "type": "string", + "maxGraphemes": 1024, + "maxLength": 10240 + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "updatedAt": { + "type": "string", + "format": "datetime" + }, + "managerRole": { + "type": "string", + "knownValues": [ + "tools.ozone.team.defs#roleModerator", + "tools.ozone.team.defs#roleTriage", + "tools.ozone.team.defs#roleAdmin" + ] + }, + "scope": { + "type": "string", + "knownValues": ["instance", "personal"] + }, + "createdBy": { + "type": "string", + "format": "did" + }, + "lastUpdatedBy": { + "type": "string", + "format": "did" + } + } + } + } +} diff --git a/lexicons/tools/ozone/setting/listOptions.json b/lexicons/tools/ozone/setting/listOptions.json new file mode 100644 index 00000000000..849b9c1d87d --- /dev/null +++ b/lexicons/tools/ozone/setting/listOptions.json @@ -0,0 +1,61 @@ +{ + "lexicon": 1, + "id": "tools.ozone.setting.listOptions", + "defs": { + "main": { + "type": "query", + "description": "List settings with optional filtering", + "parameters": { + "type": "params", + "properties": { + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 + }, + "cursor": { + "type": "string" + }, + "scope": { + "type": "string", + "knownValues": ["instance", "personal"], + "default": "instance" + }, + "prefix": { + "type": "string", + "description": "Filter keys by prefix" + }, + "keys": { + "type": "array", + "maxLength": 100, + "items": { + "type": "string", + "format": "nsid" + }, + "description": "Filter for only the specified keys. Ignored if prefix is provided" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["options"], + "properties": { + "cursor": { + "type": "string" + }, + "options": { + "type": "array", + "items": { + "type": "ref", + "ref": "tools.ozone.setting.defs#option" + } + } + } + } + } + } + } +} diff --git a/lexicons/tools/ozone/setting/removeOptions.json b/lexicons/tools/ozone/setting/removeOptions.json new file mode 100644 index 00000000000..ae55d97186a --- /dev/null +++ b/lexicons/tools/ozone/setting/removeOptions.json @@ -0,0 +1,39 @@ +{ + "lexicon": 1, + "id": "tools.ozone.setting.removeOptions", + "defs": { + "main": { + "type": "procedure", + "description": "Delete settings by key", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["keys", "scope"], + "properties": { + "keys": { + "type": "array", + "minLength": 1, + "maxLength": 200, + "items": { + "type": "string", + "format": "nsid" + } + }, + "scope": { + "type": "string", + "knownValues": ["instance", "personal"] + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "properties": {} + } + } + } + } +} diff --git a/lexicons/tools/ozone/setting/upsertOption.json b/lexicons/tools/ozone/setting/upsertOption.json new file mode 100644 index 00000000000..86e2cb2d2f0 --- /dev/null +++ b/lexicons/tools/ozone/setting/upsertOption.json @@ -0,0 +1,55 @@ +{ + "lexicon": 1, + "id": "tools.ozone.setting.upsertOption", + "defs": { + "main": { + "type": "procedure", + "description": "Create or update setting option", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["key", "scope", "value"], + "properties": { + "key": { + "type": "string", + "format": "nsid" + }, + "scope": { + "type": "string", + "knownValues": ["instance", "personal"] + }, + "value": { + "type": "unknown" + }, + "description": { + "type": "string", + "maxLength": 2000 + }, + "managerRole": { + "type": "string", + "knownValues": [ + "tools.ozone.team.defs#roleModerator", + "tools.ozone.team.defs#roleTriage", + "tools.ozone.team.defs#roleAdmin" + ] + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["option"], + "properties": { + "option": { + "type": "ref", + "ref": "tools.ozone.setting.defs#option" + } + } + } + } + } + } +} diff --git a/packages/api/src/client/index.ts b/packages/api/src/client/index.ts index 6055d6ac273..3850e0abef7 100644 --- a/packages/api/src/client/index.ts +++ b/packages/api/src/client/index.ts @@ -214,6 +214,10 @@ import * as ToolsOzoneSetDeleteValues from './types/tools/ozone/set/deleteValues import * as ToolsOzoneSetGetValues from './types/tools/ozone/set/getValues' import * as ToolsOzoneSetQuerySets from './types/tools/ozone/set/querySets' import * as ToolsOzoneSetUpsertSet from './types/tools/ozone/set/upsertSet' +import * as ToolsOzoneSettingDefs from './types/tools/ozone/setting/defs' +import * as ToolsOzoneSettingListOptions from './types/tools/ozone/setting/listOptions' +import * as ToolsOzoneSettingRemoveOptions from './types/tools/ozone/setting/removeOptions' +import * as ToolsOzoneSettingUpsertOption from './types/tools/ozone/setting/upsertOption' import * as ToolsOzoneSignatureDefs from './types/tools/ozone/signature/defs' import * as ToolsOzoneSignatureFindCorrelation from './types/tools/ozone/signature/findCorrelation' import * as ToolsOzoneSignatureFindRelatedAccounts from './types/tools/ozone/signature/findRelatedAccounts' @@ -434,6 +438,10 @@ export * as ToolsOzoneSetDeleteValues from './types/tools/ozone/set/deleteValues export * as ToolsOzoneSetGetValues from './types/tools/ozone/set/getValues' export * as ToolsOzoneSetQuerySets from './types/tools/ozone/set/querySets' export * as ToolsOzoneSetUpsertSet from './types/tools/ozone/set/upsertSet' +export * as ToolsOzoneSettingDefs from './types/tools/ozone/setting/defs' +export * as ToolsOzoneSettingListOptions from './types/tools/ozone/setting/listOptions' +export * as ToolsOzoneSettingRemoveOptions from './types/tools/ozone/setting/removeOptions' +export * as ToolsOzoneSettingUpsertOption from './types/tools/ozone/setting/upsertOption' export * as ToolsOzoneSignatureDefs from './types/tools/ozone/signature/defs' export * as ToolsOzoneSignatureFindCorrelation from './types/tools/ozone/signature/findCorrelation' export * as ToolsOzoneSignatureFindRelatedAccounts from './types/tools/ozone/signature/findRelatedAccounts' @@ -3433,6 +3441,7 @@ export class ToolsOzoneNS { moderation: ToolsOzoneModerationNS server: ToolsOzoneServerNS set: ToolsOzoneSetNS + setting: ToolsOzoneSettingNS signature: ToolsOzoneSignatureNS team: ToolsOzoneTeamNS @@ -3442,6 +3451,7 @@ export class ToolsOzoneNS { this.moderation = new ToolsOzoneModerationNS(client) this.server = new ToolsOzoneServerNS(client) this.set = new ToolsOzoneSetNS(client) + this.setting = new ToolsOzoneSettingNS(client) this.signature = new ToolsOzoneSignatureNS(client) this.team = new ToolsOzoneTeamNS(client) } @@ -3701,6 +3711,50 @@ export class ToolsOzoneSetNS { } } +export class ToolsOzoneSettingNS { + _client: XrpcClient + + constructor(client: XrpcClient) { + this._client = client + } + + listOptions( + params?: ToolsOzoneSettingListOptions.QueryParams, + opts?: ToolsOzoneSettingListOptions.CallOptions, + ): Promise { + return this._client.call( + 'tools.ozone.setting.listOptions', + params, + undefined, + opts, + ) + } + + removeOptions( + data?: ToolsOzoneSettingRemoveOptions.InputSchema, + opts?: ToolsOzoneSettingRemoveOptions.CallOptions, + ): Promise { + return this._client.call( + 'tools.ozone.setting.removeOptions', + opts?.qp, + data, + opts, + ) + } + + upsertOption( + data?: ToolsOzoneSettingUpsertOption.InputSchema, + opts?: ToolsOzoneSettingUpsertOption.CallOptions, + ): Promise { + return this._client.call( + 'tools.ozone.setting.upsertOption', + opts?.qp, + data, + opts, + ) + } +} + export class ToolsOzoneSignatureNS { _client: XrpcClient diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index 9042d881745..3d6ae795019 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -12520,6 +12520,225 @@ export const schemaDict = { }, }, }, + ToolsOzoneSettingDefs: { + lexicon: 1, + id: 'tools.ozone.setting.defs', + defs: { + option: { + type: 'object', + required: [ + 'key', + 'value', + 'did', + 'scope', + 'createdBy', + 'lastUpdatedBy', + ], + properties: { + key: { + type: 'string', + format: 'nsid', + }, + did: { + type: 'string', + format: 'did', + }, + value: { + type: 'unknown', + }, + description: { + type: 'string', + maxGraphemes: 1024, + maxLength: 10240, + }, + createdAt: { + type: 'string', + format: 'datetime', + }, + updatedAt: { + type: 'string', + format: 'datetime', + }, + managerRole: { + type: 'string', + knownValues: [ + 'tools.ozone.team.defs#roleModerator', + 'tools.ozone.team.defs#roleTriage', + 'tools.ozone.team.defs#roleAdmin', + ], + }, + scope: { + type: 'string', + knownValues: ['instance', 'personal'], + }, + createdBy: { + type: 'string', + format: 'did', + }, + lastUpdatedBy: { + type: 'string', + format: 'did', + }, + }, + }, + }, + }, + ToolsOzoneSettingListOptions: { + lexicon: 1, + id: 'tools.ozone.setting.listOptions', + defs: { + main: { + type: 'query', + description: 'List settings with optional filtering', + parameters: { + type: 'params', + properties: { + limit: { + type: 'integer', + minimum: 1, + maximum: 100, + default: 50, + }, + cursor: { + type: 'string', + }, + scope: { + type: 'string', + knownValues: ['instance', 'personal'], + default: 'instance', + }, + prefix: { + type: 'string', + description: 'Filter keys by prefix', + }, + keys: { + type: 'array', + maxLength: 100, + items: { + type: 'string', + format: 'nsid', + }, + description: + 'Filter for only the specified keys. Ignored if prefix is provided', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['options'], + properties: { + cursor: { + type: 'string', + }, + options: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:tools.ozone.setting.defs#option', + }, + }, + }, + }, + }, + }, + }, + }, + ToolsOzoneSettingRemoveOptions: { + lexicon: 1, + id: 'tools.ozone.setting.removeOptions', + defs: { + main: { + type: 'procedure', + description: 'Delete settings by key', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['keys', 'scope'], + properties: { + keys: { + type: 'array', + minLength: 1, + maxLength: 200, + items: { + type: 'string', + format: 'nsid', + }, + }, + scope: { + type: 'string', + knownValues: ['instance', 'personal'], + }, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + properties: {}, + }, + }, + }, + }, + }, + ToolsOzoneSettingUpsertOption: { + lexicon: 1, + id: 'tools.ozone.setting.upsertOption', + defs: { + main: { + type: 'procedure', + description: 'Create or update setting option', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['key', 'scope', 'value'], + properties: { + key: { + type: 'string', + format: 'nsid', + }, + scope: { + type: 'string', + knownValues: ['instance', 'personal'], + }, + value: { + type: 'unknown', + }, + description: { + type: 'string', + maxLength: 2000, + }, + managerRole: { + type: 'string', + knownValues: [ + 'tools.ozone.team.defs#roleModerator', + 'tools.ozone.team.defs#roleTriage', + 'tools.ozone.team.defs#roleAdmin', + ], + }, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['option'], + properties: { + option: { + type: 'ref', + ref: 'lex:tools.ozone.setting.defs#option', + }, + }, + }, + }, + }, + }, + }, ToolsOzoneSignatureDefs: { lexicon: 1, id: 'tools.ozone.signature.defs', @@ -13153,6 +13372,10 @@ export const ids = { ToolsOzoneSetGetValues: 'tools.ozone.set.getValues', ToolsOzoneSetQuerySets: 'tools.ozone.set.querySets', ToolsOzoneSetUpsertSet: 'tools.ozone.set.upsertSet', + ToolsOzoneSettingDefs: 'tools.ozone.setting.defs', + ToolsOzoneSettingListOptions: 'tools.ozone.setting.listOptions', + ToolsOzoneSettingRemoveOptions: 'tools.ozone.setting.removeOptions', + ToolsOzoneSettingUpsertOption: 'tools.ozone.setting.upsertOption', ToolsOzoneSignatureDefs: 'tools.ozone.signature.defs', ToolsOzoneSignatureFindCorrelation: 'tools.ozone.signature.findCorrelation', ToolsOzoneSignatureFindRelatedAccounts: diff --git a/packages/api/src/client/types/tools/ozone/setting/defs.ts b/packages/api/src/client/types/tools/ozone/setting/defs.ts new file mode 100644 index 00000000000..0193b78b811 --- /dev/null +++ b/packages/api/src/client/types/tools/ozone/setting/defs.ts @@ -0,0 +1,37 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' + +export interface Option { + key: string + did: string + value: {} + description?: string + createdAt?: string + updatedAt?: string + managerRole?: + | 'tools.ozone.team.defs#roleModerator' + | 'tools.ozone.team.defs#roleTriage' + | 'tools.ozone.team.defs#roleAdmin' + | (string & {}) + scope: 'instance' | 'personal' | (string & {}) + createdBy: string + lastUpdatedBy: string + [k: string]: unknown +} + +export function isOption(v: unknown): v is Option { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'tools.ozone.setting.defs#option' + ) +} + +export function validateOption(v: unknown): ValidationResult { + return lexicons.validate('tools.ozone.setting.defs#option', v) +} diff --git a/packages/api/src/client/types/tools/ozone/setting/listOptions.ts b/packages/api/src/client/types/tools/ozone/setting/listOptions.ts new file mode 100644 index 00000000000..d4a91230440 --- /dev/null +++ b/packages/api/src/client/types/tools/ozone/setting/listOptions.ts @@ -0,0 +1,42 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { HeadersMap, XRPCError } from '@atproto/xrpc' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' +import * as ToolsOzoneSettingDefs from './defs' + +export interface QueryParams { + limit?: number + cursor?: string + scope?: 'instance' | 'personal' | (string & {}) + /** Filter keys by prefix */ + prefix?: string + /** Filter for only the specified keys. Ignored if prefix is provided */ + keys?: string[] +} + +export type InputSchema = undefined + +export interface OutputSchema { + cursor?: string + options: ToolsOzoneSettingDefs.Option[] + [k: string]: unknown +} + +export interface CallOptions { + signal?: AbortSignal + headers?: HeadersMap +} + +export interface Response { + success: boolean + headers: HeadersMap + data: OutputSchema +} + +export function toKnownErr(e: any) { + return e +} diff --git a/packages/api/src/client/types/tools/ozone/setting/removeOptions.ts b/packages/api/src/client/types/tools/ozone/setting/removeOptions.ts new file mode 100644 index 00000000000..2073a3e298a --- /dev/null +++ b/packages/api/src/client/types/tools/ozone/setting/removeOptions.ts @@ -0,0 +1,37 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { HeadersMap, 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 interface InputSchema { + keys: string[] + scope: 'instance' | 'personal' | (string & {}) + [k: string]: unknown +} + +export interface OutputSchema { + [k: string]: unknown +} + +export interface CallOptions { + signal?: AbortSignal + headers?: HeadersMap + qp?: QueryParams + encoding?: 'application/json' +} + +export interface Response { + success: boolean + headers: HeadersMap + data: OutputSchema +} + +export function toKnownErr(e: any) { + return e +} diff --git a/packages/api/src/client/types/tools/ozone/setting/upsertOption.ts b/packages/api/src/client/types/tools/ozone/setting/upsertOption.ts new file mode 100644 index 00000000000..b37e2d790b5 --- /dev/null +++ b/packages/api/src/client/types/tools/ozone/setting/upsertOption.ts @@ -0,0 +1,46 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { HeadersMap, XRPCError } from '@atproto/xrpc' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' +import * as ToolsOzoneSettingDefs from './defs' + +export interface QueryParams {} + +export interface InputSchema { + key: string + scope: 'instance' | 'personal' | (string & {}) + value: {} + description?: string + managerRole?: + | 'tools.ozone.team.defs#roleModerator' + | 'tools.ozone.team.defs#roleTriage' + | 'tools.ozone.team.defs#roleAdmin' + | (string & {}) + [k: string]: unknown +} + +export interface OutputSchema { + option: ToolsOzoneSettingDefs.Option + [k: string]: unknown +} + +export interface CallOptions { + signal?: AbortSignal + headers?: HeadersMap + qp?: QueryParams + encoding?: 'application/json' +} + +export interface Response { + success: boolean + headers: HeadersMap + data: OutputSchema +} + +export function toKnownErr(e: any) { + return e +} diff --git a/packages/ozone/src/api/index.ts b/packages/ozone/src/api/index.ts index 650569eaf6f..a649f426c15 100644 --- a/packages/ozone/src/api/index.ts +++ b/packages/ozone/src/api/index.ts @@ -30,6 +30,9 @@ import upsertSet from './set/upsertSet' import setDeleteValues from './set/deleteValues' import deleteSet from './set/deleteSet' import getRepos from './moderation/getRepos' +import listOptions from './setting/listOptions' +import removeOptions from './setting/removeOptions' +import upsertOption from './setting/upsertOption' export * as health from './health' @@ -66,5 +69,8 @@ export default function (server: Server, ctx: AppContext) { upsertSet(server, ctx) setDeleteValues(server, ctx) deleteSet(server, ctx) + upsertOption(server, ctx) + listOptions(server, ctx) + removeOptions(server, ctx) return server } diff --git a/packages/ozone/src/api/setting/listOptions.ts b/packages/ozone/src/api/setting/listOptions.ts new file mode 100644 index 00000000000..fbaaa0c0bec --- /dev/null +++ b/packages/ozone/src/api/setting/listOptions.ts @@ -0,0 +1,44 @@ +import { AuthRequiredError } from '@atproto/xrpc-server' +import { Server } from '../../lexicon' +import AppContext from '../../context' + +export default function (server: Server, ctx: AppContext) { + server.tools.ozone.setting.listOptions({ + auth: ctx.authVerifier.modOrAdminToken, + handler: async ({ params, auth }) => { + const access = auth.credentials + const db = ctx.db + const { prefix, scope, keys, limit, cursor } = params + let did = ctx.cfg.service.did + + if (scope === 'personal') { + if (access.type !== 'moderator') { + throw new AuthRequiredError( + 'Must use moderator auth to get personal set details', + ) + } + + did = access.iss + } + + const settingService = ctx.settingService(db) + + const result = await settingService.query({ + scope: scope === 'personal' ? 'personal' : 'instance', + did, + keys, + prefix, + limit, + cursor, + }) + + return { + encoding: 'application/json', + body: { + options: result.options.map((option) => settingService.view(option)), + cursor: result.cursor, + }, + } + }, + }) +} diff --git a/packages/ozone/src/api/setting/removeOptions.ts b/packages/ozone/src/api/setting/removeOptions.ts new file mode 100644 index 00000000000..65b15590b2e --- /dev/null +++ b/packages/ozone/src/api/setting/removeOptions.ts @@ -0,0 +1,63 @@ +import { AuthRequiredError } from '@atproto/xrpc-server' +import { Server } from '../../lexicon' +import AppContext from '../../context' +import { Member } from '../../db/schema/member' + +export default function (server: Server, ctx: AppContext) { + server.tools.ozone.setting.removeOptions({ + auth: ctx.authVerifier.modOrAdminToken, + handler: async ({ input, auth }) => { + const access = auth.credentials + const db = ctx.db + const { keys, scope } = input.body + let did = ctx.cfg.service.did + let managerRole: Member['role'][] = [] + + if (scope === 'personal') { + if (access.type !== 'moderator') { + throw new AuthRequiredError( + 'Must use moderator auth to delete personal setting', + ) + } + + did = access.iss + } + + // When attempting to delete an instance setting using admin_token will allow removing any setting + // otherwise, admins can remove settings that are manageable by all roles + // moderators can remove settings that are manageable by moderator and triage roles + // triage can remove settings that are manageable by triage role + if (scope === 'instance') { + managerRole = [ + 'tools.ozone.team.defs#roleModerator', + 'tools.ozone.team.defs#roleTriage', + 'tools.ozone.team.defs#roleAdmin', + ] + + if (access.type !== 'admin_token' && !access.isAdmin) { + if (access.isModerator) { + managerRole = [ + 'tools.ozone.team.defs#roleModerator', + 'tools.ozone.team.defs#roleTriage', + ] + } else if (access.isTriage) { + managerRole = ['tools.ozone.team.defs#roleTriage'] + } + } + } + + const settingService = ctx.settingService(db) + + await settingService.removeOptions(keys, { + scope: scope === 'personal' ? 'personal' : 'instance', + managerRole, + did, + }) + + return { + encoding: 'application/json', + body: {}, + } + }, + }) +} diff --git a/packages/ozone/src/api/setting/upsertOption.ts b/packages/ozone/src/api/setting/upsertOption.ts new file mode 100644 index 00000000000..dc524c7e457 --- /dev/null +++ b/packages/ozone/src/api/setting/upsertOption.ts @@ -0,0 +1,142 @@ +import { AuthRequiredError } from '@atproto/xrpc-server' +import { Server } from '../../lexicon' +import AppContext from '../../context' +import { AdminTokenOutput, ModeratorOutput } from '../../auth-verifier' +import { SettingService } from '../../setting/service' +import { Member } from '../../db/schema/member' +import { ToolsOzoneTeamDefs } from '@atproto/api' +import assert from 'node:assert' + +export default function (server: Server, ctx: AppContext) { + server.tools.ozone.setting.upsertOption({ + auth: ctx.authVerifier.modOrAdminToken, + handler: async ({ input, auth }) => { + const access = auth.credentials + const db = ctx.db + const { key, value, description, managerRole, scope } = input.body + const serviceDid = ctx.cfg.service.did + let ownerDid = serviceDid + + if (scope === 'personal' && access.type !== 'moderator') { + throw new AuthRequiredError( + 'Must use moderator auth to create or update a personal setting', + ) + } + + // if the caller is using moderator auth and storing personal setting + // use the caller's DID as the owner + if (scope === 'personal' && access.type === 'moderator') { + ownerDid = access.iss + } + + const now = new Date() + const baseOption = { + key, + value, + did: ownerDid, + createdBy: ownerDid, + lastUpdatedBy: ownerDid, + description: description || '', + createdAt: now, + updatedAt: now, + } + + const settingService = ctx.settingService(db) + if (scope === 'personal') { + await settingService.upsert({ + ...baseOption, + scope: 'personal', + managerRole: null, + }) + } else { + const manageableRoles = getRolesForInstanceOption(access) + const existingSetting = await getExistingSetting( + settingService, + ownerDid, + key, + 'instance', + ) + + if ( + existingSetting?.managerRole && + !manageableRoles.includes(existingSetting.managerRole) + ) { + throw new AuthRequiredError(`Not permitted to update setting ${key}`) + } + await settingService.upsert({ + ...baseOption, + scope: 'instance', + managerRole: getManagerRole(managerRole), + }) + } + + const newOption = await getExistingSetting( + settingService, + ownerDid, + key, + scope, + ) + assert(newOption, 'Failed to get the updated setting') + + return { + encoding: 'application/json', + body: { + option: settingService.view(newOption), + }, + } + }, + }) +} + +const getExistingSetting = async ( + settingService: SettingService, + did: string, + key: string, + scope: string, +) => { + const result = await settingService.query({ + scope: scope === 'personal' ? 'personal' : 'instance', + keys: [key], + limit: 1, + did, + }) + + return result.options[0] +} + +const getRolesForInstanceOption = ( + access: AdminTokenOutput['credentials'] | ModeratorOutput['credentials'], +) => { + const fullPermission = [ + ToolsOzoneTeamDefs.ROLEADMIN, + ToolsOzoneTeamDefs.ROLEMODERATOR, + ToolsOzoneTeamDefs.ROLETRIAGE, + ] + if (access.type === 'admin_token') { + return fullPermission + } + + if (access.isAdmin) { + return fullPermission + } + + if (access.isModerator) { + return [ToolsOzoneTeamDefs.ROLEMODERATOR, ToolsOzoneTeamDefs.ROLETRIAGE] + } + + return [ToolsOzoneTeamDefs.ROLETRIAGE] +} + +const getManagerRole = (role?: string) => { + let managerRole: Member['role'] | null = null + + if (role === ToolsOzoneTeamDefs.ROLEADMIN) { + managerRole = ToolsOzoneTeamDefs.ROLEADMIN + } else if (role === ToolsOzoneTeamDefs.ROLEMODERATOR) { + managerRole = ToolsOzoneTeamDefs.ROLEMODERATOR + } else if (role === ToolsOzoneTeamDefs.ROLETRIAGE) { + managerRole = ToolsOzoneTeamDefs.ROLETRIAGE + } + + return managerRole +} diff --git a/packages/ozone/src/context.ts b/packages/ozone/src/context.ts index 7739b900bb4..71b8c5b8fd0 100644 --- a/packages/ozone/src/context.ts +++ b/packages/ozone/src/context.ts @@ -27,6 +27,7 @@ import { parseLabelerHeader, } from './util' import { SetService, SetServiceCreator } from './set/service' +import { SettingService, SettingServiceCreator } from './setting/service' export type AppContextOptions = { db: Database @@ -34,6 +35,7 @@ export type AppContextOptions = { modService: ModerationServiceCreator communicationTemplateService: CommunicationTemplateServiceCreator setService: SetServiceCreator + settingService: SettingServiceCreator teamService: TeamServiceCreator appviewAgent: AtpAgent pdsAgent: AtpAgent | undefined @@ -120,6 +122,7 @@ export class AppContext { const communicationTemplateService = CommunicationTemplateService.creator() const teamService = TeamService.creator() const setService = SetService.creator() + const settingService = SettingService.creator() const sequencer = new Sequencer(modService(db)) @@ -137,6 +140,7 @@ export class AppContext { communicationTemplateService, teamService, setService, + settingService, appviewAgent, pdsAgent, chatAgent, @@ -190,6 +194,10 @@ export class AppContext { return this.opts.setService } + get settingService(): SettingServiceCreator { + return this.opts.settingService + } + get appviewAgent(): AtpAgent { return this.opts.appviewAgent } diff --git a/packages/ozone/src/db/migrations/20241018T205730722Z-setting.ts b/packages/ozone/src/db/migrations/20241018T205730722Z-setting.ts new file mode 100644 index 00000000000..6406042d73c --- /dev/null +++ b/packages/ozone/src/db/migrations/20241018T205730722Z-setting.ts @@ -0,0 +1,27 @@ +import { Kysely, sql } from 'kysely' + +export async function up(db: Kysely): Promise { + await db.schema + .createTable('setting') + .addColumn('id', 'serial', (col) => col.primaryKey()) + .addColumn('key', 'text', (col) => col.notNull()) + .addColumn('did', 'text', (col) => col.notNull()) + .addColumn('value', 'jsonb', (col) => col.notNull()) + .addColumn('description', 'text') + .addColumn('createdAt', 'timestamptz', (col) => + col.defaultTo(sql`now()`).notNull(), + ) + .addColumn('updatedAt', 'timestamptz', (col) => + col.defaultTo(sql`now()`).notNull(), + ) + .addColumn('managerRole', 'text') + .addColumn('scope', 'text', (col) => col.notNull()) + .addColumn('createdBy', 'text', (col) => col.notNull()) + .addColumn('lastUpdatedBy', 'text', (col) => col.notNull()) + .addUniqueConstraint('setting_did_scope_key_idx', ['did', 'scope', 'key']) + .execute() +} + +export async function down(db: Kysely): Promise { + await db.schema.dropTable('setting').execute() +} diff --git a/packages/ozone/src/db/migrations/index.ts b/packages/ozone/src/db/migrations/index.ts index db85bad7ad4..8d996398921 100644 --- a/packages/ozone/src/db/migrations/index.ts +++ b/packages/ozone/src/db/migrations/index.ts @@ -15,3 +15,4 @@ export * as _20240903T205730722Z from './20240903T205730722Z-add-template-lang' export * as _20240904T205730722Z from './20240904T205730722Z-add-subject-did-index' export * as _20241001T205730722Z from './20241001T205730722Z-subject-status-review-state-index' export * as _20241008T205730722Z from './20241008T205730722Z-sets' +export * as _20241018T205730722Z from './20241018T205730722Z-setting' diff --git a/packages/ozone/src/db/schema/index.ts b/packages/ozone/src/db/schema/index.ts index c7c0bf81e3b..ba403c802b8 100644 --- a/packages/ozone/src/db/schema/index.ts +++ b/packages/ozone/src/db/schema/index.ts @@ -9,6 +9,7 @@ import * as signingKey from './signing_key' import * as communicationTemplate from './communication_template' import * as set from './ozone_set' import * as member from './member' +import * as setting from './setting' export type DatabaseSchemaType = modEvent.PartialDB & modSubjectStatus.PartialDB & @@ -19,7 +20,8 @@ export type DatabaseSchemaType = modEvent.PartialDB & blobPushEvent.PartialDB & communicationTemplate.PartialDB & set.PartialDB & - member.PartialDB + member.PartialDB & + setting.PartialDB export type DatabaseSchema = Kysely diff --git a/packages/ozone/src/db/schema/setting.ts b/packages/ozone/src/db/schema/setting.ts new file mode 100644 index 00000000000..cc5a0af7bb2 --- /dev/null +++ b/packages/ozone/src/db/schema/setting.ts @@ -0,0 +1,24 @@ +import { Generated, GeneratedAlways } from 'kysely' +import { Member } from './member' + +export const settingTableName = 'setting' + +export type SettingScope = 'personal' | 'instance' + +export interface Setting { + id: GeneratedAlways + key: string + value: Record + managerRole: Member['role'] | null + description: string | null + did: string + scope: SettingScope + lastUpdatedBy: string + createdBy: string + createdAt: Generated + updatedAt: Generated +} + +export type PartialDB = { + [settingTableName]: Setting +} diff --git a/packages/ozone/src/lexicon/index.ts b/packages/ozone/src/lexicon/index.ts index 39bea6ba157..9a81406bfa7 100644 --- a/packages/ozone/src/lexicon/index.ts +++ b/packages/ozone/src/lexicon/index.ts @@ -180,6 +180,9 @@ import * as ToolsOzoneSetDeleteValues from './types/tools/ozone/set/deleteValues import * as ToolsOzoneSetGetValues from './types/tools/ozone/set/getValues' import * as ToolsOzoneSetQuerySets from './types/tools/ozone/set/querySets' import * as ToolsOzoneSetUpsertSet from './types/tools/ozone/set/upsertSet' +import * as ToolsOzoneSettingListOptions from './types/tools/ozone/setting/listOptions' +import * as ToolsOzoneSettingRemoveOptions from './types/tools/ozone/setting/removeOptions' +import * as ToolsOzoneSettingUpsertOption from './types/tools/ozone/setting/upsertOption' import * as ToolsOzoneSignatureFindCorrelation from './types/tools/ozone/signature/findCorrelation' import * as ToolsOzoneSignatureFindRelatedAccounts from './types/tools/ozone/signature/findRelatedAccounts' import * as ToolsOzoneSignatureSearchAccounts from './types/tools/ozone/signature/searchAccounts' @@ -2183,6 +2186,7 @@ export class ToolsOzoneNS { moderation: ToolsOzoneModerationNS server: ToolsOzoneServerNS set: ToolsOzoneSetNS + setting: ToolsOzoneSettingNS signature: ToolsOzoneSignatureNS team: ToolsOzoneTeamNS @@ -2192,6 +2196,7 @@ export class ToolsOzoneNS { this.moderation = new ToolsOzoneModerationNS(server) this.server = new ToolsOzoneServerNS(server) this.set = new ToolsOzoneSetNS(server) + this.setting = new ToolsOzoneSettingNS(server) this.signature = new ToolsOzoneSignatureNS(server) this.team = new ToolsOzoneTeamNS(server) } @@ -2449,6 +2454,47 @@ export class ToolsOzoneSetNS { } } +export class ToolsOzoneSettingNS { + _server: Server + + constructor(server: Server) { + this._server = server + } + + listOptions( + cfg: ConfigOf< + AV, + ToolsOzoneSettingListOptions.Handler>, + ToolsOzoneSettingListOptions.HandlerReqCtx> + >, + ) { + const nsid = 'tools.ozone.setting.listOptions' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + removeOptions( + cfg: ConfigOf< + AV, + ToolsOzoneSettingRemoveOptions.Handler>, + ToolsOzoneSettingRemoveOptions.HandlerReqCtx> + >, + ) { + const nsid = 'tools.ozone.setting.removeOptions' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + upsertOption( + cfg: ConfigOf< + AV, + ToolsOzoneSettingUpsertOption.Handler>, + ToolsOzoneSettingUpsertOption.HandlerReqCtx> + >, + ) { + const nsid = 'tools.ozone.setting.upsertOption' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } +} + export class ToolsOzoneSignatureNS { _server: Server diff --git a/packages/ozone/src/lexicon/lexicons.ts b/packages/ozone/src/lexicon/lexicons.ts index 9042d881745..3d6ae795019 100644 --- a/packages/ozone/src/lexicon/lexicons.ts +++ b/packages/ozone/src/lexicon/lexicons.ts @@ -12520,6 +12520,225 @@ export const schemaDict = { }, }, }, + ToolsOzoneSettingDefs: { + lexicon: 1, + id: 'tools.ozone.setting.defs', + defs: { + option: { + type: 'object', + required: [ + 'key', + 'value', + 'did', + 'scope', + 'createdBy', + 'lastUpdatedBy', + ], + properties: { + key: { + type: 'string', + format: 'nsid', + }, + did: { + type: 'string', + format: 'did', + }, + value: { + type: 'unknown', + }, + description: { + type: 'string', + maxGraphemes: 1024, + maxLength: 10240, + }, + createdAt: { + type: 'string', + format: 'datetime', + }, + updatedAt: { + type: 'string', + format: 'datetime', + }, + managerRole: { + type: 'string', + knownValues: [ + 'tools.ozone.team.defs#roleModerator', + 'tools.ozone.team.defs#roleTriage', + 'tools.ozone.team.defs#roleAdmin', + ], + }, + scope: { + type: 'string', + knownValues: ['instance', 'personal'], + }, + createdBy: { + type: 'string', + format: 'did', + }, + lastUpdatedBy: { + type: 'string', + format: 'did', + }, + }, + }, + }, + }, + ToolsOzoneSettingListOptions: { + lexicon: 1, + id: 'tools.ozone.setting.listOptions', + defs: { + main: { + type: 'query', + description: 'List settings with optional filtering', + parameters: { + type: 'params', + properties: { + limit: { + type: 'integer', + minimum: 1, + maximum: 100, + default: 50, + }, + cursor: { + type: 'string', + }, + scope: { + type: 'string', + knownValues: ['instance', 'personal'], + default: 'instance', + }, + prefix: { + type: 'string', + description: 'Filter keys by prefix', + }, + keys: { + type: 'array', + maxLength: 100, + items: { + type: 'string', + format: 'nsid', + }, + description: + 'Filter for only the specified keys. Ignored if prefix is provided', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['options'], + properties: { + cursor: { + type: 'string', + }, + options: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:tools.ozone.setting.defs#option', + }, + }, + }, + }, + }, + }, + }, + }, + ToolsOzoneSettingRemoveOptions: { + lexicon: 1, + id: 'tools.ozone.setting.removeOptions', + defs: { + main: { + type: 'procedure', + description: 'Delete settings by key', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['keys', 'scope'], + properties: { + keys: { + type: 'array', + minLength: 1, + maxLength: 200, + items: { + type: 'string', + format: 'nsid', + }, + }, + scope: { + type: 'string', + knownValues: ['instance', 'personal'], + }, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + properties: {}, + }, + }, + }, + }, + }, + ToolsOzoneSettingUpsertOption: { + lexicon: 1, + id: 'tools.ozone.setting.upsertOption', + defs: { + main: { + type: 'procedure', + description: 'Create or update setting option', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['key', 'scope', 'value'], + properties: { + key: { + type: 'string', + format: 'nsid', + }, + scope: { + type: 'string', + knownValues: ['instance', 'personal'], + }, + value: { + type: 'unknown', + }, + description: { + type: 'string', + maxLength: 2000, + }, + managerRole: { + type: 'string', + knownValues: [ + 'tools.ozone.team.defs#roleModerator', + 'tools.ozone.team.defs#roleTriage', + 'tools.ozone.team.defs#roleAdmin', + ], + }, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['option'], + properties: { + option: { + type: 'ref', + ref: 'lex:tools.ozone.setting.defs#option', + }, + }, + }, + }, + }, + }, + }, ToolsOzoneSignatureDefs: { lexicon: 1, id: 'tools.ozone.signature.defs', @@ -13153,6 +13372,10 @@ export const ids = { ToolsOzoneSetGetValues: 'tools.ozone.set.getValues', ToolsOzoneSetQuerySets: 'tools.ozone.set.querySets', ToolsOzoneSetUpsertSet: 'tools.ozone.set.upsertSet', + ToolsOzoneSettingDefs: 'tools.ozone.setting.defs', + ToolsOzoneSettingListOptions: 'tools.ozone.setting.listOptions', + ToolsOzoneSettingRemoveOptions: 'tools.ozone.setting.removeOptions', + ToolsOzoneSettingUpsertOption: 'tools.ozone.setting.upsertOption', ToolsOzoneSignatureDefs: 'tools.ozone.signature.defs', ToolsOzoneSignatureFindCorrelation: 'tools.ozone.signature.findCorrelation', ToolsOzoneSignatureFindRelatedAccounts: diff --git a/packages/ozone/src/lexicon/types/tools/ozone/setting/defs.ts b/packages/ozone/src/lexicon/types/tools/ozone/setting/defs.ts new file mode 100644 index 00000000000..8a87cbb0e24 --- /dev/null +++ b/packages/ozone/src/lexicon/types/tools/ozone/setting/defs.ts @@ -0,0 +1,37 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' + +export interface Option { + key: string + did: string + value: {} + description?: string + createdAt?: string + updatedAt?: string + managerRole?: + | 'tools.ozone.team.defs#roleModerator' + | 'tools.ozone.team.defs#roleTriage' + | 'tools.ozone.team.defs#roleAdmin' + | (string & {}) + scope: 'instance' | 'personal' | (string & {}) + createdBy: string + lastUpdatedBy: string + [k: string]: unknown +} + +export function isOption(v: unknown): v is Option { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'tools.ozone.setting.defs#option' + ) +} + +export function validateOption(v: unknown): ValidationResult { + return lexicons.validate('tools.ozone.setting.defs#option', v) +} diff --git a/packages/ozone/src/lexicon/types/tools/ozone/setting/listOptions.ts b/packages/ozone/src/lexicon/types/tools/ozone/setting/listOptions.ts new file mode 100644 index 00000000000..54ad9608df3 --- /dev/null +++ b/packages/ozone/src/lexicon/types/tools/ozone/setting/listOptions.ts @@ -0,0 +1,53 @@ +/** + * 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' +import * as ToolsOzoneSettingDefs from './defs' + +export interface QueryParams { + limit: number + cursor?: string + scope: 'instance' | 'personal' | (string & {}) + /** Filter keys by prefix */ + prefix?: string + /** Filter for only the specified keys. Ignored if prefix is provided */ + keys?: string[] +} + +export type InputSchema = undefined + +export interface OutputSchema { + cursor?: string + options: ToolsOzoneSettingDefs.Option[] + [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 diff --git a/packages/ozone/src/lexicon/types/tools/ozone/setting/removeOptions.ts b/packages/ozone/src/lexicon/types/tools/ozone/setting/removeOptions.ts new file mode 100644 index 00000000000..1d653b314da --- /dev/null +++ b/packages/ozone/src/lexicon/types/tools/ozone/setting/removeOptions.ts @@ -0,0 +1,49 @@ +/** + * 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 interface InputSchema { + keys: string[] + scope: 'instance' | 'personal' | (string & {}) + [k: string]: unknown +} + +export interface OutputSchema { + [k: string]: unknown +} + +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} + +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 diff --git a/packages/ozone/src/lexicon/types/tools/ozone/setting/upsertOption.ts b/packages/ozone/src/lexicon/types/tools/ozone/setting/upsertOption.ts new file mode 100644 index 00000000000..b2e66d073d9 --- /dev/null +++ b/packages/ozone/src/lexicon/types/tools/ozone/setting/upsertOption.ts @@ -0,0 +1,58 @@ +/** + * 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' +import * as ToolsOzoneSettingDefs from './defs' + +export interface QueryParams {} + +export interface InputSchema { + key: string + scope: 'instance' | 'personal' | (string & {}) + value: {} + description?: string + managerRole?: + | 'tools.ozone.team.defs#roleModerator' + | 'tools.ozone.team.defs#roleTriage' + | 'tools.ozone.team.defs#roleAdmin' + | (string & {}) + [k: string]: unknown +} + +export interface OutputSchema { + option: ToolsOzoneSettingDefs.Option + [k: string]: unknown +} + +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} + +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 diff --git a/packages/ozone/src/setting/service.ts b/packages/ozone/src/setting/service.ts new file mode 100644 index 00000000000..353d61442e5 --- /dev/null +++ b/packages/ozone/src/setting/service.ts @@ -0,0 +1,148 @@ +import Database from '../db' +import { Selectable } from 'kysely' +import { Option } from '../lexicon/types/tools/ozone/setting/defs' +import { Setting, SettingScope } from '../db/schema/setting' +import { Member } from '../db/schema/member' +import assert from 'node:assert' +import { InvalidRequestError } from '@atproto/xrpc-server' + +export type SettingServiceCreator = (db: Database) => SettingService + +export class SettingService { + constructor(public db: Database) {} + + static creator() { + return (db: Database) => new SettingService(db) + } + + async query({ + limit = 100, + scope, + did, + cursor, + prefix, + keys, + }: { + limit: number + scope?: 'personal' | 'instance' + did?: string + cursor?: string + prefix?: string + keys?: string[] + }): Promise<{ + options: Selectable[] + cursor?: string + }> { + let builder = this.db.db.selectFrom('setting').selectAll() + + if (prefix) { + builder = builder.where('key', 'like', `${prefix}%`) + } else if (keys?.length) { + builder = builder.where('key', 'in', keys) + } + + if (scope) { + builder = builder.where('scope', '=', scope) + } + + if (did) { + builder = builder.where('did', '=', did) + } + + if (cursor) { + const cursorId = parseInt(cursor, 10) + if (isNaN(cursorId)) { + throw new InvalidRequestError('invalid cursor') + } + builder = builder.where('id', '<', cursorId) + } + + const options = await builder.orderBy('id', 'desc').limit(limit).execute() + + return { + options, + cursor: options[options.length - 1]?.id.toString(), + } + } + + async upsert( + option: Omit & { + createdAt: Date + updatedAt: Date + }, + ): Promise { + await this.db.db + .insertInto('setting') + .values(option) + .onConflict((oc) => { + return oc.columns(['key', 'scope', 'did']).doUpdateSet({ + value: option.value, + updatedAt: option.updatedAt, + description: option.description, + managerRole: option.managerRole, + lastUpdatedBy: option.lastUpdatedBy, + }) + }) + .execute() + } + + async removeOptions( + keys: string[], + filters: { + did?: string + scope: SettingScope + managerRole: Member['role'][] + }, + ): Promise { + if (!keys.length) return + + if (filters.scope === 'personal') { + assert(filters.did, 'did is required for personal scope') + } + + let qb = this.db.db + .deleteFrom('setting') + .where('key', 'in', keys) + .where('scope', '=', filters.scope) + + if (filters.managerRole.length) { + qb = qb.where('managerRole', 'in', filters.managerRole) + } else { + qb = qb.where('managerRole', 'is', null) + } + + if (filters.did) { + qb = qb.where('did', '=', filters.did) + } + + await qb.execute() + } + + view(setting: Selectable): Option { + const { + key, + value, + did, + description, + createdAt, + createdBy, + updatedAt, + lastUpdatedBy, + managerRole, + scope, + } = setting + + return { + key, + value, + did, + scope, + createdBy, + lastUpdatedBy, + managerRole: managerRole || undefined, + description: description || undefined, + createdAt: createdAt.toISOString(), + updatedAt: updatedAt.toISOString(), + } + } +} diff --git a/packages/ozone/tests/__snapshots__/settings.test.ts.snap b/packages/ozone/tests/__snapshots__/settings.test.ts.snap new file mode 100644 index 00000000000..1710658d64b --- /dev/null +++ b/packages/ozone/tests/__snapshots__/settings.test.ts.snap @@ -0,0 +1,52 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ozone-settings listOptions returns all personal settings 1`] = ` +Array [ + Object { + "createdAt": "1970-01-01T00:00:00.000Z", + "createdBy": "user(1)", + "description": "List of external labelers that will be plugged into the client views", + "did": "user(1)", + "key": "tools.ozone.setting.client.externalLabelers", + "lastUpdatedBy": "user(1)", + "managerRole": "tools.ozone.team.defs#roleAdmin", + "scope": "instance", + "updatedAt": "1970-01-01T00:00:00.000Z", + "value": Object { + "dids": Array [ + "user(0)", + ], + }, + }, + Object { + "createdAt": "1970-01-01T00:00:00.000Z", + "createdBy": "user(1)", + "description": "This determines how each queue is balanced when sorted by oldest first", + "did": "user(1)", + "key": "tools.ozone.setting.client.queueHash", + "lastUpdatedBy": "user(1)", + "managerRole": "tools.ozone.team.defs#roleAdmin", + "scope": "instance", + "updatedAt": "1970-01-01T00:00:00.000Z", + "value": Object { + "val": 10.5, + }, + }, + Object { + "createdAt": "1970-01-01T00:00:00.000Z", + "createdBy": "user(1)", + "description": "This determines how many queues the client interface will show", + "did": "user(1)", + "key": "tools.ozone.setting.client.queues", + "lastUpdatedBy": "user(1)", + "managerRole": "tools.ozone.team.defs#roleAdmin", + "scope": "instance", + "updatedAt": "1970-01-01T00:00:00.000Z", + "value": Object { + "stratosphere": Object { + "name": "Stratosphere", + }, + }, + }, +] +`; diff --git a/packages/ozone/tests/settings.test.ts b/packages/ozone/tests/settings.test.ts new file mode 100644 index 00000000000..65b5bb88c91 --- /dev/null +++ b/packages/ozone/tests/settings.test.ts @@ -0,0 +1,310 @@ +import { TestNetwork, SeedClient, basicSeed } from '@atproto/dev-env' +import AtpAgent, { + ToolsOzoneSettingListOptions, + ToolsOzoneSettingUpsertOption, +} from '@atproto/api' +import { ids } from '../src/lexicon/lexicons' +import { SettingScope } from '../dist/db/schema/setting' +import { forSnapshot } from './_util' + +describe('ozone-settings', () => { + let network: TestNetwork + let agent: AtpAgent + let sc: SeedClient + + const upsertOption = async ( + setting: ToolsOzoneSettingUpsertOption.InputSchema, + callerRole: 'admin' | 'moderator' | 'triage' = 'admin', + ) => { + const { data } = await agent.tools.ozone.setting.upsertOption(setting, { + encoding: 'application/json', + headers: await network.ozone.modHeaders( + ids.ToolsOzoneSettingUpsertOption, + callerRole, + ), + }) + + return data + } + + const removeOptions = async ( + keys: string[], + scope: SettingScope, + callerRole: 'admin' | 'moderator' | 'triage' = 'admin', + ) => { + await agent.tools.ozone.setting.removeOptions( + { keys, scope }, + { + encoding: 'application/json', + headers: await network.ozone.modHeaders( + ids.ToolsOzoneSettingRemoveOptions, + callerRole, + ), + }, + ) + } + + const listOptions = async ( + params: ToolsOzoneSettingListOptions.QueryParams, + callerRole: 'admin' | 'moderator' | 'triage' = 'moderator', + ) => { + const { data } = await agent.tools.ozone.setting.listOptions(params, { + headers: await network.ozone.modHeaders( + ids.ToolsOzoneSettingListOptions, + callerRole, + ), + }) + return data + } + + beforeAll(async () => { + network = await TestNetwork.create({ + dbPostgresSchema: 'ozone_settings', + }) + agent = network.ozone.getClient() + sc = network.getSeedClient() + await basicSeed(sc) + await network.processAll() + }) + + afterAll(async () => { + await network.close() + }) + + describe('upsertOption', () => { + afterAll(async () => { + await removeOptions( + ['tools.ozone.setting.upsertTest.labelers'], + 'personal', + ) + }) + it('only allows managerRole to update instance settings', async () => { + await upsertOption({ + scope: 'instance', + key: 'tools.ozone.setting.upsertTest.labelers', + value: { dids: ['did:plc:xyz'] }, + description: 'triage users can not update this', + managerRole: 'tools.ozone.team.defs#roleModerator', + }) + + await expect( + upsertOption( + { + scope: 'instance', + key: 'tools.ozone.setting.upsertTest.labelers', + value: { noDids: 'test' }, + description: 'triage users can not update this', + managerRole: 'tools.ozone.team.defs#roleModerator', + }, + 'triage', + ), + ).rejects.toThrow(/Not permitted/gi) + + await upsertOption( + { + scope: 'instance', + key: 'tools.ozone.setting.upsertTest.labelers', + value: { noDids: 'test' }, + description: + 'My personal labelers that i want to use when browsing ozone', + managerRole: 'tools.ozone.team.defs#roleModerator', + }, + 'moderator', + ) + + const afterUpdatedByModerator = await listOptions( + { + scope: 'instance', + prefix: 'tools.ozone.setting.upsertTest.labelers', + }, + 'moderator', + ) + expect(afterUpdatedByModerator.options[0].value?.['dids']).toBeFalsy() + expect(afterUpdatedByModerator.options[0].value?.['noDids']).toEqual( + 'test', + ) + await upsertOption( + { + scope: 'instance', + key: 'tools.ozone.setting.upsertTest.labelers', + value: { dids: 'test' }, + description: + 'My personal labelers that i want to use when browsing ozone', + managerRole: 'tools.ozone.team.defs#roleModerator', + }, + 'moderator', + ) + + const afterUpdatedByAdmin = await listOptions( + { + scope: 'instance', + prefix: 'tools.ozone.setting.upsertTest.labelers', + }, + 'admin', + ) + expect(afterUpdatedByAdmin.options[0].value?.['noDids']).toBeFalsy() + expect(afterUpdatedByAdmin.options[0].value?.['dids']).toEqual('test') + }) + }) + + describe('listOptions', () => { + beforeAll(async () => { + await Promise.all([ + upsertOption({ + scope: 'instance', + key: 'tools.ozone.setting.client.queues', + value: { stratosphere: { name: 'Stratosphere' } }, + description: + 'This determines how many queues the client interface will show', + managerRole: 'tools.ozone.team.defs#roleAdmin', + }), + upsertOption({ + scope: 'instance', + key: 'tools.ozone.setting.client.queueHash', + value: { val: 10.5 }, + description: + 'This determines how each queue is balanced when sorted by oldest first', + managerRole: 'tools.ozone.team.defs#roleAdmin', + }), + upsertOption({ + scope: 'instance', + key: 'tools.ozone.setting.client.externalLabelers', + value: { dids: ['did:plc:xyz'] }, + description: + 'List of external labelers that will be plugged into the client views', + managerRole: 'tools.ozone.team.defs#roleAdmin', + }), + ]) + }) + + afterAll(async () => { + await removeOptions( + [ + 'tools.ozone.setting.client.queues', + 'tools.ozone.setting.client.queueHash', + 'tools.ozone.setting.client.externalLabelers', + ], + 'instance', + ) + }) + + it('returns all personal settings', async () => { + const result = await listOptions({ prefix: 'tools.ozone.setting.client' }) + expect(result.options.length).toBe(3) + + expect(forSnapshot(result.options)).toMatchSnapshot() + }) + + it('allows paginating options', async () => { + const params = { prefix: 'tools.ozone.setting.client', limit: 1 } + const pageOne = await listOptions(params) + const pageTwo = await listOptions({ + ...params, + cursor: pageOne.cursor, + }) + const pageThree = await listOptions({ + ...params, + cursor: pageTwo.cursor, + }) + const pageFour = await listOptions({ + ...params, + cursor: pageThree.cursor, + }) + + expect(pageFour.options.length).toBe(0) + expect(pageFour.cursor).toBeUndefined() + }) + }) + + describe('removeOptions', () => { + afterAll(async () => { + await Promise.all([ + removeOptions(['tools.ozone.setting.personal.labelers'], 'personal'), + removeOptions( + ['tools.ozone.setting.only.mod', 'tools.ozone.setting.only.admin'], + 'instance', + ), + ]) + }) + + it('only allows the owner to delete personal setting', async () => { + await upsertOption({ + scope: 'personal', + key: 'tools.ozone.setting.personal.labelers', + value: { dids: ['did:plc:xyz'] }, + description: + 'My personal labelers that i want to use when browsing ozone', + managerRole: 'tools.ozone.team.defs#roleOwner', + }) + + // one user can't remove personal setting of another + await removeOptions( + ['tools.ozone.setting.personal.labelers'], + 'personal', + 'triage', + ) + const list = await listOptions({ scope: 'personal' }, 'admin') + expect(list.options.length).toBe(1) + + // the owner of the personal setting can remove their own setting + await removeOptions(['tools.ozone.setting.personal.labelers'], 'personal') + const listAfterRemoval = await listOptions({ scope: 'personal' }, 'admin') + expect(listAfterRemoval.options.length).toBe(0) + }) + + it('only allows managerRole to delete instance setting', async () => { + await Promise.all([ + upsertOption({ + scope: 'instance', + key: 'tools.ozone.setting.only.mod', + value: { dids: ['did:plc:xyz'] }, + description: 'Triage mods can not manage these', + managerRole: 'tools.ozone.team.defs#roleModerator', + }), + upsertOption({ + scope: 'instance', + key: 'tools.ozone.setting.only.admin', + value: { dids: ['did:plc:xyz'] }, + description: 'Moderators or triage mods can not manage these', + managerRole: 'tools.ozone.team.defs#roleAdmin', + }), + ]) + + await Promise.all([ + removeOptions(['tools.ozone.setting.only.mod'], 'instance', 'triage'), + removeOptions( + ['tools.ozone.setting.only.admin'], + 'instance', + 'moderator', + ), + removeOptions(['tools.ozone.setting.only.admin'], 'instance', 'triage'), + ]) + + const afterFailedAttempt = await listOptions( + { scope: 'instance', prefix: 'tools.ozone.setting.only' }, + 'admin', + ) + const keysAfterFailedAttempt = afterFailedAttempt.options.map( + (o) => o.key, + ) + + const keys = [ + 'tools.ozone.setting.only.mod', + 'tools.ozone.setting.only.admin', + ] + + keys.forEach((key) => expect(keysAfterFailedAttempt).toContain(key)) + + await Promise.all([ + removeOptions(['tools.ozone.setting.only.mod'], 'instance', 'admin'), + removeOptions(['tools.ozone.setting.only.admin'], 'instance', 'admin'), + ]) + + const afterRemoval = await listOptions( + { scope: 'instance', prefix: 'tools.ozone.setting.only' }, + 'admin', + ) + expect(afterRemoval.options.length).toBe(0) + }) + }) +}) diff --git a/packages/pds/src/lexicon/index.ts b/packages/pds/src/lexicon/index.ts index 39bea6ba157..9a81406bfa7 100644 --- a/packages/pds/src/lexicon/index.ts +++ b/packages/pds/src/lexicon/index.ts @@ -180,6 +180,9 @@ import * as ToolsOzoneSetDeleteValues from './types/tools/ozone/set/deleteValues import * as ToolsOzoneSetGetValues from './types/tools/ozone/set/getValues' import * as ToolsOzoneSetQuerySets from './types/tools/ozone/set/querySets' import * as ToolsOzoneSetUpsertSet from './types/tools/ozone/set/upsertSet' +import * as ToolsOzoneSettingListOptions from './types/tools/ozone/setting/listOptions' +import * as ToolsOzoneSettingRemoveOptions from './types/tools/ozone/setting/removeOptions' +import * as ToolsOzoneSettingUpsertOption from './types/tools/ozone/setting/upsertOption' import * as ToolsOzoneSignatureFindCorrelation from './types/tools/ozone/signature/findCorrelation' import * as ToolsOzoneSignatureFindRelatedAccounts from './types/tools/ozone/signature/findRelatedAccounts' import * as ToolsOzoneSignatureSearchAccounts from './types/tools/ozone/signature/searchAccounts' @@ -2183,6 +2186,7 @@ export class ToolsOzoneNS { moderation: ToolsOzoneModerationNS server: ToolsOzoneServerNS set: ToolsOzoneSetNS + setting: ToolsOzoneSettingNS signature: ToolsOzoneSignatureNS team: ToolsOzoneTeamNS @@ -2192,6 +2196,7 @@ export class ToolsOzoneNS { this.moderation = new ToolsOzoneModerationNS(server) this.server = new ToolsOzoneServerNS(server) this.set = new ToolsOzoneSetNS(server) + this.setting = new ToolsOzoneSettingNS(server) this.signature = new ToolsOzoneSignatureNS(server) this.team = new ToolsOzoneTeamNS(server) } @@ -2449,6 +2454,47 @@ export class ToolsOzoneSetNS { } } +export class ToolsOzoneSettingNS { + _server: Server + + constructor(server: Server) { + this._server = server + } + + listOptions( + cfg: ConfigOf< + AV, + ToolsOzoneSettingListOptions.Handler>, + ToolsOzoneSettingListOptions.HandlerReqCtx> + >, + ) { + const nsid = 'tools.ozone.setting.listOptions' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + removeOptions( + cfg: ConfigOf< + AV, + ToolsOzoneSettingRemoveOptions.Handler>, + ToolsOzoneSettingRemoveOptions.HandlerReqCtx> + >, + ) { + const nsid = 'tools.ozone.setting.removeOptions' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + upsertOption( + cfg: ConfigOf< + AV, + ToolsOzoneSettingUpsertOption.Handler>, + ToolsOzoneSettingUpsertOption.HandlerReqCtx> + >, + ) { + const nsid = 'tools.ozone.setting.upsertOption' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } +} + export class ToolsOzoneSignatureNS { _server: Server diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index 9042d881745..3d6ae795019 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -12520,6 +12520,225 @@ export const schemaDict = { }, }, }, + ToolsOzoneSettingDefs: { + lexicon: 1, + id: 'tools.ozone.setting.defs', + defs: { + option: { + type: 'object', + required: [ + 'key', + 'value', + 'did', + 'scope', + 'createdBy', + 'lastUpdatedBy', + ], + properties: { + key: { + type: 'string', + format: 'nsid', + }, + did: { + type: 'string', + format: 'did', + }, + value: { + type: 'unknown', + }, + description: { + type: 'string', + maxGraphemes: 1024, + maxLength: 10240, + }, + createdAt: { + type: 'string', + format: 'datetime', + }, + updatedAt: { + type: 'string', + format: 'datetime', + }, + managerRole: { + type: 'string', + knownValues: [ + 'tools.ozone.team.defs#roleModerator', + 'tools.ozone.team.defs#roleTriage', + 'tools.ozone.team.defs#roleAdmin', + ], + }, + scope: { + type: 'string', + knownValues: ['instance', 'personal'], + }, + createdBy: { + type: 'string', + format: 'did', + }, + lastUpdatedBy: { + type: 'string', + format: 'did', + }, + }, + }, + }, + }, + ToolsOzoneSettingListOptions: { + lexicon: 1, + id: 'tools.ozone.setting.listOptions', + defs: { + main: { + type: 'query', + description: 'List settings with optional filtering', + parameters: { + type: 'params', + properties: { + limit: { + type: 'integer', + minimum: 1, + maximum: 100, + default: 50, + }, + cursor: { + type: 'string', + }, + scope: { + type: 'string', + knownValues: ['instance', 'personal'], + default: 'instance', + }, + prefix: { + type: 'string', + description: 'Filter keys by prefix', + }, + keys: { + type: 'array', + maxLength: 100, + items: { + type: 'string', + format: 'nsid', + }, + description: + 'Filter for only the specified keys. Ignored if prefix is provided', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['options'], + properties: { + cursor: { + type: 'string', + }, + options: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:tools.ozone.setting.defs#option', + }, + }, + }, + }, + }, + }, + }, + }, + ToolsOzoneSettingRemoveOptions: { + lexicon: 1, + id: 'tools.ozone.setting.removeOptions', + defs: { + main: { + type: 'procedure', + description: 'Delete settings by key', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['keys', 'scope'], + properties: { + keys: { + type: 'array', + minLength: 1, + maxLength: 200, + items: { + type: 'string', + format: 'nsid', + }, + }, + scope: { + type: 'string', + knownValues: ['instance', 'personal'], + }, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + properties: {}, + }, + }, + }, + }, + }, + ToolsOzoneSettingUpsertOption: { + lexicon: 1, + id: 'tools.ozone.setting.upsertOption', + defs: { + main: { + type: 'procedure', + description: 'Create or update setting option', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['key', 'scope', 'value'], + properties: { + key: { + type: 'string', + format: 'nsid', + }, + scope: { + type: 'string', + knownValues: ['instance', 'personal'], + }, + value: { + type: 'unknown', + }, + description: { + type: 'string', + maxLength: 2000, + }, + managerRole: { + type: 'string', + knownValues: [ + 'tools.ozone.team.defs#roleModerator', + 'tools.ozone.team.defs#roleTriage', + 'tools.ozone.team.defs#roleAdmin', + ], + }, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['option'], + properties: { + option: { + type: 'ref', + ref: 'lex:tools.ozone.setting.defs#option', + }, + }, + }, + }, + }, + }, + }, ToolsOzoneSignatureDefs: { lexicon: 1, id: 'tools.ozone.signature.defs', @@ -13153,6 +13372,10 @@ export const ids = { ToolsOzoneSetGetValues: 'tools.ozone.set.getValues', ToolsOzoneSetQuerySets: 'tools.ozone.set.querySets', ToolsOzoneSetUpsertSet: 'tools.ozone.set.upsertSet', + ToolsOzoneSettingDefs: 'tools.ozone.setting.defs', + ToolsOzoneSettingListOptions: 'tools.ozone.setting.listOptions', + ToolsOzoneSettingRemoveOptions: 'tools.ozone.setting.removeOptions', + ToolsOzoneSettingUpsertOption: 'tools.ozone.setting.upsertOption', ToolsOzoneSignatureDefs: 'tools.ozone.signature.defs', ToolsOzoneSignatureFindCorrelation: 'tools.ozone.signature.findCorrelation', ToolsOzoneSignatureFindRelatedAccounts: diff --git a/packages/pds/src/lexicon/types/tools/ozone/setting/defs.ts b/packages/pds/src/lexicon/types/tools/ozone/setting/defs.ts new file mode 100644 index 00000000000..8a87cbb0e24 --- /dev/null +++ b/packages/pds/src/lexicon/types/tools/ozone/setting/defs.ts @@ -0,0 +1,37 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' + +export interface Option { + key: string + did: string + value: {} + description?: string + createdAt?: string + updatedAt?: string + managerRole?: + | 'tools.ozone.team.defs#roleModerator' + | 'tools.ozone.team.defs#roleTriage' + | 'tools.ozone.team.defs#roleAdmin' + | (string & {}) + scope: 'instance' | 'personal' | (string & {}) + createdBy: string + lastUpdatedBy: string + [k: string]: unknown +} + +export function isOption(v: unknown): v is Option { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'tools.ozone.setting.defs#option' + ) +} + +export function validateOption(v: unknown): ValidationResult { + return lexicons.validate('tools.ozone.setting.defs#option', v) +} diff --git a/packages/pds/src/lexicon/types/tools/ozone/setting/listOptions.ts b/packages/pds/src/lexicon/types/tools/ozone/setting/listOptions.ts new file mode 100644 index 00000000000..54ad9608df3 --- /dev/null +++ b/packages/pds/src/lexicon/types/tools/ozone/setting/listOptions.ts @@ -0,0 +1,53 @@ +/** + * 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' +import * as ToolsOzoneSettingDefs from './defs' + +export interface QueryParams { + limit: number + cursor?: string + scope: 'instance' | 'personal' | (string & {}) + /** Filter keys by prefix */ + prefix?: string + /** Filter for only the specified keys. Ignored if prefix is provided */ + keys?: string[] +} + +export type InputSchema = undefined + +export interface OutputSchema { + cursor?: string + options: ToolsOzoneSettingDefs.Option[] + [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 diff --git a/packages/pds/src/lexicon/types/tools/ozone/setting/removeOptions.ts b/packages/pds/src/lexicon/types/tools/ozone/setting/removeOptions.ts new file mode 100644 index 00000000000..1d653b314da --- /dev/null +++ b/packages/pds/src/lexicon/types/tools/ozone/setting/removeOptions.ts @@ -0,0 +1,49 @@ +/** + * 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 interface InputSchema { + keys: string[] + scope: 'instance' | 'personal' | (string & {}) + [k: string]: unknown +} + +export interface OutputSchema { + [k: string]: unknown +} + +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} + +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 diff --git a/packages/pds/src/lexicon/types/tools/ozone/setting/upsertOption.ts b/packages/pds/src/lexicon/types/tools/ozone/setting/upsertOption.ts new file mode 100644 index 00000000000..b2e66d073d9 --- /dev/null +++ b/packages/pds/src/lexicon/types/tools/ozone/setting/upsertOption.ts @@ -0,0 +1,58 @@ +/** + * 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' +import * as ToolsOzoneSettingDefs from './defs' + +export interface QueryParams {} + +export interface InputSchema { + key: string + scope: 'instance' | 'personal' | (string & {}) + value: {} + description?: string + managerRole?: + | 'tools.ozone.team.defs#roleModerator' + | 'tools.ozone.team.defs#roleTriage' + | 'tools.ozone.team.defs#roleAdmin' + | (string & {}) + [k: string]: unknown +} + +export interface OutputSchema { + option: ToolsOzoneSettingDefs.Option + [k: string]: unknown +} + +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} + +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