Skip to content

Commit

Permalink
updates
Browse files Browse the repository at this point in the history
- Add additional actions that can be taken depending on the configuration of the signature (Limit / Silence)
- Add DISABLE_SEND_REPORTS, DISABLE_SUSPEND_ACCOUNTS, DISABLE_LIMIT_ACCOUNTS to allow fully disabling signatures from taking certain actions
- Add DISABLE_SIGNATURES to allow others to opt-out of using certain signature rules
  • Loading branch information
Crashdoom committed Nov 4, 2024
1 parent 79abf78 commit 7454ff0
Show file tree
Hide file tree
Showing 6 changed files with 97 additions and 26 deletions.
11 changes: 10 additions & 1 deletion .env.sample
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
BASE_URL=https://your_base_url
ACCESS_TOKEN=your_access_token
LOG_DEBUG=false
LOG_INFO=true
LOG_INFO=true

DISABLE_SEND_REPORTS=false
DISABLE_SUSPEND_ACCOUNTS=false
DISABLE_LIMIT_ACCOUNTS=false

# If you want to disable certain signatures, you can do so by uncommenting this line
# and adding the signatures you want to disable. For example:
# DISABLE_SIGNATURES=signature1,signature2
#DISABLE_SIGNATURES=
75 changes: 53 additions & 22 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@ import { createStreamingAPIClient, createRestAPIClient } from "masto";
import * as fs from 'fs';
import * as path from 'path';
import { config } from 'dotenv';
import { SignatureDefinition } from "./types/signatureDefinition";

config();

const BaseUrl = process.env.BASE_URL;
const streamingApiUrl = BaseUrl + '/api/v1/streaming';
const accessToken = process.env.ACCESS_TOKEN;

const DISABLE_SEND_REPORTS = process.env.DISABLE_SEND_REPORTS === 'true';
const DISABLE_SUSPEND_ACCOUNTS = process.env.DISABLE_SUSPEND_ACCOUNTS === 'true';
const DISABLE_LIMIT_ACCOUNTS = process.env.DISABLE_LIMIT_ACCOUNTS === 'true';
const DISABLE_SIGNATURES = process.env.DISABLE_SIGNATURES?.split(',') || [];

