Skip to content

Commit

Permalink
Revert "Fix a couple of long-standing bugs in job board validation lo…
Browse files Browse the repository at this point in the history
…gic (#429)"

This reverts commit 6a6fb79.
  • Loading branch information
vcarl committed Jan 9, 2025
1 parent f055545 commit 04d56fa
Show file tree
Hide file tree
Showing 9 changed files with 54 additions and 131 deletions.
64 changes: 33 additions & 31 deletions src/features/jobs-moderation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,73 +106,75 @@ const jobModeration = async (bot: Client) => {
await deleteAgedPosts();

bot.on("messageCreate", async (message) => {
// Bail if it's a bot or staff message
if (message.author.bot || isStaff(message.member)) {
const { channel } = message;
if (
message.author.bot ||
message.channelId !== CHANNELS.jobBoard ||
(channel.isThread() && channel.parentId !== CHANNELS.jobBoard) ||
// Don't treat newly fetched old messages as new posts
differenceInHours(new Date(), message.createdAt) >= 1
) {
return;
}
const { channel } = message;
// If this is an existing enforcement thread, process the through a "REPL"
// that lets people test messages against the rules
if (
channel.type === ChannelType.PrivateThread &&
channel.parentId === CHANNELS.jobBoard &&
channel.ownerId === bot.user?.id
channel.ownerId === bot.user?.id &&
channel.parentId === CHANNELS.jobBoard
) {
await validationRepl(message);
validationRepl(message);
return;
}
// Bail if this isn't #job-board
if (
channel.type !== ChannelType.GuildText ||
message.channelId !== CHANNELS.jobBoard
) {
// If this is a staff member, bail early
if (channel.type !== ChannelType.GuildText || isStaff(message.member)) {
return;
}

const posts = parseContent(message.content);
const errors = validate(posts, message);
console.log(
`[DEBUG] validating new job post from @${
message.author.username
}, errors: ${JSON.stringify(errors)}`,
}, errors: [${JSON.stringify(errors)}]`,
);
if (errors) {
await handleErrors(channel, message, errors);
}
});

bot.on("messageUpdate", async (_, message) => {
const { channel } = message;
bot.on("messageUpdate", async (_, newMessage) => {
const { channel } = newMessage;
if (newMessage.author?.bot) {
return;
}
if (channel.type === ChannelType.PrivateThread) {
validationRepl(await newMessage.fetch());
return;
}
if (
message.author?.bot ||
message.channelId !== CHANNELS.jobBoard ||
newMessage.channelId !== CHANNELS.jobBoard ||
channel.type !== ChannelType.GuildText ||
isStaff(message.member)
isStaff(newMessage.member)
) {
return;
}
if (message.partial) {
message = await message.fetch();
}
const message = await newMessage.fetch();
const posts = parseContent(message.content);
// Don't validate hiring posts
if (posts.every((p) => p.tags.includes(PostType.hiring))) {
return;
}
// You can't post too frequently when editing a message, so filter those out
const errors = validate(posts, message).filter(
(e) => e.type !== POST_FAILURE_REASONS.tooFrequent,
);

if (errors) {
const isRecentEdit =
differenceInMinutes(new Date(), message.createdAt) < REPOST_THRESHOLD;
errors.unshift({
type: POST_FAILURE_REASONS.circumventedRules,
recentEdit: isRecentEdit,
});
if (isRecentEdit) {
removeSpecificJob(message);
}
await handleErrors(channel, message, errors);
if (posts.some((p) => p.tags.includes(PostType.forHire))) {
reportUser({ reason: ReportReasons.jobCircumvent, message });
// await newMessage.delete();
} else {
await handleErrors(channel, message, errors);
}
}
});
Expand Down
14 changes: 9 additions & 5 deletions src/features/jobs-moderation/job-mod-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,16 @@ import {
PostFailures,
PostType,
PostFailureLinkRequired,
CircumventedRules,
} from "../../types/jobs-moderation";

export const failedCircumventedRules = (
e: PostFailures,
): e is CircumventedRules => e.type === POST_FAILURE_REASONS.circumventedRules;
export class RuleViolation extends Error {
reasons: POST_FAILURE_REASONS[];
constructor(reasons: POST_FAILURE_REASONS[]) {
super("Job Mod Rule violation");
this.reasons = reasons;
}
}

export const failedMissingType = (
e: PostFailures,
): e is PostFailureMissingType => e.type === POST_FAILURE_REASONS.missingType;
Expand Down Expand Up @@ -278,7 +282,7 @@ export const removeSpecificJob = (message: Message) => {
const index = jobBoardMessageCache.hiring.findIndex(
(m) => m.message.id === message.id,
);
if (index !== -1) {
if (index) {
jobBoardMessageCache.hiring.splice(index);
} else
jobBoardMessageCache.forHire.splice(
Expand Down
22 changes: 0 additions & 22 deletions src/features/jobs-moderation/parse-content.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,28 +108,6 @@ many long lines of text`,
expect(parsed[0]).toMatchObject({ tags: ["hiring"], description: "" });
});

it("correctly pulls description off tags line", () => {
let parsed = parseContent(`[hiring]Lorem ipsum dolor sit amet`);
expect(parsed[0]).toMatchObject({
tags: ["hiring"],
description: "Lorem ipsum dolor sit amet",
});

parsed = parseContent(`[hiring][remote][visa]Lorem ipsum dolor sit amet`);
expect(parsed[0]).toMatchObject({
tags: ["hiring", "remote", "visa"],
description: "Lorem ipsum dolor sit amet",
});

parsed = parseContent(
`[hiring] [remote] [visa] Lorem ipsum dolor sit amet`,
);
expect(parsed[0]).toMatchObject({
tags: ["hiring", "remote", "visa"],
description: "Lorem ipsum dolor sit amet",
});
});

// Disable this, not relevant right now. Also broken as of May '23
it.skip("parses contact", () => {
const makePost = (contact: string) => `|
Expand Down
25 changes: 3 additions & 22 deletions src/features/jobs-moderation/parse-content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,31 +25,12 @@ export const parseTags = (tags: string) => {
.filter((tag) => tag !== "");
};

const splitTagsFromDescription = (
inputString: string,
): { heading: string; body: string[] } => {
const [tagsLine, ...lines] = inputString.trim().split("\n");

if (tagsLine.includes("[")) {
const cleanedTags = tagsLine.replace(/\]\w+\[/, "][");
const match = cleanedTags.match(/(.*)\](.*)/);
const trailingText = match?.[2] || "";
lines.unshift(trailingText.trim());
return { heading: match?.[1] || "", body: lines };
}
return { heading: tagsLine, body: lines };
};

export const parseContent = (inputString: string): Post[] => {
const { heading, body } = splitTagsFromDescription(inputString);
// TODO: Replace above .split() with some more logic around detecting tags
// If |, treat the complete line as tags
// if [], check for trailing text with no wrapper and add it to the description

const [tagsLine, ...lines] = inputString.trim().split("\n");
return [
{
tags: parseTags(heading),
description: body.reduce((description, line) => {
tags: parseTags(tagsLine),
description: lines.reduce((description, line) => {
if (line === "") {
return description;
}
Expand Down
36 changes: 1 addition & 35 deletions src/features/jobs-moderation/validate.test.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,11 @@
import { describe, expect, it } from "vitest";
import { PostType, POST_FAILURE_REASONS } from "../../types/jobs-moderation";
import { links, formatting } from "./validate";
import { links } from "./validate";

const makePost = (type: PostType, description: string) => [
{ tags: [type], description },
];

describe("emoji", () => {
it("isn't too crazy about emoji", () => {
const noFailure = [
"for some role and stuff\nDM me to apply ✨",
"for some role and stuff\nDM me to apply ✨",
"👉 stuff: some more details afterwards and whatever shenanigans\n👉 stuff: some more details afterwards and whatever shenanigans\n👉 stuff: some more details afterwards and whatever shenanigans\n👉 stuff: some more details afterwards and whatever shenanigans\n👉 stuff: some more details afterwards and whatever shenanigans\n",
];
for (const content of noFailure) {
expect(
formatting(
makePost(PostType.forHire, content),
// @ts-expect-error testing override
{ content: `[forhire]\n${content}` },
),
).not.toContainEqual({ type: POST_FAILURE_REASONS.tooManyEmojis });
}
});
it("stops obnoxious emoji usage", () => {
const noFailure = [
"for ✨ some role and stuff\nDM ✨ me ✨ to ✨ apply ✨",
"for some role and stuff\nDM me to apply ✨✨✨✨✨✨",
"👉 stuff: some more ✨✨ details afterwards and whatever shenanigans\n👉 stuff: some more ✨✨ details afterwards and whatever shenanigans\n👉 stuff: some more ✨✨ details afterwards and whatever shenanigans\n👉 stuff: some more ✨✨ details afterwards and whatever shenanigans\n👉 stuff: some more ✨✨ details afterwards and whatever shenanigans\n",
];
for (const content of noFailure) {
expect(
formatting(
makePost(PostType.forHire, content),
// @ts-expect-error testing override
{ content: `[forhire]\n${content}` },
),
).toContainEqual({ type: POST_FAILURE_REASONS.tooManyEmojis });
}
});
});
describe("links", () => {
it("requires links", () => {
expect(
Expand Down
2 changes: 1 addition & 1 deletion src/features/jobs-moderation/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export const formatting: JobPostValidator = (posts, message) => {
posts.forEach((post) => {
// If > 1 in 150 chars is an emoji
const emojiCount = extractEmoji(post.description).length;
if (emojiCount / post.description.length > 1 / 30) {
if (emojiCount / post.description.length > 1 / 150) {
errors.push({ type: POST_FAILURE_REASONS.tooManyEmojis });
}
const lineCount = countLines(post.description.trim());
Expand Down
9 changes: 1 addition & 8 deletions src/features/jobs-moderation/validation-messages.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import {
CircumventedRules,
POST_FAILURE_REASONS,
PostFailures,
PostFailureTooFrequent,
PostFailureTooLong,
PostFailureTooManyLines,
} from "../../types/jobs-moderation";
import {
failedCircumventedRules,
failedMissingType,
failedReplyOrMention,
failedTooManyLines,
Expand All @@ -20,8 +18,6 @@ import {
} from "./job-mod-helpers";

const ValidationMessages = {
[POST_FAILURE_REASONS.circumventedRules]: (e: CircumventedRules) =>
`Your message was removed after you edited it so that it no longer complies with our formatting rules. ${e.recentEdit ? "Please re-post." : ""}`,
[POST_FAILURE_REASONS.missingType]:
"Your post does not include our required `[HIRING]` or `[FOR HIRE]` tag. Make sure the first line of your post includes `[HIRING]` if you’re looking to pay someone for their work, and `[FOR HIRE]` if you’re offering your services.",
[POST_FAILURE_REASONS.inconsistentType]:
Expand All @@ -37,13 +33,10 @@ const ValidationMessages = {
[POST_FAILURE_REASONS.tooFrequent]: (e: PostFailureTooFrequent) =>
`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 due to a history of posters incorrectly attempting to 'apply' by replying within a thread or reply.",
"Messages in this channel may not be replies or include @-mentions of users, to ensure the channel isn’t being used to discuss postings.",
};

export const getValidationMessage = (reason: PostFailures): string => {
if (failedCircumventedRules(reason)) {
return ValidationMessages[reason.type](reason);
}
if (failedMissingType(reason)) {
return ValidationMessages[reason.type];
}
Expand Down
7 changes: 6 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,12 @@ export const bot = new discord.Client({
IntentsBitField.Flags.DirectMessageReactions,
IntentsBitField.Flags.MessageContent,
],
partials: [Partials.Channel, Partials.Reaction, Partials.GuildMember],
partials: [
Partials.Channel,
Partials.Message,
Partials.Reaction,
Partials.GuildMember,
],
});

registerCommand(resetJobCacheCommand);
Expand Down
6 changes: 0 additions & 6 deletions src/types/jobs-moderation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,11 @@ export const enum POST_FAILURE_REASONS {
tooManyGaps = "tooManyGaps",
tooFrequent = "tooFrequent",
replyOrMention = "replyOrMention",
circumventedRules = "circumventedRules",
// invalidContact = 'invalidContact',
// unknownLocation = 'unknownLocation',
// invalidPostType = 'invalidPostType',
}

export interface CircumventedRules {
type: POST_FAILURE_REASONS.circumventedRules;
recentEdit: boolean;
}
export interface PostFailureMissingType {
type: POST_FAILURE_REASONS.missingType;
}
Expand Down Expand Up @@ -69,7 +64,6 @@ export interface PostFailureReplyOrMention {
type: POST_FAILURE_REASONS.replyOrMention;
}
export type PostFailures =
| CircumventedRules
| PostFailureMissingType
| PostFailureInconsistentType
| PostFailureTooFrequent
Expand Down

0 comments on commit 04d56fa

Please sign in to comment.