Skip to content

Commit 75aeb85

Browse files
authored
Merge pull request #141 from destinygg/mute-link-spam-destiny
Mute users spamming the same link at Destiny.
2 parents 86a3c52 + 2a09dbe commit 75aeb85

File tree

5 files changed

+70
-10
lines changed

5 files changed

+70
-10
lines changed

lib/configuration/sample.config.json

+5-2
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@
2222
"messagesToKeepPerUser": 10,
2323
"maxMessagesInList": 5000,
2424
"timeToLiveSeconds": 600,
25-
"tomeStoneIntervalMilliseconds": 60000
25+
"tomeStoneIntervalMilliseconds": 60000,
26+
"messageUrlSpamSeconds": 600,
27+
"messageUrlSpamUser": "Destiny"
2628
},
2729
"punishmentCache": {
2830
"baseMuteSeconds": 60,
@@ -74,7 +76,8 @@
7476
"messagesToSearch": 75,
7577
"nukeDepth": 900,
7678
"minimumStringSearchLength": 100,
77-
"uniqueWordsThreshold": 0.45
79+
"uniqueWordsThreshold": 0.45,
80+
"messageUrlSpamCount": 3
7881
},
7982
"roleCache": {
8083
"timeToLiveSeconds": 21600,

lib/message-routing/message-router.js

+5
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,10 @@ class MessageRouter {
106106
),
107107
this.spamDetection.uniqueWordsCheck(messageContent),
108108
this.spamDetection.longRepeatedPhrase(messageContent),
109+
this.spamDetection.checkListOfRecentUrlsForSpam(
110+
messageContent,
111+
this.chatCache.getViewerUrlList(user),
112+
),
109113
];
110114