const showDebugLog = process.env.LOG_DEBUG === 'true';
if (showDebugLog) {
console.debug = console.log;
Expand All @@ -23,17 +29,22 @@ if (showInfoLog) {
console.info = () => { };
}

interface SpamSignature {
check: (status: any) => { isSpam: boolean; reason?: string; };
signatureName: string;
}

async function loadSignatureFiles(): Promise<SpamSignature[]> {
async function loadSignatureFiles(): Promise<SignatureDefinition[]> {
const signaturesDir = path.join(__dirname, 'signatures');
const files = await fs.promises.readdir(signaturesDir);
return files.map(file => {
const moduleName = file.split('.').slice(0, -1).join('.');

if (DISABLE_SIGNATURES.includes(moduleName)) {
console.info(`Disabled signature: ${moduleName}`);
return {
check: () => ({ isSpam: false }),
signatureName: moduleName
};
}

const signatureModule = require(path.join(signaturesDir, file));
console.info(`Loaded signature: ${moduleName}`);
return {
check: signatureModule.default,
signatureName: moduleName
Expand All @@ -49,6 +60,10 @@ async function main() {

console.info('Mastodon spam detecter started.');

if (DISABLE_SEND_REPORTS && DISABLE_SUSPEND_ACCOUNTS && DISABLE_LIMIT_ACCOUNTS) {
console.warn('All actions are disabled. No actions will be taken.');
}

const masto = createStreamingAPIClient({
streamingApiUrl: streamingApiUrl,
accessToken: accessToken,
Expand All @@ -64,29 +79,45 @@ async function main() {
for await (const event of masto.public.subscribe()) {
switch (event.event) {
case "update": {
console.info("New post: ", event.payload.content);

for (const { check, signatureName } of signatures) {
const { isSpam, reason } = check(event.payload);
const { id: postId } = event.payload;
const { isSpam, reason, actions } = check(event.payload);
if (isSpam) {
console.error(`Spam detected\u0007🚨: ${signatureName} ${reason} ${JSON.stringify(event.payload)}`);

rest.v1.reports.create({
accountId: event.payload.account.id,
statusIds: [event.payload.id],
comment: `spam detected by ${reason}`,
category: 'spam',
forward: true,
});

rest.v1.admin.accounts.$select(event.payload.account.id).action.create(
{ type: 'suspend' }
);
const actionsToTake = Object.entries(actions)
.filter(([_, value]) => value)
.map(([key]) => key)
.join(', ');

console.error(`[${postId}] Spam detected\u0007🚨: ${signatureName} ${reason} (Actions: ${actionsToTake}) -- ${JSON.stringify(event.payload)}`);

if (actions.sendReport && !DISABLE_SEND_REPORTS) {
const report = await rest.v1.reports.create({
accountId: event.payload.account.id,
statusIds: [event.payload.id],
comment: `spam detected by ${reason}`,
category: 'spam',
forward: true,
});
console.log(`[${postId}] Created report: ${report.id}`);
}

if (actions.suspendAccount && !DISABLE_SUSPEND_ACCOUNTS) {
await rest.v1.admin.accounts.$select(event.payload.account.id).action.create(
{ type: 'suspend' }
);
console.log(`[${postId}] Account suspended: ${event.payload.account.id}`);
} else if (actions.limitAccount && DISABLE_LIMIT_ACCOUNTS) {
await rest.v1.admin.accounts.$select(event.payload.account.id).action.create(
{ type: 'silence' }
);
console.log(`[${postId}] Account limited / silenced: ${event.payload.account.id}`);
}

break;
}
}
}

default: {
break;
}
Expand Down
7 changes: 6 additions & 1 deletion signatures/20240217.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import masto from "masto";
import { SignatureResponse } from "../types/signatureDefinition";

/**
* This signature detects a spam pattern where a status has more than 2 mentions,
* the username is 10 characters long, the account was created less than 24 hours ago,
* and the account has 0 followers.
*/
export default function (status: masto.mastodon.streaming.UpdateEvent['payload']): { isSpam: boolean; reason?: string } {
export default function (status: masto.mastodon.streaming.UpdateEvent['payload']): SignatureResponse {
const mentions = status.mentions.length;
const username = status.account.username;
const followersCount = status.account.followersCount;
Expand All @@ -21,5 +22,9 @@ export default function (status: masto.mastodon.streaming.UpdateEvent['payload']
return {
isSpam,
reason: isSpam ? reason : undefined,
actions: {
sendReport: true,
limitAccount: true,
}
};
}
7 changes: 6 additions & 1 deletion signatures/20240220.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import masto from "masto";
import { SignatureResponse } from "../types/signatureDefinition";

/**
* This signature detects a spam pattern where avatar ends with /missing.png,
* mentions > 2, followingCount = 0, and followersCount = 0.
*/
export default function (status: masto.mastodon.streaming.UpdateEvent['payload']): { isSpam: boolean; reason?: string } {
export default function (status: masto.mastodon.streaming.UpdateEvent['payload']): SignatureResponse {
const mentions = status.mentions.length;
const avatar = status.account.avatar;
const followingCount = status.account.followingCount;
Expand All @@ -22,5 +23,9 @@ export default function (status: masto.mastodon.streaming.UpdateEvent['payload']
return {
isSpam,
reason: isSpam ? reason : undefined,
actions: {
sendReport: true,
limitAccount: true,
}
};
}
7 changes: 6 additions & 1 deletion signatures/20240221.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import masto from "masto";
import { SignatureResponse } from "../types/signatureDefinition";

/**
* This signature detects a spam pattern where mentions > 2 and
* content includes https://荒らし.com/ or https://ctkpaarr.org/.
*/
export default function (status: masto.mastodon.streaming.UpdateEvent['payload']): { isSpam: boolean; reason?: string } {
export default function (status: masto.mastodon.streaming.UpdateEvent['payload']): SignatureResponse {
const mentions = status.mentions.length;

const isSpam =
Expand All @@ -17,5 +18,9 @@ export default function (status: masto.mastodon.streaming.UpdateEvent['payload']
return {
isSpam,
reason: isSpam ? reason : undefined,
actions: {
sendReport: false,
suspendAccount: true,
}
};
}
16 changes: 16 additions & 0 deletions types/signatureDefinition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import masto from "masto";

export interface SignatureDefinition {
check: (status: masto.mastodon.streaming.UpdateEvent['payload']) => SignatureResponse;
signatureName: string;
}

export interface SignatureResponse {
isSpam: boolean;
reason?: string;
actions: {
sendReport?: boolean;
limitAccount?: boolean;
suspendAccount?: boolean;
}
}

0 comments on commit 7454ff0

Please sign in to comment.