+2
-1
.claude/settings.local.json
+2
-1
.claude/settings.local.json
···
11
11
"mcp__git-mcp-server__git_push",
12
12
"Bash(bunx tsc:*)",
13
13
"Bash(DID=did:test OZONE_URL=http://test OZONE_PDS=http://test BSKY_HANDLE=test.bsky.social BSKY_PASSWORD=testpass bun --eval \"import(''./src/validateEnv.js'').then(m => m.validateEnvironment())\")",
14
-
"mcp__git-mcp-server__git_add"
14
+
"mcp__git-mcp-server__git_add",
15
+
"Bash(bunx eslint:*)"
15
16
],
16
17
"ask": [
17
18
"curl"
+3
-4
src/agent.ts
+3
-4
src/agent.ts
···
1
-
import { AtpAgent } from '@atproto/api';
2
-
import { setGlobalDispatcher, Agent as Agent } from 'undici';
3
-
1
+
import { setGlobalDispatcher, Agent as Agent } from "undici";
4
2
setGlobalDispatcher(new Agent({ connect: { timeout: 20_000 } }));
5
-
import { BSKY_HANDLE, BSKY_PASSWORD, OZONE_PDS } from './config.js';
3
+
import { BSKY_HANDLE, BSKY_PASSWORD, OZONE_PDS } from "./config.js";
4
+
import { AtpAgent } from "@atproto/api";
6
5
7
6
export const agent = new AtpAgent({
8
7
service: `https://${OZONE_PDS}`,
+45
-50
src/checkHandles.ts
+45
-50
src/checkHandles.ts
···
1
-
import { HANDLE_CHECKS } from './constants.js';
2
-
import logger from './logger.js';
1
+
import { HANDLE_CHECKS } from "./constants.js";
2
+
import logger from "./logger.js";
3
3
import {
4
4
createAccountReport,
5
5
createAccountComment,
6
6
createAccountLabel,
7
-
} from './moderation.js';
7
+
} from "./moderation.js";
8
8
9
-
export const checkHandle = (
9
+
export const checkHandle = async (
10
10
did: string,
11
11
handle: string,
12
12
time: number,
13
13
) => {
14
-
try {
15
-
// Get a list of labels
16
-
const labels: string[] = Array.from(
17
-
HANDLE_CHECKS,
18
-
(handleCheck) => handleCheck.label,
14
+
// Get a list of labels
15
+
const labels: string[] = Array.from(
16
+
HANDLE_CHECKS,
17
+
(handleCheck) => handleCheck.label,
18
+
);
19
+
20
+
// iterate through the labels
21
+
labels.forEach((label) => {
22
+
const checkList = HANDLE_CHECKS.find(
23
+
(handleCheck) => handleCheck.label === label,
19
24
);
20
25
21
-
// iterate through the labels
22
-
labels.forEach((label) => {
23
-
const checkList = HANDLE_CHECKS.find(
24
-
(handleCheck) => handleCheck.label === label,
25
-
);
26
+
if (checkList?.ignoredDIDs) {
27
+
if (checkList.ignoredDIDs.includes(did)) {
28
+
logger.info(`Whitelisted DID: ${did}`);
29
+
return;
30
+
}
31
+
}
26
32
27
-
if (checkList?.ignoredDIDs) {
28
-
if (checkList.ignoredDIDs.includes(did)) {
29
-
logger.info(`Whitelisted DID: ${did}`);
33
+
if (checkList!.check.test(handle)) {
34
+
// False-positive checks
35
+
if (checkList?.whitelist) {
36
+
if (checkList?.whitelist.test(handle)) {
37
+
logger.info(`Whitelisted phrase found for: ${handle}`);
30
38
return;
31
39
}
32
40
}
33
41
34
-
if (checkList?.check.test(handle)) {
35
-
// False-positive checks
36
-
if (checkList.whitelist) {
37
-
if (checkList.whitelist.test(handle)) {
38
-
logger.info(`Whitelisted phrase found for: ${handle}`);
39
-
return;
40
-
}
42
+
if (checkList?.toLabel === true) {
43
+
logger.info(`[CHECKHANDLE]: Labeling ${did} for ${checkList!.label}`);
44
+
{
45
+
createAccountLabel(
46
+
did,
47
+
`${checkList!.label}`,
48
+
`${time}: ${checkList!.comment} - ${handle}`,
49
+
);
41
50
}
51
+
}
42
52
43
-
if (checkList.toLabel) {
44
-
logger.info(`[CHECKHANDLE]: Labeling ${did} for ${checkList.label}`);
45
-
{
46
-
void createAccountLabel(
47
-
did,
48
-
checkList.label,
49
-
`${time.toString()}: ${checkList.comment} - ${handle}`,
50
-
);
51
-
}
52
-
}
53
+
if (checkList?.reportAcct === true) {
54
+
logger.info(`[CHECKHANDLE]: Reporting ${did} for ${checkList!.label}`);
55
+
createAccountReport(did, `${time}: ${checkList!.comment} - ${handle}`);
56
+
}
53
57
54
-
if (checkList.reportAcct) {
55
-
logger.info(`[CHECKHANDLE]: Reporting ${did} for ${checkList.label}`);
56
-
void createAccountReport(did, `${time.toString()}: ${checkList.comment} - ${handle}`);
57
-
}
58
-
59
-
if (checkList.commentAcct) {
60
-
logger.info(
61
-
`[CHECKHANDLE]: Commenting on ${did} for ${checkList.label}`,
62
-
);
63
-
void createAccountComment(did, `${time.toString()}: ${checkList.comment} - ${handle}`);
64
-
}
58
+
if (checkList?.commentAcct === true) {
59
+
logger.info(
60
+
`[CHECKHANDLE]: Commenting on ${did} for ${checkList!.label}`,
61
+
);
62
+
createAccountComment(did, `${time}: ${checkList!.comment} - ${handle}`);
65
63
}
66
-
});
67
-
} catch (error) {
68
-
logger.error(`Error in checkHandle for ${did}:`, error);
69
-
throw error;
70
-
}
64
+
}
65
+
});
71
66
};
+94
-99
src/checkPosts.ts
+94
-99
src/checkPosts.ts
···
1
-
import { LINK_SHORTENER, POST_CHECKS } from './constants.js';
2
-
import logger from './logger.js';
1
+
import { LINK_SHORTENER, POST_CHECKS, langs } from "./constants.js";
2
+
import logger from "./logger.js";
3
3
import {
4
4
createPostLabel,
5
5
createAccountReport,
6
6
createAccountComment,
7
7
createPostReport,
8
-
} from './moderation.js';
9
-
import type { Post } from './types.js';
10
-
import { getFinalUrl, getLanguage } from './utils.js';
8
+
} from "./moderation.js";
9
+
import type { Post } from "./types.js";
10
+
import { getFinalUrl, getLanguage } from "./utils.js";
11
11
12
12
export const checkPosts = async (post: Post[]) => {
13
-
try {
14
-
// Get a list of labels
15
-
const labels: string[] = Array.from(
16
-
POST_CHECKS,
17
-
(postCheck) => postCheck.label,
18
-
);
13
+
// Get a list of labels
14
+
const labels: string[] = Array.from(
15
+
POST_CHECKS,
16
+
(postCheck) => postCheck.label,
17
+
);
19
18
20
-
const urlRegex = /https?:\/\/[^\s]+/g;
19
+
const urlRegex = /https?:\/\/[^\s]+/g;
21
20
22
-
// Check for link shorteners
23
-
if (LINK_SHORTENER.test(post[0].text)) {
24
-
try {
25
-
const url = post[0].text.match(urlRegex);
26
-
if (url && LINK_SHORTENER.test(url[0])) {
27
-
logger.info(`[CHECKPOSTS]: Checking shortened URL: ${url[0]}`);
28
-
const finalUrl = await getFinalUrl(url[0]);
29
-
if (finalUrl) {
30
-
const originalUrl = post[0].text;
31
-
post[0].text = post[0].text.replace(url[0], finalUrl);
32
-
logger.info(
33
-
`[CHECKPOSTS]: Shortened URL resolved: ${originalUrl} -> ${finalUrl}`,
34
-
);
35
-
}
21
+
// Check for link shorteners
22
+
if (LINK_SHORTENER.test(post[0].text)) {
23
+
try {
24
+
const url = post[0].text.match(urlRegex);
25
+
if (url && LINK_SHORTENER.test(url[0])) {
26
+
logger.info(`[CHECKPOSTS]: Checking shortened URL: ${url[0]}`);
27
+
const finalUrl = await getFinalUrl(url[0]);
28
+
if (finalUrl) {
29
+
const originalUrl = post[0].text;
30
+
post[0].text = post[0].text.replace(url[0], finalUrl);
31
+
logger.info(
32
+
`[CHECKPOSTS]: Shortened URL resolved: ${originalUrl} -> ${finalUrl}`,
33
+
);
36
34
}
37
-
} catch (error) {
38
-
logger.error(
39
-
`[CHECKPOSTS]: Failed to resolve shortened URL: ${post[0].text}`,
40
-
error,
41
-
);
42
-
// Keep the original URL if resolution fails
43
35
}
36
+
} catch (error) {
37
+
logger.error(
38
+
`[CHECKPOSTS]: Failed to resolve shortened URL: ${post[0].text}`,
39
+
error,
40
+
);
41
+
// Keep the original URL if resolution fails
44
42
}
43
+
}
45
44
46
-
// Get the post's language
47
-
const lang = await getLanguage(post[0].text);
45
+
// Get the post's language
46
+
const lang = await getLanguage(post[0].text);
48
47
49
-
// iterate through the labels
50
-
labels.forEach((label) => {
51
-
const checkPost = POST_CHECKS.find(
52
-
(postCheck) => postCheck.label === label,
53
-
);
48
+
// iterate through the labels
49
+
labels.forEach((label) => {
50
+
const checkPost = POST_CHECKS.find(
51
+
(postCheck) => postCheck.label === label,
52
+
);
54
53
55
-
if (checkPost?.language || checkPost?.language !== undefined) {
56
-
if (!checkPost.language.includes(lang)) {
57
-
return;
58
-
}
54
+
if (checkPost?.language || checkPost?.language !== undefined) {
55
+
if (!checkPost?.language.includes(lang)) {
56
+
return;
59
57
}
58
+
}
60
59
61
-
if (checkPost?.ignoredDIDs) {
62
-
if (checkPost.ignoredDIDs.includes(post[0].did)) {
63
-
logger.info(`[CHECKPOSTS]: Whitelisted DID: ${post[0].did}`);
64
-
return;
65
-
}
60
+
if (checkPost?.ignoredDIDs) {
61
+
if (checkPost?.ignoredDIDs.includes(post[0].did)) {
62
+
logger.info(`[CHECKPOSTS]: Whitelisted DID: ${post[0].did}`);
63
+
return;
66
64
}
65
+
}
67
66
68
-
if (checkPost?.check.test(post[0].text)) {
67
+
if (checkPost!.check.test(post[0].text)) {
69
68
// Check if post is whitelisted
70
-
if (checkPost.whitelist) {
71
-
if (checkPost.whitelist.test(post[0].text)) {
72
-
logger.info('[CHECKPOSTS]: Whitelisted phrase found"');
73
-
return;
74
-
}
69
+
if (checkPost?.whitelist) {
70
+
if (checkPost?.whitelist.test(post[0].text)) {
71
+
logger.info(`[CHECKPOSTS]: Whitelisted phrase found"`);
72
+
return;
75
73
}
74
+
}
76
75
77
-
if (checkPost.toLabel) {
78
-
logger.info(
79
-
`[CHECKPOSTS]: Labeling ${post[0].atURI} for ${checkPost.label}`,
80
-
);
81
-
void createPostLabel(
82
-
post[0].atURI,
83
-
post[0].cid,
84
-
checkPost.label,
85
-
`${post[0].time.toString()}: ${checkPost.comment} at ${post[0].atURI} with text "${post[0].text}"`,
86
-
);
87
-
}
76
+
if (checkPost!.toLabel === true) {
77
+
logger.info(
78
+
`[CHECKPOSTS]: Labeling ${post[0].atURI} for ${checkPost!.label}`,
79
+
);
80
+
createPostLabel(
81
+
post[0].atURI,
82
+
post[0].cid,
83
+
`${checkPost!.label}`,
84
+
`${post[0].time}: ${checkPost!.comment} at ${post[0].atURI} with text "${post[0].text}"`,
85
+
);
86
+
}
88
87
89
-
if (checkPost.reportPost) {
90
-
logger.info(
91
-
`[CHECKPOSTS]: Reporting ${post[0].atURI} for ${checkPost.label}`,
92
-
);
93
-
logger.info(`Reporting: ${post[0].atURI}`);
94
-
void createPostReport(
95
-
post[0].atURI,
96
-
post[0].cid,
97
-
`${post[0].time.toString()}: ${checkPost.comment} at ${post[0].atURI} with text "${post[0].text}"`,
98
-
);
99
-
}
88
+
if (checkPost!.reportPost === true) {
89
+
logger.info(
90
+
`[CHECKPOSTS]: Reporting ${post[0].atURI} for ${checkPost!.label}`,
91
+
);
92
+
logger.info(`Reporting: ${post[0].atURI}`);
93
+
createPostReport(
94
+
post[0].atURI,
95
+
post[0].cid,
96
+
`${post[0].time}: ${checkPost!.comment} at ${post[0].atURI} with text "${post[0].text}"`,
97
+
);
98
+
}
100
99
101
-
if (checkPost.reportAcct) {
102
-
logger.info(
103
-
`[CHECKPOSTS]: Reporting on ${post[0].did} for ${checkPost.label} in ${post[0].atURI}`,
104
-
);
105
-
void createAccountReport(
106
-
post[0].did,
107
-
`${post[0].time.toString()}: ${checkPost.comment} at ${post[0].atURI} with text "${post[0].text}"`,
108
-
);
109
-
}
100
+
if (checkPost!.reportAcct === true) {
101
+
logger.info(
102
+
`[CHECKPOSTS]: Reporting on ${post[0].did} for ${checkPost!.label} in ${post[0].atURI}`,
103
+
);
104
+
createAccountReport(
105
+
post[0].did,
106
+
`${post[0].time}: ${checkPost?.comment} at ${post[0].atURI} with text "${post[0].text}"`,
107
+
);
108
+
}
110
109
111
-
if (checkPost.commentAcct) {
112
-
logger.info(
113
-
`[CHECKPOSTS]: Commenting on ${post[0].did} for ${checkPost.label} in ${post[0].atURI}`,
114
-
);
115
-
void createAccountComment(
116
-
post[0].did,
117
-
`${post[0].time.toString()}: ${checkPost.comment} at ${post[0].atURI} with text "${post[0].text}"`,
118
-
);
119
-
}
110
+
if (checkPost!.commentAcct === true) {
111
+
logger.info(
112
+
`[CHECKPOSTS]: Commenting on ${post[0].did} for ${checkPost!.label} in ${post[0].atURI}`,
113
+
);
114
+
createAccountComment(
115
+
post[0].did,
116
+
`${post[0].time}: ${checkPost?.comment} at ${post[0].atURI} with text "${post[0].text}"`,
117
+
);
120
118
}
121
-
});
122
-
} catch (error) {
123
-
logger.error(`Error in checkPosts for ${post[0]?.did}:`, error);
124
-
throw error;
125
-
}
119
+
}
120
+
});
126
121
};
+8
-8
src/checkProfiles.ts
+8
-8
src/checkProfiles.ts
···
1
-
import { PROFILE_CHECKS } from './constants.js';
2
-
import logger from './logger.js';
1
+
import { PROFILE_CHECKS } from "./constants.js";
2
+
import logger from "./logger.js";
3
3
import {
4
4
createAccountReport,
5
5
createAccountLabel,
6
6
createAccountComment,
7
-
} from './moderation.js';
8
-
import { getLanguage } from './utils.js';
7
+
} from "./moderation.js";
8
+
import { getLanguage } from "./utils.js";
9
9
10
10
export const checkDescription = async (
11
11
did: string,
···
44
44
if (description) {
45
45
if (checkProfiles?.description === true) {
46
46
if (checkProfiles.check.test(description)) {
47
-
// Check if description is whitelisted
47
+
// Check if description is whitelisted
48
48
if (checkProfiles.whitelist) {
49
49
if (checkProfiles.whitelist.test(description)) {
50
-
logger.info('[CHECKDESCRIPTION]: Whitelisted phrase found.');
50
+
logger.info("[CHECKDESCRIPTION]: Whitelisted phrase found.");
51
51
return;
52
52
}
53
53
}
···
130
130
if (displayName) {
131
131
if (checkProfiles?.displayName === true) {
132
132
if (checkProfiles.check.test(displayName)) {
133
-
// Check if displayName is whitelisted
133
+
// Check if displayName is whitelisted
134
134
if (checkProfiles.whitelist) {
135
135
if (checkProfiles.whitelist.test(displayName)) {
136
-
logger.info('[CHECKDISPLAYNAME]: Whitelisted phrase found.');
136
+
logger.info("[CHECKDISPLAYNAME]: Whitelisted phrase found.");
137
137
return;
138
138
}
139
139
}
+74
-81
src/checkStarterPack.ts
+74
-81
src/checkStarterPack.ts
···
1
-
import { PROFILE_CHECKS, STARTERPACK_CHECKS } from './constants.js';
2
-
import logger from './logger.js';
1
+
import { PROFILE_CHECKS, STARTERPACK_CHECKS } from "./constants.js";
2
+
import logger from "./logger.js";
3
3
import {
4
4
createAccountLabel,
5
5
createAccountReport,
6
6
createPostLabel,
7
-
} from './moderation.js';
7
+
} from "./moderation.js";
8
8
9
-
export const checkStarterPack = (
9
+
export const checkStarterPack = async (
10
10
did: string,
11
11
time: number,
12
12
atURI: string,
13
13
) => {
14
-
try {
15
-
// Get a list of labels
16
-
const labels: string[] = Array.from(
17
-
PROFILE_CHECKS,
18
-
(profileCheck) => profileCheck.label,
14
+
// Get a list of labels
15
+
const labels: string[] = Array.from(
16
+
PROFILE_CHECKS,
17
+
(profileCheck) => profileCheck.label,
18
+
);
19
+
20
+
// iterate through the labels
21
+
labels.forEach((label) => {
22
+
const checkProfiles = PROFILE_CHECKS.find(
23
+
(profileCheck) => profileCheck.label === label,
19
24
);
20
25
21
-
// iterate through the labels
22
-
labels.forEach((label) => {
23
-
const checkProfiles = PROFILE_CHECKS.find(
24
-
(profileCheck) => profileCheck.label === label,
25
-
);
26
-
27
-
// Check if DID is whitelisted
28
-
if (checkProfiles?.ignoredDIDs) {
29
-
if (checkProfiles.ignoredDIDs.includes(did)) {
30
-
logger.info(`Whitelisted DID: ${did}`); return;
31
-
}
26
+
// Check if DID is whitelisted
27
+
if (checkProfiles?.ignoredDIDs) {
28
+
if (checkProfiles.ignoredDIDs.includes(did)) {
29
+
return logger.info(`Whitelisted DID: ${did}`);
32
30
}
31
+
}
33
32
34
-
if (atURI) {
35
-
if (checkProfiles?.starterPacks) {
36
-
if (checkProfiles.starterPacks.includes(atURI)) {
37
-
logger.info(`Account joined via starter pack at: ${atURI}`);
38
-
void createAccountLabel(
39
-
did,
40
-
checkProfiles.label,
41
-
`${time.toString()}: ${checkProfiles.comment} - Account joined via starter pack at: ${atURI}`,
42
-
);
43
-
}
33
+
if (atURI) {
34
+
if (checkProfiles?.starterPacks) {
35
+
if (checkProfiles?.starterPacks.includes(atURI)) {
36
+
logger.info(`Account joined via starter pack at: ${atURI}`);
37
+
createAccountLabel(
38
+
did,
39
+
`${checkProfiles!.label}`,
40
+
`${time}: ${checkProfiles!.comment} - Account joined via starter pack at: ${atURI}`,
41
+
);
44
42
}
45
43
}
46
-
});
47
-
} catch (error) {
48
-
logger.error(`Error in checkStarterPack for ${did}:`, error);
49
-
throw error;
50
-
}
44
+
}
45
+
});
51
46
};
52
47
53
-
export const checkNewStarterPack = (
48
+
export const checkNewStarterPack = async (
54
49
did: string,
55
50
time: number,
56
51
atURI: string,
···
58
53
packName: string | undefined,
59
54
description: string | undefined,
60
55
) => {
61
-
try {
62
-
const labels: string[] = STARTERPACK_CHECKS.map((SPCheck) => SPCheck.label);
56
+
const labels: string[] = Array.from(
57
+
STARTERPACK_CHECKS,
58
+
(SPCheck) => SPCheck.label,
59
+
);
60
+
61
+
labels.forEach((label) => {
62
+
const checkList = PROFILE_CHECKS.find((SPCheck) => SPCheck.label === label);
63
63
64
-
labels.forEach((label) => {
65
-
const checkList = STARTERPACK_CHECKS.find((SPCheck) => SPCheck.label === label);
64
+
if (checkList?.knownVectors?.includes(did)) {
65
+
createPostLabel(
66
+
atURI,
67
+
cid,
68
+
`${checkList!.label}`,
69
+
`${time}: Starter pack created by known vector for ${checkList!.label} at: ${atURI}"`,
70
+
);
71
+
createAccountReport(
72
+
did,
73
+
`${time}: Starter pack created by known vector for ${checkList!.label} at: ${atURI}"`,
74
+
);
75
+
}
66
76
67
-
if (checkList?.knownVectors?.includes(did)) {
68
-
void createPostLabel(
77
+
if (description) {
78
+
if (checkList!.check.test(description)) {
79
+
logger.info(`Labeling post: ${atURI}`);
80
+
createPostLabel(
69
81
atURI,
70
82
cid,
71
-
checkList.label,
72
-
`${time.toString()}: Starter pack created by known vector for ${checkList.label} at: ${atURI}"`,
83
+
`${checkList!.label}`,
84
+
`${time}: ${checkList!.comment} at ${atURI} with text "${description}"`,
73
85
);
74
-
void createAccountReport(
86
+
createAccountReport(
75
87
did,
76
-
`${time.toString()}: Starter pack created by known vector for ${checkList.label} at: ${atURI}"`,
88
+
`${time}: ${checkList!.comment} at ${atURI} with text "${description}"`,
77
89
);
78
90
}
91
+
}
79
92
80
-
if (description) {
81
-
if (checkList?.check.test(description)) {
82
-
logger.info(`Labeling post: ${atURI}`);
83
-
void createPostLabel(
84
-
atURI,
85
-
cid,
86
-
checkList.label,
87
-
`${time.toString()}: ${checkList.comment} at ${atURI} with text "${description}"`,
88
-
);
89
-
void createAccountReport(
90
-
did,
91
-
`${time.toString()}: ${checkList.comment} at ${atURI} with text "${description}"`,
92
-
);
93
-
}
94
-
}
95
-
96
-
if (packName) {
97
-
if (checkList?.check.test(packName)) {
98
-
logger.info(`Labeling post: ${atURI}`);
99
-
void createPostLabel(
100
-
atURI,
101
-
cid,
102
-
checkList.label,
103
-
`${time.toString()}: ${checkList.comment} at ${atURI} with pack name "${packName}"`,
104
-
);
105
-
void createAccountReport(
106
-
did,
107
-
`${time.toString()}: ${checkList.comment} at ${atURI} with pack name "${packName}"`,
108
-
);
109
-
}
93
+
if (packName) {
94
+
if (checkList!.check.test(packName)) {
95
+
logger.info(`Labeling post: ${atURI}`);
96
+
createPostLabel(
97
+
atURI,
98
+
cid,
99
+
`${checkList!.label}`,
100
+
`${time}: ${checkList!.comment} at ${atURI} with pack name "${packName}"`,
101
+
);
102
+
createAccountReport(
103
+
did,
104
+
`${time}: ${checkList!.comment} at ${atURI} with pack name "${packName}"`,
105
+
);
110
106
}
111
-
});
112
-
} catch (error) {
113
-
logger.error(`Error in checkNewStarterPack for ${did}:`, error);
114
-
throw error;
115
-
}
107
+
}
108
+
});
116
109
};
+13
-13
src/config.ts
+13
-13
src/config.ts
···
1
-
import 'dotenv/config';
1
+
import "dotenv/config";
2
2
3
-
export const MOD_DID = process.env.DID ?? '';
4
-
export const OZONE_URL = process.env.OZONE_URL ?? '';
5
-
export const OZONE_PDS = process.env.OZONE_PDS ?? '';
6
-
export const BSKY_HANDLE = process.env.BSKY_HANDLE ?? '';
7
-
export const BSKY_PASSWORD = process.env.BSKY_PASSWORD ?? '';
8
-
export const HOST = process.env.HOST ?? '127.0.0.1';
3
+
export const MOD_DID = process.env.DID ?? "";
4
+
export const OZONE_URL = process.env.OZONE_URL ?? "";
5
+
export const OZONE_PDS = process.env.OZONE_PDS ?? "";
6
+
export const BSKY_HANDLE = process.env.BSKY_HANDLE ?? "";
7
+
export const BSKY_PASSWORD = process.env.BSKY_PASSWORD ?? "";
8
+
export const HOST = process.env.HOST ?? "127.0.0.1";
9
9
export const PORT = process.env.PORT ? Number(process.env.PORT) : 4100;
10
10
export const METRICS_PORT = process.env.METRICS_PORT
11
11
? Number(process.env.METRICS_PORT)
12
12
: 4101; // Left this intact from the code I adapted this from
13
13
export const FIREHOSE_URL =
14
-
process.env.FIREHOSE_URL ?? 'wss://jetstream1.us-east.bsky.network/subscribe';
14
+
process.env.FIREHOSE_URL ?? "wss://jetstream.atproto.tools/subscribe";
15
15
export const WANTED_COLLECTION = [
16
-
'app.bsky.feed.post',
17
-
'app.bsky.actor.defs',
18
-
'app.bsky.actor.profile',
16
+
"app.bsky.feed.post",
17
+
"app.bsky.actor.defs",
18
+
"app.bsky.actor.profile",
19
19
];
20
20
export const CURSOR_UPDATE_INTERVAL = process.env.CURSOR_UPDATE_INTERVAL
21
21
? Number(process.env.CURSOR_UPDATE_INTERVAL)
22
22
: 60000;
23
-
export const { LABEL_LIMIT } = process.env;
24
-
export const { LABEL_LIMIT_WAIT } = process.env;
23
+
export const LABEL_LIMIT = process.env.LABEL_LIMIT;
24
+
export const LABEL_LIMIT_WAIT = process.env.LABEL_LIMIT_WAIT;
+308
src/homoglyphs.ts
+308
src/homoglyphs.ts
···
1
+
/* eslint-disable no-misleading-character-class */
2
+
3
+
export const homoglyphMap: Record<string, string> = {
4
+
// Confusables for 'a'
5
+
á: "a",
6
+
à: "a",
7
+
ă: "a",
8
+
ắ: "a",
9
+
ằ: "a",
10
+
ẵ: "a",
11
+
ẳ: "a",
12
+
â: "a",
13
+
ấ: "a",
14
+
ầ: "a",
15
+
ẫ: "a",
16
+
ẩ: "a",
17
+
ǎ: "a",
18
+
å: "a",
19
+
ǻ: "a",
20
+
ä: "a",
21
+
ǟ: "a",
22
+
ã: "a",
23
+
ȧ: "a",
24
+
ǡ: "a",
25
+
ą: "a",
26
+
ą́: "a",
27
+
ą̃: "a",
28
+
ā: "a",
29
+
ā̀: "a",
30
+
ả: "a",
31
+
ȁ: "a",
32
+
a̋: "a",
33
+
ȃ: "a",
34
+
ạ: "a",
35
+
ặ: "a",
36
+
ậ: "a",
37
+
ḁ: "a",
38
+
ⱥ: "a",
39
+
"ꞻ": "a",
40
+
ᶏ: "a",
41
+
ẚ: "a",
42
+
a: "a",
43
+
"@": "a",
44
+
"4": "a",
45
+
46
+
// Confusables for 'e'
47
+
"3": "e",
48
+
є: "e",
49
+
е: "e",
50
+
é: "e",
51
+
è: "e",
52
+
ĕ: "e",
53
+
ê: "e",
54
+
ế: "e",
55
+
ề: "e",
56
+
ễ: "e",
57
+
ể: "e",
58
+
ê̄: "e",
59
+
ê̌: "e",
60
+
ě: "e",
61
+
ë: "e",
62
+
ẽ: "e",
63
+
ė: "e",
64
+
ė́: "e",
65
+
ė̃: "e",
66
+
ȩ: "e",
67
+
ḝ: "e",
68
+
ę: "e",
69
+
ę́: "e",
70
+
ę̃: "e",
71
+
ē: "e",
72
+
ḗ: "e",
73
+
ḕ: "e",
74
+
ẻ: "e",
75
+
ȅ: "e",
76
+
e̋: "e",
77
+
ȇ: "e",
78
+
ẹ: "e",
79
+
ệ: "e",
80
+
ḙ: "e",
81
+
ḛ: "e",
82
+
ɇ: "e",
83
+
e̩: "e",
84
+
è̩: "e",
85
+
é̩: "e",
86
+
ᶒ: "e",
87
+
ⱸ: "e",
88
+
ꬴ: "e",
89
+
ꬳ: "e",
90
+
e: "e",
91
+
92
+
// Confusables for 'g'
93
+
ǵ: "g",
94
+
ğ: "g",
95
+
ĝ: "g",
96
+
ǧ: "g",
97
+
ġ: "g",
98
+
g̃: "g",
99
+
ģ: "g",
100
+
ḡ: "g",
101
+
ǥ: "g",
102
+
ꞡ: "g",
103
+
ɠ: "g",
104
+
ᶃ: "g",
105
+
ꬶ: "g",
106
+
g: "g",
107
+
q: "g",
108
+
ꝗ: "g",
109
+
ꝙ: "g",
110
+
ɋ: "g",
111
+
ʠ: "g",
112
+
113
+
// Confusables for 'i' and 'l'
114
+
í: "i",
115
+
i̇́: "i",
116
+
ì: "i",
117
+
i̇̀: "i",
118
+
ĭ: "i",
119
+
î: "i",
120
+
ǐ: "i",
121
+
ï: "i",
122
+
ḯ: "i",
123
+
ĩ: "i",
124
+
i̇̃: "i",
125
+
į: "i",
126
+
į̇́: "i",
127
+
į̇̃: "i",
128
+
ī: "i",
129
+
ī̀: "i",
130
+
ỉ: "i",
131
+
ȉ: "i",
132
+
i̋: "i",
133
+
ȋ: "i",
134
+
ị: "i",
135
+
"ꞽ": "i",
136
+
ḭ: "i",
137
+
ɨ: "i",
138
+
ᶖ: "i",
139
+
ı: "i",
140
+
i: "i",
141
+
"1": "i",
142
+
ĺ: "l",
143
+
ľ: "l",
144
+
ļ: "l",
145
+
ḷ: "l",
146
+
ḹ: "l",
147
+
l̃: "l",
148
+
ḽ: "l",
149
+
ḻ: "l",
150
+
ł: "l",
151
+
ŀ: "l",
152
+
ƚ: "l",
153
+
ꝉ: "l",
154
+
ⱡ: "l",
155
+
ɫ: "l",
156
+
ɬ: "l",
157
+
ꞎ: "l",
158
+
ꬷ: "l",
159
+
ꬸ: "l",
160
+
ꬹ: "l",
161
+
ᶅ: "l",
162
+
ɭ: "l",
163
+
ȴ: "l",
164
+
l: "l",
165
+
166
+
// Confusables for 'k'
167
+
ḱ: "k",
168
+
ǩ: "k",
169
+
ķ: "k",
170
+
ḳ: "k",
171
+
ḵ: "k",
172
+
ƙ: "k",
173
+
ⱪ: "k",
174
+
ᶄ: "k",
175
+
ꝁ: "k",
176
+
ꝃ: "k",
177
+
ꝅ: "k",
178
+
ꞣ: "k",
179
+
180
+
// Confusables for 'n'
181
+
ń: "n",
182
+
ň: "n",
183
+
ñ: "n",
184
+
ṅ: "n",
185
+
ņ: "n",
186
+
ṇ: "n",
187
+
ṋ: "n",
188
+
ǹ: "n",
189
+
Ƞ: "n",
190
+
ƞ: "n",
191
+
ᵰ: "n",
192
+
ᶇ: "n",
193
+
ɳ: "n",
194
+
ȵ: "n",
195
+
ⁿ: "n",
196
+
n: "n",
197
+
מ: "n",
198
+
и: "n",
199
+
п: "n",
200
+
ꬻ: "n",
201
+
ꬼ: "n",
202
+
n̈: "n",
203
+
ɲ: "n",
204
+
ŋ: "n",
205
+
ꞑ: "n",
206
+
ꞥ: "n",
207
+
208
+
// Confusables for 'o'
209
+
ó: "o",
210
+
ò: "o",
211
+
ŏ: "o",
212
+
ô: "o",
213
+
ố: "o",
214
+
ồ: "o",
215
+
ỗ: "o",
216
+
ổ: "o",
217
+
ǒ: "o",
218
+
ö: "o",
219
+
ȫ: "o",
220
+
ő: "o",
221
+
õ: "o",
222
+
ṍ: "o",
223
+
ṏ: "o",
224
+
ȭ: "o",
225
+
ȯ: "o",
226
+
o͘: "o",
227
+
ȱ: "o",
228
+
ø: "o",
229
+
ǿ: "o",
230
+
ǫ: "o",
231
+
ǭ: "o",
232
+
ō: "o",
233
+
ṓ: "o",
234
+
ṑ: "o",
235
+
ỏ: "o",
236
+
ȍ: "o",
237
+
ȏ: "o",
238
+
ơ: "o",
239
+
ớ: "o",
240
+
ờ: "o",
241
+
ỡ: "o",
242
+
ở: "o",
243
+
ợ: "o",
244
+
ọ: "o",
245
+
ộ: "o",
246
+
o̩: "o",
247
+
ò̩: "o",
248
+
ó̩: "o",
249
+
ɵ: "o",
250
+
ꝋ: "o",
251
+
ꝍ: "o",
252
+
ⱺ: "o",
253
+
o: "o",
254
+
"0": "o",
255
+
256
+
// Confusables for 'r'
257
+
ŕ: "r",
258
+
ř: "r",
259
+
ṙ: "r",
260
+
ŗ: "r",
261
+
ȑ: "r",
262
+
ȓ: "r",
263
+
ṛ: "r",
264
+
ṝ: "r",
265
+
ṟ: "r",
266
+
r̃: "r",
267
+
ɍ: "r",
268
+
ꞧ: "r",
269
+
ɽ: "r",
270
+
ᵲ: "r",
271
+
ᶉ: "r",
272
+
ꭉ: "r",
273
+
274
+
// Confusables for 's'
275
+
ś: "s",
276
+
ṥ: "s",
277
+
ŝ: "s",
278
+
š: "s",
279
+
ṧ: "s",
280
+
ṡ: "s",
281
+
ş: "s",
282
+
ṣ: "s",
283
+
ṩ: "s",
284
+
ș: "s",
285
+
s̩: "s",
286
+
ꞩ: "s",
287
+
ȿ: "s",
288
+
ʂ: "s",
289
+
ᶊ: "s",
290
+
ᵴ: "s",
291
+
292
+
// Confusables for 't'
293
+
ť: "t",
294
+
ṫ: "t",
295
+
ţ: "t",
296
+
ṭ: "t",
297
+
ț: "t",
298
+
ṱ: "t",
299
+
ṯ: "t",
300
+
ŧ: "t",
301
+
ⱦ: "t",
302
+
ƭ: "t",
303
+
ʈ: "t",
304
+
ẗ: "t",
305
+
ᵵ: "t",
306
+
ƫ: "t",
307
+
ȶ: "t",
308
+
};
+1
-1
src/limits.ts
+1
-1
src/limits.ts
+21
-21
src/lists.ts
+21
-21
src/lists.ts
···
1
-
import type { List } from './types.js';
1
+
import { List } from "./types.js";
2
2
3
3
export const LISTS: List[] = [
4
4
{
5
-
label: 'blue-heart-emoji',
6
-
rkey: '3lfbtgosyyi22',
5
+
label: "blue-heart-emoji",
6
+
rkey: "3lfbtgosyyi22",
7
7
},
8
8
{
9
-
label: 'troll',
10
-
rkey: '3lbckxhgu3r2v',
9
+
label: "troll",
10
+
rkey: "3lbckxhgu3r2v",
11
11
},
12
12
{
13
-
label: 'maga-trump',
14
-
rkey: '3l53cjwlt4o2s',
13
+
label: "maga-trump",
14
+
rkey: "3l53cjwlt4o2s",
15
15
},
16
16
{
17
-
label: 'elon-musk',
18
-
rkey: '3l72tte74wa2m',
17
+
label: "elon-musk",
18
+
rkey: "3l72tte74wa2m",
19
19
},
20
20
{
21
-
label: 'rmve-imve',
22
-
rkey: '3l6tfurf7li27',
21
+
label: "rmve-imve",
22
+
rkey: "3l6tfurf7li27",
23
23
},
24
24
{
25
-
label: 'nazi-symbolism',
26
-
rkey: '3l6vdudxgeb2z',
25
+
label: "nazi-symbolism",
26
+
rkey: "3l6vdudxgeb2z",
27
27
},
28
28
{
29
-
label: 'hammer-sickle',
30
-
rkey: '3l4ue6w2aur2v',
29
+
label: "hammer-sickle",
30
+
rkey: "3l4ue6w2aur2v",
31
31
},
32
32
{
33
-
label: 'inverted-red-triangle',
34
-
rkey: '3l4ueabtpec2a',
33
+
label: "inverted-red-triangle",
34
+
rkey: "3l4ueabtpec2a",
35
35
},
36
36
{
37
-
label: 'automated-reply-guy',
38
-
rkey: '3lch7qbvzpx23',
37
+
label: "automated-reply-guy",
38
+
rkey: "3lch7qbvzpx23",
39
39
},
40
40
{
41
-
label: 'terf-gc',
42
-
rkey: '3lcqjqjdejs2x',
41
+
label: "terf-gc",
42
+
rkey: "3lcqjqjdejs2x",
43
43
},
44
44
];
+10
-10
src/logger.ts
+10
-10
src/logger.ts
···
1
-
import { pino } from 'pino';
1
+
import { pino } from "pino";
2
2
3
3
const logger = pino({
4
-
level: process.env.LOG_LEVEL ?? 'info',
4
+
level: process.env.LOG_LEVEL ?? "info",
5
5
transport:
6
-
process.env.NODE_ENV !== 'production'
6
+
process.env.NODE_ENV !== "production"
7
7
? {
8
-
target: 'pino-pretty',
9
-
options: {
10
-
colorize: true,
11
-
translateTime: 'SYS:standard',
12
-
ignore: 'pid,hostname',
13
-
},
14
-
}
8
+
target: "pino-pretty",
9
+
options: {
10
+
colorize: true,
11
+
translateTime: "SYS:standard",
12
+
ignore: "pid,hostname",
13
+
},
14
+
}
15
15
: undefined,
16
16
timestamp: pino.stdTimeFunctions.isoTime,
17
17
});
+1
-1
src/main.ts
+1
-1
src/main.ts
···
17
17
METRICS_PORT,
18
18
WANTED_COLLECTION,
19
19
} from "./config.js";
20
-
import { validateEnvironment } from "./validateEnv.js";
21
20
import logger from "./logger.js";
22
21
import { startMetricsServer } from "./metrics.js";
22
+
import { validateEnvironment } from "./validateEnv.js";
23
23
import type { Post, LinkFeature } from "./types.js";
24
24
25
25
validateEnvironment();
+6
-6
src/metrics.ts
+6
-6
src/metrics.ts
···
1
-
import express from 'express';
2
-
import { Registry, collectDefaultMetrics } from 'prom-client';
1
+
import express from "express";
2
+
import { Registry, collectDefaultMetrics } from "prom-client";
3
3
4
-
import logger from './logger.js';
4
+
import logger from "./logger.js";
5
5
6
6
const register = new Registry();
7
7
collectDefaultMetrics({ register });
8
8
9
9
const app = express();
10
10
11
-
app.get('/metrics', (req, res) => {
11
+
app.get("/metrics", (req, res) => {
12
12
register
13
13
.metrics()
14
14
.then((metrics) => {
15
-
res.set('Content-Type', register.contentType);
15
+
res.set("Content-Type", register.contentType);
16
16
res.send(metrics);
17
17
})
18
18
.catch((ex: unknown) => {
···
21
21
});
22
22
});
23
23
24
-
export const startMetricsServer = (port: number, host = '127.0.0.1') => {
24
+
export const startMetricsServer = (port: number, host = "127.0.0.1") => {
25
25
return app.listen(port, host, () => {
26
26
logger.info(`Metrics server is listening on ${host}:${port}`);
27
27
});
+43
-43
src/moderation.ts
+43
-43
src/moderation.ts
···
1
-
import { agent, isLoggedIn } from './agent.js';
2
-
import { MOD_DID } from './config.js';
3
-
import { limit } from './limits.js';
4
-
import { LISTS } from './lists.js';
5
-
import logger from './logger.js';
1
+
import { agent, isLoggedIn } from "./agent.js";
2
+
import { MOD_DID } from "./config.js";
3
+
import { limit } from "./limits.js";
4
+
import { LISTS } from "./lists.js";
5
+
import logger from "./logger.js";
6
6
7
7
export const createPostLabel = async (
8
8
uri: string,
···
16
16
return await agent.tools.ozone.moderation.emitEvent(
17
17
{
18
18
event: {
19
-
$type: 'tools.ozone.moderation.defs#modEventLabel',
19
+
$type: "tools.ozone.moderation.defs#modEventLabel",
20
20
comment,
21
21
createLabelVals: [label],
22
22
negateLabelVals: [],
23
23
},
24
24
// specify the labeled post by strongRef
25
25
subject: {
26
-
$type: 'com.atproto.repo.strongRef',
26
+
$type: "com.atproto.repo.strongRef",
27
27
uri,
28
28
cid,
29
29
},
30
30
// put in the rest of the metadata
31
-
createdBy: agent.did ?? '',
31
+
createdBy: agent.did ?? "",
32
32
createdAt: new Date().toISOString(),
33
33
},
34
34
{
35
-
encoding: 'application/json',
35
+
encoding: "application/json",
36
36
headers: {
37
-
'atproto-proxy': `${MOD_DID}#atproto_labeler`,
38
-
'atproto-accept-labelers':
39
-
'did:plc:ar7c4by46qjdydhdevvrndac;redact',
37
+
"atproto-proxy": `${MOD_DID}#atproto_labeler`,
38
+
"atproto-accept-labelers":
39
+
"did:plc:ar7c4by46qjdydhdevvrndac;redact",
40
40
},
41
41
},
42
42
);
···
58
58
await agent.tools.ozone.moderation.emitEvent(
59
59
{
60
60
event: {
61
-
$type: 'tools.ozone.moderation.defs#modEventLabel',
61
+
$type: "tools.ozone.moderation.defs#modEventLabel",
62
62
comment,
63
63
createLabelVals: [label],
64
64
negateLabelVals: [],
65
65
},
66
66
// specify the labeled post by strongRef
67
67
subject: {
68
-
$type: 'com.atproto.admin.defs#repoRef',
68
+
$type: "com.atproto.admin.defs#repoRef",
69
69
did,
70
70
},
71
71
// put in the rest of the metadata
72
-
createdBy: agent.did ?? '',
72
+
createdBy: agent.did ?? "",
73
73
createdAt: new Date().toISOString(),
74
74
},
75
75
{
76
-
encoding: 'application/json',
76
+
encoding: "application/json",
77
77
headers: {
78
-
'atproto-proxy': `${MOD_DID}#atproto_labeler`,
79
-
'atproto-accept-labelers':
80
-
'did:plc:ar7c4by46qjdydhdevvrndac;redact',
78
+
"atproto-proxy": `${MOD_DID}#atproto_labeler`,
79
+
"atproto-accept-labelers":
80
+
"did:plc:ar7c4by46qjdydhdevvrndac;redact",
81
81
},
82
82
},
83
83
);
···
99
99
return await agent.tools.ozone.moderation.emitEvent(
100
100
{
101
101
event: {
102
-
$type: 'tools.ozone.moderation.defs#modEventReport',
102
+
$type: "tools.ozone.moderation.defs#modEventReport",
103
103
comment,
104
-
reportType: 'com.atproto.moderation.defs#reasonOther',
104
+
reportType: "com.atproto.moderation.defs#reasonOther",
105
105
},
106
106
// specify the labeled post by strongRef
107
107
subject: {
108
-
$type: 'com.atproto.repo.strongRef',
108
+
$type: "com.atproto.repo.strongRef",
109
109
uri,
110
110
cid,
111
111
},
112
112
// put in the rest of the metadata
113
-
createdBy: agent.did ?? '',
113
+
createdBy: agent.did ?? "",
114
114
createdAt: new Date().toISOString(),
115
115
},
116
116
{
117
-
encoding: 'application/json',
117
+
encoding: "application/json",
118
118
headers: {
119
-
'atproto-proxy': `${MOD_DID}#atproto_labeler`,
120
-
'atproto-accept-labelers':
121
-
'did:plc:ar7c4by46qjdydhdevvrndac;redact',
119
+
"atproto-proxy": `${MOD_DID}#atproto_labeler`,
120
+
"atproto-accept-labelers":
121
+
"did:plc:ar7c4by46qjdydhdevvrndac;redact",
122
122
},
123
123
},
124
124
);
···
136
136
await agent.tools.ozone.moderation.emitEvent(
137
137
{
138
138
event: {
139
-
$type: 'tools.ozone.moderation.defs#modEventComment',
139
+
$type: "tools.ozone.moderation.defs#modEventComment",
140
140
comment,
141
141
},
142
142
// specify the labeled post by strongRef
143
143
subject: {
144
-
$type: 'com.atproto.admin.defs#repoRef',
144
+
$type: "com.atproto.admin.defs#repoRef",
145
145
did,
146
146
},
147
147
// put in the rest of the metadata
148
-
createdBy: agent.did ?? '',
148
+
createdBy: agent.did ?? "",
149
149
createdAt: new Date().toISOString(),
150
150
},
151
151
{
152
-
encoding: 'application/json',
152
+
encoding: "application/json",
153
153
headers: {
154
-
'atproto-proxy': `${MOD_DID}#atproto_labeler`,
155
-
'atproto-accept-labelers':
156
-
'did:plc:ar7c4by46qjdydhdevvrndac;redact',
154
+
"atproto-proxy": `${MOD_DID}#atproto_labeler`,
155
+
"atproto-accept-labelers":
156
+
"did:plc:ar7c4by46qjdydhdevvrndac;redact",
157
157
},
158
158
},
159
159
);
···
171
171
await agent.tools.ozone.moderation.emitEvent(
172
172
{
173
173
event: {
174
-
$type: 'tools.ozone.moderation.defs#modEventReport',
174
+
$type: "tools.ozone.moderation.defs#modEventReport",
175
175
comment,
176
-
reportType: 'com.atproto.moderation.defs#reasonOther',
176
+
reportType: "com.atproto.moderation.defs#reasonOther",
177
177
},
178
178
// specify the labeled post by strongRef
179
179
subject: {
180
-
$type: 'com.atproto.admin.defs#repoRef',
180
+
$type: "com.atproto.admin.defs#repoRef",
181
181
did,
182
182
},
183
183
// put in the rest of the metadata
184
-
createdBy: agent.did ?? '',
184
+
createdBy: agent.did ?? "",
185
185
createdAt: new Date().toISOString(),
186
186
},
187
187
{
188
-
encoding: 'application/json',
188
+
encoding: "application/json",
189
189
headers: {
190
-
'atproto-proxy': `${MOD_DID}#atproto_labeler`,
191
-
'atproto-accept-labelers':
192
-
'did:plc:ar7c4by46qjdydhdevvrndac;redact',
190
+
"atproto-proxy": `${MOD_DID}#atproto_labeler`,
191
+
"atproto-accept-labelers":
192
+
"did:plc:ar7c4by46qjdydhdevvrndac;redact",
193
193
},
194
194
},
195
195
);
···
217
217
await limit(async () => {
218
218
try {
219
219
await agent.com.atproto.repo.createRecord({
220
-
collection: 'app.bsky.graph.listitem',
220
+
collection: "app.bsky.graph.listitem",
221
221
repo: MOD_DID,
222
222
record: {
223
223
subject: did,
+24
-25
src/monitor.ts
+24
-25
src/monitor.ts
···
1
-
import { describe } from 'node:test';
2
-
3
-
import { PROFILE_CHECKS } from './constants.js';
4
-
import logger from './logger.js';
5
-
import { createAccountReport, createAccountLabel } from './moderation.js';
1
+
import { describe } from "node:test";
2
+
import { PROFILE_CHECKS } from "./constants.js";
3
+
import logger from "./logger.js";
4
+
import { createAccountReport, createAccountLabel } from "./moderation.js";
6
5
7
6
export const monitorDescription = async (
8
7
did: string,
···
25
24
// Check if DID is whitelisted
26
25
if (checkProfiles?.ignoredDIDs) {
27
26
if (checkProfiles.ignoredDIDs.includes(did)) {
28
-
logger.info(`Whitelisted DID: ${did}`); return;
27
+
return logger.info(`Whitelisted DID: ${did}`);
29
28
}
30
29
}
31
30
32
31
if (description) {
33
32
if (checkProfiles?.description === true) {
34
-
if (checkProfiles.check.test(description)) {
35
-
if (checkProfiles.whitelist) {
36
-
if (checkProfiles.whitelist.test(description)) {
37
-
logger.info('Whitelisted phrase found.');
33
+
if (checkProfiles!.check.test(description)) {
34
+
if (checkProfiles!.whitelist) {
35
+
if (checkProfiles!.whitelist.test(description)) {
36
+
logger.info(`Whitelisted phrase found.`);
38
37
return;
39
38
}
40
39
} else {
41
-
logger.info(`${checkProfiles.label} in description for ${did}`);
40
+
logger.info(`${checkProfiles!.label} in description for ${did}`);
42
41
}
43
42
44
-
if (checkProfiles.reportOnly === true) {
43
+
if (checkProfiles!.reportOnly === true) {
45
44
createAccountReport(
46
45
did,
47
-
`${time}: ${checkProfiles.comment} - ${displayName} - ${description}`,
46
+
`${time}: ${checkProfiles!.comment} - ${displayName} - ${description}`,
48
47
);
49
48
return;
50
49
} else {
51
50
createAccountLabel(
52
51
did,
53
-
checkProfiles.label,
54
-
`${time}: ${checkProfiles.comment}`,
52
+
`${checkProfiles!.label}`,
53
+
`${time}: ${checkProfiles!.comment}`,
55
54
);
56
55
}
57
56
}
···
81
80
// Check if DID is whitelisted
82
81
if (checkProfiles?.ignoredDIDs) {
83
82
if (checkProfiles.ignoredDIDs.includes(did)) {
84
-
logger.info(`Whitelisted DID: ${did}`); return;
83
+
return logger.info(`Whitelisted DID: ${did}`);
85
84
}
86
85
}
87
86
88
87
if (displayName) {
89
88
if (checkProfiles?.displayName === true) {
90
-
if (checkProfiles.check.test(displayName)) {
91
-
if (checkProfiles.whitelist) {
92
-
if (checkProfiles.whitelist.test(displayName)) {
93
-
logger.info('Whitelisted phrase found.');
89
+
if (checkProfiles!.check.test(displayName)) {
90
+
if (checkProfiles!.whitelist) {
91
+
if (checkProfiles!.whitelist.test(displayName)) {
92
+
logger.info(`Whitelisted phrase found.`);
94
93
return;
95
94
}
96
95
} else {
97
-
logger.info(`${checkProfiles.label} in displayName for ${did}`);
96
+
logger.info(`${checkProfiles!.label} in displayName for ${did}`);
98
97
}
99
98
100
-
if (checkProfiles.reportOnly === true) {
99
+
if (checkProfiles!.reportOnly === true) {
101
100
createAccountReport(
102
101
did,
103
-
`${time}: ${checkProfiles.comment} - ${displayName} - ${description}`,
102
+
`${time}: ${checkProfiles!.comment} - ${displayName} - ${description}`,
104
103
);
105
104
return;
106
105
} else {
107
106
createAccountLabel(
108
107
did,
109
-
checkProfiles.label,
110
-
`${time}: ${checkProfiles.comment}`,
108
+
`${checkProfiles!.label}`,
109
+
`${time}: ${checkProfiles!.comment}`,
111
110
);
112
111
}
113
112
}
+1
-1
src/types.ts
+1
-1
src/types.ts
+51
-50
src/utils.ts
+51
-50
src/utils.ts
···
1
-
import logger from './logger.js';
1
+
import logger from "./logger.js";
2
+
3
+
import { homoglyphMap } from "./homoglyphs";
2
4
3
-
/* Normalize the Unicode characters: this doesn't consistently work yet, there is something about certain bluesky strings that causes it to fail. */
5
+
/**
6
+
* Normalizes a string by converting it to lowercase, replacing homoglyphs,
7
+
* and stripping diacritics. This is useful for sanitizing user input
8
+
* before performing checks for forbidden words.
9
+
*
10
+
* The process is as follows:
11
+
* 1. Convert the entire string to lowercase.
12
+
* 2. Replace characters that are visually similar to ASCII letters (homoglyphs)
13
+
* with their ASCII counterparts based on the `homoglyphMap`.
14
+
* 3. Apply NFD (Normalization Form D) Unicode normalization to decompose
15
+
* characters into their base characters and combining marks.
16
+
* 4. Remove all Unicode combining diacritical marks.
17
+
* 5. Apply NFKC (Normalization Form KC) Unicode normalization for a final
18
+
* cleanup, which handles compatibility characters.
19
+
*
20
+
* @param text The input string to normalize.
21
+
* @returns The normalized string.
22
+
*/
4
23
export function normalizeUnicode(text: string): string {
5
-
// First decompose the characters (NFD)
6
-
const decomposed = text.normalize('NFD');
24
+
// Convert to lowercase to match the homoglyph map keys
25
+
const lowercased = text.toLowerCase();
7
26
8
-
// Remove diacritics and combining marks
9
-
const withoutDiacritics = decomposed.replace(/[\u0300-\u036f]/g, '');
27
+
// Replace characters using the homoglyph map.
28
+
// This is done before NFD so that pre-composed characters are caught.
29
+
let replaced = "";
30
+
for (const char of lowercased) {
31
+
replaced += homoglyphMap[char] || char;
32
+
}
10
33
11
-
// Remove mathematical alphanumeric symbols
12
-
const withoutMath = withoutDiacritics.replace(
13
-
/[\uD835][\uDC00-\uDFFF]/g,
14
-
(char) => {
15
-
// Get the base character from the mathematical symbol
16
-
const code = char.codePointAt(0);
17
-
if (code >= 0x1d400 && code <= 0x1d433)
18
-
// Mathematical bold
19
-
return String.fromCharCode(code - 0x1d400 + 0x41);
20
-
if (code >= 0x1d434 && code <= 0x1d467)
21
-
// Mathematical italic
22
-
return String.fromCharCode(code - 0x1d434 + 0x61);
23
-
if (code >= 0x1d468 && code <= 0x1d49b)
24
-
// Mathematical bold italic
25
-
return String.fromCharCode(code - 0x1d468 + 0x41);
26
-
if (code >= 0x1d49c && code <= 0x1d4cf)
27
-
// Mathematical script
28
-
return String.fromCharCode(code - 0x1d49c + 0x61);
29
-
return char;
30
-
},
31
-
);
34
+
// First decompose the characters (NFD), then remove diacritics.
35
+
const withoutDiacritics = replaced
36
+
.normalize("NFD")
37
+
.replace(/[\u0300-\u036f]/g, "");
32
38
33
-
// Final NFKC normalization to handle any remaining special characters
34
-
return withoutMath.normalize('NFKC');
39
+
// Final NFKC normalization to handle any remaining special characters.
40
+
return withoutDiacritics.normalize("NFKC");
35
41
}
36
42
37
43
export async function getFinalUrl(url: string): Promise<string> {
38
44
const controller = new AbortController();
39
-
const timeoutId = setTimeout(() => { controller.abort(); }, 10000); // 10-second timeout
45
+
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10-second timeout
40
46
41
47
try {
42
48
const response = await fetch(url, {
43
-
method: 'HEAD',
44
-
redirect: 'follow', // This will follow redirects automatically
49
+
method: "HEAD",
50
+
redirect: "follow", // This will follow redirects automatically
45
51
signal: controller.signal, // Pass the abort signal to fetch
46
52
});
47
53
clearTimeout(timeoutId); // Clear the timeout if fetch completes
···
49
55
} catch (error) {
50
56
clearTimeout(timeoutId); // Clear the timeout if fetch fails
51
57
// Log the error with more specific information if it's a timeout
52
-
if (error instanceof Error && error.name === 'AbortError') {
58
+
if (error instanceof Error && error.name === "AbortError") {
53
59
logger.warn(`Timeout fetching URL: ${url}`, error);
54
60
} else {
55
61
logger.warn(`Error fetching URL: ${url}`, error);
···
59
65
}
60
66
61
67
export async function getLanguage(profile: string): Promise<string> {
62
-
if (!profile) {
68
+
if (typeof profile !== "string" || profile === null) {
63
69
logger.warn(
64
-
'[GETLANGUAGE] getLanguage called with empty profile data, defaulting to \'eng\'.',
70
+
"[GETLANGUAGE] getLanguage called with invalid profile data, defaulting to 'eng'.",
65
71
profile,
66
72
);
67
-
return 'eng'; // Default or throw an error
73
+
return "eng"; // Default or throw an error
68
74
}
69
75
70
76
const profileText = profile.trim();
71
77
72
78
if (profileText.length === 0) {
73
-
return 'eng';
79
+
return "eng";
74
80
}
75
81
76
-
try {
77
-
const lande = (await import('lande')).default;
78
-
const langsProbabilityMap = lande(profileText);
82
+
const lande = (await import("lande")).default;
83
+
let langsProbabilityMap = lande(profileText);
79
84
80
-
// Sort by probability in descending order
81
-
langsProbabilityMap.sort(
82
-
(a: [string, number], b: [string, number]) => b[1] - a[1],
83
-
);
85
+
// Sort by probability in descending order
86
+
langsProbabilityMap.sort(
87
+
(a: [string, number], b: [string, number]) => b[1] - a[1],
88
+
);
84
89
85
-
// Return the language code with the highest probability
86
-
return langsProbabilityMap[0][0];
87
-
} catch (error) {
88
-
logger.error('Error detecting language, defaulting to \'eng\':', error);
89
-
return 'eng'; // Fallback to English on error
90
-
}
90
+
// Return the language code with the highest probability
91
+
return langsProbabilityMap[0][0];
91
92
}
+53
-49
src/validateEnv.ts
+53
-49
src/validateEnv.ts
···
1
-
import logger from './logger.js';
1
+
import logger from "./logger.js";
2
2
3
3
interface EnvironmentVariable {
4
4
name: string;
···
9
9
10
10
const ENV_VARIABLES: EnvironmentVariable[] = [
11
11
{
12
-
name: 'DID',
12
+
name: "DID",
13
13
required: true,
14
-
description: 'Moderator DID for labeling operations',
15
-
validator: (value) => value.startsWith('did:'),
14
+
description: "Moderator DID for labeling operations",
15
+
validator: (value) => value.startsWith("did:"),
16
16
},
17
17
{
18
-
name: 'OZONE_URL',
18
+
name: "OZONE_URL",
19
19
required: true,
20
-
description: 'Ozone server URL',
21
-
validator: (value) => value.includes('.') && value.length > 3,
20
+
description: "Ozone server URL",
21
+
validator: (value) => value.includes(".") && value.length > 3,
22
22
},
23
23
{
24
-
name: 'OZONE_PDS',
24
+
name: "OZONE_PDS",
25
25
required: true,
26
-
description: 'Ozone PDS URL',
27
-
validator: (value) => value.includes('.') && value.length > 3,
26
+
description: "Ozone PDS URL",
27
+
validator: (value) => value.includes(".") && value.length > 3,
28
28
},
29
29
{
30
-
name: 'BSKY_HANDLE',
30
+
name: "BSKY_HANDLE",
31
31
required: true,
32
-
description: 'Bluesky handle for authentication',
33
-
validator: (value) => value.includes('.'),
32
+
description: "Bluesky handle for authentication",
33
+
validator: (value) => value.includes("."),
34
34
},
35
35
{
36
-
name: 'BSKY_PASSWORD',
36
+
name: "BSKY_PASSWORD",
37
37
required: true,
38
-
description: 'Bluesky password for authentication',
38
+
description: "Bluesky password for authentication",
39
39
validator: (value) => value.length > 0,
40
40
},
41
41
{
42
-
name: 'HOST',
42
+
name: "HOST",
43
43
required: false,
44
-
description: 'Host address for the server (defaults to 127.0.0.1)',
44
+
description: "Host address for the server (defaults to 127.0.0.1)",
45
45
},
46
46
{
47
-
name: 'PORT',
47
+
name: "PORT",
48
48
required: false,
49
-
description: 'Port for the main server (defaults to 4100)',
49
+
description: "Port for the main server (defaults to 4100)",
50
50
validator: (value) => !isNaN(Number(value)) && Number(value) > 0,
51
51
},
52
52
{
53
-
name: 'METRICS_PORT',
53
+
name: "METRICS_PORT",
54
54
required: false,
55
-
description: 'Port for metrics server (defaults to 4101)',
55
+
description: "Port for metrics server (defaults to 4101)",
56
56
validator: (value) => !isNaN(Number(value)) && Number(value) > 0,
57
57
},
58
58
{
59
-
name: 'FIREHOSE_URL',
59
+
name: "FIREHOSE_URL",
60
60
required: false,
61
-
description: 'Jetstream firehose WebSocket URL',
62
-
validator: (value) => value.startsWith('ws'),
61
+
description: "Jetstream firehose WebSocket URL",
62
+
validator: (value) => value.startsWith("ws"),
63
63
},
64
64
{
65
-
name: 'CURSOR_UPDATE_INTERVAL',
65
+
name: "CURSOR_UPDATE_INTERVAL",
66
66
required: false,
67
-
description: 'Cursor update interval in milliseconds (defaults to 60000)',
67
+
description: "Cursor update interval in milliseconds (defaults to 60000)",
68
68
validator: (value) => !isNaN(Number(value)) && Number(value) > 0,
69
69
},
70
70
{
71
-
name: 'LABEL_LIMIT',
71
+
name: "LABEL_LIMIT",
72
72
required: false,
73
-
description: 'Rate limit for labeling operations',
73
+
description: "Rate limit for labeling operations",
74
74
validator: (value) => {
75
75
// Allow "number * number" format or plain numbers
76
76
const multiplyMatch = /^(\d+)\s*\*\s*(\d+)$/.exec(value);
···
82
82
},
83
83
},
84
84
{
85
-
name: 'LABEL_LIMIT_WAIT',
85
+
name: "LABEL_LIMIT_WAIT",
86
86
required: false,
87
-
description: 'Wait time between rate limited operations',
87
+
description: "Wait time between rate limited operations",
88
88
validator: (value) => {
89
89
// Allow "number * number" format or plain numbers
90
90
const multiplyMatch = /^(\d+)\s*\*\s*(\d+)$/.exec(value);
···
96
96
},
97
97
},
98
98
{
99
-
name: 'LOG_LEVEL',
99
+
name: "LOG_LEVEL",
100
100
required: false,
101
-
description: 'Logging level (trace, debug, info, warn, error, fatal)',
101
+
description: "Logging level (trace, debug, info, warn, error, fatal)",
102
102
validator: (value) =>
103
-
['trace', 'debug', 'info', 'warn', 'error', 'fatal'].includes(value),
103
+
["trace", "debug", "info", "warn", "error", "fatal"].includes(value),
104
104
},
105
105
{
106
-
name: 'NODE_ENV',
106
+
name: "NODE_ENV",
107
107
required: false,
108
-
description: 'Node environment (development, production, test)',
109
-
validator: (value) => ['development', 'production', 'test'].includes(value),
108
+
description: "Node environment (development, production, test)",
109
+
validator: (value) => ["development", "production", "test"].includes(value),
110
110
},
111
111
];
112
112
···
114
114
const errors: string[] = [];
115
115
const warnings: string[] = [];
116
116
117
-
logger.info('Validating environment variables...');
117
+
logger.info("Validating environment variables...");
118
118
119
119
for (const envVar of ENV_VARIABLES) {
120
120
const value = process.env[envVar.name];
121
121
122
122
if (envVar.required) {
123
-
if (!value || value.trim() === '') {
123
+
if (!value || value.trim() === "") {
124
124
errors.push(
125
-
`Required environment variable ${envVar.name} is missing. ${envVar.description}`
125
+
`Required environment variable ${envVar.name} is missing. ${envVar.description}`,
126
126
);
127
127
continue;
128
128
}
···
132
132
try {
133
133
if (!envVar.validator(value)) {
134
134
errors.push(
135
-
`Environment variable ${envVar.name} has invalid format. ${envVar.description}`
135
+
`Environment variable ${envVar.name} has invalid format. ${envVar.description}`,
136
136
);
137
137
}
138
138
} catch (error) {
139
139
errors.push(
140
-
`Environment variable ${envVar.name} validation failed: ${String(error)}. ${envVar.description}`
140
+
`Environment variable ${envVar.name} validation failed: ${String(error)}. ${envVar.description}`,
141
141
);
142
142
}
143
143
}
144
144
145
145
if (!envVar.required && !value) {
146
146
warnings.push(
147
-
`Optional environment variable ${envVar.name} not set, using default. ${envVar.description}`
147
+
`Optional environment variable ${envVar.name} not set, using default. ${envVar.description}`,
148
148
);
149
149
}
150
150
}
151
151
152
152
if (warnings.length > 0) {
153
-
logger.warn('Environment variable warnings:');
154
-
warnings.forEach((warning) => { logger.warn(` - ${warning}`); });
153
+
logger.warn("Environment variable warnings:");
154
+
warnings.forEach((warning) => {
155
+
logger.warn(` - ${warning}`);
156
+
});
155
157
}
156
158
157
159
if (errors.length > 0) {
158
-
logger.error('Environment variable validation failed:');
159
-
errors.forEach((error) => { logger.error(` - ${error}`); });
160
-
logger.error('Please check your environment configuration and try again.');
160
+
logger.error("Environment variable validation failed:");
161
+
errors.forEach((error) => {
162
+
logger.error(` - ${error}`);
163
+
});
164
+
logger.error("Please check your environment configuration and try again.");
161
165
process.exit(1);
162
166
}
163
167
164
-
logger.info('Environment variable validation completed successfully');
165
-
}
168
+
logger.info("Environment variable validation completed successfully");
169
+
}