tangled
alpha
login
or
join now
baileytownsend.dev
/
label-watcher
36
fork
atom
PDS Admin tool make it easier to moderate your PDS with labels
36
fork
atom
overview
issues
pulls
pipelines
wip
baileytownsend.dev
3 weeks ago
7cf1c92d
3eadb197
+259
-29
11 changed files
expand all
collapse all
unified
split
.env
.env.example
drizzle
0001_workable_leech.sql
meta
0001_snapshot.json
_journal.json
package.json
src
db
schema.ts
handlers
handleNewLabel.ts
lablerSubscriber.ts
index.ts
logger.ts
+5
.env
···
1
1
+
DATABASE_URL=file:./label-watcher.db
2
2
+
MIGRATIONS_FOLDER=drizzle
3
3
+
NOTIFY_SMTP_URL=smtps://resend:....
4
4
+
NOTIFY_SENDER_EMAIL=yougotmail@pdsmoover.com
5
5
+
LOG_LEVEL=debug
+2
.env.example
···
1
1
DATABASE_URL=file:./label-watcher.db
2
2
MIGRATIONS_FOLDER=drizzle
3
3
NOTIFY_SMTP_URL=smtps://resend:....
4
4
+
NOTIFY_SENDER_EMAIL=yougotmail@pdsmoover.com
5
5
+
LOG_LEVEL=info
+1
drizzle/0001_workable_leech.sql
···
1
1
+
ALTER TABLE `labels_applied` ADD `labeler` text NOT NULL;
+170
drizzle/meta/0001_snapshot.json
···
1
1
+
{
2
2
+
"version": "6",
3
3
+
"dialect": "sqlite",
4
4
+
"id": "368e7243-8e16-4e27-a7af-a906d976d75e",
5
5
+
"prevId": "5a06d315-4901-4f9e-aab2-1c8dd03bb750",
6
6
+
"tables": {
7
7
+
"labeler_cursors": {
8
8
+
"name": "labeler_cursors",
9
9
+
"columns": {
10
10
+
"labeler_id": {
11
11
+
"name": "labeler_id",
12
12
+
"type": "text",
13
13
+
"primaryKey": false,
14
14
+
"notNull": false,
15
15
+
"autoincrement": false
16
16
+
},
17
17
+
"cursor": {
18
18
+
"name": "cursor",
19
19
+
"type": "integer",
20
20
+
"primaryKey": false,
21
21
+
"notNull": true,
22
22
+
"autoincrement": false
23
23
+
}
24
24
+
},
25
25
+
"indexes": {
26
26
+
"labeler_cursors_labeler_id_unique": {
27
27
+
"name": "labeler_cursors_labeler_id_unique",
28
28
+
"columns": [
29
29
+
"labeler_id"
30
30
+
],
31
31
+
"isUnique": true
32
32
+
}
33
33
+
},
34
34
+
"foreignKeys": {},
35
35
+
"compositePrimaryKeys": {},
36
36
+
"uniqueConstraints": {},
37
37
+
"checkConstraints": {}
38
38
+
},
39
39
+
"labels_applied": {
40
40
+
"name": "labels_applied",
41
41
+
"columns": {
42
42
+
"id": {
43
43
+
"name": "id",
44
44
+
"type": "integer",
45
45
+
"primaryKey": true,
46
46
+
"notNull": true,
47
47
+
"autoincrement": true
48
48
+
},
49
49
+
"did": {
50
50
+
"name": "did",
51
51
+
"type": "text",
52
52
+
"primaryKey": false,
53
53
+
"notNull": true,
54
54
+
"autoincrement": false
55
55
+
},
56
56
+
"label": {
57
57
+
"name": "label",
58
58
+
"type": "text",
59
59
+
"primaryKey": false,
60
60
+
"notNull": true,
61
61
+
"autoincrement": false
62
62
+
},
63
63
+
"labeler": {
64
64
+
"name": "labeler",
65
65
+
"type": "text",
66
66
+
"primaryKey": false,
67
67
+
"notNull": true,
68
68
+
"autoincrement": false
69
69
+
},
70
70
+
"action": {
71
71
+
"name": "action",
72
72
+
"type": "text",
73
73
+
"primaryKey": false,
74
74
+
"notNull": true,
75
75
+
"autoincrement": false
76
76
+
},
77
77
+
"negated": {
78
78
+
"name": "negated",
79
79
+
"type": "integer",
80
80
+
"primaryKey": false,
81
81
+
"notNull": true,
82
82
+
"autoincrement": false,
83
83
+
"default": false
84
84
+
},
85
85
+
"date_applied": {
86
86
+
"name": "date_applied",
87
87
+
"type": "integer",
88
88
+
"primaryKey": false,
89
89
+
"notNull": true,
90
90
+
"autoincrement": false
91
91
+
}
92
92
+
},
93
93
+
"indexes": {},
94
94
+
"foreignKeys": {
95
95
+
"labels_applied_did_watched_repos_did_fk": {
96
96
+
"name": "labels_applied_did_watched_repos_did_fk",
97
97
+
"tableFrom": "labels_applied",
98
98
+
"tableTo": "watched_repos",
99
99
+
"columnsFrom": [
100
100
+
"did"
101
101
+
],
102
102
+
"columnsTo": [
103
103
+
"did"
104
104
+
],
105
105
+
"onDelete": "no action",
106
106
+
"onUpdate": "no action"
107
107
+
}
108
108
+
},
109
109
+
"compositePrimaryKeys": {},
110
110
+
"uniqueConstraints": {},
111
111
+
"checkConstraints": {}
112
112
+
},
113
113
+
"watched_repos": {
114
114
+
"name": "watched_repos",
115
115
+
"columns": {
116
116
+
"did": {
117
117
+
"name": "did",
118
118
+
"type": "text",
119
119
+
"primaryKey": true,
120
120
+
"notNull": true,
121
121
+
"autoincrement": false
122
122
+
},
123
123
+
"pds_host": {
124
124
+
"name": "pds_host",
125
125
+
"type": "text",
126
126
+
"primaryKey": false,
127
127
+
"notNull": true,
128
128
+
"autoincrement": false
129
129
+
},
130
130
+
"active": {
131
131
+
"name": "active",
132
132
+
"type": "integer",
133
133
+
"primaryKey": false,
134
134
+
"notNull": true,
135
135
+
"autoincrement": false
136
136
+
},
137
137
+
"date_first_seen": {
138
138
+
"name": "date_first_seen",
139
139
+
"type": "integer",
140
140
+
"primaryKey": false,
141
141
+
"notNull": true,
142
142
+
"autoincrement": false
143
143
+
}
144
144
+
},
145
145
+
"indexes": {
146
146
+
"watched_repos_did_unique": {
147
147
+
"name": "watched_repos_did_unique",
148
148
+
"columns": [
149
149
+
"did"
150
150
+
],
151
151
+
"isUnique": true
152
152
+
}
153
153
+
},
154
154
+
"foreignKeys": {},
155
155
+
"compositePrimaryKeys": {},
156
156
+
"uniqueConstraints": {},
157
157
+
"checkConstraints": {}
158
158
+
}
159
159
+
},
160
160
+
"views": {},
161
161
+
"enums": {},
162
162
+
"_meta": {
163
163
+
"schemas": {},
164
164
+
"tables": {},
165
165
+
"columns": {}
166
166
+
},
167
167
+
"internal": {
168
168
+
"indexes": {}
169
169
+
}
170
170
+
}
+7
drizzle/meta/_journal.json
···
8
8
"when": 1771615394802,
9
9
"tag": "0000_crazy_wallflower",
10
10
"breakpoints": true
11
11
+
},
12
12
+
{
13
13
+
"idx": 1,
14
14
+
"version": "6",
15
15
+
"when": 1771625783853,
16
16
+
"tag": "0001_workable_leech",
17
17
+
"breakpoints": true
11
18
}
12
19
]
13
20
}
+1
-1
package.json
···
5
5
"description": "",
6
6
"main": "index.js",
7
7
"scripts": {
8
8
-
"start": "tsc && node dist/index.js | pino-pretty",
8
8
+
"start": "tsc && node --env-file=.env dist/index.js | pino-pretty",
9
9
"db:generate": "drizzle-kit generate",
10
10
"db:migrate": "drizzle-kit migrate",
11
11
"db:studio": "drizzle-kit studio"
+1
src/db/schema.ts
···
14
14
.notNull()
15
15
.references(() => watchedRepos.did),
16
16
label: text("label").notNull(),
17
17
+
labeler: text("labeler").notNull(),
17
18
action: text("action").notNull(),
18
19
negated: integer("negated", { mode: "boolean" }).default(false).notNull(),
19
20
dateApplied: integer("date_applied", { mode: "timestamp" }).notNull(),
+46
-17
src/handlers/handleNewLabel.ts
···
3
3
import { logger } from "../logger.js";
4
4
import type { LibSQLDatabase } from "drizzle-orm/libsql";
5
5
import * as schema from "../db/schema.js";
6
6
+
import { count, eq } from "drizzle-orm";
6
7
7
8
export const handleNewLabel = async (
8
9
config: LabelerConfig,
9
10
label: Label,
10
11
db: LibSQLDatabase<typeof schema>,
11
12
) => {
12
12
-
// TODO: MAKE SURE TO CHECK NEG
13
13
-
logger.info({ host: config.host }, "From");
14
14
-
let labledDate = new Date(label.cts);
15
15
-
if (config.labels[label.val]) {
16
16
-
logger.info(
17
17
-
{ action: config.labels[label.val]?.action },
18
18
-
"Listed label found. Performing the action",
13
13
+
try {
14
14
+
// TODO: MAKE SURE TO CHECK NEG
15
15
+
let labledDate = new Date(label.cts);
16
16
+
logger.debug(
17
17
+
{
18
18
+
labeler: config.host,
19
19
+
val: label.val,
20
20
+
uri: label.uri,
21
21
+
neg: label.neg,
22
22
+
date: labledDate,
23
23
+
},
24
24
+
"Label",
19
25
);
26
26
+
27
27
+
let labelConfig = config.labels[label.val];
28
28
+
if (labelConfig) {
29
29
+
const isRepoWatched = await db
30
30
+
.select()
31
31
+
.from(schema.watchedRepos)
32
32
+
.where(eq(schema.watchedRepos.did, label.uri))
33
33
+
.limit(1);
34
34
+
35
35
+
if (isRepoWatched.length > 0) {
36
36
+
logger.info(
37
37
+
{ action: config.labels[label.val]?.action },
38
38
+
`Listed label: ${label.val} found. Performing the action against: ${label.uri}`,
39
39
+
);
40
40
+
41
41
+
await db.insert(schema.labelsApplied).values({
42
42
+
did: label.uri,
43
43
+
label: label.val,
44
44
+
labeler: config.host,
45
45
+
action: labelConfig.action,
46
46
+
negated: label.neg ?? false,
47
47
+
dateApplied: labledDate,
48
48
+
});
49
49
+
50
50
+
return;
51
51
+
}
52
52
+
logger.warn(
53
53
+
{ action: config.labels[label.val]?.action },
54
54
+
"Listed label found but repo is not watched. Skipping",
55
55
+
);
56
56
+
}
57
57
+
} catch (error) {
58
58
+
logger.error({ error }, "Error handling new label");
20
59
}
21
21
-
logger.info(
22
22
-
{
23
23
-
src: label.src,
24
24
-
val: label.val,
25
25
-
uri: label.uri,
26
26
-
neg: label.neg,
27
27
-
date: labledDate,
28
28
-
},
29
29
-
"Label",
30
30
-
);
31
60
};
+6
-3
src/handlers/lablerSubscriber.ts
···
48
48
}
49
49
case "com.atproto.label.subscribeLabels#labels": {
50
50
for (const label of message.labels) {
51
51
-
queue.add(async () => {
52
52
-
await handleNewLabel(config, label, db);
53
53
-
});
51
51
+
// We only care about labels for identities, not content for now
52
52
+
if (label.uri.startsWith("did:")) {
53
53
+
queue.add(async () => {
54
54
+
await handleNewLabel(config, label, db);
55
55
+
});
56
56
+
}
54
57
}
55
58
break;
56
59
}
+17
-7
src/index.ts
···
45
45
const lastCursors = await db.select().from(labelerCursor);
46
46
47
47
// Sets up the subscribers to the labelers
48
48
-
const labelSubscribers = Object.entries(settings.labeler).map(([_, config]) => {
49
49
-
let lastCursorRow = lastCursors.find(
50
50
-
(cursor) => cursor.labelerId === config.host,
51
51
-
);
52
52
-
let lastCursor = lastCursorRow?.cursor ?? undefined;
53
53
-
return labelerSubscriber(config, lastCursor, db, labelQueue);
54
54
-
});
48
48
+
const labelSubscribers = Object.entries(settings.labeler)
49
49
+
.map(([_, config]) => {
50
50
+
if (config.labels == undefined) {
51
51
+
logger.info(
52
52
+
{ host: config.host },
53
53
+
"No labels to watch not starting subscriber for this one",
54
54
+
);
55
55
+
return null;
56
56
+
}
57
57
+
58
58
+
let lastCursorRow = lastCursors.find(
59
59
+
(cursor) => cursor.labelerId === config.host,
60
60
+
);
61
61
+
let lastCursor = lastCursorRow?.cursor ?? undefined;
62
62
+
return labelerSubscriber(config, lastCursor, db, labelQueue);
63
63
+
})
64
64
+
.filter((x) => x !== null);
55
65
56
66
const pdsSubscribers = Object.entries(settings.pds)
57
67
.map(([_, config]) => {
+3
-1
src/logger.ts
···
1
1
import pino from "pino";
2
2
3
3
-
export const logger = pino();
3
3
+
export const logger = pino({
4
4
+
level: process.env.LOG_LEVEL ?? "info",
5
5
+
});