111115
if (_.some(spamDetectionList)) {
@@ -117,6 +121,7 @@ class MessageRouter {
117121
2: 'Too similar to other chatters past text.',
118122
3: 'Spamming similar phrases too much.',
119123
4: 'Spamming similar phrases too much.',
124+
5: 'Spamming the same link too much.',
120125
};
121126

122127
this.logger.info({ AUTO_MUTE_REASON: reasons[reasonIndex] }, newMessage);

lib/services/dgg-rolling-chat-cache.js

+42-4
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,19 @@ const similarity = require('../chat-utils/string-similarity');
55
// and then a simple formula to get the change in %
66

77
class ChatCache {
8-
constructor(config) {
8+
constructor(config, messageMatching) {
9+
this.messageMatching = messageMatching;
910
this.messsagesToKeepPerUser = config.messsagesToKeepPerUser || 2;
1011
this.maxMessagesInList = config.maxMessagesInList || 2000;
1112
this.timeToLive = config.timeToLiveSeconds || 1800;
1213
this.tombStoneInterval = config.tomeStoneIntervalMilliseconds || 120000;
1314
this.rateLimitMaxMessages = config.rateLimitMaxMessages || 4;
1415
this.rateLimitSecondsToRefresh = config.rateLimitSecondsToRefresh || 3;
1516
this.viewerMessageMinimumLength = config.viewerMessageMinimumLength || 20;
17+
this.messageUrlSpamSeconds = config.messageUrlSpamSeconds || 600;
18+
this.messageUrlSpamUser = config.messageUrlSpamUser || null;
1619
this.viewerMap = {};
20+
this.viewerUrlMap = {};
1721
this.rateLimitMap = {};
1822
this.runningMessageList = [];
1923
this.tombStoneMap = {};
@@ -27,10 +31,18 @@ class ChatCache {
2731
}
2832

2933
tombStoneCleanup() {
30-
if (_.isEmpty(this.tombStoneMap)) {
31-
return;
32-
}
3334
const now = moment().unix();
35+
36+
_.forIn(this.viewerUrlMap, (urls, user) => {
37+
const filteredOldUrls = urls.filter((link) => now - link.exp < this.messageUrlSpamSeconds);
38+
if (filteredOldUrls.length === 0) {
39+
delete this.viewerUrlMap[user];
40+
} else {
41+
this.viewerUrlMap[user] = filteredOldUrls;
42+
}
43+
});
44+
45+
if (_.isEmpty(this.tombStoneMap)) return;
3446
_.forIn(this.tombStoneMap, (value, key) => {
3547
if (now - value >= this.timeToLive) {
3648
delete this.tombStoneMap[key];
@@ -44,6 +56,7 @@ class ChatCache {
4456
if (message.length >= this.viewerMessageMinimumLength) {
4557
this.addMessageToViewerMap(user, message);
4658
}
59+
this.addMessageToViewerUrlMap(user, message);
4760
this.addMessageToRunningList(user, message);
4861
this.addMessageToRateLimitMap(user);
4962
}
@@ -62,6 +75,31 @@ class ChatCache {
6275
this.tombStoneMap[user] = moment().unix();
6376
}
6477

78+
addMessageToViewerUrlMap(user, message) {
79+
if (
80+
this.messageUrlSpamUser &&
81+
!this.messageMatching.mentionsUser(message, this.messageUrlSpamUser)
82+
) {
83+
return;
84+
}
85+
this.messageMatching.getLinks(message).forEach((link) => {
86+
if (!_.has(this.viewerUrlMap, user)) this.viewerUrlMap[user] = [];
87+
this.viewerUrlMap[user].push({
88+
url: link.hostname + link.pathname,
89+
exp: moment().unix(),
90+
});
91+
});
92+
}
93+
94+
getViewerUrlList(user) {
95+
if (!user || !_.has(this.viewerUrlMap, user)) return [];
96+
const now = moment().unix();
97+
const filteredOldUrls = this.viewerUrlMap[user]
98+
.filter((link) => now - link.exp < this.messageUrlSpamSeconds)
99+
.map((link) => link.url);
100+
return filteredOldUrls;
101+
}
102+
65103
isPastRateLimit(user) {
66104
const lastMessages = this.rateLimitMap[user];
67105
if (lastMessages.length === this.rateLimitMaxMessages) {

lib/services/service-index.js

+6-3
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,15 @@ class Services {
2929
this.logger = logger(serviceConfigurations.logger);
3030
this.sql = new Sql(serviceConfigurations.sql);
3131
this.commandRegistry = new CommandRegistry();
32-
this.chatCache = new ChatCache(serviceConfigurations.chatCache);
32+
this.messageMatching = messageMatchingService;
33+
this.chatCache = new ChatCache(serviceConfigurations.chatCache, this.messageMatching);
3334
this.punishmentCache = new PunishmentCache(serviceConfigurations.punishmentCache);
3435
this.roleCache = new RoleCache(serviceConfigurations.roleCache);
3536
this.punishmentStream = new PunishmentStream(this);
36-
this.spamDetection = new SpamDetection(serviceConfigurations.spamDetection);
37+
this.spamDetection = new SpamDetection(
38+
serviceConfigurations.spamDetection,
39+
this.messageMatching,
40+
);
3741
this.scheduledCommands = new ScheduledCommands(serviceConfigurations.schedule);
3842
this.gulag = gulagService;
3943
this.lastfm = new LastFm(serviceConfigurations.lastFm);
@@ -43,7 +47,6 @@ class Services {
4347
this.dggApi = new DggApi(serviceConfigurations.dggApi, this.logger);
4448
this.twitterApi = new TwitterApi(serviceConfigurations.twitter, this.logger);
4549
this.messageRelay = new MessageRelay();
46-
this.messageMatching = messageMatchingService;
4750
this.htmlMetadata = new HTMLMetadata();
4851
// Since reddit relies on managing a single instance of a script, it only runs on the DGG bot.
4952
if (chatConnectedTo === 'dgg') {

lib/services/spam-detection.js

+12-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ const matchStringOrRegex = (message, phrase) => {
1414
return _.includes(cleanMessage, cleanPhrase.toLowerCase());
1515
};
1616
class SpamDetection {
17-
constructor(config) {
17+
constructor(config, messageMatching) {
18+
this.messageMatching = messageMatching;
1819
this.asciiArtThreshold = config.asciiArtThreshold || 20;
1920
this.asciiPunctuationCount = config.asciiPunctuationCount || 40;
2021
this.matchPercentPerUserThreshold = config.matchPercentPerUserThreshold || 0.9;
@@ -26,6 +27,7 @@ class SpamDetection {
2627
this.uniqueWordsThreshold = config.uniqueWordsThreshold || 0.45;
2728
this.longWordThreshold = 90;
2829
this.longWordAllowedSpaces = 4;
30+
this.messageUrlSpamCount = config.messageUrlSpamCount || 3;
2931
}
3032

3133
// Checks whether there's a large number of non ascii characters
@@ -113,6 +115,15 @@ class SpamDetection {
113115
return this.isSimilarityAboveThreshold(matchPercents);
114116
}
115117

118+
checkListOfRecentUrlsForSpam(message, urlList) {
119+
return this.messageMatching.getLinks(message).some((link) => {
120+
return (
121+
urlList.filter((url) => url === link.hostname + link.pathname).length >=
122+
this.messageUrlSpamCount - 1
123+
);
124+
});
125+
}
126+
116127
static isMessageNuked(nukedPhrases, messageContent) {
117128
let nukeDuration = 0;
118129
let nukePhrase = '';

0 commit comments

Comments
 (0)