A tool for parsing traffic on the jetstream and applying a moderation workstream based on regexp based rules

v2.1.0: Starter pack threshold, label negation, login retry & documentation #4

closed opened by skywatch.blue targeting main from 2.1.0

Summary#

  • New starter pack threshold monitoring system for detecting follow-farming
  • Account label negation support (remove labels when criteria no longer match)
  • Login retry logic with configurable attempts
  • Configurable time window units (minutes, hours, days) for threshold labeling
  • Refactored profile checking into single unified function

New Features#

Starter Pack Threshold System#

  • New src/starterPackThreshold.ts module
  • Monitors starter pack creation per account within rolling time windows
  • Configurable threshold, window, and actions (label/report/comment)
  • Per-config allowlist support
  • 3 new Prometheus metrics for monitoring

Label Negation#

  • New negateAccountLabel() function in accountModeration
  • New getAllAccountLabels() to retrieve current labels
  • New deleteAccountLabelClaim() for Redis cache invalidation
  • Enables unlabel: true in check configs to auto-remove labels

Configurable Window Units#

  • Changed from windowDays: number to window: number + windowUnit: WindowUnit
  • Supports "minutes", "hours", "days"
  • Applies to both account threshold and starter pack threshold

Login Retry#

  • Agent now retries login up to 3 times with 2s delay
  • Graceful failure after exhausting retries

Profile Check Refactor#

  • Combined checkDescription + checkDisplayName into single checkProfile function
  • Cleaner event handling in main.ts
Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:e4elbtctnfqocyfcml6h2lf7/sh.tangled.repo.pull/3mbsrdxeqv722
+249 -71
Interdiff #0 #1
.claude/settings.local.json

This file has not been changed.

CLAUDE.md

This file has not been changed.

README.md

This file has not been changed.

docs/automod/main.md

This file has not been changed.

docs/automod/rules/account/age.md

This file has not been changed.

docs/automod/rules/account/countStarterPacks.md

This file has not been changed.

docs/automod/rules/facets/facets.md

This file has not been changed.

docs/automod/rules/handles/checkHandles.md

This file has not been changed.

docs/automod/rules/posts/checkPosts.md

This file has not been changed.

docs/automod/rules/profiles/checkProfiles.md

This file has not been changed.

docs/automod/utils/getFinalUrl.md

This file has not been changed.

docs/automod/utils/getLanguage.md

This file has not been changed.

docs/automod/utils/normalizeUnicode.md

This file has not been changed.

docs/common/accountModeration.md

This file has not been changed.

docs/common/accountThreshold.md

This file has not been changed.

docs/common/agent.md

This file has not been changed.

docs/common/config.md

This file has not been changed.

docs/common/limits.md

This file has not been changed.

docs/common/logger.md

This file has not been changed.

docs/common/metrics.md

This file has not been changed.

docs/common/moderation.md

This file has not been changed.

docs/common/redis.md

This file has not been changed.

docs/common/session.md

This file has not been changed.

docs/common/starterPackThreshold.md

This file has not been changed.

docs/common/types.md

This file has not been changed.

rules/accountAge.ts

This file has not been changed.

rules/accountThreshold.ts

This file has not been changed.

rules/constants.ts

This file has not been changed.

rules/developing_checks.md

This file has not been changed.

rules/handles.ts

This file has not been changed.

rules/posts.ts

This file has not been changed.

rules/profiles.ts

This file has not been changed.

rules/starterPackThreshold.ts

This file has not been changed.

+4
src/accountModeration.ts
··· 101 101 { process: "MODERATION", error: e }, 102 102 "Failed to create account label", 103 103 ); 104 + throw e; 104 105 } 105 106 }); 106 107 }; ··· 161 162 { process: "MODERATION", error: e }, 162 163 "Failed to create account comment", 163 164 ); 165 + throw e; 164 166 } 165 167 }); 166 168 }; ··· 206 208 { process: "MODERATION", error: e }, 207 209 "Failed to create account report", 208 210 ); 211 + throw e; 209 212 } 210 213 }); 211 214 }; ··· 270 273 { process: "MODERATION", error: e }, 271 274 "Failed to negate account label", 272 275 ); 276 + throw e; 273 277 } 274 278 }); 275 279 };
src/accountThreshold.ts

This file has not been changed.

