Skip to content

Commit e4e052a

Browse files
snomiaoclaudeCopilot
authored
refactor: clean up coreping implementation and improve spam handling (#115)
* refactor: extract slack message URL parsing and add coreping spam cleanup improvements - Extract slackMessageUrlParse and slackMessageUrlStringify to separate module for reusability - Add 23-hour cooldown between new coreping messages to prevent spam - Improve spam cleanup function with user confirmation before deletion - Add confirm utility function to lib/utils.ts for interactive prompts - Update imports across affected files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Update lib/utils.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update app/tasks/coreping/coreping.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update app/tasks/coreping/coreping.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update app/tasks/gh-design/slackMessageUrlParse.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix: remove space in slack message URL and improve spam cleanup confirmation - Remove extra space in Slack message URL formatting for CorePing notifications - Add explicit abort handling for spam cleanup confirmation dialog 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Update app/tasks/gh-design/slackMessageUrlParse.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update app/tasks/gh-desktop-release-notification/upsertSlackMessage.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 0773fe5 commit e4e052a

5 files changed

Lines changed: 87 additions & 35 deletions

File tree

app/tasks/coreping/coreping.ts

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import { yaml } from "@/src/utils/yaml";
2121
import { upsertSlackMessage } from "../gh-desktop-release-notification/upsertSlackMessage";
2222
import { getSlack, slack } from "@/src/slack";
2323
import { slackCached } from "@/src/slack/slackCached";
24+
import { slackMessageUrlParse } from "../gh-design/slackMessageUrlParse";
25+
import { confirm } from "@/lib/utils";
2426

2527
// yeah, if the bot could ping when updates have been made to a previously-reviewed PR, would be extremely helpful
2628

@@ -139,6 +141,7 @@ if (import.meta.main) {
139141
// const url = "https://github.com/comfyanonymous/ComfyUI/pull/8351";
140142
// await determinePullRequestReviewStatus(url);
141143

144+
// await _cleanSpammyMessages20251117()
142145
await runCorePingTaskFull();
143146
// await runCorePingTaskIncremental();
144147
// reviewCommentsCheckpoint()
@@ -325,7 +328,7 @@ async function runCorePingTaskFull() {
325328

326329
// console.log("ready to send slack message to notify @comfy");
327330
// console.log(processedTasks);
328-
const tail = `Sent from < https://github.com/Comfy-Org/Comfy-PR/blob/main/app/tasks/coreping/coreping.ts|CorePing.ts> by <@snomiao>`;
331+
const tail = `Sent from <https://github.com/Comfy-Org/Comfy-PR/blob/main/app/tasks/coreping/coreping.ts|CorePing.ts> by <@snomiao>`;
329332
const notifyMessage = !pendingCorePRs.length
330333
? `Congratulations! All Core/Important PRs are reviewed! 🎉🎉🎉 \n${tail}`
331334
: `Hey <@comfy>, Here's x${pendingCorePRs.length} Core/Important PRs waiting your feedback!\n\n${pendingCorePRs.map((pr) => pr.statusMsg || `- <${pr.url}|${pr.title}> ${pr.labels}`).join("\n")}\n\n${tail}`;
@@ -335,7 +338,13 @@ async function runCorePingTaskFull() {
335338
// // send or update slack message
336339
let meta = await Meta.$upsert({});
337340

338-
// can only post new message: tz: PST, day: working day + sat, time: 10-12am
341+
const lastMessageTsDate = meta.lastSlackMessage?.url
342+
? new Date(+(slackMessageUrlParse(meta.lastSlackMessage.url).ts) * 1000,)
343+
: null;
344+
345+
// can only post new message when:
346+
// 1. tz: PST, day: working day + sat, time: 10-12am
347+
// 2. or last message sent time( Note: not edited time ) >23h ago (if have last msg)
339348
const canPostNewMessage = (() => {
340349
const now = new Date();
341350
const pstTime = new Date(
@@ -350,7 +359,10 @@ async function runCorePingTaskFull() {
350359
const isValidTime = hour >= 10 && hour < 12;
351360

352361
return isValidDay && isValidTime;
353-
})();
362+
})() && (
363+
!lastMessageTsDate ||
364+
Date.now() - lastMessageTsDate.getTime() >= 23 * 60 * 60 * 1000
365+
);
354366

355367
const canUpdateExistingMessage =
356368
meta.lastSlackMessage?.sendAt &&
@@ -487,9 +499,9 @@ async function processPullRequestCorePingTask(
487499
}
488500

489501
async function _cleanSpammyMessages20251117() {
490-
// list 2025-11-16 2:11 to 3:49 (in HKT)
491-
const st = new Date('2025-11-16T02:00:00+0800');
492-
const et = new Date('2025-11-16T03:50:00+0800');
502+
// list 2025-11-18 2:00 to 3:55 (in HKT)
503+
const st = new Date('2025-11-18T02:00:00+0800');
504+
const et = new Date('2025-11-18T03:55:00+0800');
493505
console.log(`fetch slack messages from ${st.toISOString()} to ${et.toISOString()}`);
494506
// slack.history.list(getslackchannel)
495507
const channel = await pageFlow(undefined as string | undefined, async (cursor, limit = 3) => {
@@ -510,6 +522,7 @@ async function _cleanSpammyMessages20251117() {
510522
// .log(e => yaml.stringify({}))
511523
.toAtLeastOne()
512524

525+
console.log('checking messages sent by ComfyPR-Bot:', comfyPrBot.id, comfyPrBot.name);
513526
const myspammessages = await pageFlow(undefined as string | undefined, async (cursor, limit = 100) => {
514527
const resp = await slackCached.conversations.history({
515528
channel: channelId,
@@ -519,13 +532,14 @@ async function _cleanSpammyMessages20251117() {
519532
oldest: String((+st) / 1000),
520533
latest: String((+et) / 1000),
521534
})
522-
console.log(`+${resp.messages?.length} messages by ${await sflow(resp.messages || [])
523-
?.mapMixin(async e => ({ info: await slackCached.users.info({ user: e.user || undefined }).then(u => u.user) }))
524-
?.map(m => String(m.username || m.info?.real_name || m.user || ''))
525-
.filter()
526-
.join(", ")
527-
.text()
528-
}`);
535+
console.log(resp.messages?.length)
536+
// console.log(`+${resp.messages?.length} messages by ${await sflow(resp.messages || [])
537+
// ?.mapMixin(async e => ({ info: await slackCached.users.info({ user: e.user || undefined }).then(u => u.user) }))
538+
// ?.map(m => String(m.username || m.info?.real_name || m.user || ''))
539+
// .filter()
540+
// .join(", ")
541+
// .text()
542+
// }`);
529543

530544
return { next: resp.response_metadata?.next_cursor || undefined, data: resp.messages || [] };
531545
})
@@ -542,7 +556,6 @@ async function _cleanSpammyMessages20251117() {
542556
// .takeWhile(e => +(e.ts || DIE(`Fatal: msg have no.ts ${e}`)) * 1000 >= (+st))
543557
// .filter(e => e.username)
544558
.filter(e => e.user === comfyPrBot.id)
545-
.forEach(async e => await slack.chat.delete({ channel: channelId, ts: e.ts || DIE() }))
546559
// throw check
547560
.toArray()
548561

@@ -553,4 +566,14 @@ async function _cleanSpammyMessages20251117() {
553566
)
554567
}))
555568

556-
}
569+
const confirmed = await confirm(`About to delete ${myspammessages.length} messages sent by ComfyPR-Bot in #${channel.name} between ${st.toISOString()} and ${et.toISOString()}. Proceed?`);
570+
if (!confirmed) {
571+
console.log("Operation cancelled");
572+
return;
573+
}
574+
575+
const deletedMessages = await sflow(myspammessages)
576+
.forEach(async e => await slack.chat.delete({ channel: channelId, ts: e.ts || DIE() }))
577+
.log(e => `Deleted message at ${new Date(+(e.ts || DIE()) * 1000).toISOString()}: ${e.text?.replace(/\n/g, ' ').slice(0, 30)}...`)
578+
.run()
579+
}

app/tasks/gh-design/gh-design.ts

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import sha256 from "sha256";
1212
import { z } from "zod";
1313
import { createTimeLogger } from "./createTimeLogger";
1414
import { ghDesignDefaultConfig } from "./default-config";
15+
import { slackMessageUrlParse, slackMessageUrlStringify } from "./slackMessageUrlParse";
1516
const tlog = createTimeLogger();
1617
// Task bot to scan for [Design] labels on PRs and issues and send notifications to product channel
1718

@@ -275,22 +276,4 @@ export async function runGithubDesignTask() {
275276
});
276277
}
277278

278-
/**
279-
* @deprecated use slack.chat.getPermalink instead
280-
*/
281-
export function slackMessageUrlStringify({ channel, ts }: { channel: string; ts: string }) {
282-
// slack use microsecond as message id, uniq by channel
283-
// TODO: move organization to env variable
284-
return `https://comfy-organization.slack.com/archives/{{CHANNEL_ID}}/p{{TSNODOT}}`
285-
.replace("{{CHANNEL_ID}}", channel)
286-
.replace("{{TSNODOT}}", ts.replace(/\./g, ""));
287-
}
288-
export function slackMessageUrlParse(url: string) {
289-
// slack use microsecond as message id, uniq by channel
290-
const match = url.match(/archives\/([^\/]+)\/p(\d+)/);
291-
if (!match) throw new Error(`Invalid Slack message URL: ${url}`);
292-
return {
293-
channel: match[1],
294-
ts: match[2].replace(/^(\d+)(\d{6})$/, "$1.$2"), // convert to full timestamp
295-
};
296-
}
279+
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
export function slackMessageUrlParse(url: string) {
2+
// slack use microsecond as message id, uniq by channel
3+
const match = url.match(/archives\/([^\/]+)\/p(\d+)/);
4+
if (!match) throw new Error(`Invalid Slack message URL: ${url}`);
5+
return {
6+
channel: match[1],
7+
ts: match[2].replace(/^(\d+)(\d{6})$/, "$1.$2"), // convert Slack message ID (e.g., "1234567890123456") to Slack timestamp format with decimal (e.g., "1234567890.123456")
8+
};
9+
}
10+
11+
/**
12+
* @deprecated use slack.chat.getPermalink instead
13+
*/
14+
export function slackMessageUrlStringify({ channel, ts }: { channel: string; ts: string; }) {
15+
// slack use microsecond as message id, uniq by channel
16+
// TODO: move organization to env variable
17+
return `https://comfy-organization.slack.com/archives/{{CHANNEL_ID}}/p{{TSNODOT}}`
18+
.replace("{{CHANNEL_ID}}", channel)
19+
.replace("{{TSNODOT}}", ts.replace(/\./g, ""));
20+
}
21+

app/tasks/gh-desktop-release-notification/upsertSlackMessage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import KeyvSqlite from "@keyv/sqlite";
55
import DIE from "@snomiao/die";
66
import chalk from "chalk";
77
import Keyv from "keyv";
8-
import { slackMessageUrlParse, slackMessageUrlStringify } from "../gh-design/gh-design";
8+
import { slackMessageUrlStringify, slackMessageUrlParse } from "../gh-design/slackMessageUrlParse";
99
import { COMFY_PR_CACHE_DIR } from "./COMFY_PR_CACHE_DIR";
1010

1111
const SlackChannelIdsCache = new Keyv<string>({

lib/utils.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,28 @@ import { twMerge } from "tailwind-merge";
44
export function cn(...inputs: ClassValue[]) {
55
return twMerge(clsx(inputs));
66
}
7+
/**
8+
* Prompts the user with a yes/no question in the terminal and returns their response as a boolean.
9+
*
10+
* @param {string} question - The question to display to the user.
11+
* @returns {Promise<boolean>} - Resolves to `true` if the user answers "y" or "yes" (case-insensitive), otherwise `false`.
12+
*
13+
* @example
14+
* const answer = await confirm("Do you want to continue?");
15+
* if (answer) {
16+
* // User confirmed
17+
* } else {
18+
* // User declined
19+
* }
20+
*/
21+
export async function confirm(question: string) {
22+
return new Promise<boolean>((resolve) => {
23+
process.stdin.resume();
24+
process.stdout.write(`${question} (y/n): `);
25+
process.stdin.once("data", function (data) {
26+
const answer = data.toString().trim().toLowerCase();
27+
process.stdin.pause();
28+
resolve(answer === "y" || answer === "yes");
29+
});
30+
});
31+
}

0 commit comments

Comments
 (0)