@atproto/ozone
Version:
Backend service for moderating the Bluesky network.
329 lines (289 loc) • 8.58 kB
text/typescript
import assert from 'node:assert'
import * as plc from '@did-plc/lib'
import express from 'express'
import { AtpAgent } from '@atproto/api'
import { Keypair, Secp256k1Keypair } from '@atproto/crypto'
import { DidCache, IdResolver, MemoryCache } from '@atproto/identity'
import { createServiceAuthHeaders } from '@atproto/xrpc-server'
import { AuthVerifier } from './auth-verifier'
import { BackgroundQueue } from './background'
import {
CommunicationTemplateService,
CommunicationTemplateServiceCreator,
} from './communication-service/template'
import { OzoneConfig, OzoneSecrets } from './config'
import { EventPusher } from './daemon'
import { BlobDiverter } from './daemon/blob-diverter'
import { Database } from './db'
import { ImageInvalidator } from './image-invalidator'
import { ModerationService, ModerationServiceCreator } from './mod-service'
import {
SafelinkRuleService,
SafelinkRuleServiceCreator,
} from './safelink/service'
import { Sequencer } from './sequencer/sequencer'
import { SetService, SetServiceCreator } from './set/service'
import { SettingService, SettingServiceCreator } from './setting/service'
import { TeamService, TeamServiceCreator } from './team'
import {
LABELER_HEADER_NAME,
ParsedLabelers,
defaultLabelerHeader,
getSigningKeyId,
parseLabelerHeader,
} from './util'
import {
VerificationIssuer,
VerificationIssuerCreator,
} from './verification/issuer'
import {
VerificationService,
VerificationServiceCreator,
} from './verification/service'
export type AppContextOptions = {
db: Database
cfg: OzoneConfig
modService: ModerationServiceCreator
communicationTemplateService: CommunicationTemplateServiceCreator
safelinkRuleService: SafelinkRuleServiceCreator
setService: SetServiceCreator
settingService: SettingServiceCreator
teamService: TeamServiceCreator
appviewAgent: AtpAgent
pdsAgent: AtpAgent | undefined
chatAgent: AtpAgent | undefined
blobDiverter?: BlobDiverter
signingKey: Keypair
signingKeyId: number
didCache: DidCache
idResolver: IdResolver
imgInvalidator?: ImageInvalidator
backgroundQueue: BackgroundQueue
sequencer: Sequencer
authVerifier: AuthVerifier
verificationService: VerificationServiceCreator
verificationIssuer: VerificationIssuerCreator
}
export class AppContext {
constructor(
private opts: AppContextOptions,
private secrets: OzoneSecrets,
) {}
static async fromConfig(
cfg: OzoneConfig,
secrets: OzoneSecrets,
overrides?: Partial<AppContextOptions>,
): Promise<AppContext> {
const db = new Database({
url: cfg.db.postgresUrl,
schema: cfg.db.postgresSchema,
poolSize: cfg.db.poolSize,
poolMaxUses: cfg.db.poolMaxUses,
poolIdleTimeoutMs: cfg.db.poolIdleTimeoutMs,
})
const signingKey = await Secp256k1Keypair.import(secrets.signingKeyHex)
const signingKeyId = await getSigningKeyId(db, signingKey.did())
const appviewAgent = new AtpAgent({ service: cfg.appview.url })
const pdsAgent = cfg.pds
? new AtpAgent({ service: cfg.pds.url })
: undefined
const chatAgent = cfg.chat
? new AtpAgent({ service: cfg.chat.url })
: undefined
const didCache = new MemoryCache(
cfg.identity.cacheStaleTTL,
cfg.identity.cacheMaxTTL,
)
const idResolver = new IdResolver({
plcUrl: cfg.identity.plcUrl,
didCache,
})
const createAuthHeaders = (aud: string, lxm: string) =>
createServiceAuthHeaders({
iss: `${cfg.service.did}#atproto_labeler`,
aud,
lxm,
keypair: signingKey,
})
const backgroundQueue = new BackgroundQueue(db)
const blobDiverter = cfg.blobDivert
? new BlobDiverter(db, {
idResolver,
serviceConfig: cfg.blobDivert,
})
: undefined
const eventPusher = new EventPusher(db, createAuthHeaders, {
appview: cfg.appview.pushEvents ? cfg.appview : undefined,
pds: cfg.pds ?? undefined,
})
const modService = ModerationService.creator(
signingKey,
signingKeyId,
cfg,
backgroundQueue,
idResolver,
eventPusher,
appviewAgent,
createAuthHeaders,
overrides?.imgInvalidator,
)
const communicationTemplateService = CommunicationTemplateService.creator()
const safelinkRuleService = SafelinkRuleService.creator()
const teamService = TeamService.creator(
appviewAgent,
cfg.appview.did,
createAuthHeaders,
)
const setService = SetService.creator()
const settingService = SettingService.creator()
const verificationService = VerificationService.creator()
const verificationIssuer = VerificationIssuer.creator()
const sequencer = new Sequencer(modService(db))
const authVerifier = new AuthVerifier(idResolver, {
serviceDid: cfg.service.did,
adminPassword: secrets.adminPassword,
teamService: teamService(db),
})
return new AppContext(
{
db,
cfg,
modService,
communicationTemplateService,
safelinkRuleService,
teamService,
setService,
settingService,
appviewAgent,
pdsAgent,
chatAgent,
signingKey,
signingKeyId,
didCache,
idResolver,
backgroundQueue,
sequencer,
authVerifier,
blobDiverter,
verificationService,
verificationIssuer,
...(overrides ?? {}),
},
secrets,
)
}
assignPort(port: number) {
assert(
!this.cfg.service.port || this.cfg.service.port === port,
'Conflicting port in config',
)
this.opts.cfg.service.port = port
}
get db(): Database {
return this.opts.db
}
get cfg(): OzoneConfig {
return this.opts.cfg
}
get modService(): ModerationServiceCreator {
return this.opts.modService
}
get blobDiverter(): BlobDiverter | undefined {
return this.opts.blobDiverter
}
get communicationTemplateService(): CommunicationTemplateServiceCreator {
return this.opts.communicationTemplateService
}
get safelinkRuleService(): SafelinkRuleServiceCreator {
return this.opts.safelinkRuleService
}
get teamService(): TeamServiceCreator {
return this.opts.teamService
}
get setService(): SetServiceCreator {
return this.opts.setService
}
get settingService(): SettingServiceCreator {
return this.opts.settingService
}
get verificationService(): VerificationServiceCreator {
return this.opts.verificationService
}
get verificationIssuer(): VerificationIssuerCreator {
return this.opts.verificationIssuer
}
get appviewAgent(): AtpAgent {
return this.opts.appviewAgent
}
get pdsAgent(): AtpAgent | undefined {
return this.opts.pdsAgent
}
get chatAgent(): AtpAgent | undefined {
return this.opts.chatAgent
}
get signingKey(): Keypair {
return this.opts.signingKey
}
get signingKeyId(): number {
return this.opts.signingKeyId
}
get plcClient(): plc.Client {
return new plc.Client(this.cfg.identity.plcUrl)
}
get didCache(): DidCache {
return this.opts.didCache
}
get idResolver(): IdResolver {
return this.opts.idResolver
}
get backgroundQueue(): BackgroundQueue {
return this.opts.backgroundQueue
}
get sequencer(): Sequencer {
return this.opts.sequencer
}
get authVerifier(): AuthVerifier {
return this.opts.authVerifier
}
async serviceAuthHeaders(aud: string, lxm: string) {
const iss = `${this.cfg.service.did}#atproto_labeler`
return createServiceAuthHeaders({
iss,
aud,
lxm,
keypair: this.signingKey,
})
}
async pdsAuth(lxm: string) {
if (!this.cfg.pds) {
return undefined
}
return this.serviceAuthHeaders(this.cfg.pds.did, lxm)
}
async appviewAuth(lxm: string) {
return this.serviceAuthHeaders(this.cfg.appview.did, lxm)
}
async chatAuth(lxm: string) {
if (!this.cfg.chat) {
throw new Error('No chat service configured')
}
return this.serviceAuthHeaders(this.cfg.chat.did, lxm)
}
devOverride(overrides: Partial<AppContextOptions>) {
this.opts = {
...this.opts,
...overrides,
}
}
reqLabelers(req: express.Request): ParsedLabelers {
const val = req.header(LABELER_HEADER_NAME)
let parsed: ParsedLabelers | null
try {
parsed = parseLabelerHeader(val, this.cfg.service.did)
} catch (err) {
parsed = null
}
if (!parsed) return defaultLabelerHeader([])
return parsed
}
}