+18 -1
src/agent.ts
··· 170 170 } 171 171 172 172 export const login = authenticateWithRetry; 173 - export const isLoggedIn = authenticateWithRetry().then(() => true); 173 + 174 + // Lazy getter for isLoggedIn - authentication only starts when first accessed 175 + let _isLoggedIn: Promise<boolean> | null = null; 176 + 177 + export function getIsLoggedIn(): Promise<boolean> { 178 + if (!_isLoggedIn) { 179 + _isLoggedIn = authenticateWithRetry().then(() => true); 180 + } 181 + return _isLoggedIn; 182 + } 183 + 184 + // For backward compatibility - callers can still use `await isLoggedIn` 185 + // but authentication is now lazy instead of eager 186 + export const isLoggedIn = { 187 + then<T>(onFulfilled: (value: boolean) => T | PromiseLike<T>): Promise<T> { 188 + return getIsLoggedIn().then(onFulfilled); 189 + }, 190 + };
src/config.ts

This file has not been changed.

src/main.ts

This file has not been changed.

+7
src/metrics.ts
··· 68 68 registers: [register], 69 69 }); 70 70 71 + export const moderationActionsFailedCounter = new Counter({ 72 + name: "skywatch_moderation_actions_failed_total", 73 + help: "Total number of moderation actions that failed", 74 + labelNames: ["action", "target_type"], 75 + registers: [register], 76 + }); 77 + 71 78 const app = express(); 72 79 73 80 app.get("/metrics", (req, res) => {
+3 -1
src/moderation.ts
··· 130 130 { process: "MODERATION", error: e }, 131 131 "Failed to create post label", 132 132 ); 133 + throw e; 133 134 } 134 135 }); 135 136 }; ··· 178 179 } catch (e) { 179 180 logger.error( 180 181 { process: "MODERATION", error: e }, 181 - "Failed to create post label", 182 + "Failed to create post report", 182 183 ); 184 + throw e; 183 185 } 184 186 }); 185 187 };
src/redis.ts

This file has not been changed.

src/rules/account/age.ts

This file has not been changed.

