auron
Version:
Interact with your ATProto labeler from your terminal
278 lines (244 loc) • 6.5 kB
text/typescript
import {
AppBskyActorDefs,
AtpAgent,
ToolsOzoneModerationDefs,
ToolsOzoneModerationQueryEvents,
ToolsOzoneModerationQueryStatuses,
ComAtprotoServerCreateSession,
} from "@atproto/api";
import { chunkArray } from "@atproto/common";
import { EventEmitter } from "events";
import * as readline from "readline";
import { getGlobalSpinner } from "../utils/loader";
let agent: AtpAgent;
const prompt2FA = (): Promise<string> => {
return new Promise((resolve) => {
const spinner = getGlobalSpinner();
// Stop the spinner to allow clean input
if (spinner) {
spinner.stop();
}
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
rl.question("Enter your 2FA code: ", (answer) => {
rl.close();
// Restart the spinner
if (spinner) {
spinner.start();
}
resolve(answer.trim());
});
});
};
export const getAgent = async () => {
if (agent) return agent;
if (!process.env.SERVICE_URL) {
throw new Error("SERVICE_URL env var is required");
}
if (!process.env.SERVICE_DID) {
throw new Error("SERVICE_DID env var is required");
}
agent = new AtpAgent({ service: process.env.SERVICE_URL });
// @ts-ignore
agent.configureProxy(process.env.SERVICE_DID!);
try {
await agent.login({
identifier: process.env.USERNAME!,
password: process.env.PASSWORD!,
});
} catch (error) {
if (error instanceof ComAtprotoServerCreateSession.AuthFactorTokenRequiredError) {
const authFactorToken = await prompt2FA();
await agent.login({
identifier: process.env.USERNAME!,
password: process.env.PASSWORD!,
authFactorToken,
});
} else {
throw error;
}
}
return agent;
};
export const getQueueItems = async (
{
reviewState,
maxCount = 500,
takendown,
cursor,
}: {
reviewState?: string;
maxCount?: number;
cursor?: string;
takendown?: boolean;
},
emitter: EventEmitter
) => {
const agent = await getAgent();
let nextCursor = cursor;
let counter = 0;
const result: ToolsOzoneModerationQueryStatuses.OutputSchema = {
cursor,
subjectStatuses: [],
};
do {
try {
const params: ToolsOzoneModerationQueryStatuses.QueryParams = {
cursor: nextCursor,
limit: Math.min(100, maxCount),
reviewState: reviewState || ToolsOzoneModerationDefs.REVIEWOPEN,
};
if (takendown !== undefined) {
params.takendown = takendown;
}
const { data } = await agent.tools.ozone.moderation.queryStatuses(params);
nextCursor = data.cursor;
result.subjectStatuses.push(...data.subjectStatuses);
emitter.emit("update", {
maxCount,
nextCursor,
subjectCount: result.subjectStatuses.length,
});
} catch (err) {
console.error(err);
break;
}
// Every 5th request, wait for 500ms to avoid potential rate limiting
if (counter % 5) {
await new Promise((resolve) => setTimeout(resolve, 500));
}
counter++;
} while (
nextCursor &&
(!maxCount || result.subjectStatuses.length < maxCount)
);
return { ...result, cursor: nextCursor };
};
export const getEvents = async (
{
types,
createdAfter,
createdBefore,
cursor,
}: {
types?: string[];
createdAfter?: string;
createdBefore?: string;
cursor?: string;
},
emitter: EventEmitter
) => {
const agent = await getAgent();
let nextCursor = cursor;
let counter = 0;
const result: ToolsOzoneModerationQueryEvents.OutputSchema = {
cursor,
events: [],
};
do {
try {
const { data } = await agent.tools.ozone.moderation.queryEvents({
types,
limit: 100,
createdAfter,
createdBefore,
cursor: nextCursor,
});
nextCursor = data.cursor;
result.events.push(...data.events);
emitter.emit("update", {
nextCursor,
eventCount: result.events.length,
});
} catch (err) {
console.error(err);
break;
}
// Every 5th request, wait for 500ms to avoid potential rate limiting
if (counter % 5) {
await new Promise((resolve) => setTimeout(resolve, 500));
}
counter++;
} while (nextCursor);
return { ...result, cursor: nextCursor };
};
export const getRepos = async (
{ dids }: { dids: string[] },
emitter: EventEmitter
) => {
const agent = await getAgent();
const repos: ToolsOzoneModerationDefs.RepoViewDetail[] = [];
for (const chunk of chunkArray(dids, 100)) {
try {
const { data } = await agent.tools.ozone.moderation.getRepos({
dids: chunk,
});
repos.push(
...data.repos.filter((r) =>
ToolsOzoneModerationDefs.isRepoViewDetail(r)
)
);
emitter.emit("update", {
total: dids.length,
repoCount: repos.length,
});
} catch (err) {
console.log(`Error fetching repos for ${chunk}`);
}
}
return repos;
};
export const getProfiles = async (
{ dids }: { dids: string[] },
emitter: EventEmitter
) => {
const agent = await getAgent();
const profiles: AppBskyActorDefs.ProfileViewDetailed[] = [];
let i = 0;
for (const chunk of chunkArray(dids, 25)) {
if (i % 10 === 0) {
await new Promise((res) => setTimeout(res, 1000));
}
try {
const { data } = await agent.app.bsky.actor.getProfiles({
actors: chunk,
});
profiles.push(...data.profiles);
emitter.emit("update", {
total: dids.length,
profileCount: profiles.length,
});
} catch (err) {
console.log(`Error fetching profiles for ${chunk}`);
}
}
return profiles;
};
export const getRecords = async (
{ uris }: { uris: string[] },
emitter: EventEmitter
) => {
const agent = await getAgent();
const records: ToolsOzoneModerationDefs.RecordViewDetail[] = [];
for (const chunk of chunkArray(uris, 100)) {
try {
const { data } = await agent.tools.ozone.moderation.getRecords({
uris: chunk,
});
records.push(
...data.records.filter((r) =>
ToolsOzoneModerationDefs.isRecordViewDetail(r)
)
);
emitter.emit("update", {
total: uris.length,
recordCount: records.length,
});
} catch (err) {
console.log(`Error fetching records for ${chunk}`);
}
}
return records;
};