···6869The backend server will:
7071-- Serve the API at `/api/*` endpoints
72- Serve the frontend static files from the client's build directory
73- Handle client-side routing by serving index.html for all non-API routes
74
···6869The backend server will:
7071+- Serve the API at `/xrpc/*` and `/oauth/*` endpoints
72- Serve the frontend static files from the client's build directory
73- Handle client-side routing by serving index.html for all non-API routes
74
···1+import { AppContext } from '#/context'
2+import { Server } from '#/lexicons'
3+import getStatuses from './lexicons/getStatuses'
4+import getUser from './lexicons/getUser'
5+import sendStatus from './lexicons/sendStatus'
6+7+export * as health from './health'
8+export * as oauth from './oauth'
9+10+export default function (server: Server, ctx: AppContext) {
11+ getStatuses(server, ctx)
12+ sendStatus(server, ctx)
13+ getUser(server, ctx)
14+ return server
15+}
···2import fs from 'node:fs'
3import type http from 'node:http'
4import path from 'node:path'
5-import type { OAuthClient } from '@atproto/oauth-client-node'
6-import { Firehose } from '@atproto/sync'
7import cors from 'cors'
8-import express, { type Express } from 'express'
9import { pino } from 'pino'
10011import { createClient } from '#/auth/client'
012import { createDb, migrateToLatest } from '#/db'
13-import type { Database } from '#/db'
14-import {
15- BidirectionalResolver,
16- createBidirectionalResolver,
17- createIdResolver,
18-} from '#/id-resolver'
19import { createIngester } from '#/ingester'
020import { env } from '#/lib/env'
21-import { createRouter } from '#/routes'
22-23-// Application state passed to the router and elsewhere
24-export type AppContext = {
25- db: Database
26- ingester: Firehose
27- logger: pino.Logger
28- oauthClient: OAuthClient
29- resolver: BidirectionalResolver
30-}
3132export class Server {
33 constructor(
···60 // Subscribe to events on the firehose
61 ingester.start()
6263- // Create our server
64- const app: Express = express()
65- app.set('trust proxy', true)
66-67- // CORS configuration based on environment
68- if (env.NODE_ENV === 'development') {
69- // In development, allow multiple origins including ngrok
70- app.use(
71- cors({
72- origin: function (origin, callback) {
73- // Allow requests with no origin (like mobile apps, curl)
74- if (!origin) return callback(null, true)
75-76- // List of allowed origins
77- const allowedOrigins = [
78- 'http://localhost:3000', // Standard React port
79- 'http://127.0.0.1:3000', // Alternative React address
80- ]
81-82- // Check if the request origin is in our allowed list or is an ngrok domain
83- if (allowedOrigins.indexOf(origin) !== -1) {
84- callback(null, true)
85- } else {
86- console.warn(`⚠️ CORS blocked origin: ${origin}`)
87- callback(null, false)
88- }
89- },
90- credentials: true,
91- methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
92- allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
93- }),
94- )
95- } else {
96- // In production, CORS is not needed if frontend and API are on same domain
97- // But we'll still enable it for flexibility with minimal configuration
98- app.use(
99- cors({
100- origin: true, // Use req.origin, which means same-origin requests will always be allowed
101- credentials: true,
102- }),
103- )
104- }
105-106- // Routes & middlewares
107- const router = createRouter(ctx)
108 app.use(express.json())
109 app.use(express.urlencoded({ extended: true }))
110111- app.use('/api', router)
0000000000000000112113 // Serve static files from the frontend build - prod only
114 if (env.isProduction) {
···124 // Serve static files
125 app.use(express.static(frontendPath))
126127- // Heathcheck
128- app.get('/health', (req, res) => {
129- res.status(200).json({ status: 'ok' })
130- })
131-132 // For any other requests, send the index.html file
133 app.get('*', (req, res) => {
134 // Only handle non-API paths
135- if (!req.path.startsWith('/api/')) {
136 res.sendFile(path.join(frontendPath, 'index.html'))
137 } else {
138 res.status(404).json({ error: 'API endpoint not found' })
···144 res.sendStatus(404)
145 })
146 }
00147 }
148149 // Use the port from env (should be 3001 for the API server)
150- const server = app.listen(env.PORT)
151- await events.once(server, 'listening')
152 logger.info(
153 `API Server (${NODE_ENV}) running on port http://${HOST}:${env.PORT}`,
154 )
155156- return new Server(app, server, ctx)
157 }
158159 async close() {
···2import fs from 'node:fs'
3import type http from 'node:http'
4import path from 'node:path'
5+import { DAY, SECOND } from '@atproto/common'
6+import compression from 'compression'
7import cors from 'cors'
8+import express from 'express'
9import { pino } from 'pino'
1011+import API, { health, oauth } from '#/api'
12import { createClient } from '#/auth/client'
13+import { AppContext } from '#/context'
14import { createDb, migrateToLatest } from '#/db'
15+import * as error from '#/error'
16+import { createBidirectionalResolver, createIdResolver } from '#/id-resolver'
000017import { createIngester } from '#/ingester'
18+import { createServer } from '#/lexicons'
19import { env } from '#/lib/env'
00000000002021export class Server {
22 constructor(
···49 // Subscribe to events on the firehose
50 ingester.start()
5152+ const app = express()
53+ app.use(cors({ maxAge: DAY / SECOND }))
54+ app.use(compression())
00000000000000000000000000000000000000000055 app.use(express.json())
56 app.use(express.urlencoded({ extended: true }))
5758+ // Create our server
59+ let server = createServer({
60+ validateResponse: env.isDevelopment,
61+ payload: {
62+ jsonLimit: 100 * 1024, // 100kb
63+ textLimit: 100 * 1024, // 100kb
64+ // no blobs
65+ blobLimit: 0,
66+ },
67+ })
68+69+ server = API(server, ctx)
70+71+ app.use(health.createRouter(ctx))
72+ app.use(oauth.createRouter(ctx))
73+ app.use(server.xrpc.router)
74+ app.use(error.createHandler(ctx))
7576 // Serve static files from the frontend build - prod only
77 if (env.isProduction) {
···87 // Serve static files
88 app.use(express.static(frontendPath))
890000090 // For any other requests, send the index.html file
91 app.get('*', (req, res) => {
92 // Only handle non-API paths
93+ if (!req.path.startsWith('/xrpc/')) {
94 res.sendFile(path.join(frontendPath, 'index.html'))
95 } else {
96 res.status(404).json({ error: 'API endpoint not found' })
···102 res.sendStatus(404)
103 })
104 }
105+ } else {
106+ server.xrpc.router.set('trust proxy', true)
107 }
108109 // Use the port from env (should be 3001 for the API server)
110+ const httpServer = app.listen(env.PORT)
111+ await events.once(httpServer, 'listening')
112 logger.info(
113 `API Server (${NODE_ENV}) running on port http://${HOST}:${env.PORT}`,
114 )
115116+ return new Server(app, httpServer, ctx)
117 }
118119 async close() {
···1+/**
2+ * GENERATED CODE - DO NOT MODIFY
3+ */
4+import { BlobRef, ValidationResult } from '@atproto/lexicon'
5+import { CID } from 'multiformats/cid'
6+7+import { validate as _validate } from '../../../../lexicons'
8+import { is$typed as _is$typed, $Typed, OmitKey } from '../../../../util'
9+10+const is$typed = _is$typed,
11+ validate = _validate
12+const id = 'com.atproto.label.defs'
13+14+/** Metadata tag on an atproto resource (eg, repo or record). */
15+export interface Label {
16+ $type?: 'com.atproto.label.defs#label'
17+ /** The AT Protocol version of the label object. */
18+ ver?: number
19+ /** DID of the actor who created this label. */
20+ src: string
21+ /** AT URI of the record, repository (account), or other resource that this label applies to. */
22+ uri: string
23+ /** Optionally, CID specifying the specific version of 'uri' resource this label applies to. */
24+ cid?: string
25+ /** The short string name of the value or type of this label. */
26+ val: string
27+ /** If true, this is a negation label, overwriting a previous label. */
28+ neg?: boolean
29+ /** Timestamp when this label was created. */
30+ cts: string
31+ /** Timestamp at which this label expires (no longer applies). */
32+ exp?: string
33+ /** Signature of dag-cbor encoded label. */
34+ sig?: Uint8Array
35+}
36+37+const hashLabel = 'label'
38+39+export function isLabel<V>(v: V) {
40+ return is$typed(v, id, hashLabel)
41+}
42+43+export function validateLabel<V>(v: V) {
44+ return validate<Label & V>(v, id, hashLabel)
45+}
46+47+/** Metadata tags on an atproto record, published by the author within the record. */
48+export interface SelfLabels {
49+ $type?: 'com.atproto.label.defs#selfLabels'
50+ values: SelfLabel[]
51+}
52+53+const hashSelfLabels = 'selfLabels'
54+55+export function isSelfLabels<V>(v: V) {
56+ return is$typed(v, id, hashSelfLabels)
57+}
58+59+export function validateSelfLabels<V>(v: V) {
60+ return validate<SelfLabels & V>(v, id, hashSelfLabels)
61+}
62+63+/** Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel. */
64+export interface SelfLabel {
65+ $type?: 'com.atproto.label.defs#selfLabel'
66+ /** The short string name of the value or type of this label. */
67+ val: string
68+}
69+70+const hashSelfLabel = 'selfLabel'
71+72+export function isSelfLabel<V>(v: V) {
73+ return is$typed(v, id, hashSelfLabel)
74+}
75+76+export function validateSelfLabel<V>(v: V) {
77+ return validate<SelfLabel & V>(v, id, hashSelfLabel)
78+}
79+80+/** Declares a label value and its expected interpretations and behaviors. */
81+export interface LabelValueDefinition {
82+ $type?: 'com.atproto.label.defs#labelValueDefinition'
83+ /** The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+). */
84+ identifier: string
85+ /** How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing. */
86+ severity: 'inform' | 'alert' | 'none' | (string & {})
87+ /** What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing. */
88+ blurs: 'content' | 'media' | 'none' | (string & {})
89+ /** The default setting for this label. */
90+ defaultSetting: 'ignore' | 'warn' | 'hide' | (string & {})
91+ /** Does the user need to have adult content enabled in order to configure this label? */
92+ adultOnly?: boolean
93+ locales: LabelValueDefinitionStrings[]
94+}
95+96+const hashLabelValueDefinition = 'labelValueDefinition'
97+98+export function isLabelValueDefinition<V>(v: V) {
99+ return is$typed(v, id, hashLabelValueDefinition)
100+}
101+102+export function validateLabelValueDefinition<V>(v: V) {
103+ return validate<LabelValueDefinition & V>(v, id, hashLabelValueDefinition)
104+}
105+106+/** Strings which describe the label in the UI, localized into a specific language. */
107+export interface LabelValueDefinitionStrings {
108+ $type?: 'com.atproto.label.defs#labelValueDefinitionStrings'
109+ /** The code of the language these strings are written in. */
110+ lang: string
111+ /** A short human-readable name for the label. */
112+ name: string
113+ /** A longer description of what the label means and why it might be applied. */
114+ description: string
115+}
116+117+const hashLabelValueDefinitionStrings = 'labelValueDefinitionStrings'
118+119+export function isLabelValueDefinitionStrings<V>(v: V) {
120+ return is$typed(v, id, hashLabelValueDefinitionStrings)
121+}
122+123+export function validateLabelValueDefinitionStrings<V>(v: V) {
124+ return validate<LabelValueDefinitionStrings & V>(
125+ v,
126+ id,
127+ hashLabelValueDefinitionStrings,
128+ )
129+}
130+131+export type LabelValue =
132+ | '!hide'
133+ | '!no-promote'
134+ | '!warn'
135+ | '!no-unauthenticated'
136+ | 'dmca-violation'
137+ | 'doxxing'
138+ | 'porn'
139+ | 'sexual'
140+ | 'nudity'
141+ | 'nsfl'
142+ | 'gore'
143+ | (string & {})
···1+/**
2+ * GENERATED CODE - DO NOT MODIFY
3+ */
4+5+import { ValidationResult } from '@atproto/lexicon'
6+7+export type OmitKey<T, K extends keyof T> = {
8+ [K2 in keyof T as K2 extends K ? never : K2]: T[K2]
9+}
10+11+export type $Typed<V, T extends string = string> = V & { $type: T }
12+export type Un$Typed<V extends { $type?: string }> = OmitKey<V, '$type'>
13+14+export type $Type<Id extends string, Hash extends string> = Hash extends 'main'
15+ ? Id
16+ : `${Id}#${Hash}`
17+18+function isObject<V>(v: V): v is V & object {
19+ return v != null && typeof v === 'object'
20+}
21+22+function is$type<Id extends string, Hash extends string>(
23+ $type: unknown,
24+ id: Id,
25+ hash: Hash,
26+): $type is $Type<Id, Hash> {
27+ return hash === 'main'
28+ ? $type === id
29+ : // $type === `${id}#${hash}`
30+ typeof $type === 'string' &&
31+ $type.length === id.length + 1 + hash.length &&
32+ $type.charCodeAt(id.length) === 35 /* '#' */ &&
33+ $type.startsWith(id) &&
34+ $type.endsWith(hash)
35+}
36+37+export type $TypedObject<
38+ V,
39+ Id extends string,
40+ Hash extends string,
41+> = V extends {
42+ $type: $Type<Id, Hash>
43+}
44+ ? V
45+ : V extends { $type?: string }
46+ ? V extends { $type?: infer T extends $Type<Id, Hash> }
47+ ? V & { $type: T }
48+ : never
49+ : V & { $type: $Type<Id, Hash> }
50+51+export function is$typed<V, Id extends string, Hash extends string>(
52+ v: V,
53+ id: Id,
54+ hash: Hash,
55+): v is $TypedObject<V, Id, Hash> {
56+ return isObject(v) && '$type' in v && is$type(v.$type, id, hash)
57+}
58+59+export function maybe$typed<V, Id extends string, Hash extends string>(
60+ v: V,
61+ id: Id,
62+ hash: Hash,
63+): v is V & object & { $type?: $Type<Id, Hash> } {
64+ return (
65+ isObject(v) &&
66+ ('$type' in v ? v.$type === undefined || is$type(v.$type, id, hash) : true)
67+ )
68+}
69+70+export type Validator<R = unknown> = (v: unknown) => ValidationResult<R>
71+export type ValidatorParam<V extends Validator> =
72+ V extends Validator<infer R> ? R : never
73+74+/**
75+ * Utility function that allows to convert a "validate*" utility function into a
76+ * type predicate.
77+ */
78+export function asPredicate<V extends Validator>(validate: V) {
79+ return function <T>(v: T): v is T & ValidatorParam<V> {
80+ return validate(v).success
81+ }
82+}
+1-1
packages/appview/src/lib/hydrate.ts
···4 XyzStatusphereDefs,
5} from '@statusphere/lexicon'
607import { Status } from '#/db'
8-import { AppContext } from '#/index'
910export async function statusToStatusView(
11 status: Status,
···4 XyzStatusphereDefs,
5} from '@statusphere/lexicon'
67+import { AppContext } from '#/context'
8import { Status } from '#/db'
0910export async function statusToStatusView(
11 status: Status,
-347
packages/appview/src/routes.ts
···1-import type { IncomingMessage, ServerResponse } from 'node:http'
2-import { Agent } from '@atproto/api'
3-import { TID } from '@atproto/common'
4-import { OAuthResolverError } from '@atproto/oauth-client-node'
5-import { isValidHandle } from '@atproto/syntax'
6-import {
7- AppBskyActorDefs,
8- AppBskyActorProfile,
9- XyzStatusphereStatus,
10-} from '@statusphere/lexicon'
11-import express from 'express'
12-import { getIronSession, SessionOptions } from 'iron-session'
13-14-import type { AppContext } from '#/index'
15-import { env } from '#/lib/env'
16-import { bskyProfileToProfileView, statusToStatusView } from '#/lib/hydrate'
17-18-type Session = { did: string }
19-20-// Common session options
21-const sessionOptions: SessionOptions = {
22- cookieName: 'sid',
23- password: env.COOKIE_SECRET,
24- cookieOptions: {
25- secure: env.NODE_ENV === 'production',
26- httpOnly: true,
27- sameSite: true,
28- path: '/',
29- // Don't set domain explicitly - let browser determine it
30- domain: undefined,
31- },
32-}
33-34-// Helper function for defining routes
35-const handler =
36- (
37- fn: (
38- req: express.Request,
39- res: express.Response,
40- next: express.NextFunction,
41- ) => Promise<void> | void,
42- ) =>
43- async (
44- req: express.Request,
45- res: express.Response,
46- next: express.NextFunction,
47- ) => {
48- try {
49- await fn(req, res, next)
50- } catch (err) {
51- next(err)
52- }
53- }
54-55-// Helper function to get the Atproto Agent for the active session
56-async function getSessionAgent(
57- req: IncomingMessage | express.Request,
58- res: ServerResponse<IncomingMessage> | express.Response,
59- ctx: AppContext,
60-) {
61- const session = await getIronSession<Session>(req, res, sessionOptions)
62-63- if (!session.did) {
64- return null
65- }
66-67- try {
68- const oauthSession = await ctx.oauthClient.restore(session.did)
69- return oauthSession ? new Agent(oauthSession) : null
70- } catch (err) {
71- ctx.logger.warn({ err }, 'oauth restore failed')
72- session.destroy()
73- return null
74- }
75-}
76-77-export const createRouter = (ctx: AppContext) => {
78- const router = express.Router()
79-80- // Simple CORS configuration for all routes
81- router.use((req, res, next) => {
82- // Allow requests from either the specific origin or any origin during development
83- res.header('Access-Control-Allow-Origin', req.headers.origin || '*')
84- res.header('Access-Control-Allow-Credentials', 'true')
85- res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
86- res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization')
87-88- if (req.method === 'OPTIONS') {
89- res.status(200).end()
90- return
91- }
92- next()
93- })
94-95- // OAuth metadata
96- router.get(
97- '/client-metadata.json',
98- handler((_req, res) => {
99- res.json(ctx.oauthClient.clientMetadata)
100- }),
101- )
102-103- // OAuth callback to complete session creation
104- router.get(
105- '/oauth/callback',
106- handler(async (req, res) => {
107- // Get the query parameters from the URL
108- const params = new URLSearchParams(req.originalUrl.split('?')[1])
109-110- try {
111- const { session } = await ctx.oauthClient.callback(params)
112-113- // Use the common session options
114- const clientSession = await getIronSession<Session>(
115- req,
116- res,
117- sessionOptions,
118- )
119-120- // Set the DID on the session
121- clientSession.did = session.did
122- await clientSession.save()
123-124- // Get the origin and determine appropriate redirect
125- const host = req.get('host') || ''
126- const protocol = req.protocol || 'http'
127- const baseUrl = `${protocol}://${host}`
128-129- ctx.logger.info(
130- `OAuth callback successful, redirecting to ${baseUrl}/oauth-callback`,
131- )
132-133- // Redirect to the frontend oauth-callback page
134- res.redirect('/oauth-callback')
135- } catch (err) {
136- ctx.logger.error({ err }, 'oauth callback failed')
137-138- // Handle error redirect - stay on same domain
139- res.redirect('/oauth-callback?error=auth')
140- }
141- }),
142- )
143-144- // Login handler
145- router.post(
146- '/login',
147- handler(async (req, res) => {
148- // Validate
149- const handle = req.body?.handle
150- if (typeof handle !== 'string' || !isValidHandle(handle)) {
151- res.status(400).json({ error: 'invalid handle' })
152- return
153- }
154-155- // Initiate the OAuth flow
156- try {
157- const url = await ctx.oauthClient.authorize(handle, {
158- scope: 'atproto transition:generic',
159- })
160- res.json({ redirectUrl: url.toString() })
161- } catch (err) {
162- ctx.logger.error({ err }, 'oauth authorize failed')
163- const errorMsg =
164- err instanceof OAuthResolverError
165- ? err.message
166- : "couldn't initiate login"
167- res.status(500).json({ error: errorMsg })
168- }
169- }),
170- )
171-172- // Logout handler
173- router.post(
174- '/logout',
175- handler(async (req, res) => {
176- const session = await getIronSession<Session>(req, res, sessionOptions)
177- session.destroy()
178- res.json({ success: true })
179- }),
180- )
181-182- // Get current user info
183- router.get(
184- '/user',
185- handler(async (req, res) => {
186- const agent = await getSessionAgent(req, res, ctx)
187- if (!agent) {
188- res.status(401).json({ error: 'Not logged in' })
189- return
190- }
191-192- const did = agent.assertDid
193-194- // Fetch user profile
195- try {
196- const profileResponse = await agent.com.atproto.repo
197- .getRecord({
198- repo: did,
199- collection: 'app.bsky.actor.profile',
200- rkey: 'self',
201- })
202- .catch(() => undefined)
203-204- const profileRecord = profileResponse?.data
205- let profile: AppBskyActorProfile.Record =
206- {} as AppBskyActorProfile.Record
207-208- if (
209- profileRecord &&
210- AppBskyActorProfile.isRecord(profileRecord.value)
211- ) {
212- const validated = AppBskyActorProfile.validateRecord(
213- profileRecord.value,
214- )
215- if (validated.success) {
216- profile = profileRecord.value
217- } else {
218- ctx.logger.error(
219- { err: validated.error },
220- 'Failed to validate user profile',
221- )
222- }
223- }
224-225- // Fetch user status
226- const status = await ctx.db
227- .selectFrom('status')
228- .selectAll()
229- .where('authorDid', '=', did)
230- .orderBy('indexedAt', 'desc')
231- .executeTakeFirst()
232-233- res.json({
234- did: agent.assertDid,
235- profile: await bskyProfileToProfileView(did, profile, ctx),
236- status: status ? await statusToStatusView(status, ctx) : undefined,
237- })
238- } catch (err) {
239- ctx.logger.error({ err }, 'Failed to get user info')
240- res.status(500).json({ error: 'Failed to get user info' })
241- }
242- }),
243- )
244-245- // Get statuses
246- router.get(
247- '/statuses',
248- handler(async (req, res) => {
249- try {
250- // Fetch data stored in our SQLite
251- const statuses = await ctx.db
252- .selectFrom('status')
253- .selectAll()
254- .orderBy('indexedAt', 'desc')
255- .limit(30)
256- .execute()
257-258- res.json({
259- statuses: await Promise.all(
260- statuses.map((status) => statusToStatusView(status, ctx)),
261- ),
262- })
263- } catch (err) {
264- ctx.logger.error({ err }, 'Failed to get statuses')
265- res.status(500).json({ error: 'Failed to get statuses' })
266- }
267- }),
268- )
269-270- // Create status
271- router.post(
272- '/status',
273- handler(async (req, res) => {
274- // If the user is signed in, get an agent which communicates with their server
275- const agent = await getSessionAgent(req, res, ctx)
276- if (!agent) {
277- res.status(401).json({ error: 'Session required' })
278- return
279- }
280-281- // Construct & validate their status record
282- const rkey = TID.nextStr()
283- const record = {
284- $type: 'xyz.statusphere.status',
285- status: req.body?.status,
286- createdAt: new Date().toISOString(),
287- }
288- if (!XyzStatusphereStatus.validateRecord(record).success) {
289- res.status(400).json({ error: 'Invalid status' })
290- return
291- }
292-293- let uri
294- try {
295- // Write the status record to the user's repository
296- const response = await agent.com.atproto.repo.putRecord({
297- repo: agent.assertDid,
298- collection: 'xyz.statusphere.status',
299- rkey,
300- record,
301- validate: false,
302- })
303- uri = response.data.uri
304- } catch (err) {
305- ctx.logger.warn({ err }, 'failed to write record')
306- res.status(500).json({ error: 'Failed to write record' })
307- return
308- }
309-310- try {
311- // Optimistically update our SQLite
312- // This isn't strictly necessary because the write event will be
313- // handled in #/firehose/ingestor.ts, but it ensures that future reads
314- // will be up-to-date after this method finishes.
315- await ctx.db
316- .insertInto('status')
317- .values({
318- uri,
319- authorDid: agent.assertDid,
320- status: record.status,
321- createdAt: record.createdAt,
322- indexedAt: new Date().toISOString(),
323- })
324- .execute()
325-326- res.json({
327- success: true,
328- uri,
329- status: await statusToStatusView(record.status, ctx),
330- })
331- } catch (err) {
332- ctx.logger.warn(
333- { err },
334- 'failed to update computed view; ignoring as it should be caught by the firehose',
335- )
336- res.json({
337- success: true,
338- uri,
339- status: await statusToStatusView(record.status, ctx),
340- warning: 'Database not updated',
341- })
342- }
343- }),
344- )
345-346- return router
347-}
···21import * as ComAtprotoRepoStrongRef from './types/com/atproto/repo/strongRef.js'
22import * as ComAtprotoRepoUploadBlob from './types/com/atproto/repo/uploadBlob.js'
23import * as XyzStatusphereDefs from './types/xyz/statusphere/defs.js'
00024import * as XyzStatusphereStatus from './types/xyz/statusphere/status.js'
25import { OmitKey, Un$Typed } from './util.js'
2627export * as XyzStatusphereDefs from './types/xyz/statusphere/defs.js'
00028export * as XyzStatusphereStatus from './types/xyz/statusphere/status.js'
29export * as ComAtprotoLabelDefs from './types/com/atproto/label/defs.js'
30export * as ComAtprotoRepoApplyWrites from './types/com/atproto/repo/applyWrites.js'
···77 constructor(client: XrpcClient) {
78 this._client = client
79 this.status = new StatusRecord(client)
0000000000000000000000000080 }
81}
82
···21import * as ComAtprotoRepoStrongRef from './types/com/atproto/repo/strongRef.js'
22import * as ComAtprotoRepoUploadBlob from './types/com/atproto/repo/uploadBlob.js'
23import * as XyzStatusphereDefs from './types/xyz/statusphere/defs.js'
24+import * as XyzStatusphereGetStatuses from './types/xyz/statusphere/getStatuses.js'
25+import * as XyzStatusphereGetUser from './types/xyz/statusphere/getUser.js'
26+import * as XyzStatusphereSendStatus from './types/xyz/statusphere/sendStatus.js'
27import * as XyzStatusphereStatus from './types/xyz/statusphere/status.js'
28import { OmitKey, Un$Typed } from './util.js'
2930export * as XyzStatusphereDefs from './types/xyz/statusphere/defs.js'
31+export * as XyzStatusphereGetStatuses from './types/xyz/statusphere/getStatuses.js'
32+export * as XyzStatusphereGetUser from './types/xyz/statusphere/getUser.js'
33+export * as XyzStatusphereSendStatus from './types/xyz/statusphere/sendStatus.js'
34export * as XyzStatusphereStatus from './types/xyz/statusphere/status.js'
35export * as ComAtprotoLabelDefs from './types/com/atproto/label/defs.js'
36export * as ComAtprotoRepoApplyWrites from './types/com/atproto/repo/applyWrites.js'
···83 constructor(client: XrpcClient) {
84 this._client = client
85 this.status = new StatusRecord(client)
86+ }
87+88+ getStatuses(
89+ params?: XyzStatusphereGetStatuses.QueryParams,
90+ opts?: XyzStatusphereGetStatuses.CallOptions,
91+ ): Promise<XyzStatusphereGetStatuses.Response> {
92+ return this._client.call(
93+ 'xyz.statusphere.getStatuses',
94+ params,
95+ undefined,
96+ opts,
97+ )
98+ }
99+100+ getUser(
101+ params?: XyzStatusphereGetUser.QueryParams,
102+ opts?: XyzStatusphereGetUser.CallOptions,
103+ ): Promise<XyzStatusphereGetUser.Response> {
104+ return this._client.call('xyz.statusphere.getUser', params, undefined, opts)
105+ }
106+107+ sendStatus(
108+ data?: XyzStatusphereSendStatus.InputSchema,
109+ opts?: XyzStatusphereSendStatus.CallOptions,
110+ ): Promise<XyzStatusphereSendStatus.Response> {
111+ return this._client.call('xyz.statusphere.sendStatus', opts?.qp, data, opts)
112 }
113}
114