Skip to content

Commit

Permalink
Merge pull request #362 from lenscas/feature/improve_scam_message_det…
Browse files Browse the repository at this point in the history
…ection

improve scam protection
  • Loading branch information
lenscas authored Dec 26, 2022
2 parents 6f96514 + ad7cf58 commit c9c1593
Show file tree
Hide file tree
Showing 3 changed files with 554 additions and 846 deletions.
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@
"discord.js": "^13",
"fast-levenshtein": "^3.0.0",
"get-urls": "^11.0.0",
"node-html-parser": "^5.3.3",
"pg": "^8.3.3",
"typescript-pattern-matching": "^1.0.1",
"node-html-parser": "^5.3.3"
"typescript-pattern-matching": "^1.0.1"
},
"devDependencies": {
"@pgtyped/cli": "^1",
Expand All @@ -56,4 +56,4 @@
"ts-node": "^10",
"typescript": "^4"
}
}
}
183 changes: 110 additions & 73 deletions src/protection/scam.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,18 @@ import { checkIfAllUrlsAreWhitelisted } from './queries.queries';

const loadGetUrls = async (): Promise<typeof import('get-urls')> => new Function(`return import('get-urls')`)();

type offendReason = 'PingAndLinks' | 'SuspiciousLink';

export const peopleWhoSendPossibleScam: {
[server: string]: {
[userId: string]: {
messageToDelete: {
messagesToDelete: Array<{
messageId: string;
channelId: string;
};
}>;
offendDate: Date;
reason: offendReason;
times: number;
};
};
} = {};
Expand Down Expand Up @@ -48,6 +52,48 @@ const urls = [
],
},
];

const getUrlsInDistance = async (arr: string[], db: PoolWrapper) => {
const firstFilter = arr
.map((v) => new URL(v))
.filter((v) => urls.every((a) => a.link != v.host))
.map((v) => {
const partsToCheck = v.host.split('.').flatMap((v) => v.split('-'));
return {
url: v,
distances: urls
.map((compareAgainst) => {
return {
distance: levenshteinDistance(v.host, compareAgainst.link),
comparedTo: compareAgainst,
};
})
.filter((x) => {
return x.distance <= x.comparedTo.distance;
}),
distanceParts: urls
.flatMap((x) => (x.keywords ? x.keywords : []))
.flatMap((x) =>
partsToCheck.map((urlPart) => {
return {
distance: levenshteinDistance(urlPart, x.name),
needed: x.distance,
};
}),
)
.filter((x) => x.distance <= x.needed),
};
})
.filter((v) => v.distances.length > 0 || v.distanceParts.length > 0);
if (firstFilter.length == 0) {
return false;
}
const urls_to_check = [...new Set(firstFilter.map((x) => x.url.host))];
await checkIfAllUrlsAreWhitelisted
.run({ urls: urls_to_check }, db)
.then((res) => res[0].count == urls_to_check.length.toString());
};

const cleanWarningEveryMS = 300000;
export const loadCheckScam = async (): Promise<
(message: Message, client: Client, db: PoolWrapper) => Promise<boolean>
Expand All @@ -60,77 +106,57 @@ export const loadCheckScam = async (): Promise<
if (!(message.guild && message.member)) {
return false;
}

