@atproto/ozone
Version:
Backend service for moderating the Bluesky network.
318 lines • 11.3 kB
JavaScript
"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