Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix a couple of long-standing bugs in job board validation logic #428

Merged
merged 5 commits into from
Jan 8, 2025
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
47 changes: 26 additions & 21 deletions src/features/jobs-moderation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,9 @@ const jobModeration = async (bot: Client) => {
const { channel } = message;
if (
message.author.bot ||
message.channelId !== CHANNELS.jobBoard ||
(channel.isThread() && channel.parentId !== CHANNELS.jobBoard) ||
(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
) {
Expand All @@ -123,7 +124,7 @@ const jobModeration = async (bot: Client) => {
channel.ownerId === bot.user?.id &&
channel.parentId === CHANNELS.jobBoard
) {
validationRepl(message);
await validationRepl(message);
return;
}
// If this is a staff member, bail early
Expand All @@ -135,46 +136,50 @@ const jobModeration = async (bot: Client) => {
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 (_, newMessage) => {
const { channel } = newMessage;
if (newMessage.author?.bot) {
return;
}
if (channel.type === ChannelType.PrivateThread) {
validationRepl(await newMessage.fetch());
bot.on("messageUpdate", async (_, message) => {
const { channel } = message;
if (message.author?.bot) {
return;
}
if (
newMessage.channelId !== CHANNELS.jobBoard ||
message.channelId !== CHANNELS.jobBoard ||
channel.type !== ChannelType.GuildText ||
isStaff(newMessage.member)
isStaff(message.member)
) {
return;
}
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;
if (message.partial) {
message = await message.fetch();
}
const posts = parseContent(message.content);
// 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);
console.log(
`[INFO] Deleting a recently edited post from ${message.author.username}`,
);
}
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: 5 additions & 9 deletions src/features/jobs-moderation/job-mod-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,12 @@ import {
PostFailures,
PostType,
PostFailureLinkRequired,
CircumventedRules,
} from "../../types/jobs-moderation";

export class RuleViolation extends Error {
reasons: POST_FAILURE_REASONS[];
constructor(reasons: POST_FAILURE_REASONS[]) {
super("Job Mod Rule violation");
this.reasons = reasons;
}
}

export const failedCircumventedRules = (
e: PostFailures,
): e is CircumventedRules => e.type === POST_FAILURE_REASONS.circumventedRules;
export const failedMissingType = (
e: PostFailures,
): e is PostFailureMissingType => e.type === POST_FAILURE_REASONS.missingType;
Expand Down Expand Up @@ -268,7 +264,7 @@ export const removeSpecificJob = (message: Message) => {
const index = jobBoardMessageCache.hiring.findIndex(
(m) => m.message.id === message.id,
);
if (index) {
if (index !== -1) {
jobBoardMessageCache.hiring.splice(index);
} else
jobBoardMessageCache.forHire.splice(
Expand Down
22 changes: 22 additions & 0 deletions src/features/jobs-moderation/parse-content.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,28 @@ 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: 22 additions & 3 deletions src/features/jobs-moderation/parse-content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,31 @@ export const parseTags = (tags: string) => {
.filter((tag) => tag !== "");
};

export const parseContent = (inputString: string): Post[] => {
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

return [
{
tags: parseTags(tagsLine),
description: lines.reduce((description, line) => {
tags: parseTags(heading),
description: body.reduce((description, line) => {
if (line === "") {
return description;
}
Expand Down
9 changes: 8 additions & 1 deletion src/features/jobs-moderation/validation-messages.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import {
CircumventedRules,
POST_FAILURE_REASONS,
PostFailures,
PostFailureTooFrequent,
PostFailureTooLong,
PostFailureTooManyLines,
} from "../../types/jobs-moderation";
import {
failedCircumventedRules,
failedMissingType,
failedReplyOrMention,
failedTooManyLines,
Expand All @@ -18,6 +20,8 @@ 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 @@ -33,10 +37,13 @@ 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, to ensure the channel isn’t being used to discuss postings.",
"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.",
};

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: 1 addition & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,7 @@ export const bot = new discord.Client({
IntentsBitField.Flags.DirectMessageReactions,
IntentsBitField.Flags.MessageContent,
],
partials: [
Partials.Channel,
Partials.Message,
Partials.Reaction,
Partials.GuildMember,
],
partials: [Partials.Channel, Partials.Reaction, Partials.GuildMember],
});

registerCommand(resetJobCacheCommand);
Expand Down
6 changes: 6 additions & 0 deletions src/types/jobs-moderation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,16 @@ 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 @@ -64,6 +69,7 @@ export interface PostFailureReplyOrMention {
type: POST_FAILURE_REASONS.replyOrMention;
}
export type PostFailures =
| CircumventedRules
| PostFailureMissingType
| PostFailureInconsistentType
| PostFailureTooFrequent
Expand Down
Loading