From 8a018700d4140ed283b54137ee1778c8f0a9762c Mon Sep 17 00:00:00 2001 From: Yuto Nishida Date: Sun, 5 Oct 2025 03:26:36 -0700 Subject: [PATCH] cd ./experimental/chatproto && pnpm --filter @statusphere/appview lexgen --- .../chatproto/packages/appview/src/db.ts | 4 +- .../appview/src/ingestors/firehose.ts | 48 ++-- .../packages/appview/src/lexicons/index.ts | 44 +++ .../packages/appview/src/lexicons/lexicons.ts | 256 ++++++++++++++++++ .../types/app/andref/chatproto/channel.ts | 28 ++ .../types/app/andref/chatproto/defs.ts | 79 ++++++ .../types/app/andref/chatproto/getMessages.ts | 53 ++++ .../types/app/andref/chatproto/message.ts | 30 ++ .../types/app/andref/chatproto/sendMessage.ts | 53 ++++ .../types/app/andref/chatproto/space.ts | 31 +++ .../types/app/andref/chatproto/writers.ts | 14 + 11 files changed, 617 insertions(+), 23 deletions(-) create mode 100644 experimental/chatproto/packages/appview/src/lexicons/types/app/andref/chatproto/channel.ts create mode 100644 experimental/chatproto/packages/appview/src/lexicons/types/app/andref/chatproto/defs.ts create mode 100644 experimental/chatproto/packages/appview/src/lexicons/types/app/andref/chatproto/getMessages.ts create mode 100644 experimental/chatproto/packages/appview/src/lexicons/types/app/andref/chatproto/message.ts create mode 100644 experimental/chatproto/packages/appview/src/lexicons/types/app/andref/chatproto/sendMessage.ts create mode 100644 experimental/chatproto/packages/appview/src/lexicons/types/app/andref/chatproto/space.ts create mode 100644 experimental/chatproto/packages/appview/src/lexicons/types/app/andref/chatproto/writers.ts diff --git a/experimental/chatproto/packages/appview/src/db.ts b/experimental/chatproto/packages/appview/src/db.ts index fa81fc4..39186dc 100644 --- a/experimental/chatproto/packages/appview/src/db.ts +++ b/experimental/chatproto/packages/appview/src/db.ts @@ -139,7 +139,9 @@ migrations['001'] = { .createTable('channel') .addColumn('uri', 'varchar', (col) => col.primaryKey()) .addColumn('name', 'varchar', (col) => col.notNull()) - .addColumn('useSpaceWriters', 'boolean', (col) => col.notNull().defaultTo(true)) + .addColumn('useSpaceWriters', 'boolean', (col) => + col.notNull().defaultTo(true), + ) .addColumn('spaceUri', 'varchar', (col) => col.notNull()) .addForeignKeyConstraint( 'spaceUri_fkey', diff --git a/experimental/chatproto/packages/appview/src/ingestors/firehose.ts b/experimental/chatproto/packages/appview/src/ingestors/firehose.ts index f85bcb2..906472a 100644 --- a/experimental/chatproto/packages/appview/src/ingestors/firehose.ts +++ b/experimental/chatproto/packages/appview/src/ingestors/firehose.ts @@ -1,6 +1,10 @@ import { IdResolver } from '@atproto/identity' import { Firehose, MemoryRunner, type Event } from '@atproto/sync' -import { XyzStatusphereStatus, AppAndrefChatprotoChannel, AppAndrefChatprotoMessage } from '@statusphere/lexicon' +import { + AppAndrefChatprotoChannel, + AppAndrefChatprotoMessage, + XyzStatusphereStatus, +} from '@statusphere/lexicon' import pino from 'pino' import type { Database } from '#/db' @@ -52,27 +56,28 @@ export async function createFirehoseIngester( if (!XyzStatusphereStatus.isRecord(record)) return const validatedRecord = XyzStatusphereStatus.validateRecord(record) if (!validatedRecord.success) return - // Store the status in our SQLite - await db - .insertInto('status') - .values({ - uri: evt.uri.toString(), - authorDid: evt.did, + // Store the status in our SQLite + await db + .insertInto('status') + .values({ + uri: evt.uri.toString(), + authorDid: evt.did, + status: validatedRecord.value.status, + createdAt: validatedRecord.value.createdAt, + indexedAt: now.toISOString(), + }) + .onConflict((oc) => + oc.column('uri').doUpdateSet({ status: validatedRecord.value.status, - createdAt: validatedRecord.value.createdAt, indexedAt: now.toISOString(), - }) - .onConflict((oc) => - oc.column('uri').doUpdateSet({ - status: validatedRecord.value.status, - indexedAt: now.toISOString(), - }), - ) - .execute() + }), + ) + .execute() } case 'app.andref.chatproto.message': { if (!AppAndrefChatprotoMessage.isRecord(record)) return - const validatedRecord = AppAndrefChatprotoMessage.validateRecord(record) + const validatedRecord = + AppAndrefChatprotoMessage.validateRecord(record) if (!validatedRecord.success) return // Plaintext is required for now, since it is the only content type we support if (validatedRecord.value.plaintext === undefined) return @@ -97,15 +102,14 @@ export async function createFirehoseIngester( } case 'app.andref.chatproto.channel': { if (!AppAndrefChatprotoChannel.isRecord(record)) return - const validatedRecord = AppAndrefChatprotoChannel.validateRecord(record) + const validatedRecord = + AppAndrefChatprotoChannel.validateRecord(record) if (!validatedRecord.success) return // Channel is getting created or updated } } - } else if ( - evt.event === 'delete' - ) { + } else if (evt.event === 'delete') { switch (evt.collection) { case 'xyz.statusphere.status': { // Remove the status from our SQLite @@ -134,7 +138,7 @@ export async function createFirehoseIngester( 'xyz.statusphere.status', 'app.andref.chatproto.message', 'app.andref.chatproto.space', - 'app.andref.chatproto.channel', + 'app.andref.chatproto.channel', ], excludeIdentity: true, excludeAccount: true, diff --git a/experimental/chatproto/packages/appview/src/lexicons/index.ts b/experimental/chatproto/packages/appview/src/lexicons/index.ts index 862b1a4..0f9413c 100644 --- a/experimental/chatproto/packages/appview/src/lexicons/index.ts +++ b/experimental/chatproto/packages/appview/src/lexicons/index.ts @@ -10,6 +10,8 @@ import { } from '@atproto/xrpc-server' import { schemas } from './lexicons.js' +import * as AppAndrefChatprotoGetMessages from './types/app/andref/chatproto/getMessages.js' +import * as AppAndrefChatprotoSendMessage from './types/app/andref/chatproto/sendMessage.js' import * as ComAtprotoRepoApplyWrites from './types/com/atproto/repo/applyWrites.js' import * as ComAtprotoRepoCreateRecord from './types/com/atproto/repo/createRecord.js' import * as ComAtprotoRepoDeleteRecord from './types/com/atproto/repo/deleteRecord.js' @@ -234,10 +236,12 @@ export class ComAtprotoRepoNS { export class AppNS { _server: Server bsky: AppBskyNS + andref: AppAndrefNS constructor(server: Server) { this._server = server this.bsky = new AppBskyNS(server) + this.andref = new AppAndrefNS(server) } } @@ -259,6 +263,46 @@ export class AppBskyActorNS { } } +export class AppAndrefNS { + _server: Server + chatproto: AppAndrefChatprotoNS + + constructor(server: Server) { + this._server = server + this.chatproto = new AppAndrefChatprotoNS(server) + } +} + +export class AppAndrefChatprotoNS { + _server: Server + + constructor(server: Server) { + this._server = server + } + + getMessages( + cfg: ConfigOf< + AV, + AppAndrefChatprotoGetMessages.Handler>, + AppAndrefChatprotoGetMessages.HandlerReqCtx> + >, + ) { + const nsid = 'app.andref.chatproto.getMessages' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + sendMessage( + cfg: ConfigOf< + AV, + AppAndrefChatprotoSendMessage.Handler>, + AppAndrefChatprotoSendMessage.HandlerReqCtx> + >, + ) { + const nsid = 'app.andref.chatproto.sendMessage' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } +} + type SharedRateLimitOpts = { name: string calcKey?: (ctx: T) => string | null diff --git a/experimental/chatproto/packages/appview/src/lexicons/lexicons.ts b/experimental/chatproto/packages/appview/src/lexicons/lexicons.ts index c6c06cc..045f06c 100644 --- a/experimental/chatproto/packages/appview/src/lexicons/lexicons.ts +++ b/experimental/chatproto/packages/appview/src/lexicons/lexicons.ts @@ -1240,6 +1240,255 @@ export const schemaDict = { }, }, }, + AppAndrefChatprotoChannel: { + lexicon: 1, + id: 'app.andref.chatproto.channel', + defs: { + main: { + type: 'record', + key: 'nsid', + record: { + type: 'object', + required: ['name'], + properties: { + name: { + type: 'string', + minLength: 1, + maxGraphemes: 128, + }, + }, + }, + }, + }, + }, + AppAndrefChatprotoDefs: { + lexicon: 1, + id: 'app.andref.chatproto.defs', + defs: { + spaceView: { + type: 'object', + required: ['uri', 'name'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + name: { + type: 'string', + minLength: 1, + maxGraphemes: 128, + }, + }, + }, + channelView: { + type: 'object', + required: ['uri', 'name'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + name: { + type: 'string', + minLength: 1, + maxGraphemes: 128, + }, + }, + }, + messageView: { + type: 'object', + required: ['tid', 'createdAt', 'channel', 'author'], + properties: { + tid: { + type: 'string', + format: 'tid', + }, + plaintext: { + type: 'string', + }, + createdAt: { + type: 'string', + format: 'datetime', + }, + channel: { + type: 'string', + format: 'at-uri', + }, + author: { + type: 'ref', + ref: 'lex:app.andref.chatproto.defs#profileView', + }, + }, + }, + profileView: { + type: 'object', + required: ['did', 'handle'], + properties: { + did: { + type: 'string', + format: 'did', + }, + handle: { + type: 'string', + format: 'handle', + }, + }, + }, + }, + }, + AppAndrefChatprotoGetMessages: { + lexicon: 1, + id: 'app.andref.chatproto.getMessages', + defs: { + main: { + type: 'query', + description: 'Get a list of the most recent messages on the network.', + parameters: { + type: 'params', + required: ['channelNsid'], + properties: { + channelNsid: { + type: 'string', + format: 'nsid', + }, + before: { + type: 'string', + format: 'tid', + }, + hintChannelOwner: { + type: 'string', + format: 'at-identifier', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['messages'], + properties: { + messages: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.andref.chatproto.defs#messageView', + }, + }, + }, + }, + }, + }, + }, + }, + AppAndrefChatprotoMessage: { + lexicon: 1, + id: 'app.andref.chatproto.message', + defs: { + main: { + type: 'record', + key: 'tid', + record: { + type: 'object', + required: ['createdAt', 'channel'], + properties: { + plaintext: { + type: 'string', + minLength: 1, + maxGraphemes: 4000, + }, + createdAt: { + type: 'string', + format: 'datetime', + }, + channel: { + type: 'string', + format: 'at-uri', + }, + }, + }, + }, + }, + }, + AppAndrefChatprotoSendMessage: { + lexicon: 1, + id: 'app.andref.chatproto.sendMessage', + defs: { + main: { + type: 'procedure', + description: 'Send a message into the ATmosphere.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['channel'], + properties: { + plaintext: { + type: 'string', + minLength: 1, + maxGraphemes: 4000, + }, + channel: { + type: 'string', + format: 'at-uri', + }, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'ref', + ref: 'lex:app.andref.chatproto.defs#messageView', + }, + }, + }, + }, + }, + AppAndrefChatprotoSpace: { + lexicon: 1, + id: 'app.andref.chatproto.space', + defs: { + main: { + type: 'record', + key: 'nsid', + record: { + type: 'object', + required: ['name', 'channels'], + properties: { + name: { + type: 'string', + minLength: 1, + maxGraphemes: 128, + }, + writers: { + type: 'ref', + ref: 'lex:app.andref.chatproto.writers', + }, + channels: { + type: 'array', + items: { + type: 'string', + format: 'at-uri', + }, + }, + }, + }, + }, + }, + }, + AppAndrefChatprotoWriters: { + lexicon: 1, + id: 'app.andref.chatproto.writers', + defs: { + main: { + type: 'array', + items: { + type: 'string', + format: 'at-identifier', + }, + }, + }, + }, } as const satisfies Record export const schemas = Object.values(schemaDict) satisfies LexiconDoc[] @@ -1294,4 +1543,11 @@ export const ids = { ComAtprotoRepoUploadBlob: 'com.atproto.repo.uploadBlob', AppBskyActorDefs: 'app.bsky.actor.defs', AppBskyActorProfile: 'app.bsky.actor.profile', + AppAndrefChatprotoChannel: 'app.andref.chatproto.channel', + AppAndrefChatprotoDefs: 'app.andref.chatproto.defs', + AppAndrefChatprotoGetMessages: 'app.andref.chatproto.getMessages', + AppAndrefChatprotoMessage: 'app.andref.chatproto.message', + AppAndrefChatprotoSendMessage: 'app.andref.chatproto.sendMessage', + AppAndrefChatprotoSpace: 'app.andref.chatproto.space', + AppAndrefChatprotoWriters: 'app.andref.chatproto.writers', } as const diff --git a/experimental/chatproto/packages/appview/src/lexicons/types/app/andref/chatproto/channel.ts b/experimental/chatproto/packages/appview/src/lexicons/types/app/andref/chatproto/channel.ts new file mode 100644 index 0000000..a6d970d --- /dev/null +++ b/experimental/chatproto/packages/appview/src/lexicons/types/app/andref/chatproto/channel.ts @@ -0,0 +1,28 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { BlobRef, ValidationResult } from '@atproto/lexicon' +import { CID } from 'multiformats/cid' + +import { validate as _validate } from '../../../../lexicons' +import { is$typed as _is$typed, $Typed, OmitKey } from '../../../../util' + +const is$typed = _is$typed, + validate = _validate +const id = 'app.andref.chatproto.channel' + +export interface Record { + $type: 'app.andref.chatproto.channel' + name: string + [k: string]: unknown +} + +const hashRecord = 'main' + +export function isRecord(v: V) { + return is$typed(v, id, hashRecord) +} + +export function validateRecord(v: V) { + return validate(v, id, hashRecord, true) +} diff --git a/experimental/chatproto/packages/appview/src/lexicons/types/app/andref/chatproto/defs.ts b/experimental/chatproto/packages/appview/src/lexicons/types/app/andref/chatproto/defs.ts new file mode 100644 index 0000000..86fea11 --- /dev/null +++ b/experimental/chatproto/packages/appview/src/lexicons/types/app/andref/chatproto/defs.ts @@ -0,0 +1,79 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { BlobRef, ValidationResult } from '@atproto/lexicon' +import { CID } from 'multiformats/cid' + +import { validate as _validate } from '../../../../lexicons' +import { is$typed as _is$typed, $Typed, OmitKey } from '../../../../util' + +const is$typed = _is$typed, + validate = _validate +const id = 'app.andref.chatproto.defs' + +export interface SpaceView { + $type?: 'app.andref.chatproto.defs#spaceView' + uri: string + name: string +} + +const hashSpaceView = 'spaceView' + +export function isSpaceView(v: V) { + return is$typed(v, id, hashSpaceView) +} + +export function validateSpaceView(v: V) { + return validate(v, id, hashSpaceView) +} + +export interface ChannelView { + $type?: 'app.andref.chatproto.defs#channelView' + uri: string + name: string +} + +const hashChannelView = 'channelView' + +export function isChannelView(v: V) { + return is$typed(v, id, hashChannelView) +} + +export function validateChannelView(v: V) { + return validate(v, id, hashChannelView) +} + +export interface MessageView { + $type?: 'app.andref.chatproto.defs#messageView' + tid: string + plaintext?: string + createdAt: string + channel: string + author: ProfileView +} + +const hashMessageView = 'messageView' + +export function isMessageView(v: V) { + return is$typed(v, id, hashMessageView) +} + +export function validateMessageView(v: V) { + return validate(v, id, hashMessageView) +} + +export interface ProfileView { + $type?: 'app.andref.chatproto.defs#profileView' + did: string + handle: string +} + +const hashProfileView = 'profileView' + +export function isProfileView(v: V) { + return is$typed(v, id, hashProfileView) +} + +export function validateProfileView(v: V) { + return validate(v, id, hashProfileView) +} diff --git a/experimental/chatproto/packages/appview/src/lexicons/types/app/andref/chatproto/getMessages.ts b/experimental/chatproto/packages/appview/src/lexicons/types/app/andref/chatproto/getMessages.ts new file mode 100644 index 0000000..4eeb50b --- /dev/null +++ b/experimental/chatproto/packages/appview/src/lexicons/types/app/andref/chatproto/getMessages.ts @@ -0,0 +1,53 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { BlobRef, ValidationResult } from '@atproto/lexicon' +import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' +import express from 'express' +import { CID } from 'multiformats/cid' + +import { validate as _validate } from '../../../../lexicons' +import { is$typed as _is$typed, $Typed, OmitKey } from '../../../../util' +import type * as AppAndrefChatprotoDefs from './defs.js' + +const is$typed = _is$typed, + validate = _validate +const id = 'app.andref.chatproto.getMessages' + +export interface QueryParams { + channelNsid: string + before?: string + hintChannelOwner?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + messages: AppAndrefChatprotoDefs.MessageView[] +} + +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 + resetRouteRateLimits: () => Promise +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/experimental/chatproto/packages/appview/src/lexicons/types/app/andref/chatproto/message.ts b/experimental/chatproto/packages/appview/src/lexicons/types/app/andref/chatproto/message.ts new file mode 100644 index 0000000..376a53c --- /dev/null +++ b/experimental/chatproto/packages/appview/src/lexicons/types/app/andref/chatproto/message.ts @@ -0,0 +1,30 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { BlobRef, ValidationResult } from '@atproto/lexicon' +import { CID } from 'multiformats/cid' + +import { validate as _validate } from '../../../../lexicons' +import { is$typed as _is$typed, $Typed, OmitKey } from '../../../../util' + +const is$typed = _is$typed, + validate = _validate +const id = 'app.andref.chatproto.message' + +export interface Record { + $type: 'app.andref.chatproto.message' + plaintext?: string + createdAt: string + channel: string + [k: string]: unknown +} + +const hashRecord = 'main' + +export function isRecord(v: V) { + return is$typed(v, id, hashRecord) +} + +export function validateRecord(v: V) { + return validate(v, id, hashRecord, true) +} diff --git a/experimental/chatproto/packages/appview/src/lexicons/types/app/andref/chatproto/sendMessage.ts b/experimental/chatproto/packages/appview/src/lexicons/types/app/andref/chatproto/sendMessage.ts new file mode 100644 index 0000000..d3b0d0a --- /dev/null +++ b/experimental/chatproto/packages/appview/src/lexicons/types/app/andref/chatproto/sendMessage.ts @@ -0,0 +1,53 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { BlobRef, ValidationResult } from '@atproto/lexicon' +import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' +import express from 'express' +import { CID } from 'multiformats/cid' + +import { validate as _validate } from '../../../../lexicons' +import { is$typed as _is$typed, $Typed, OmitKey } from '../../../../util' +import type * as AppAndrefChatprotoDefs from './defs.js' + +const is$typed = _is$typed, + validate = _validate +const id = 'app.andref.chatproto.sendMessage' + +export interface QueryParams {} + +export interface InputSchema { + plaintext?: string + channel: string +} + +export type OutputSchema = AppAndrefChatprotoDefs.MessageView + +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 + resetRouteRateLimits: () => Promise +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/experimental/chatproto/packages/appview/src/lexicons/types/app/andref/chatproto/space.ts b/experimental/chatproto/packages/appview/src/lexicons/types/app/andref/chatproto/space.ts new file mode 100644 index 0000000..90d70b6 --- /dev/null +++ b/experimental/chatproto/packages/appview/src/lexicons/types/app/andref/chatproto/space.ts @@ -0,0 +1,31 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { BlobRef, ValidationResult } from '@atproto/lexicon' +import { CID } from 'multiformats/cid' + +import { validate as _validate } from '../../../../lexicons' +import { is$typed as _is$typed, $Typed, OmitKey } from '../../../../util' +import type * as AppAndrefChatprotoWriters from './writers.js' + +const is$typed = _is$typed, + validate = _validate +const id = 'app.andref.chatproto.space' + +export interface Record { + $type: 'app.andref.chatproto.space' + name: string + writers?: AppAndrefChatprotoWriters.Main + channels: string[] + [k: string]: unknown +} + +const hashRecord = 'main' + +export function isRecord(v: V) { + return is$typed(v, id, hashRecord) +} + +export function validateRecord(v: V) { + return validate(v, id, hashRecord, true) +} diff --git a/experimental/chatproto/packages/appview/src/lexicons/types/app/andref/chatproto/writers.ts b/experimental/chatproto/packages/appview/src/lexicons/types/app/andref/chatproto/writers.ts new file mode 100644 index 0000000..7f62452 --- /dev/null +++ b/experimental/chatproto/packages/appview/src/lexicons/types/app/andref/chatproto/writers.ts @@ -0,0 +1,14 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { BlobRef, ValidationResult } from '@atproto/lexicon' +import { CID } from 'multiformats/cid' + +import { validate as _validate } from '../../../../lexicons' +import { is$typed as _is$typed, $Typed, OmitKey } from '../../../../util' + +const is$typed = _is$typed, + validate = _validate +const id = 'app.andref.chatproto.writers' + +export type Main = string[] -- 2.54.0