+18 -8
src/rules/account/countStarterPacks.ts
··· 2 2 import { agent, isLoggedIn } from "../../agent.js"; 3 3 import { limit } from "../../limits.js"; 4 4 import { logger } from "../../logger.js"; 5 + import { moderationActionsFailedCounter } from "../../metrics.js"; 5 6 6 - const ALLOWED_DIDS = [ 7 - "did:plc:example" 8 - ]; 7 + const ALLOWED_DIDS = ["did:plc:example"]; 9 8 10 9 export const countStarterPacks = async (did: string, time: number) => { 11 10 await isLoggedIn; ··· 34 33 "Labeling account with excessive starter packs", 35 34 ); 36 35 37 - void createAccountLabel( 38 - did, 39 - "follow-farming", 40 - `${time.toString()}: Account has ${starterPacks.toString()} starter packs`, 41 - ); 36 + try { 37 + await createAccountLabel( 38 + did, 39 + "follow-farming", 40 + `${time.toString()}: Account has ${starterPacks.toString()} starter packs`, 41 + ); 42 + } catch (labelError) { 43 + logger.error( 44 + { process: "COUNTSTARTERPACKS", did, time, error: labelError }, 45 + "Failed to apply follow-farming label", 46 + ); 47 + moderationActionsFailedCounter.inc({ 48 + action: "label", 49 + target_type: "account", 50 + }); 51 + } 42 52 } 43 53 } catch (error) { 44 54 const errorInfo =
src/rules/facets/facets.ts

This file has not been changed.

src/rules/facets/tests/facets.test.ts

This file has not been changed.

src/rules/handles/checkHandles.test.ts

This file has not been changed.

src/rules/handles/checkHandles.ts

This file has not been changed.

+86 -25
src/rules/posts/checkPosts.ts
··· 6 6 } from "../../accountModeration.js"; 7 7 import { checkAccountThreshold } from "../../accountThreshold.js"; 8 8 import { logger } from "../../logger.js"; 9 + import { moderationActionsFailedCounter } from "../../metrics.js"; 9 10 import { createPostLabel, createPostReport } from "../../moderation.js"; 10 - import type { Post } from "../../types.js"; 11 + import type { ModerationResult, Post } from "../../types.js"; 11 12 import { getFinalUrl } from "../../utils/getFinalUrl.js"; 12 13 import { getLanguage } from "../../utils/getLanguage.js"; 13 14 import { countStarterPacks } from "../account/countStarterPacks.js"; ··· 74 75 const lang = await getLanguage(post[0].text); 75 76 76 77 // iterate through the checks 77 - POST_CHECKS.forEach((checkPost) => { 78 + for (const checkPost of POST_CHECKS) { 78 79 if (checkPost.language) { 79 80 if (!checkPost.language.includes(lang)) { 80 - return; 81 + continue; 81 82 } 82 83 } 83 84 ··· 87 88 { process: "CHECKPOSTS", did: post[0].did, atURI: post[0].atURI }, 88 89 "Whitelisted DID", 89 90 ); 90 - return; 91 + continue; 91 92 } 92 93 } 93 94 ··· 99 100 { process: "CHECKPOSTS", did: post[0].did, atURI: post[0].atURI }, 100 101 "Whitelisted phrase found", 101 102 ); 102 - return; 103 + continue; 103 104 } 104 105 } 105 106 106 - void countStarterPacks(post[0].did, post[0].time); 107 + await countStarterPacks(post[0].did, post[0].time); 107 108 108 109 const postURL = `https://pdsls.dev/${post[0].atURI}`; 109 110 const formattedComment = `${checkPost.comment}\n\nPost: ${postURL}\n\nText: "${post[0].text}"`; 110 111 112 + const results: ModerationResult = { success: true, errors: [] }; 113 + 111 114 if (checkPost.toLabel) { 112 - void createPostLabel( 113 - post[0].atURI, 114 - post[0].cid, 115 - checkPost.label, 116 - formattedComment, 117 - checkPost.duration, 118 - post[0].did, 119 - post[0].time, 120 - ); 115 + try { 116 + await createPostLabel( 117 + post[0].atURI, 118 + post[0].cid, 119 + checkPost.label, 120 + formattedComment, 121 + checkPost.duration, 122 + post[0].did, 123 + post[0].time, 124 + ); 125 + } catch (error) { 126 + results.success = false; 127 + results.errors.push({ action: "label", error }); 128 + } 121 129 } else if (checkPost.trackOnly) { 122 - void checkAccountThreshold( 123 - post[0].did, 124 - post[0].atURI, 125 - checkPost.label, 126 - post[0].time, 127 - ); 130 + try { 131 + await checkAccountThreshold( 132 + post[0].did, 133 + post[0].atURI, 134 + checkPost.label, 135 + post[0].time, 136 + ); 137 + } catch (error) { 138 + // Threshold check failures are logged but don't add to results.errors 139 + // since it's not a direct moderation action 140 + logger.error( 141 + { 142 + process: "CHECKPOSTS", 143 + did: post[0].did, 144 + atURI: post[0].atURI, 145 + error, 146 + }, 147 + "Account threshold check failed", 148 + ); 149 + } 128 150 } 129 151 130 152 if (checkPost.reportPost === true) { ··· 137 159 }, 138 160 "Reporting post", 139 161 ); 140 - void createPostReport(post[0].atURI, post[0].cid, formattedComment); 162 + try { 163 + await createPostReport(post[0].atURI, post[0].cid, formattedComment); 164 + } catch (error) { 165 + results.success = false; 166 + results.errors.push({ action: "report", error }); 167 + } 141 168 } 142 169 143 170 if (checkPost.reportAcct) { ··· 150 177 }, 151 178 "Reporting account", 152 179 ); 153 - void createAccountReport(post[0].did, formattedComment); 180 + try { 181 + await createAccountReport(post[0].did, formattedComment); 182 + } catch (error) { 183 + results.success = false; 184 + results.errors.push({ action: "report", error }); 185 + } 154 186 } 155 187 156 188 if (checkPost.commentAcct) { 157 - void createAccountComment(post[0].did, formattedComment, post[0].atURI); 189 + try { 190 + await createAccountComment( 191 + post[0].did, 192 + formattedComment, 193 + post[0].atURI, 194 + ); 195 + } catch (error) { 196 + results.success = false; 197 + results.errors.push({ action: "comment", error }); 198 + } 158 199 } 200 + 201 + // Log and track any failures 202 + if (!results.success) { 203 + for (const error of results.errors) { 204 + logger.error( 205 + { 206 + process: "CHECKPOSTS", 207 + did: post[0].did, 208 + atURI: post[0].atURI, 209 + action: error.action, 210 + error: error.error, 211 + }, 212 + "Moderation action failed", 213 + ); 214 + moderationActionsFailedCounter.inc({ 215 + action: error.action, 216 + target_type: "post", 217 + }); 218 + } 219 + } 159 220 } 160 - }); 221 + } 161 222 };
src/rules/posts/tests/checkPosts.test.ts

