UNPKG

@atproto/ozone

Version:

Backend service for moderating the Bluesky network.

318 lines 11.3 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.EventPusher = void 0; const node_assert_1 = __importDefault(require("node:assert")); const api_1 = require("@atproto/api"); const common_1 = require("@atproto/common"); const lexicons_1 = require("../lexicon/lexicons"); const logger_1 = require("../logger"); const util_1 = require("../util"); class EventPusher { constructor(db, createAuthHeaders, services) { Object.defineProperty(this, "db", { enumerable: true, configurable: true, writable: true, value: db }); Object.defineProperty(this, "createAuthHeaders", { enumerable: true, configurable: true, writable: true, value: createAuthHeaders }); Object.defineProperty(this, "destroyed", { enumerable: true, configurable: true, writable: true, value: false }); Object.defineProperty(this, "repoPollState", { enumerable: true, configurable: true, writable: true, value: { promise: Promise.resolve(), } }); Object.defineProperty(this, "recordPollState", { enumerable: true, configurable: true, writable: true, value: { promise: Promise.resolve(), } }); Object.defineProperty(this, "blobPollState", { enumerable: true, configurable: true, writable: true, value: { promise: Promise.resolve(), } }); Object.defineProperty(this, "appview", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "pds", { enumerable: true, configurable: true, writable: true, value: void 0 }); if (services.appview) { this.appview = { agent: new api_1.AtpAgent({ service: services.appview.url }), did: services.appview.did, }; } if (services.pds) { this.pds = { agent: new api_1.AtpAgent({ service: services.pds.url }), did: services.pds.did, }; } } start() { this.poll(this.repoPollState, () => this.pushRepoEvents()); this.poll(this.recordPollState, () => this.pushRecordEvents()); this.poll(this.blobPollState, () => this.pushBlobEvents()); } // event pusher may be configured with both appview and pds // but the takedown may particularly want only one of them // unless the target services are specified, we will push to all configured services getTakedownServices(targetServices) { let configured = []; if (this.pds) configured.push('pds_takedown'); if (this.appview) configured.push('appview_takedown'); if (!targetServices.size) { return configured; } if (!targetServices.has('appview')) { configured = configured.filter((service) => service !== 'appview_takedown'); } if (!targetServices.has('pds')) { configured = configured.filter((service) => service !== 'pds_takedown'); } return configured; } poll(state, fn) { if (this.destroyed) return; state.promise = fn() .catch((err) => { logger_1.dbLogger.error({ err }, 'event push failed'); }) .finally(() => { state.timer = setTimeout(() => this.poll(state, fn), 30 * common_1.SECOND); }); } async processAll() { await Promise.all([ this.pushRepoEvents(), this.pushRecordEvents(), this.pushBlobEvents(), this.repoPollState.promise, this.recordPollState.promise, this.blobPollState.promise, ]); } async destroy() { this.destroyed = true; const destroyState = (state) => { if (state.timer) { clearTimeout(state.timer); } return state.promise; }; await Promise.all([ destroyState(this.repoPollState), destroyState(this.recordPollState), destroyState(this.blobPollState), ]); } async pushRepoEvents() { const toPush = await this.db.db .selectFrom('repo_push_event') .select('id') .forUpdate() .skipLocked() .where('confirmedAt', 'is', null) .where('attempts', '<', 10) .execute(); await Promise.all(toPush.map((evt) => this.attemptRepoEvent(evt.id))); } async pushRecordEvents() { const toPush = await this.db.db .selectFrom('record_push_event') .select('id') .forUpdate() .skipLocked() .where('confirmedAt', 'is', null) .where('attempts', '<', 10) .execute(); await Promise.all(toPush.map((evt) => this.attemptRecordEvent(evt.id))); } async pushBlobEvents() { const toPush = await this.db.db .selectFrom('blob_push_event') .select('id') .forUpdate() .skipLocked() .where('confirmedAt', 'is', null) .where('attempts', '<', 10) .execute(); await Promise.all(toPush.map((evt) => this.attemptBlobEvent(evt.id))); } async updateSubjectOnService(service, subject, takedownRef) { const auth = await this.createAuthHeaders(service.did, lexicons_1.ids.ComAtprotoAdminUpdateSubjectStatus); try { await (0, util_1.retryHttp)(() => service.agent.com.atproto.admin.updateSubjectStatus({ subject, takedown: { applied: !!takedownRef, ref: takedownRef ?? undefined, }, }, { ...auth, encoding: 'application/json', })); return true; } catch (err) { logger_1.dbLogger.error({ err, subject, takedownRef }, 'failed to push out event'); return false; } } async attemptRepoEvent(id) { await this.db.transaction(async (dbTxn) => { const evt = await dbTxn.db .selectFrom('repo_push_event') .selectAll() .forUpdate() .skipLocked() .where('id', '=', id) .where('confirmedAt', 'is', null) .executeTakeFirst(); if (!evt) return; const service = evt.eventType === 'pds_takedown' ? this.pds : this.appview; (0, node_assert_1.default)(service); const subject = { $type: 'com.atproto.admin.defs#repoRef', did: evt.subjectDid, }; const succeeded = await this.updateSubjectOnService(service, subject, evt.takedownRef); await dbTxn.db .updateTable('repo_push_event') .set(succeeded ? { confirmedAt: new Date() } : { lastAttempted: new Date(), attempts: (evt.attempts ?? 0) + 1, }) .where('subjectDid', '=', evt.subjectDid) .where('eventType', '=', evt.eventType) .execute(); }); } async attemptRecordEvent(id) { await this.db.transaction(async (dbTxn) => { const evt = await dbTxn.db .selectFrom('record_push_event') .selectAll() .forUpdate() .skipLocked() .where('id', '=', id) .where('confirmedAt', 'is', null) .executeTakeFirst(); if (!evt) return; const service = evt.eventType === 'pds_takedown' ? this.pds : this.appview; (0, node_assert_1.default)(service); const subject = { $type: 'com.atproto.repo.strongRef', uri: evt.subjectUri, cid: evt.subjectCid, }; const succeeded = await this.updateSubjectOnService(service, subject, evt.takedownRef); await dbTxn.db .updateTable('record_push_event') .set(succeeded ? { confirmedAt: new Date() } : { lastAttempted: new Date(), attempts: (evt.attempts ?? 0) + 1, }) .where('subjectUri', '=', evt.subjectUri) .where('eventType', '=', evt.eventType) .execute(); }); } async attemptBlobEvent(id) { await this.db.transaction(async (dbTxn) => { const evt = await dbTxn.db .selectFrom('blob_push_event') .selectAll() .forUpdate() .skipLocked() .where('id', '=', id) .where('confirmedAt', 'is', null) .executeTakeFirst(); if (!evt) return; const service = evt.eventType === 'pds_takedown' ? this.pds : this.appview; (0, node_assert_1.default)(service); const subject = { $type: 'com.atproto.admin.defs#repoBlobRef', did: evt.subjectDid, cid: evt.subjectBlobCid, }; const succeeded = await this.updateSubjectOnService(service, subject, evt.takedownRef); await this.markBlobEventAttempt(dbTxn, evt, succeeded); }); } async markBlobEventAttempt(dbTxn, event, succeeded) { await dbTxn.db .updateTable('blob_push_event') .set(succeeded ? { confirmedAt: new Date() } : { lastAttempted: new Date(), attempts: (event.attempts ?? 0) + 1, }) .where('subjectDid', '=', event.subjectDid) .where('subjectBlobCid', '=', event.subjectBlobCid) .where('eventType', '=', event.eventType) .execute(); } async logBlobPushEvent(blobValues, takedownRef) { return this.db.db .insertInto('blob_push_event') .values(blobValues) .onConflict((oc) => oc.columns(['subjectDid', 'subjectBlobCid', 'eventType']).doUpdateSet({ takedownRef, confirmedAt: null, attempts: 0, lastAttempted: null, })) .returning([ 'id', 'subjectDid', 'subjectUri', 'subjectBlobCid', 'eventType', ]) .execute(); } } exports.EventPusher = EventPusher; //# sourceMappingURL=event-pusher.js.map