A tool for parsing traffic on the jetstream and applying a moderation workstream based on regexp based rules
1import { agent, isLoggedIn } from "./agent.js";
2import { MOD_DID } from "./config.js";
3import { limit } from "./limits.js";
4import { logger } from "./logger.js";
5import { labelsAppliedCounter, labelsCachedCounter } from "./metrics.js";
6import { tryClaimPostLabel } from "./redis.js";
7
8const doesLabelExist = (
9 labels: { val: string }[] | undefined,
10 labelVal: string,
11): boolean => {
12 if (!labels) {
13 return false;
14 }
15 return labels.some((label) => label.val === labelVal);
16};
17
18export const createPostLabel = async (
19 uri: string,
20 cid: string,
21 label: string,
22 comment: string,
23 duration: number | undefined,
24 did?: string,
25 time?: number,
26) => {
27 await isLoggedIn;
28
29 const claimed = await tryClaimPostLabel(uri, label);
30 if (!claimed) {
31 logger.debug(
32 { process: "MODERATION", uri, label },
33 "Post label already claimed in Redis, skipping",
34 );
35 labelsCachedCounter.inc({
36 label_type: label,
37 target_type: "post",
38 reason: "redis_cache",
39 });
40 return;
41 }
42
43 const hasLabel = await checkRecordLabels(uri, label);
44 if (hasLabel) {
45 logger.debug(
46 { process: "MODERATION", uri, label },
47 "Post already has label, skipping",
48 );
49 labelsCachedCounter.inc({
50 label_type: label,
51 target_type: "post",
52 reason: "existing_label",
53 });
54 return;
55 }
56
57 logger.info(
58 { process: "MODERATION", label, did, atURI: uri },
59 "Labeling post",
60 );
61 labelsAppliedCounter.inc({ label_type: label, target_type: "post" });
62
63 await limit(async () => {
64 try {
65 const event: {
66 $type: string;
67 comment: string;
68 createLabelVals: string[];
69 negateLabelVals: string[];
70 durationInHours?: number;
71 } = {
72 $type: "tools.ozone.moderation.defs#modEventLabel",
73 comment,
74 createLabelVals: [label],
75 negateLabelVals: [],
76 };
77
78 if (duration) {
79 event.durationInHours = duration;
80 }
81
82 await agent.tools.ozone.moderation.emitEvent(
83 {
84 event,
85 // specify the labeled post by strongRef
86 subject: {
87 $type: "com.atproto.repo.strongRef",
88 uri,
89 cid,
90 },
91 // put in the rest of the metadata
92 createdBy: agent.did ?? "",
93 createdAt: new Date().toISOString(),
94 modTool: {
95 name: "skywatch/skywatch-automod",
96 meta: {
97 time: new Date().toISOString(),
98 externalUrl: `https://pdsls.dev/${uri}`,
99 },
100 },
101 },
102 {
103 encoding: "application/json",
104 headers: {
105 "atproto-proxy": `${MOD_DID}#atproto_labeler`,
106 "atproto-accept-labelers":
107 "did:plc:ar7c4by46qjdydhdevvrndac;redact",
108 },
109 },
110 );
111
112 if (did && time) {
113 try {
114 // Dynamic import to avoid circular dependency:
115 // accountThreshold imports from moderation (createAccountLabel, etc.)
116 // moderation imports from accountThreshold (checkAccountThreshold)
117 const { checkAccountThreshold } = await import(
118 "./accountThreshold.js"
119 );
120 await checkAccountThreshold(did, uri, label, time);
121 } catch (error) {
122 logger.error(
123 { process: "ACCOUNT_THRESHOLD", did, label, error },
124 "Failed to check account threshold",
125 );
126 }
127 }
128 } catch (e) {
129 logger.error(
130 { process: "MODERATION", error: e },
131 "Failed to create post label",
132 );
133 throw e;
134 }
135 });
136};
137
138export const createPostReport = async (
139 uri: string,
140 cid: string,
141 comment: string,
142) => {
143 await isLoggedIn;
144 await limit(async () => {
145 try {
146 return await agent.tools.ozone.moderation.emitEvent(
147 {
148 event: {
149 $type: "tools.ozone.moderation.defs#modEventReport",
150 comment,
151 reportType: "com.atproto.moderation.defs#reasonOther",
152 },
153 // specify the labeled post by strongRef
154 subject: {
155 $type: "com.atproto.repo.strongRef",
156 uri,
157 cid,
158 },
159 // put in the rest of the metadata
160 createdBy: agent.did ?? "",
161 createdAt: new Date().toISOString(),
162 modTool: {
163 name: "skywatch/skywatch-automod",
164 meta: {
165 time: new Date().toISOString(),
166 externalUrl: `https://pdsls.dev/${uri}`,
167 },
168 },
169 },
170 {
171 encoding: "application/json",
172 headers: {
173 "atproto-proxy": `${MOD_DID}#atproto_labeler`,
174 "atproto-accept-labelers":
175 "did:plc:ar7c4by46qjdydhdevvrndac;redact",
176 },
177 },
178 );
179 } catch (e) {
180 logger.error(
181 { process: "MODERATION", error: e },
182 "Failed to create post report",
183 );
184 throw e;
185 }
186 });
187};
188
189export const checkRecordLabels = async (
190 uri: string,
191 label: string,
192): Promise<boolean> => {
193 await isLoggedIn;
194 return await limit(async () => {
195 try {
196 const response = await agent.tools.ozone.moderation.getRecord(
197 { uri },
198 {
199 headers: {
200 "atproto-proxy": `${MOD_DID}#atproto_labeler`,
201 "atproto-accept-labelers":
202 "did:plc:ar7c4by46qjdydhdevvrndac;redact",
203 },
204 },
205 );
206
207 return doesLabelExist(response.data.labels, label);
208 } catch (e) {
209 logger.error(
210 { process: "MODERATION", uri, error: e },
211 "Failed to check record labels",
212 );
213 return false;
214 }
215 });
216};