···11+import { agent, isLoggedIn } from "../agent.js";
22+import { logger } from "../logger.js";
33+import { createAccountLabel } from "../moderation.js";
44+import { ACCOUNT_AGE_CHECKS } from "./ageConstants.js";
55+import { PLC_URL } from "../config.js";
66+77+interface ReplyContext {
88+ replyToDid: string;
99+ replyingDid: string;
1010+ atURI: string;
1111+ time: number;
1212+}
1313+1414+/**
1515+ * Gets the account creation date from a DID
1616+ * Uses the plc directory to get DID document creation timestamp
1717+ */
1818+export const getAccountCreationDate = async (
1919+ did: string,
2020+): Promise<Date | null> => {
2121+ try {
2222+ await isLoggedIn;
2323+2424+ // For plc DIDs, try to extract creation from the DID document
2525+ if (did.startsWith("did:plc:")) {
2626+ try {
2727+ const response = await fetch(`https://${PLC_URL}/${did}`);
2828+ if (response.ok) {
2929+ const didDoc = await response.json();
3030+3131+ // The plc directory returns an array of operations, first one is creation
3232+ if (Array.isArray(didDoc) && didDoc.length > 0) {
3333+ const createdAt = didDoc[0].createdAt;
3434+ if (createdAt) {
3535+ return new Date(createdAt);
3636+ }
3737+ }
3838+ } else {
3939+ logger.debug(
4040+ { process: "ACCOUNT_AGE", did },
4141+ "Failed to fetch DID document, trying profile fallback",
4242+ );
4343+ }
4444+ } catch (plcError) {
4545+ logger.debug(
4646+ { process: "ACCOUNT_AGE", did },
4747+ "Error fetching from plc directory, trying profile fallback",
4848+ );
4949+ }
5050+ }
5151+5252+ // Fallback: try getting profile for any DID type
5353+ try {
5454+ const profile = await agent.getProfile({ actor: did });
5555+ if (profile.data.indexedAt) {
5656+ return new Date(profile.data.indexedAt);
5757+ }
5858+ } catch (profileError) {
5959+ logger.debug(
6060+ { process: "ACCOUNT_AGE", did },
6161+ "Failed to get profile",
6262+ );
6363+ }
6464+6565+ logger.warn(
6666+ { process: "ACCOUNT_AGE", did },
6767+ "Could not determine account creation date",
6868+ );
6969+ return null;
7070+ } catch (error) {
7171+ logger.error(
7272+ { process: "ACCOUNT_AGE", did, error },
7373+ "Error fetching account creation date",
7474+ );
7575+ return null;
7676+ }
7777+};
7878+7979+/**
8080+ * Calculates the age of an account in days at a specific reference date
8181+ */
8282+export const calculateAccountAge = (
8383+ creationDate: Date,
8484+ referenceDate: Date,
8585+): number => {
8686+ const diffMs = referenceDate.getTime() - creationDate.getTime();
8787+ return Math.floor(diffMs / (1000 * 60 * 60 * 24));
8888+};
8989+9090+/**
9191+ * Checks if a reply meets age criteria and applies labels accordingly
9292+ */
9393+export const checkAccountAge = async (
9494+ context: ReplyContext,
9595+): Promise<void> => {
9696+ // Skip if no checks configured
9797+ if (ACCOUNT_AGE_CHECKS.length === 0) {
9898+ return;
9999+ }
100100+101101+ // Check each configuration
102102+ for (const check of ACCOUNT_AGE_CHECKS) {
103103+ // Check if this reply is to a monitored DID
104104+ if (!check.monitoredDIDs.includes(context.replyToDid)) {
105105+ continue;
106106+ }
107107+108108+ logger.debug(
109109+ {
110110+ process: "ACCOUNT_AGE",
111111+ replyingDid: context.replyingDid,
112112+ replyToDid: context.replyToDid,
113113+ },
114114+ "Checking account age for reply to monitored DID",
115115+ );
116116+117117+ // Get account creation date
118118+ const creationDate = await getAccountCreationDate(context.replyingDid);
119119+ if (!creationDate) {
120120+ logger.warn(
121121+ {
122122+ process: "ACCOUNT_AGE",
123123+ replyingDid: context.replyingDid,
124124+ },
125125+ "Could not determine creation date, skipping",
126126+ );
127127+ continue;
128128+ }
129129+130130+ // Calculate age at anchor date
131131+ const anchorDate = new Date(check.anchorDate);
132132+ const accountAge = calculateAccountAge(creationDate, anchorDate);
133133+134134+ logger.debug(
135135+ {
136136+ process: "ACCOUNT_AGE",
137137+ replyingDid: context.replyingDid,
138138+ creationDate: creationDate.toISOString(),
139139+ anchorDate: check.anchorDate,
140140+ accountAge,
141141+ threshold: check.maxAgeDays,
142142+ },
143143+ "Account age calculated",
144144+ );
145145+146146+ // Check if account is too new
147147+ if (accountAge < check.maxAgeDays) {
148148+ logger.info(
149149+ {
150150+ process: "ACCOUNT_AGE",
151151+ replyingDid: context.replyingDid,
152152+ replyToDid: context.replyToDid,
153153+ accountAge,
154154+ threshold: check.maxAgeDays,
155155+ atURI: context.atURI,
156156+ },
157157+ "Labeling new account replying to monitored DID",
158158+ );
159159+160160+ await createAccountLabel(
161161+ context.replyingDid,
162162+ check.label,
163163+ `${context.time}: ${check.comment} - Account age: ${accountAge} days (threshold: ${check.maxAgeDays} days) - Reply: ${context.atURI}`,
164164+ );
165165+166166+ // Only apply one label per reply
167167+ return;
168168+ }
169169+ }
170170+};
+25
src/account/ageConstants.ts
···11+import { AccountAgeCheck } from "../types.js";
22+33+/**
44+ * Account age monitoring configurations
55+ *
66+ * Each configuration monitors replies to specified DIDs and labels accounts
77+ * that are newer than the threshold relative to the anchor date.
88+ *
99+ * Example use case:
1010+ * - Monitor replies to high-profile accounts during harassment campaigns
1111+ * - Flag sock puppet accounts created to participate in coordinated harassment
1212+ */
1313+export const ACCOUNT_AGE_CHECKS: AccountAgeCheck[] = [
1414+ // Example configuration (disabled by default)
1515+ // {
1616+ // monitoredDIDs: [
1717+ // "did:plc:example123", // High-profile account 1
1818+ // "did:plc:example456", // High-profile account 2
1919+ // ],
2020+ // anchorDate: "2025-01-15", // Date when harassment campaign started
2121+ // maxAgeDays: 7, // Flag accounts less than 7 days old
2222+ // label: "new-account-reply",
2323+ // comment: "New account replying to monitored user during campaign",
2424+ // },
2525+];
···5858 index: FacetIndex;
5959 features: Array<{ $type: string; [key: string]: any }>;
6060}
6161+6262+export interface AccountAgeCheck {
6363+ monitoredDIDs: string[]; // DIDs to monitor for replies
6464+ anchorDate: string; // ISO 8601 date string (e.g., "2025-01-15")
6565+ maxAgeDays: number; // Maximum account age in days
6666+ label: string; // Label to apply if account is too new
6767+ comment: string; // Comment for the label
6868+}
6969+