···11+# Development override for docker-compose
22+# Usage: docker compose -f compose.yaml -f compose.dev.yaml up
33+#
44+# This configuration:
55+# - Runs the app in watch mode (auto-reloads on file changes)
66+# - Mounts source code so changes are picked up without rebuild
77+88+services:
99+ automod:
1010+ command: ["bun", "run", "dev"]
1111+ volumes:
1212+ - ./src:/app/src
+22-1
compose.yaml
···1313 image: redis:7-alpine
1414 container_name: skywatch-automod-redis
1515 restart: unless-stopped
1616+ command: redis-server --appendonly yes --appendfsync everysec
1617 volumes:
1718 - redis-data:/data
1819 networks:
···33343435 # Expose the metrics server port to the host machine.
3536 ports:
3636- - "4100:4101"
3737+ - "4101:4101"
37383839 # Load environment variables from a .env file in the same directory.
3940 # This is where you should put your BSKY_HANDLE, BSKY_PASSWORD, etc.
···5556 volumes:
5657 - ./cursor.txt:/app/cursor.txt
5758 - ./.session:/app/.session
5959+ - ./rules:/app/rules
58605961 environment:
6062 - NODE_ENV=production
6163 - REDIS_URL=redis://redis:6379
62646565+ prometheus:
6666+ image: prom/prometheus:latest
6767+ container_name: skywatch-prometheus
6868+ restart: unless-stopped
6969+ ports:
7070+ - "9090:9090"
7171+ volumes:
7272+ - ./prometheus.yml:/etc/prometheus/prometheus.yml
7373+ - prometheus-data:/prometheus
7474+ command:
7575+ - '--config.file=/etc/prometheus/prometheus.yml'
7676+ - '--storage.tsdb.path=/prometheus'
7777+ networks:
7878+ - skywatch-network
7979+ depends_on:
8080+ - automod
8181+6382volumes:
6483 redis-data:
8484+ prometheus-data:
65856686networks:
6787 skywatch-network:
6888 driver: bridge
8989+ name: skywatch-network
···11import { agent, isLoggedIn } from "../../agent.js";
22import { PLC_URL } from "../../config.js";
33-import { GLOBAL_ALLOW } from "../../constants.js";
33+import { GLOBAL_ALLOW } from "../../../rules/constants.js";
44import { logger } from "../../logger.js";
55-import { checkAccountLabels, createAccountLabel } from "../../moderation.js";
66-import { ACCOUNT_AGE_CHECKS } from "./ageConstants.js";
55+import { checkAccountLabels, createAccountLabel } from "../../accountModeration.js";
66+import { ACCOUNT_AGE_CHECKS } from "../../../rules/accountAge.js";
7788interface InteractionContext {
99 // For replies
-78
src/rules/account/ageConstants.ts
···11-import { AccountAgeCheck } from "../../types.js";
22-33-/**
44- * Account age monitoring configurations
55- *
66- * Each configuration monitors replies and/or quote posts to specified DIDs or posts
77- * and labels accounts that were created within a specific time window.
88- *
99- * Example use cases:
1010- * - Monitor replies/quotes to high-profile accounts during harassment campaigns
1111- * - Flag sock puppet accounts created to participate in coordinated harassment
1212- * - Detect brigading on specific controversial posts
1313- */
1414-export const ACCOUNT_AGE_CHECKS: AccountAgeCheck[] = [
1515- {
1616- monitoredDIDs: [
1717- "did:plc:b2ecyhl2z2tro25ltrcyiytd", // DHS
1818- "did:plc:iw2wxg46hm4ezguswhwej6t6", // actual whitehouse
1919- "did:plc:fhnl65q3us5evynqc4f2qak6", // HHS
2020- "did:plc:wrz4athzuf2u5js2ltrktiqk", // DOL
2121- "did:plc:3mqcgvyu4exg3pkx4bkfppih", // VA
2222- "did:plc:pqn2sfkx5klnytms4uwqt5wo", // Treasurer
2323- "did:plc:v4kvjftk6kr5ci3zqmfawwpb", // State
2424- "did:plc:rlymk4d5qmq5udjdznojmvel", // Interior
2525- "did:plc:f7a5etif42x56oyrbzuek6so", // USDA
2626- "did:plc:7kusimwlnf4v5jo757jvkeaj", // DOE
2727- "did:plc:jgq3vko3g6zg72457bda2snd", // SBA
2828- "did:plc:h2iujdjlry6fpniofjtiqqmb", // DoD
2929- "did:plc:jwncvpznkwe4luzvdroes45b", // CBP
3030- "did:plc:azfxx5mdxcuoc2bkuqizs4kd",
3131- "did:plc:vostkism5vbzjqfcmllmd6gz",
3232- "did:plc:etthv4ychwti4b6i2hhe76c2",
3333- "did:plc:swf7zddjselkcpbn6iw323gy",
3434- "did:plc:h3zq65wioggctyxpovfpi6ec",
3535- "did:plc:nofnc2xpdihktxkufkq7tn3w",
3636- "did:plc:quezcqejcqw6g5t3om7wldns",
3737- "did:plc:vlvqht2v3nsc4k7xaho6bjaf",
3838- "did:plc:syyfuvqiabipi5mf3x632qij",
3939- "did:plc:6vpxzm6mxjzcfvccnuw2pyd7",
4040- "did:plc:yxqdgravj27gtxkpqhrnzhlx",
4141- "did:plc:nrhrdxqa2v7hfxw2jnuy7rk7",
4242- "did:plc:pr27argcmniiwxp7d7facqwy",
4343- "did:plc:azfxx5mdxcuoc2bkuqizs4kd",
4444- "did:plc:y42muzveli3sjyr3tufaq765",
4545- "did:plc:22wazjq4e4yjafxlew2c6kov",
4646- "did:plc:iw64z65wzkmqvftssb2nldj5",
4747- ],
4848- anchorDate: "2025-10-17", // Date when harassment campaign started
4949- maxAgeDays: 7, // Flag accounts less than 7 days old
5050- label: "suspect-inauthentic",
5151- comment: "New account replying to monitored user during campaign",
5252- },
5353- // Example: Monitor replies to specific accounts
5454- // {
5555- // monitoredDIDs: [
5656- // "did:plc:example123", // High-profile account 1
5757- // "did:plc:example456", // High-profile account 2
5858- // ],
5959- // anchorDate: "2025-01-15", // Date when harassment campaign started
6060- // maxAgeDays: 7, // Flag accounts less than 7 days old
6161- // label: "new-account-reply",
6262- // comment: "New account replying to monitored user during campaign",
6363- // expires: "2025-02-15", // Optional: automatically stop this check after this date
6464- // },
6565- //
6666- // Example: Monitor replies to specific posts
6767- // {
6868- // monitoredPostURIs: [
6969- // "at://did:plc:example123/app.bsky.feed.post/abc123",
7070- // "at://did:plc:example456/app.bsky.feed.post/def456",
7171- // ],
7272- // anchorDate: "2025-01-15",
7373- // maxAgeDays: 7,
7474- // label: "brigading-suspect",
7575- // comment: "New account replying to specific targeted post",
7676- // expires: "2025-02-15",
7777- // },
7878-];
+1-1
src/rules/account/countStarterPacks.ts
···11import { agent, isLoggedIn } from "../../agent.js";
22import { limit } from "../../limits.js";
33import { logger } from "../../logger.js";
44-import { createAccountLabel } from "../../moderation.js";
44+import { createAccountLabel } from "../../accountModeration.js";
5566const ALLOWED_DIDS = ["did:plc:gpunjjgvlyb4racypz3yfiq4"];
77
+5-5
src/rules/account/tests/age.test.ts
···11import { beforeEach, describe, expect, it, vi } from "vitest";
22import { agent } from "../../../agent.js";
33-import { GLOBAL_ALLOW } from "../../../constants.js";
33+import { GLOBAL_ALLOW } from "../../../../rules/constants.js";
44import { logger } from "../../../logger.js";
55-import { checkAccountLabels, createAccountLabel } from "../../../moderation.js";
55+import { checkAccountLabels, createAccountLabel } from "../../../accountModeration.js";
66import {
77 calculateAccountAge,
88 checkAccountAge,
99 getAccountCreationDate,
1010} from "../age.js";
1111-import { ACCOUNT_AGE_CHECKS } from "../ageConstants.js";
1111+import { ACCOUNT_AGE_CHECKS } from "../../../../rules/accountAge.js";
12121313// Mock dependencies
1414vi.mock("../../../agent.js", () => ({
···2727 },
2828}));
29293030-vi.mock("../../../moderation.js", () => ({
3030+vi.mock("../../../accountModeration.js", () => ({
3131 createAccountLabel: vi.fn(),
3232 checkAccountLabels: vi.fn(),
3333}));
34343535-vi.mock("../../../constants.js", () => ({
3535+vi.mock("../../../../rules/constants.js", () => ({
3636 GLOBAL_ALLOW: [],
3737}));
3838
+2-2
src/rules/account/tests/countStarterPacks.test.ts
···22import { agent } from "../../../agent.js";
33import { limit } from "../../../limits.js";
44import { logger } from "../../../logger.js";
55-import { createAccountLabel } from "../../../moderation.js";
55+import { createAccountLabel } from "../../../accountModeration.js";
66import { countStarterPacks } from "../countStarterPacks.js";
7788// Mock dependencies
···2828 },
2929}));
30303131-vi.mock("../../../moderation.js", () => ({
3131+vi.mock("../../../accountModeration.js", () => ({
3232 createAccountLabel: vi.fn(),
3333}));
3434
+1-1
src/rules/facets/facets.ts
···11import { logger } from "../../logger.js";
22-import { createAccountLabel } from "../../moderation.js";
22+import { createAccountLabel } from "../../accountModeration.js";
33import { Facet } from "../../types.js";
4455// Threshold for duplicate facet positions before flagging as spam
+2-2
src/rules/facets/tests/facets.test.ts
···11import { beforeEach, describe, expect, it, vi } from "vitest";
22import { logger } from "../../../logger.js";
33-import { createAccountLabel } from "../../../moderation.js";
33+import { createAccountLabel } from "../../../accountModeration.js";
44import { Facet } from "../../../types.js";
55import {
66 FACET_SPAM_ALLOWLIST,
···1111} from "../facets.js";
12121313// Mock dependencies
1414-vi.mock("../../../moderation.js", () => ({
1414+vi.mock("../../../accountModeration.js", () => ({
1515 createAccountLabel: vi.fn(),
1616}));
1717
···6868 comment: string; // Comment for the label
6969 expires?: string; // Optional expiration date (ISO 8601) - check will be skipped after this date
7070}
7171+7272+export interface AccountThresholdConfig {
7373+ labels: string | string[]; // Single label or array for OR matching
7474+ threshold: number; // Number of labeled posts required to trigger account action
7575+ accountLabel: string; // Label to apply to the account
7676+ accountComment: string; // Comment for the account action
7777+ windowDays: number; // Rolling window in days
7878+ reportAcct: boolean; // Whether to report the account
7979+ commentAcct: boolean; // Whether to comment on the account
8080+ toLabel?: boolean; // Whether to apply label (defaults to true)
8181+}