@atproto/ozone
Version:
Backend service for moderating the Bluesky network.
210 lines • 7.77 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.TeamService = void 0;
const common_1 = require("@atproto/common");
const xrpc_server_1 = require("@atproto/xrpc-server");
const lexicons_1 = require("../lexicon/lexicons");
const logger_1 = require("../logger");
class TeamService {
constructor(db, appviewAgent, appviewDid, createAuthHeaders) {
Object.defineProperty(this, "db", {
enumerable: true,
configurable: true,
writable: true,
value: db
});
Object.defineProperty(this, "appviewAgent", {
enumerable: true,
configurable: true,
writable: true,
value: appviewAgent
});
Object.defineProperty(this, "appviewDid", {
enumerable: true,
configurable: true,
writable: true,
value: appviewDid
});
Object.defineProperty(this, "createAuthHeaders", {
enumerable: true,
configurable: true,
writable: true,
value: createAuthHeaders
});
}
static creator(appviewAgent, appviewDid, createAuthHeaders) {
return (db) => new TeamService(db, appviewAgent, appviewDid, createAuthHeaders);
}
async list({ cursor, limit = 25, roles, disabled, q, }) {
let builder = this.db.db.selectFrom('member').selectAll();
if (cursor) {
builder = builder.where('createdAt', '>', new Date(cursor));
}
if (roles !== undefined) {
const knownRoles = roles.filter((r) => r === 'tools.ozone.team.defs#roleAdmin' ||
r === 'tools.ozone.team.defs#roleModerator' ||
r === 'tools.ozone.team.defs#roleVerifier' ||
r === 'tools.ozone.team.defs#roleTriage');
// Optimization: no need to query to know that no values will be returned
if (!knownRoles.length)
return { members: [] };
builder = builder.where('role', 'in', knownRoles);
}
if (disabled !== undefined) {
builder = builder.where('disabled', disabled ? 'is' : 'is not', true);
}
if (q) {
builder = builder.where((qb) => qb
.orWhere('handle', 'ilike', `%${q}%`)
.orWhere('displayName', 'ilike', `%${q}%`));
}
const members = await builder
.limit(limit)
.orderBy('createdAt', 'asc')
.orderBy('handle', 'asc')
.execute();
return { members, cursor: members.at(-1)?.createdAt.toISOString() };
}
async create({ role, did, disabled, updatedAt, createdAt, lastUpdatedBy, }) {
const now = new Date();
const newMember = await this.db.db
.insertInto('member')
.values({
role,
did,
disabled,
lastUpdatedBy,
updatedAt: updatedAt || now,
createdAt: createdAt || now,
})
.returningAll()
.executeTakeFirstOrThrow();
return newMember;
}
async upsert({ role, did, lastUpdatedBy, }) {
const now = new Date();
await this.db.db
.insertInto('member')
.values({
role,
did,
lastUpdatedBy,
disabled: false,
updatedAt: now,
createdAt: now,
})
.onConflict((oc) => oc.column('did').doUpdateSet({ role, updatedAt: now, lastUpdatedBy }))
.execute();
}
async update(did, updates) {
const { role, disabled, lastUpdatedBy, updatedAt = new Date() } = updates;
const updatedMember = await this.db.db
.updateTable('member')
.where('did', '=', did)
.set({
role,
disabled,
lastUpdatedBy,
updatedAt,
})
.returningAll()
.executeTakeFirstOrThrow();
return updatedMember;
}
async delete(did) {
await this.db.db.deleteFrom('member').where('did', '=', did).execute();
}
async assertCanDelete(did) {
const memberExists = await this.doesMemberExist(did);
if (!memberExists) {
throw new xrpc_server_1.InvalidRequestError('member not found', 'MemberNotFound');
}
}
async doesMemberExist(did) {
const member = await this.db.db
.selectFrom('member')
.select('did')
.where('did', '=', did)
.executeTakeFirst();
return !!member;
}
async getMember(did) {
const member = await this.db.db
.selectFrom('member')
.selectAll()
.where('did', '=', did)
.executeTakeFirst();
return member;
}
getMemberRole(member) {
const isAdmin = member?.role === 'tools.ozone.team.defs#roleAdmin';
const isModerator = isAdmin || member?.role === 'tools.ozone.team.defs#roleModerator';
const isTriage = isModerator || member?.role === 'tools.ozone.team.defs#roleTriage';
const isVerifier = isAdmin || member?.role === 'tools.ozone.team.defs#roleVerifier';
return {
isModerator,
isAdmin,
isTriage,
isVerifier,
};
}
// getProfiles() only allows 25 DIDs at a time so we need to query in chunks
async getProfiles(dids) {
const profiles = new Map();
try {
const headers = await this.createAuthHeaders(this.appviewDid, lexicons_1.ids.AppBskyActorGetProfiles);
for (const actors of (0, common_1.chunkArray)(dids, 25)) {
const { data } = await this.appviewAgent.getProfiles({ actors }, headers);
data.profiles.forEach((profile) => {
profiles.set(profile.did, profile);
});
}
}
catch (err) {
logger_1.httpLogger.error({ err, dids }, 'Failed to get profiles for team members');
}
return profiles;
}
async syncMemberProfiles() {
let lastDid = '';
// Max 25 profiles can be fetched at a time so let's pull 25 members at a time from the db and update their profile details
do {
const members = await this.db.db
.selectFrom('member')
.select(['did'])
.limit(25)
.if(!!lastDid, (q) => q.where('did', '>', lastDid))
.orderBy('did', 'asc')
.execute();
const dids = members.map((member) => member.did);
const profiles = await this.getProfiles(dids);
for (const profile of profiles.values()) {
await this.db.db
.updateTable('member')
.where('did', '=', profile.did)
.set({
handle: profile.handle,
displayName: profile.displayName || null,
})
.execute();
}
lastDid = dids.at(-1) || '';
} while (lastDid);
}
async view(members) {
const profiles = await this.getProfiles(members.map(({ did }) => did));
return members.map((member) => {
return {
did: member.did,
role: member.role,
disabled: member.disabled,
profile: profiles.get(member.did),
createdAt: member.createdAt.toISOString(),
updatedAt: member.updatedAt.toISOString(),
lastUpdatedBy: member.lastUpdatedBy,
};
});
}
}
exports.TeamService = TeamService;
//# sourceMappingURL=index.js.map