PDS Admin tool make it easier to moderate your PDS with labels
43
fork

Configure Feed

Select the types of activity you want to include in your feed.

some schema changes

+191 -9
+1
drizzle/0003_kind_the_liberteens.sql
··· 1 + ALTER TABLE `labels_applied` ADD `negated` integer DEFAULT 0 NOT NULL;
+163
drizzle/meta/0003_snapshot.json
··· 1 + { 2 + "version": "6", 3 + "dialect": "sqlite", 4 + "id": "c01e52ce-5a45-4633-9fa4-0c85803cf3d8", 5 + "prevId": "d53a3c1c-688b-4aed-a866-be1a3366eda4", 6 + "tables": { 7 + "labeler_cursors": { 8 + "name": "labeler_cursors", 9 + "columns": { 10 + "labeler_id": { 11 + "name": "labeler_id", 12 + "type": "text", 13 + "primaryKey": false, 14 + "notNull": false, 15 + "autoincrement": false 16 + }, 17 + "cursor": { 18 + "name": "cursor", 19 + "type": "integer", 20 + "primaryKey": false, 21 + "notNull": true, 22 + "autoincrement": false 23 + } 24 + }, 25 + "indexes": { 26 + "labeler_cursors_labeler_id_unique": { 27 + "name": "labeler_cursors_labeler_id_unique", 28 + "columns": [ 29 + "labeler_id" 30 + ], 31 + "isUnique": true 32 + } 33 + }, 34 + "foreignKeys": {}, 35 + "compositePrimaryKeys": {}, 36 + "uniqueConstraints": {}, 37 + "checkConstraints": {} 38 + }, 39 + "labels_applied": { 40 + "name": "labels_applied", 41 + "columns": { 42 + "id": { 43 + "name": "id", 44 + "type": "integer", 45 + "primaryKey": true, 46 + "notNull": true, 47 + "autoincrement": true 48 + }, 49 + "did": { 50 + "name": "did", 51 + "type": "text", 52 + "primaryKey": false, 53 + "notNull": true, 54 + "autoincrement": false 55 + }, 56 + "label": { 57 + "name": "label", 58 + "type": "text", 59 + "primaryKey": false, 60 + "notNull": true, 61 + "autoincrement": false 62 + }, 63 + "action": { 64 + "name": "action", 65 + "type": "text", 66 + "primaryKey": false, 67 + "notNull": true, 68 + "autoincrement": false 69 + }, 70 + "negated": { 71 + "name": "negated", 72 + "type": "integer", 73 + "primaryKey": false, 74 + "notNull": true, 75 + "autoincrement": false, 76 + "default": 0 77 + }, 78 + "date_applied": { 79 + "name": "date_applied", 80 + "type": "integer", 81 + "primaryKey": false, 82 + "notNull": true, 83 + "autoincrement": false 84 + } 85 + }, 86 + "indexes": {}, 87 + "foreignKeys": { 88 + "labels_applied_did_watched_repos_did_fk": { 89 + "name": "labels_applied_did_watched_repos_did_fk", 90 + "tableFrom": "labels_applied", 91 + "tableTo": "watched_repos", 92 + "columnsFrom": [ 93 + "did" 94 + ], 95 + "columnsTo": [ 96 + "did" 97 + ], 98 + "onDelete": "no action", 99 + "onUpdate": "no action" 100 + } 101 + }, 102 + "compositePrimaryKeys": {}, 103 + "uniqueConstraints": {}, 104 + "checkConstraints": {} 105 + }, 106 + "watched_repos": { 107 + "name": "watched_repos", 108 + "columns": { 109 + "did": { 110 + "name": "did", 111 + "type": "text", 112 + "primaryKey": true, 113 + "notNull": true, 114 + "autoincrement": false 115 + }, 116 + "pds_host": { 117 + "name": "pds_host", 118 + "type": "text", 119 + "primaryKey": false, 120 + "notNull": true, 121 + "autoincrement": false 122 + }, 123 + "active": { 124 + "name": "active", 125 + "type": "integer", 126 + "primaryKey": false, 127 + "notNull": true, 128 + "autoincrement": false 129 + }, 130 + "date_first_seen": { 131 + "name": "date_first_seen", 132 + "type": "integer", 133 + "primaryKey": false, 134 + "notNull": true, 135 + "autoincrement": false 136 + } 137 + }, 138 + "indexes": { 139 + "watched_repos_did_unique": { 140 + "name": "watched_repos_did_unique", 141 + "columns": [ 142 + "did" 143 + ], 144 + "isUnique": true 145 + } 146 + }, 147 + "foreignKeys": {}, 148 + "compositePrimaryKeys": {}, 149 + "uniqueConstraints": {}, 150 + "checkConstraints": {} 151 + } 152 + }, 153 + "views": {}, 154 + "enums": {}, 155 + "_meta": { 156 + "schemas": {}, 157 + "tables": {}, 158 + "columns": {} 159 + }, 160 + "internal": { 161 + "indexes": {} 162 + } 163 + }
+7
drizzle/meta/_journal.json
··· 22 22 "when": 1771611742268, 23 23 "tag": "0002_greedy_dormammu", 24 24 "breakpoints": true 25 + }, 26 + { 27 + "idx": 3, 28 + "version": "6", 29 + "when": 1771613785897, 30 + "tag": "0003_kind_the_liberteens", 31 + "breakpoints": true 25 32 } 26 33 ] 27 34 }
+2 -1
src/db/schema.ts
··· 1 - import { sqliteTable, text, integer, int } from "drizzle-orm/sqlite-core"; 1 + import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"; 2 2 import { relations } from "drizzle-orm"; 3 3 4 4 export const watchedRepos = sqliteTable("watched_repos", { ··· 15 15 .references(() => watchedRepos.did), 16 16 label: text("label").notNull(), 17 17 action: text("action").notNull(), 18 + negated: integer("negated").default(0).notNull(), 18 19 dateApplied: integer("date_applied", { mode: "timestamp" }).notNull(), 19 20 }); 20 21
+11 -2
src/handlers/handleNewLabel.ts
··· 1 1 import type { Label } from "@atcute/atproto/types/label/defs"; 2 2 import type { LabelerConfig } from "../types/settings.js"; 3 3 import { logger } from "../logger.js"; 4 + import type { LibSQLDatabase } from "drizzle-orm/libsql"; 5 + import * as schema from "../db/schema.js"; 4 6 5 - export const handleNewLabel = async (config: LabelerConfig, label: Label) => { 7 + export const handleNewLabel = async ( 8 + config: LabelerConfig, 9 + label: Label, 10 + db: LibSQLDatabase<typeof schema>, 11 + ) => { 6 12 // TODO: MAKE SURE TO CHECK NEG 7 13 logger.info({ host: config.host }, "From"); 8 14 ··· 14 20 "Listed label found. Performing the action", 15 21 ); 16 22 } 17 - logger.info({ src: label.src, val: label.val, uri: label.uri, neg: label.neg }, "Label"); 23 + logger.info( 24 + { src: label.src, val: label.val, uri: label.uri, neg: label.neg }, 25 + "Label", 26 + ); 18 27 };
+2 -2
src/handlers/lablerSubscriber.ts
··· 6 6 import { logger } from "../logger.js"; 7 7 import type { LibSQLDatabase } from "drizzle-orm/libsql"; 8 8 import { labelerCursor } from "../db/schema.js"; 9 - import { eq } from "drizzle-orm"; 10 9 import * as schema from "../db/schema.js"; 11 10 12 11 export const labelerSubscriber = ( ··· 31 30 const run = async () => { 32 31 logger.info({ host: config.host }, "Listening"); 33 32 for await (const message of iterator) { 33 + // Saves the cursor for resume and re connect 34 34 if ("seq" in message) { 35 35 cursor = message.seq; 36 36 await db ··· 49 49 case "com.atproto.label.subscribeLabels#labels": { 50 50 for (const label of message.labels) { 51 51 queue.add(async () => { 52 - await handleNewLabel(config, label); 52 + await handleNewLabel(config, label, db); 53 53 }); 54 54 } 55 55 break;
+5 -4
src/index.ts
··· 7 7 import type { Settings } from "./types/settings.js"; 8 8 import { logger } from "./logger.js"; 9 9 import { labelerCursor } from "./db/schema.js"; 10 - import { eq } from "drizzle-orm"; 10 + 11 11 const queue = new PQueue({ concurrency: 2 }); 12 12 13 13 // TODO ··· 25 25 //TODO I really really don't like this unknown to settings. Figure that out later. Cause. It does work >.> 26 26 const settings = parse(settingsFile) as unknown as Settings; 27 27 28 + // Gets the last saved cursors for Labelers from db for resume 29 + const lastCursors = await db.select().from(labelerCursor); 30 + 28 31 const labelers = settings.labeler; 29 - 30 - const lastCursors = await db.select().from(labelerCursor); 31 32 32 33 const subscribers = Object.entries(labelers).map(([_, config]) => { 33 34 let lastCursorRow = lastCursors.find( ··· 37 38 return labelerSubscriber(config, lastCursor, db, queue); 38 39 }); 39 40 40 - // --- Graceful shutdown --- 41 + // Graceful shutdown 41 42 async function shutdown(signal: string) { 42 43 logger.info(`Received ${signal}, shutting down...`); 43 44