Skip to content

Commit 04d56fa

Browse files
committed
Revert "Fix a couple of long-standing bugs in job board validation logic (#429)"
This reverts commit 6a6fb79.
1 parent f055545 commit 04d56fa

File tree

9 files changed

+54
-131
lines changed

9 files changed

+54
-131
lines changed

src/features/jobs-moderation.ts

Lines changed: 33 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -106,73 +106,75 @@ const jobModeration = async (bot: Client) => {
106106
await deleteAgedPosts();
107107

108108
bot.on("messageCreate", async (message) => {
109-
// Bail if it's a bot or staff message
110-
if (message.author.bot || isStaff(message.member)) {
109+
const { channel } = message;
110+
if (
111+
message.author.bot ||
112+
message.channelId !== CHANNELS.jobBoard ||
113+
(channel.isThread() && channel.parentId !== CHANNELS.jobBoard) ||
114+
// Don't treat newly fetched old messages as new posts
115+
differenceInHours(new Date(), message.createdAt) >= 1
116+
) {
111117
return;
112118
}
113-
const { channel } = message;
114119
// If this is an existing enforcement thread, process the through a "REPL"
115120
// that lets people test messages against the rules
116121
if (
117122
channel.type === ChannelType.PrivateThread &&
118-
channel.parentId === CHANNELS.jobBoard &&
119-
channel.ownerId === bot.user?.id
123+
channel.ownerId === bot.user?.id &&
124+
channel.parentId === CHANNELS.jobBoard
120125
) {
121-
await validationRepl(message);
126+
validationRepl(message);
122127
return;
123128
}
124-
// Bail if this isn't #job-board
125-
if (
126-
channel.type !== ChannelType.GuildText ||
127-
message.channelId !== CHANNELS.jobBoard
128-
) {
129+
// If this is a staff member, bail early
130+
if (channel.type !== ChannelType.GuildText || isStaff(message.member)) {
129131
return;
130132
}
131-
132133
const posts = parseContent(message.content);
133134
const errors = validate(posts, message);
134135
console.log(
135136
`[DEBUG] validating new job post from @${
136137
message.author.username
137-
}, errors: ${JSON.stringify(errors)}`,
138+
}, errors: [${JSON.stringify(errors)}]`,
138139
);
139140
if (errors) {
140141
await handleErrors(channel, message, errors);
141142
}
142143
});
143144

144-
bot.on("messageUpdate", async (_, message) => {
145-
const { channel } = message;
145+
bot.on("messageUpdate", async (_, newMessage) => {
146+
const { channel } = newMessage;
147+
if (newMessage.author?.bot) {
148+
return;
149+
}
150+
if (channel.type === ChannelType.PrivateThread) {
151+
validationRepl(await newMessage.fetch());
152+
return;
153+
}
146154
if (
147-
message.author?.bot ||
148-
message.channelId !== CHANNELS.jobBoard ||
155+
newMessage.channelId !== CHANNELS.jobBoard ||
149156
channel.type !== ChannelType.GuildText ||
150-
isStaff(message.member)
157+
isStaff(newMessage.member)
151158
) {
152159
return;
153160
}
154-
if (message.partial) {
155-
message = await message.fetch();
156-
}
161+
const message = await newMessage.fetch();
157162
const posts = parseContent(message.content);
163+
// Don't validate hiring posts
164+
if (posts.every((p) => p.tags.includes(PostType.hiring))) {
165+
return;
166+
}
158167
// You can't post too frequently when editing a message, so filter those out
159168
const errors = validate(posts, message).filter(
160169
(e) => e.type !== POST_FAILURE_REASONS.tooFrequent,
161170
);
162171

163172
if (errors) {
164-
const isRecentEdit =
165-
differenceInMinutes(new Date(), message.createdAt) < REPOST_THRESHOLD;
166-
errors.unshift({
167-
type: POST_FAILURE_REASONS.circumventedRules,
168-
recentEdit: isRecentEdit,
169-
});
170-
if (isRecentEdit) {
171-
removeSpecificJob(message);
172-
}
173-
await handleErrors(channel, message, errors);
174173
if (posts.some((p) => p.tags.includes(PostType.forHire))) {
175174
reportUser({ reason: ReportReasons.jobCircumvent, message });
175+
// await newMessage.delete();
176+
} else {
177+
await handleErrors(channel, message, errors);
176178
}
177179
}
178180
});

src/features/jobs-moderation/job-mod-helpers.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,16 @@ import {
2929
PostFailures,
3030
PostType,
3131
PostFailureLinkRequired,
32-
CircumventedRules,
3332
} from "../../types/jobs-moderation";
3433

35-
export const failedCircumventedRules = (
36-
e: PostFailures,
37-
): e is CircumventedRules => e.type === POST_FAILURE_REASONS.circumventedRules;
34+
export class RuleViolation extends Error {
35+
reasons: POST_FAILURE_REASONS[];
36+
constructor(reasons: POST_FAILURE_REASONS[]) {
37+
super("Job Mod Rule violation");
38+
this.reasons = reasons;
39+
}
40+
}
41+
3842
export const failedMissingType = (
3943
e: PostFailures,
4044
): e is PostFailureMissingType => e.type === POST_FAILURE_REASONS.missingType;
@@ -278,7 +282,7 @@ export const removeSpecificJob = (message: Message) => {
278282
const index = jobBoardMessageCache.hiring.findIndex(
279283
(m) => m.message.id === message.id,
280284
);
281-
if (index !== -1) {
285+
if (index) {
282286
jobBoardMessageCache.hiring.splice(index);
283287
} else
284288
jobBoardMessageCache.forHire.splice(

src/features/jobs-moderation/parse-content.test.ts

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -108,28 +108,6 @@ many long lines of text`,
108108
expect(parsed[0]).toMatchObject({ tags: ["hiring"], description: "" });
109109
});
110110

111-
it("correctly pulls description off tags line", () => {
112-
let parsed = parseContent(`[hiring]Lorem ipsum dolor sit amet`);
113-
expect(parsed[0]).toMatchObject({
114-
tags: ["hiring"],
115-
description: "Lorem ipsum dolor sit amet",
116-
});
117-
118-
parsed = parseContent(`[hiring][remote][visa]Lorem ipsum dolor sit amet`);
119-
expect(parsed[0]).toMatchObject({
120-
tags: ["hiring", "remote", "visa"],
121-
description: "Lorem ipsum dolor sit amet",
122-
});
123-
124-
parsed = parseContent(
125-
`[hiring] [remote] [visa] Lorem ipsum dolor sit amet`,
126-
);
127-
expect(parsed[0]).toMatchObject({
128-
tags: ["hiring", "remote", "visa"],
129-
description: "Lorem ipsum dolor sit amet",
130-
});
131-
});
132-
133111
// Disable this, not relevant right now. Also broken as of May '23
134112
it.skip("parses contact", () => {
135113
const makePost = (contact: string) => `|

src/features/jobs-moderation/parse-content.ts

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -25,31 +25,12 @@ export const parseTags = (tags: string) => {
2525
.filter((tag) => tag !== "");
2626
};
2727

28-
const splitTagsFromDescription = (
29-
inputString: string,
30-
): { heading: string; body: string[] } => {
31-
const [tagsLine, ...lines] = inputString.trim().split("\n");
32-
33-
if (tagsLine.includes("[")) {
34-
const cleanedTags = tagsLine.replace(/\]\w+\[/, "][");
35-
const match = cleanedTags.match(/(.*)\](.*)/);
36-
const trailingText = match?.[2] || "";
37-
lines.unshift(trailingText.trim());
38-
return { heading: match?.[1] || "", body: lines };
39-
}
40-
return { heading: tagsLine, body: lines };
41-
};
42-
4328
export const parseContent = (inputString: string): Post[] => {
44-
const { heading, body } = splitTagsFromDescription(inputString);
45-
// TODO: Replace above .split() with some more logic around detecting tags
46-
// If |, treat the complete line as tags
47-
// if [], check for trailing text with no wrapper and add it to the description
48-
29+
const [tagsLine, ...lines] = inputString.trim().split("\n");
4930
return [
5031
{
51-
tags: parseTags(heading),
52-
description: body.reduce((description, line) => {
32+
tags: parseTags(tagsLine),
33+
description: lines.reduce((description, line) => {
5334
if (line === "") {
5435
return description;
5536
}

src/features/jobs-moderation/validate.test.ts

Lines changed: 1 addition & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,11 @@
11
import { describe, expect, it } from "vitest";
22
import { PostType, POST_FAILURE_REASONS } from "../../types/jobs-moderation";
3-
import { links, formatting } from "./validate";
3+
import { links } from "./validate";
44

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

9-
describe("emoji", () => {
10-
it("isn't too crazy about emoji", () => {
11-
const noFailure = [
12-
"for some role and stuff\nDM me to apply ✨",
13-
"for some role and stuff\nDM me to apply ✨",
14-
"👉 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",
15-
];
16-
for (const content of noFailure) {
17-
expect(
18-
formatting(
19-
makePost(PostType.forHire, content),
20-
// @ts-expect-error testing override
21-
{ content: `[forhire]\n${content}` },
22-
),
23-
).not.toContainEqual({ type: POST_FAILURE_REASONS.tooManyEmojis });
24-
}
25-
});
26-
it("stops obnoxious emoji usage", () => {
27-
const noFailure = [
28-
"for ✨ some role and stuff\nDM ✨ me ✨ to ✨ apply ✨",
29-
"for some role and stuff\nDM me to apply ✨✨✨✨✨✨",
30-
"👉 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",
31-
];
32-
for (const content of noFailure) {
33-
expect(
34-
formatting(
35-
makePost(PostType.forHire, content),
36-
// @ts-expect-error testing override
37-
{ content: `[forhire]\n${content}` },
38-
),
39-
).toContainEqual({ type: POST_FAILURE_REASONS.tooManyEmojis });
40-
}
41-
});
42-
});
439
describe("links", () => {
4410
it("requires links", () => {
4511
expect(

src/features/jobs-moderation/validate.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export const formatting: JobPostValidator = (posts, message) => {
5353
posts.forEach((post) => {
5454
// If > 1 in 150 chars is an emoji
5555
const emojiCount = extractEmoji(post.description).length;
56-
if (emojiCount / post.description.length > 1 / 30) {
56+
if (emojiCount / post.description.length > 1 / 150) {
5757
errors.push({ type: POST_FAILURE_REASONS.tooManyEmojis });
5858
}
5959
const lineCount = countLines(post.description.trim());

src/features/jobs-moderation/validation-messages.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
import {
2-
CircumventedRules,
32
POST_FAILURE_REASONS,
43
PostFailures,
54
PostFailureTooFrequent,
65
PostFailureTooLong,
76
PostFailureTooManyLines,
87
} from "../../types/jobs-moderation";
98
import {
10-
failedCircumventedRules,
119
failedMissingType,
1210
failedReplyOrMention,
1311
failedTooManyLines,
@@ -20,8 +18,6 @@ import {
2018
} from "./job-mod-helpers";
2119

2220
const ValidationMessages = {
23-
[POST_FAILURE_REASONS.circumventedRules]: (e: CircumventedRules) =>
24-
`Your message was removed after you edited it so that it no longer complies with our formatting rules. ${e.recentEdit ? "Please re-post." : ""}`,
2521
[POST_FAILURE_REASONS.missingType]:
2622
"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.",
2723
[POST_FAILURE_REASONS.inconsistentType]:
@@ -37,13 +33,10 @@ const ValidationMessages = {
3733
[POST_FAILURE_REASONS.tooFrequent]: (e: PostFailureTooFrequent) =>
3834
`You’re posting too frequently. You last posted ${e.lastSent} days ago, please wait at least 7 days.`,
3935
[POST_FAILURE_REASONS.replyOrMention]:
40-
"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.",
36+
"Messages in this channel may not be replies or include @-mentions of users, to ensure the channel isn’t being used to discuss postings.",
4137
};
4238

4339
export const getValidationMessage = (reason: PostFailures): string => {
44-
if (failedCircumventedRules(reason)) {
45-
return ValidationMessages[reason.type](reason);
46-
}
4740
if (failedMissingType(reason)) {
4841
return ValidationMessages[reason.type];
4942
}

src/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,12 @@ export const bot = new discord.Client({
5151
IntentsBitField.Flags.DirectMessageReactions,
5252
IntentsBitField.Flags.MessageContent,
5353
],
54-
partials: [Partials.Channel, Partials.Reaction, Partials.GuildMember],
54+
partials: [
55+
Partials.Channel,
56+
Partials.Message,
57+
Partials.Reaction,
58+
Partials.GuildMember,
59+
],
5560
});
5661

5762
registerCommand(resetJobCacheCommand);

src/types/jobs-moderation.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,11 @@ export const enum POST_FAILURE_REASONS {
2828
tooManyGaps = "tooManyGaps",
2929
tooFrequent = "tooFrequent",
3030
replyOrMention = "replyOrMention",
31-
circumventedRules = "circumventedRules",
3231
// invalidContact = 'invalidContact',
3332
// unknownLocation = 'unknownLocation',
3433
// invalidPostType = 'invalidPostType',
3534
}
3635

37-
export interface CircumventedRules {
38-
type: POST_FAILURE_REASONS.circumventedRules;
39-
recentEdit: boolean;
40-
}
4136
export interface PostFailureMissingType {
4237
type: POST_FAILURE_REASONS.missingType;
4338
}
@@ -69,7 +64,6 @@ export interface PostFailureReplyOrMention {
6964
type: POST_FAILURE_REASONS.replyOrMention;
7065
}
7166
export type PostFailures =
72-
| CircumventedRules
7367
| PostFailureMissingType
7468
| PostFailureInconsistentType
7569
| PostFailureTooFrequent

0 commit comments

Comments
 (0)