This file has not been changed.

+101 -35
src/rules/profiles/checkProfiles.ts
··· 7 7 negateAccountLabel, 8 8 } from "../../accountModeration.js"; 9 9 import { logger } from "../../logger.js"; 10 - import type { Checks } from "../../types.js"; 10 + import { moderationActionsFailedCounter } from "../../metrics.js"; 11 + import type { Checks, ModerationResult } from "../../types.js"; 11 12 import { getLanguage } from "../../utils/getLanguage.js"; 12 13 13 14 export class ProfileChecker { ··· 21 22 this.time = time; 22 23 } 23 24 24 - checkDescription(description: string): void { 25 + async checkDescription(description: string): Promise<void> { 25 26 if (!description) return; 26 - this.performActions(description, "CHECKDESCRIPTION"); 27 + await this.performActions(description, "CHECKDESCRIPTION"); 27 28 } 28 29 29 - checkDisplayName(displayName: string): void { 30 + async checkDisplayName(displayName: string): Promise<void> { 30 31 if (!displayName) return; 31 - this.performActions(displayName, "CHECKDISPLAYNAME"); 32 + await this.performActions(displayName, "CHECKDISPLAYNAME"); 32 33 } 33 34 34 - checkBoth(displayName: string, description: string): void { 35 + async checkBoth(displayName: string, description: string): Promise<void> { 35 36 const profile = `${displayName} ${description}`; 36 37 if (!profile) return; 37 - this.performActions(profile, "CHECKPROFILE"); 38 + await this.performActions(profile, "CHECKPROFILE"); 38 39 } 39 40 40 - private performActions( 41 + private async performActions( 41 42 content: string, 42 43 processType: "CHECKPROFILE" | "CHECKDESCRIPTION" | "CHECKDISPLAYNAME", 43 - ): void { 44 + ): Promise<void> { 44 45 const matched = this.check.check.test(content); 45 46 46 47 if (matched) { ··· 52 53 return; 53 54 } 54 55 55 - this.applyActions(content, processType); 56 + const result = await this.applyActions(content, processType); 57 + if (!result.success) { 58 + for (const error of result.errors) { 59 + logger.error( 60 + { 61 + process: processType, 62 + did: this.did, 63 + action: error.action, 64 + error: error.error, 65 + }, 66 + "Moderation action failed", 67 + ); 68 + moderationActionsFailedCounter.inc({ 69 + action: error.action, 70 + target_type: "account", 71 + }); 72 + } 73 + } 56 74 } else { 57 75 if (this.check.unlabel) { 58 - this.removeLabel(content, processType); 76 + const result = await this.removeLabel(content, processType); 77 + if (!result.success) { 78 + for (const error of result.errors) { 79 + logger.error( 80 + { 81 + process: processType, 82 + did: this.did, 83 + action: error.action, 84 + error: error.error, 85 + }, 86 + "Moderation action failed", 87 + ); 88 + moderationActionsFailedCounter.inc({ 89 + action: error.action, 90 + target_type: "account", 91 + }); 92 + } 93 + } 59 94 } 60 95 } 61 96 } 62 97 63 - private applyActions(content: string, processType: string): void { 98 + private async applyActions( 99 + content: string, 100 + processType: string, 101 + ): Promise<ModerationResult> { 102 + const results: ModerationResult = { success: true, errors: [] }; 64 103 const formattedComment = `${this.time.toString()}: ${this.check.comment}\n\nContent: ${content}`; 65 104 66 105 if (this.check.toLabel) { 67 - void createAccountLabel(this.did, this.check.label, formattedComment); 106 + try { 107 + await createAccountLabel(this.did, this.check.label, formattedComment); 108 + } catch (error) { 109 + results.success = false; 110 + results.errors.push({ action: "label", error }); 111 + } 68 112 } 69 113 70 114 if (this.check.reportAcct) { 71 - void createAccountReport(this.did, formattedComment); 72 - logger.info( 73 - { 74 - process: processType, 75 - did: this.did, 76 - time: this.time, 77 - label: this.check.label, 78 - }, 79 - "Reporting account", 80 - ); 115 + try { 116 + await createAccountReport(this.did, formattedComment); 117 + logger.info( 118 + { 119 + process: processType, 120 + did: this.did, 121 + time: this.time, 122 + label: this.check.label, 123 + }, 124 + "Reporting account", 125 + ); 126 + } catch (error) { 127 + results.success = false; 128 + results.errors.push({ action: "report", error }); 129 + } 81 130 } 82 131 83 132 if (this.check.commentAcct) { 84 - void createAccountComment( 85 - this.did, 86 - formattedComment, 87 - `profile:${this.did}`, 88 - ); 133 + try { 134 + await createAccountComment( 135 + this.did, 136 + formattedComment, 137 + `profile:${this.did}`, 138 + ); 139 + } catch (error) { 140 + results.success = false; 141 + results.errors.push({ action: "comment", error }); 142 + } 89 143 } 144 + 145 + return results; 90 146 } 91 147 92 - private removeLabel(content: string, _processType: string): void { 148 + private async removeLabel( 149 + content: string, 150 + _processType: string, 151 + ): Promise<ModerationResult> { 152 + const results: ModerationResult = { success: true, errors: [] }; 93 153 const formattedComment = `${this.check.comment}\n\nContent: ${content}`; 94 - void negateAccountLabel(this.did, this.check.label, formattedComment); 154 + try { 155 + await negateAccountLabel(this.did, this.check.label, formattedComment); 156 + } catch (error) { 157 + results.success = false; 158 + results.errors.push({ action: "unlabel", error }); 159 + } 160 + return results; 95 161 } 96 162 } 97 163 ··· 129 195 130 196 if (checkRule.description === true) { 131 197 const checker = new ProfileChecker(checkRule, did, time); 132 - checker.checkDescription(description); 198 + await checker.checkDescription(description); 133 199 } 134 200 } 135 201 }; ··· 168 234 169 235 if (checkRule.displayName === true) { 170 236 const checker = new ProfileChecker(checkRule, did, time); 171 - checker.checkDisplayName(displayName); 237 + await checker.checkDisplayName(displayName); 172 238 } 173 239 } 174 240 }; ··· 213 279 const checker = new ProfileChecker(checkRule, did, time); 214 280 215 281 if (checkRule.description === true && checkRule.displayName === true) { 216 - checker.checkBoth(displayName, description); 282 + await checker.checkBoth(displayName, description); 217 283 } else if (checkRule.description === true) { 218 - checker.checkDescription(description); 284 + await checker.checkDescription(description); 219 285 } else if (checkRule.displayName === true) { 220 - checker.checkDisplayName(displayName); 286 + await checker.checkDisplayName(displayName); 221 287 } 222 288 } 223 289 };
src/rules/profiles/tests/checkProfiles.test.ts

