WIP: PDS Admin tool to hopefully make it easier to moderate your PDS
0
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