Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 1 addition & 26 deletions src/features/jobs-moderation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ import {
updateJobs,
trackModeratedMessage,
failedTooFrequent,
failedWeb3Content,
failedWeb3Poster,
deleteAgedPosts,
} from "./jobs-moderation/job-mod-helpers";
import { getValidationMessage } from "./jobs-moderation/validation-messages";
Expand Down Expand Up @@ -79,9 +77,6 @@ const rulesThreadCache = new LRUCache<string, ThreadChannel>({
},
});

const freeflowHiring = "<https://discord.gg/gTWTwZPDYT>";
const freeflowForHire = "<https://vjlup8tch3g.typeform.com/to/T8w8qWzl>";

const jobModeration = async (bot: Client) => {
const jobBoard = await bot.channels.fetch(CHANNELS.jobBoard);
if (jobBoard?.type !== ChannelType.GuildText) return;
Expand Down Expand Up @@ -195,8 +190,6 @@ const jobModeration = async (bot: Client) => {
* There's a 10 minute grace period where people are allowed to re-post if they
* delete their own message.
* After 10 minutes, they must wait 6.75 days before reposting
* If it's been removed by this bot for being a web3 related post, they are
* warned twice and timed out after a third post.
*/
bot.on("messageDelete", async (message) => {
// TODO: look up audit log, early return if member was banned
Expand Down Expand Up @@ -301,29 +294,11 @@ More details & apply: https://example.com/apply
}

// Handle missing post type
let error: PostFailures | undefined = errors.find(failedTooFrequent);
const error: PostFailures | undefined = errors.find(failedTooFrequent);
if (error) {
reportUser({ reason: ReportReasons.jobFrequency, message });
}

// Handle posts that contain web3 content and posters who have been blocked
// for posting web3 roles
error = errors.find(failedWeb3Poster) || errors.find(failedWeb3Content);
if (error) {
reportUser({ reason: ReportReasons.jobCrypto, message });
if (error.count >= 3) {
await message.member?.timeout(20 * 60 * 60 * 1000);
}
const { hiring, forHire } = error;
await thread.send(
!hiring && !forHire
? `If you're hiring: ${freeflowHiring}
If you're seeking work: ${freeflowForHire}`
: hiring
? `Join FreeFlow's server to start hiring for web3: ${freeflowHiring}`
: `Apply to join FreeFlow's talent pool for web3: ${freeflowForHire}`,
);
}
await thread.send("Your post:");
await thread.send({
content: message.content,
Expand Down
21 changes: 1 addition & 20 deletions src/features/jobs-moderation/job-mod-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ import {
PostFailureTooManyEmojis,
PostFailureTooManyGaps,
PostFailureTooManyLines,
PostFailureWeb3Content,
PostFailureWeb3Poster,
PostFailures,
PostType,
PostFailureLinkRequired,
Expand Down Expand Up @@ -70,11 +68,6 @@ export const failedTooManyEmojis = (
e: PostFailures,
): e is PostFailureTooManyEmojis =>
e.type === POST_FAILURE_REASONS.tooManyEmojis;
export const failedWeb3Content = (
e: PostFailures,
): e is PostFailureWeb3Content => e.type === POST_FAILURE_REASONS.web3Content;
export const failedWeb3Poster = (e: PostFailures): e is PostFailureWeb3Poster =>
e.type === POST_FAILURE_REASONS.web3Poster;

interface StoredMessage {
message: Message;
Expand Down Expand Up @@ -286,8 +279,7 @@ export const removeSpecificJob = (message: Message) => {
};

export const purgeMember = (idToRemove: string) => {
let removed = removeFromCryptoCache(idToRemove);

let removed = 0;
let index = jobBoardMessageCache.hiring.findIndex(
(x) => x.authorId === idToRemove,
);
Expand Down Expand Up @@ -322,14 +314,3 @@ export const untrackModeratedMessage = (message: Message | PartialMessage) => {
}
return false;
};

const cryptoPosters: Map<string, { count: number; last: Date }> = new Map();
export const removeFromCryptoCache = (idToClear: string) => {
if (cryptoPosters.has(idToClear)) {
cryptoPosters.delete(idToClear);
return 1;
}
return 0;
};
export const getCryptoCache = cryptoPosters.get.bind(cryptoPosters);
export const setCryptoCache = cryptoPosters.set.bind(cryptoPosters);
51 changes: 1 addition & 50 deletions src/features/jobs-moderation/validate.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { Message, MessageType } from "discord.js";
import { differenceInHours } from "date-fns";

import { getLastPostAge } from "./job-mod-helpers";

import { countLines, simplifyString } from "../../helpers/string";
import { countLines } from "../../helpers/string";
import { extractEmoji } from "../../helpers/string";
import { getCryptoCache, setCryptoCache } from "./job-mod-helpers";
import { parseContent } from "./parse-content";
import {
JobPostValidator,
Expand All @@ -17,7 +15,6 @@ import {
const validate = (posts: ReturnType<typeof parseContent>, message: Message) => {
const errors: PostFailures[] = [];
errors.push(...participation(posts, message));
errors.push(...web3(posts, message));
errors.push(...formatting(posts, message));
errors.push(...links(posts, message));
return errors;
Expand Down Expand Up @@ -82,52 +79,6 @@ export const formatting: JobPostValidator = (posts, message) => {
return errors;
};

const CRYPTO_COOLDOWN = 6; // hours
const bannedWords = /(blockchain|nft|cryptocurrency|token|web3|web 3)/;

export const web3: JobPostValidator = (posts, message) => {
const now = new Date();
const lastCryptoPost = getCryptoCache(message.author.id);
// Fail posts that are sent by someone who was already blocked for posting
// web3 jobs
if (
lastCryptoPost &&
// extend duration for each repeated post
differenceInHours(now, lastCryptoPost.last) <
CRYPTO_COOLDOWN * lastCryptoPost.count
) {
const newCount = lastCryptoPost.count + 1;
setCryptoCache(message.author.id, {
...lastCryptoPost,
count: newCount,
});
return [
{
type: POST_FAILURE_REASONS.web3Poster,
count: newCount,
hiring: posts.some((p) => p.tags.includes(PostType.hiring)),
forHire: posts.some((p) => p.tags.includes(PostType.forHire)),
},
];
}

// Block posts that trigger our web3 detection
if (
posts.some((post) => bannedWords.test(simplifyString(post.description)))
) {
setCryptoCache(message.author.id, { count: 1, last: new Date() });
return [
{
type: POST_FAILURE_REASONS.web3Content,
count: 1,
hiring: posts.some((p) => p.tags.includes(PostType.hiring)),
forHire: posts.some((p) => p.tags.includes(PostType.forHire)),
},
];
}
return [];
};

export const participation: JobPostValidator = (posts, message) => {
const { members: mentions } = message.mentions;
if (
Expand Down
12 changes: 0 additions & 12 deletions src/features/jobs-moderation/validation-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ import {
failedTooManyLines,
failedTooManyEmojis,
failedTooFrequent,
failedWeb3Content,
failedWeb3Poster,
failedInconsistentType,
failedTooLong,
failedTooManyGaps,
Expand All @@ -36,10 +34,6 @@ const ValidationMessages = {
`You’re posting too frequently. You last posted ${e.lastSent} days ago, please wait at least 7 days.`,
[POST_FAILURE_REASONS.replyOrMention]:
"Messages in this channel may not be replies or include @-mentions of users, to ensure the channel isn’t being used to discuss postings.",
[POST_FAILURE_REASONS.web3Content]:
"We do not allow web3 positions to be advertised here. If you continue posting, you’ll be timed out overnight.",
[POST_FAILURE_REASONS.web3Poster]:
"We do not allow posers who arrived to post web3 positions to create posts. If you continue posting, you’ll be timed out overnight.",
};

export const getValidationMessage = (reason: PostFailures): string => {
Expand Down Expand Up @@ -70,11 +64,5 @@ export const getValidationMessage = (reason: PostFailures): string => {
if (failedTooManyEmojis(reason)) {
return ValidationMessages[reason.type];
}
if (failedWeb3Content(reason)) {
return ValidationMessages[reason.type];
}
if (failedWeb3Poster(reason)) {
return ValidationMessages[reason.type];
}
return "";
};
3 changes: 0 additions & 3 deletions src/helpers/modLog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ export const enum ReportReasons {
jobAge = "jobAge",
jobFrequency = "jobFrequency",
jobRemoved = "jobRemoved",
jobCrypto = "jobCrypto",
lowEffortQuestionRemoved = "lowEffortQuestionRemoved",
}

Expand Down Expand Up @@ -167,8 +166,6 @@ ${reportedMessage}`;
return `${preface}, for hire post expired.`;
case ReportReasons.jobFrequency:
return `${preface}, posting too frequently.`;
case ReportReasons.jobCrypto:
return `<@${message.author.id}> posted a crypto job.`;

case ReportReasons.lowEffortQuestionRemoved:
return `
Expand Down
18 changes: 1 addition & 17 deletions src/types/jobs-moderation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@ export const enum POST_FAILURE_REASONS {
tooManyGaps = "tooManyGaps",
tooFrequent = "tooFrequent",
replyOrMention = "replyOrMention",
web3Content = "web3Content",
web3Poster = "web3Poster",
// invalidContact = 'invalidContact',
// unknownLocation = 'unknownLocation',
// invalidPostType = 'invalidPostType',
Expand Down Expand Up @@ -65,18 +63,6 @@ export interface PostFailureTooFrequent {
export interface PostFailureReplyOrMention {
type: POST_FAILURE_REASONS.replyOrMention;
}
export interface PostFailureWeb3Content {
type: POST_FAILURE_REASONS.web3Content;
count: number;
hiring: boolean;
forHire: boolean;
}
export interface PostFailureWeb3Poster {
type: POST_FAILURE_REASONS.web3Poster;
count: number;
hiring: boolean;
forHire: boolean;
}
export type PostFailures =
| PostFailureMissingType
| PostFailureInconsistentType
Expand All @@ -86,6 +72,4 @@ export type PostFailures =
| PostFailureTooLong
| PostFailureTooManyLines
| PostFailureTooManyGaps
| PostFailureTooManyEmojis
| PostFailureWeb3Content
| PostFailureWeb3Poster;
| PostFailureTooManyEmojis;