Skip to content

Commit 8ee355d

Browse files
committed
comment mop working
1 parent f7d4724 commit 8ee355d

6 files changed

Lines changed: 404 additions & 17 deletions

File tree

devvit.json

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,40 @@
11
{
22
"$schema": "https://developers.reddit.com/schema/config-file.v1.json",
3-
"name": "erercever88866",
3+
"name": "<% name %>",
44
"server": {
55
"dir": "dist/server",
66
"entry": "index.cjs"
77
},
88
"menu": {
99
"items": [
1010
{
11-
"label": "Create a new post",
12-
"description": "<% name %>",
13-
"location": "subreddit",
11+
"label": "Mop comments",
12+
"description": "Remove this comment and all child comments. This might take a few seconds to run.",
13+
"location": "comment",
1414
"forUserType": "moderator",
15-
"endpoint": "/internal/menu/post-create"
15+
"endpoint": "/internal/menu/mop-comment"
16+
},
17+
{
18+
"label": "Mop post comments",
19+
"description": "Remove all comments of this post. This might take a few seconds to run.",
20+
"location": "post",
21+
"forUserType": "moderator",
22+
"endpoint": "/internal/menu/mop-post"
1623
}
1724
]
1825
},
26+
"forms": {
27+
"mopComment": "/internal/form/mop-comment-submit",
28+
"mopPost": "/internal/form/mop-post-submit"
29+
},
1930
"triggers": {
2031
"onAppInstall": "/internal/triggers/on-app-install"
2132
},
2233
"scripts": {
2334
"build": "vite build",
2435
"dev": "vite build --watch"
36+
},
37+
"permissions": {
38+
"reddit": true
2539
}
2640
}

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@
1919
"node": ">=22.12.0"
2020
},
2121
"dependencies": {
22-
"@devvit/start": "0.12.11-next-2026-01-30-17-09-49-e9c512a0d.0",
23-
"@devvit/web": "0.12.11-next-2026-01-30-17-09-49-e9c512a0d.0",
22+
"@devvit/start": "0.12.12-next-2026-02-02-21-50-00-da69a93fa.0",
23+
"@devvit/web": "0.12.12-next-2026-02-02-21-50-00-da69a93fa.0",
2424
"@hono/node-server": "^1.19.9",
25-
"devvit": "0.12.11-next-2026-01-30-17-09-49-e9c512a0d.0",
25+
"devvit": "0.12.12-next-2026-02-02-21-50-00-da69a93fa.0",
2626
"hono": "4.11.7"
2727
},
2828
"devDependencies": {

src/core/nuke.ts

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
import { reddit } from '@devvit/web/server';
2+
import type { Comment, Post } from '@devvit/web/server';
3+
import type { T1, T3, T5 } from '@devvit/shared-types/tid.js';
4+
5+
export type NukeProps = {
6+
remove: boolean;
7+
lock: boolean;
8+
skipDistinguished: boolean;
9+
commentId: T1;
10+
subredditId: T5;
11+
};
12+
13+
export type NukePostProps = {
14+
remove: boolean;
15+
lock: boolean;
16+
skipDistinguished: boolean;
17+
postId: T3;
18+
subredditId: T5;
19+
};
20+
21+
const shouldIncludeComment = (comment: Comment, skipDistinguished: boolean) =>
22+
!skipDistinguished || !comment.isDistinguished();
23+
24+
async function* getAllCommentsInThread(
25+
comment: Comment,
26+
skipDistinguished: boolean
27+
): AsyncGenerator<Comment> {
28+
if (shouldIncludeComment(comment, skipDistinguished)) {
29+
yield comment;
30+
}
31+
32+
const replies = await comment.replies.all();
33+
for (const reply of replies) {
34+
yield* getAllCommentsInThread(reply, skipDistinguished);
35+
}
36+
}
37+
38+
async function* getAllCommentsInPost(
39+
post: Post,
40+
skipDistinguished: boolean
41+
): AsyncGenerator<Comment> {
42+
const comments = await post.comments.all();
43+
for (const comment of comments) {
44+
yield* getAllCommentsInThread(comment, skipDistinguished);
45+
}
46+
}
47+
48+
export async function handleNukePost(props: NukePostProps) {
49+
const startTime = Date.now();
50+
let success = true;
51+
let message: string;
52+
53+
const shouldLock = props.lock;
54+
const shouldRemove = props.remove;
55+
const skipDistinguished = props.skipDistinguished;
56+
57+
try {
58+
const [user, post] = await Promise.all([
59+
reddit.getCurrentUser(),
60+
reddit.getPostById(props.postId),
61+
]);
62+
63+
if (!user) {
64+
return { success: false, message: "Can't get user" };
65+
}
66+
67+
const modPermissions = await user.getModPermissionsForSubreddit(
68+
post.subredditName
69+
);
70+
const canManagePosts =
71+
modPermissions.includes('all') || modPermissions.includes('posts');
72+
73+
console.log(
74+
`Mod Info: r/${post.subredditName} u/${
75+
user.username
76+
} permissions:${modPermissions}: ${
77+
canManagePosts ? 'Can mod' : 'Cannot mod'
78+
}`
79+
);
80+
81+
if (!canManagePosts) {
82+
console.info(
83+
'A user without the correct mod permissions tried to nuke all comments of a post.'
84+
);
85+
return {
86+
message: 'You do not have the correct mod permissions to do this.',
87+
success: false,
88+
};
89+
}
90+
91+
const comments: Comment[] = [];
92+
for await (const eachComment of getAllCommentsInPost(
93+
post,
94+
skipDistinguished
95+
)) {
96+
comments.push(eachComment);
97+
}
98+
99+
if (shouldLock) {
100+
await Promise.all(
101+
comments.map((comment) => comment.locked || comment.lock())
102+
);
103+
}
104+
105+
if (shouldRemove) {
106+
await Promise.all(
107+
comments.map((comment) => comment.removed || comment.remove())
108+
);
109+
}
110+
111+
const verbage =
112+
shouldLock && shouldRemove
113+
? 'removed and locked'
114+
: shouldLock
115+
? 'locked'
116+
: 'removed';
117+
118+
success = true;
119+
message = `Comments ${verbage}! Refresh the page to see the cleanup.`;
120+
const finishTime = Date.now();
121+
const timeElapsed = (finishTime - startTime) / 1000;
122+
console.info(
123+
`${comments.length} comment(s) handled in ${timeElapsed} seconds.`
124+
);
125+
} catch (err: unknown) {
126+
success = false;
127+
message = 'Mop failed! Please try again later.';
128+
console.error(err);
129+
}
130+
131+
return { success, message };
132+
}
133+
134+
export async function handleNuke(props: NukeProps) {
135+
const startTime = Date.now();
136+
let success = true;
137+
let message: string;
138+
139+
const shouldLock = props.lock;
140+
const shouldRemove = props.remove;
141+
const skipDistinguished = props.skipDistinguished;
142+
143+
try {
144+
const comment = await reddit.getCommentById(props.commentId);
145+
const user = await reddit.getCurrentUser();
146+
147+
if (!user) {
148+
return { success: false, message: "Can't get user" };
149+
}
150+
151+
const modPermissions = await user.getModPermissionsForSubreddit(
152+
comment.subredditName
153+
);
154+
const canManagePosts =
155+
modPermissions.includes('all') || modPermissions.includes('posts');
156+
157+
console.log(
158+
`Mod Info: r/${comment.subredditName} u/${
159+
user.username
160+
} permissions:${modPermissions}: ${
161+
canManagePosts ? 'Can mod' : 'Cannot mod'
162+
}`
163+
);
164+
165+
if (!canManagePosts) {
166+
console.info(
167+
'A user without the correct mod permissions tried to comment mop.'
168+
);
169+
return {
170+
message: 'You do not have the correct mod permissions to do this.',
171+
success: false,
172+
};
173+
}
174+
175+
const comments: Comment[] = [];
176+
for await (const eachComment of getAllCommentsInThread(
177+
comment,
178+
skipDistinguished
179+
)) {
180+
comments.push(eachComment);
181+
}
182+
183+
if (shouldLock) {
184+
await Promise.all(
185+
comments.map((comment) => comment.locked || comment.lock())
186+
);
187+
}
188+
189+
if (shouldRemove) {
190+
await Promise.all(
191+
comments.map((comment) => comment.removed || comment.remove())
192+
);
193+
}
194+
195+
const verbage =
196+
shouldLock && shouldRemove
197+
? 'removed and locked'
198+
: shouldLock
199+
? 'locked'
200+
: 'removed';
201+
202+
success = true;
203+
message = `Comments ${verbage}! Refresh the page to see the cleanup.`;
204+
const finishTime = Date.now();
205+
const timeElapsed = (finishTime - startTime) / 1000;
206+
console.info(
207+
`${comments.length} comment(s) handled in ${timeElapsed} seconds.`
208+
);
209+
} catch (err: unknown) {
210+
success = false;
211+
message = 'Mop failed! Please try again later.';
212+
console.error(err);
213+
}
214+
215+
return { success, message };
216+
}

src/routes/forms.ts

Lines changed: 100 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,113 @@
11
import { Hono } from 'hono';
22
import type { UiResponse } from '@devvit/web/shared';
3+
import { context } from '@devvit/web/server';
4+
import { isT1, isT3 } from '@devvit/shared-types/tid.js';
5+
import { handleNuke, handleNukePost } from '../core/nuke';
36

4-
type ExampleFormValues = {
5-
message?: string;
7+
type NukeFormValues = {
8+
remove?: boolean;
9+
lock?: boolean;
10+
skipDistinguished?: boolean;
11+
targetId?: string;
612
};
713

814
export const forms = new Hono();
915

10-
forms.post('/example-submit', async (c) => {
11-
const { message } = await c.req.json<ExampleFormValues>();
12-
const trimmedMessage = typeof message === 'string' ? message.trim() : '';
16+
const normalizeValues = (values: NukeFormValues) => ({
17+
remove: Boolean(values.remove),
18+
lock: Boolean(values.lock),
19+
skipDistinguished: Boolean(values.skipDistinguished),
20+
});
21+
22+
const getTargetId = (values: NukeFormValues) => {
23+
if (typeof values.targetId === 'string' && values.targetId.trim()) {
24+
return values.targetId.trim();
25+
}
26+
27+
return context.postId;
28+
};
29+
30+
forms.post('/mop-comment-submit', async (c) => {
31+
const values = await c.req.json<NukeFormValues>();
32+
console.log('values', values);
33+
const normalized = normalizeValues(values);
34+
35+
if (!normalized.lock && !normalized.remove) {
36+
return c.json<UiResponse>(
37+
{
38+
showToast: 'You must select either lock or remove.',
39+
},
40+
200
41+
);
42+
}
43+
44+
const targetId = getTargetId(values);
45+
if (!isT1(targetId)) {
46+
console.error('targetId is not a T1', targetId);
47+
return c.json<UiResponse>(
48+
{
49+
showToast: 'Mop failed! Please try again later.',
50+
},
51+
200
52+
);
53+
}
54+
55+
const result = await handleNuke({
56+
...normalized,
57+
commentId: targetId,
58+
subredditId: context.subredditId,
59+
});
60+
61+
console.log(
62+
`Mop result - ${result.success ? 'success' : 'fail'} - ${result.message}`
63+
);
64+
65+
return c.json<UiResponse>(
66+
{
67+
showToast: `${result.success ? 'Success' : 'Failed'} : ${result.message}`,
68+
},
69+
200
70+
);
71+
});
72+
73+
forms.post('/mop-post-submit', async (c) => {
74+
const values = await c.req.json<NukeFormValues>();
75+
console.log('values', values);
76+
const normalized = normalizeValues(values);
77+
78+
if (!normalized.lock && !normalized.remove) {
79+
return c.json<UiResponse>(
80+
{
81+
showToast: 'You must select either lock or remove.',
82+
},
83+
200
84+
);
85+
}
86+
87+
const targetId = getTargetId(values);
88+
if (!isT3(targetId)) {
89+
console.error('targetId is not a T3', targetId);
90+
return c.json<UiResponse>(
91+
{
92+
showToast: 'Mop failed! Please try again later.',
93+
},
94+
200
95+
);
96+
}
97+
98+
const result = await handleNukePost({
99+
...normalized,
100+
postId: targetId,
101+
subredditId: context.subredditId,
102+
});
103+
104+
console.log(
105+
`Mop result - ${result.success ? 'success' : 'fail'} - ${result.message}`
106+
);
13107

14108
return c.json<UiResponse>(
15109
{
16-
showToast: trimmedMessage
17-
? `Form says: ${trimmedMessage}`
18-
: 'Form submitted with no message',
110+
showToast: `${result.success ? 'Success' : 'Failed'} : ${result.message}`,
19111
},
20112
200
21113
);

0 commit comments

Comments
 (0)