This file has not been changed.

src/starterPackThreshold.ts

This file has not been changed.

src/tests/accountModeration.test.ts

This file has not been changed.

src/tests/accountThreshold.test.ts

This file has not been changed.

src/tests/agent.test.ts

This file has not been changed.

src/tests/redis.test.ts

This file has not been changed.

src/tests/starterPackThreshold.test.ts

This file has not been changed.

+10
src/types.ts
··· 89 89 commentAcct?: boolean; 90 90 allowlist?: string[]; 91 91 } 92 + 93 + export interface ModerationError { 94 + action: "label" | "report" | "comment" | "unlabel"; 95 + error: unknown; 96 + } 97 + 98 + export interface ModerationResult { 99 + success: boolean; 100 + errors: ModerationError[]; 101 + }
+1
.gitignore
··· 5 5 labels.db* 6 6 .DS_Store 7 7 coverage/ 8 + .session
+1 -1
package.json
··· 1 1 { 2 2 "name": "skywatch-automod", 3 - "version": "2.0.2", 3 + "version": "2.1.0", 4 4 "type": "module", 5 5 "scripts": { 6 6 "start": "npx tsx src/main.ts",

History

2 rounds 0 comments
sign up or login to add to the discussion
95 commits
expand
Update lists.ts
Refactor moderation flags for granular control
Updates from private repo to add functionality for unpacking shortened links.
Enhance moderation checks with detailed logging and new monitoring functions for descriptions and display names
Update package.json
Add missing dependency
10: Update constants.ts.example to add const langs
17: Removed skywatch specific label hardcoding from checkPosts
Create FUNDING.yml
16: Update readme.md
16: Minor updates to defaults
Adds a docker-compose file for easier container management and cursor persistence
Updated to account for docker compose
Update language management to be more explicit
Update conditionals
Minor fixes
Minor fixes
Documentation updates
Added support for checking embeds to external sources
Revert "Added support for checking embeds to external sources"
Added support for checking embeds to external sources
Test updates
Fix: Add error handling and cleanup
feat: Handle embeds in posts
fix: resolve unsafe type assertions in main.ts
Added claude instructions
fix: improve async error handling in main.ts
fix: resolve linting issues in moderation.ts
fix: resolve linting issues in checkProfiles.ts
fix: resolve linting issues in check modules
fix: remove unnecessary type check in utils.ts
docs: update CLAUDE.md to reflect completed async error handling fixes
Add eslint and git add to allowed commands
Add ESLint configuration
feat: add comprehensive environment variable validation at startup
Add more tasks to local settings
feat: Add environment variable validation
Reverting
feat: Add workflow guidelines and project documentation
feat(language-detection): replace lande with franc for improved language detection
test: add comprehensive tests for franc language detection
$(cat <<'EOF' refactor: revert franc, improve code quality, and cleanup unused files
$(cat <<'EOF' feat(language-detection): restore franc implementation
Remove GEMINI.md and Update README
Remove TODO section from README
Added rule and logic to detect if accounts are abusing faceting by pushing multiple facets in same byte space
Corrected path name and added tests
non-malicious bots were catching strays over hashtags
Update facet detection to allow for exact duplicates
feat: Add global allowlist for DIDs
Add facet spam allowlist (to allow for test scenarios)
Added functionality to detect accounts created specifically to harrass, spam, or astroturf
Refactor account age module
Fixed stupid mistake in global allow listing
Refactor account age check to use a date window
feat: Add check to avoid duplicate account age labels
Fix: Use plc.directory and improve account age lookups
src/types.ts:68 - added optional expires?: string field to AccountAgeCheck interface 2. src/rules/account/age.ts:117-128 - added logic to skip expired checks by comparing current date against expires date 3. src/rules/account/ageConstants.ts:24 - updated example to show how to use the new field 4. added 3 tests to verify: - expired checks are skipped - non-expired checks still work - checks without expires field work (backward compatible)
src/types.ts:63-64 - added optional monitoredPostURIs field, made monitoredDIDs optional (at least one must be provided) 2. src/rules/account/age.ts:13 - added optional replyToPostURI to ReplyContext interface 3. src/rules/account/age.ts:113-123 - updated logic to check both monitoredDIDs and monitoredPostURIs (matches if either condition is true) 4. src/rules/account/ageConstants.ts:27-38 - added example showing how to monitor specific post URIs 5. added 4 tests covering: - monitoring post URIs - ignoring non-matching post URIs - matching either DIDs OR post URIs - backward compatibility (works without replyToPostURI)
src/rules/account/age.ts:8-22 - renamed ReplyContext → InteractionContext, added quotedDid and quotedPostURI fields for quote posts, actorDid as the common actor field 2. src/rules/account/age.ts:123-149 - updated matching logic to check both replies (replyToDid/replyToPostURI) AND quotes (quotedDid/quotedPostURI) 3. src/rules/account/ageConstants.ts:3-12 - updated documentation to reflect reply/quote monitoring 4. added 4 new tests covering: - labeling when quoting monitored DID - labeling when quoting monitored post URI - not labeling when quoting different DID - matching either reply OR quote to monitored target
fixed two issues in main.ts:
Update version number
Fix: Remove unused import and formatting
Added tests
Refactor CI to use Bun instead of Node.js and npm
feat: Add @stylistic/eslint-plugin and update dependencies
feat: Add eslint-config-prettier
Disable linting in CI workflow
feat: Add type checking and update tsconfig
Refactor: Move account age checks to rules folder
Use Bun for type checking and testing
Add example config files for CI tests
Add post check constants example
feat: Upgrade dependencies
Add coverage directory to .gitignore
feat: Add Redis caching and connection
feat: Add Redis caching and connection
feat: Add session management and rate limiting
Add tests for agent, session, and rate limits
Linted the whole codebase
Further fixes
feat: Add example rule configurations
Refactor CI setup to use simplified config copying
Remove example config files and CI setup
feat: Add example rule configuration files
Add LINK_SHORTENER constant
Remove unused LINK_SHORTENER constant
Refactor: Move LINK_SHORTENER to constants file
Fix(deps): Update dependencies
feat: Add retry logic to authentication
Bumped version number
docs: Update documentation to reflect starter pack threshold feature
versioning
fix: eliminate fire-and-forget async patterns in moderation actions
fix(auth): convert isLoggedIn to lazy initialization
expand 0 comments
closed without merging
1 commit
expand
docs: Update documentation to reflect starter pack threshold feature
expand 0 comments