const url_strings = getUrls(message.content, { requireSchemeOrWww: true });
const as_arr = [...url_strings.values()];
const links_in_range = as_arr
.map((v) => new URL(v))
.filter((v) => urls.every((a) => a.link != v.host))
.map((v) => {
const partsToCheck = v.host.split('.').flatMap((v) => v.split('-'));
return {
url: v,
distances: urls
.map((compareAgainst) => {
return {
distance: levenshteinDistance(v.host, compareAgainst.link),
comparedTo: compareAgainst,
};
})
.filter((x) => {
return x.distance <= x.comparedTo.distance;
}),
distanceParts: urls
.flatMap((x) => (x.keywords ? x.keywords : []))
.flatMap((x) =>
partsToCheck.map((urlPart) => {
return {
distance: levenshteinDistance(urlPart, x.name),
needed: x.distance,
};
}),
)
.filter((x) => x.distance <= x.needed),
};
})
.filter((v) => v.distances.length > 0 || v.distanceParts.length > 0);
if (links_in_range.length == 0) {
return false;
let offended: 'None' | offendReason = 'None';
if (await getUrlsInDistance(as_arr, db)) {
offended = 'SuspiciousLink';
} else if (
as_arr.length >= 1 &&
(message.content.includes('@everyone') || message.content.includes('@here'))
) {
offended = 'PingAndLinks';
}
const urls_to_check = [...new Set(links_in_range.map((x) => x.url.host))];
const every_url_is_safe = await checkIfAllUrlsAreWhitelisted
.run({ urls: urls_to_check }, db)
.then((res) => res[0].count == urls_to_check.length.toString());

peopleWhoSendPossibleScam[message.guild.id] = peopleWhoSendPossibleScam[message.guild.id] || {};
if (every_url_is_safe) {
if (offended == 'None') {
return false;
}
peopleWhoSendPossibleScam[message.guild.id] = peopleWhoSendPossibleScam[message.guild.id] || {};
let warnedStruct = peopleWhoSendPossibleScam[message.guild.id][message.member.id];
if (!warnedStruct || warnedStruct.offendDate.getTime() < Date.now() - cleanWarningEveryMS) {

if (!warnedStruct) {
warnedStruct = {
messageToDelete: {
messageId: message.id,
channelId: message.channel.id,
},
messagesToDelete: [],
offendDate: new Date(),
reason: offended,
times: 0,
};
peopleWhoSendPossibleScam[message.guild.id][message.member.id] = warnedStruct;
const content =
'***WARNING!***\nPossible scam link detected.\n' +
message.member.toString() +
' please refrain from sending links to this site. Next offence will result in automatic moderator action.';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await (client as any).api.channels[message.channel.id].messages.post({
data: {
content,
message_reference: {
message_id: message.id,
channel_id: message.channel.id,
guild_id: message.guild.id,
},
},
});
}

let content =
'***WARNING!***\nPossible scam link detected.\n' +
message.member.toString() +
' please refrain from sending links to this site. Next offense will result in automatic moderator action.';
if (warnedStruct.reason == 'PingAndLinks') {
content =
'***WANING!***\nLinks inside messages that ping everyone/here are often scam messages.\nYes, this _especially_ includes discord server invites.\n\n' +
message.member.toString() +
', Please, do not send messages like this in the future or automatic moderating actions will be taken';
}
const promise = message.reply({
content,
});
warnedStruct.times++;
warnedStruct.messagesToDelete.push({ messageId: message.id, channelId: message.channelId });
peopleWhoSendPossibleScam[message.guild.id][message.member.id] = warnedStruct;
if (
(warnedStruct.reason == 'PingAndLinks' && warnedStruct.times < 3) ||
((warnedStruct.reason == 'SuspiciousLink' || offended == 'SuspiciousLink') &&
warnedStruct.times < 2 &&
warnedStruct.offendDate.getTime() < Date.now() - cleanWarningEveryMS)
) {
await promise;
return true;
}

Expand All @@ -151,31 +177,42 @@ export const loadCheckScam = async (): Promise<
)
)[0];
try {
message.member.send(
await message.member.send(
'You have sent too many messages with suspicious links and have been automatically muted to prevent further incidents.\nPlease contact a moderator.',
);
} catch (_) {}
if (!success) {
await message.channel.send('Possible scam detected, but could not properly report it.');
}

await promise;
await message.delete();
try {
await message.guild.channels
.resolve(warnedStruct.messageToDelete.channelId)
?.fetch()
.then((x) => {
if (x.isText()) {
return x.messages.delete(warnedStruct.messageToDelete.messageId);
const promises = warnedStruct.messagesToDelete.map(async (x) => {
try {
const channel = await message.guild?.channels.resolve(x.channelId)?.fetch();
if (channel?.isText() || channel?.isThread()) {
const message = await channel.messages.fetch(x.messageId);
await message.delete();
}
});
} catch (_) {}
} catch (e) {
console.log('failure to delete a scam message');
console.log('Channel Id: ', x.channelId + '\nmessage id: ' + x.messageId);
console.error(e);
}
});
await Promise.all(promises);
} catch (x) {
console.error(x);
}
return true;
} catch (e) {
console.error(e);
try {
await message.channel.send('Something has gone wrong while checking if a message was a scam.');
} catch (e) {}
} catch (e) {
console.log('failed writing a failure message');
console.error(e);
}
return true;
}
};
Expand Down
Loading

0 comments on commit c9c1593

Please sign in to comment.