Clean duplicate dashboard status messages
Clean duplicate Status dashboard messages by marker, preserve the tracked message, and avoid full-channel purge on startup.
This commit is contained in:
90
src/channels/discord-message-cleanup.test.ts
Normal file
90
src/channels/discord-message-cleanup.test.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { deleteRecentDiscordMessagesByContent } from './discord-message-cleanup.js';
|
||||||
|
|
||||||
|
class MockMessageCollection extends Map<string, any> {
|
||||||
|
filter(predicate: (message: any, id: string) => boolean) {
|
||||||
|
const filtered = new MockMessageCollection();
|
||||||
|
for (const [id, message] of this.entries()) {
|
||||||
|
if (predicate(message, id)) filtered.set(id, message);
|
||||||
|
}
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('deleteRecentDiscordMessagesByContent', () => {
|
||||||
|
it('deletes duplicate own dashboard messages while keeping the tracked one', async () => {
|
||||||
|
const keepDelete = vi.fn();
|
||||||
|
const duplicateDelete = vi.fn();
|
||||||
|
const otherBotDelete = vi.fn();
|
||||||
|
const normalDelete = vi.fn();
|
||||||
|
const messages = new MockMessageCollection([
|
||||||
|
[
|
||||||
|
'tracked',
|
||||||
|
{
|
||||||
|
id: 'tracked',
|
||||||
|
author: { id: '999888777' },
|
||||||
|
content: '🤖 *모델 구성*\ntracked',
|
||||||
|
createdTimestamp: Date.now(),
|
||||||
|
delete: keepDelete,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'duplicate',
|
||||||
|
{
|
||||||
|
id: 'duplicate',
|
||||||
|
author: { id: '999888777' },
|
||||||
|
content: '🤖 *모델 구성*\nduplicate',
|
||||||
|
createdTimestamp: Date.now(),
|
||||||
|
delete: duplicateDelete,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'other-bot',
|
||||||
|
{
|
||||||
|
id: 'other-bot',
|
||||||
|
author: { id: 'other-bot' },
|
||||||
|
content: '🤖 *모델 구성*\nother bot',
|
||||||
|
createdTimestamp: Date.now(),
|
||||||
|
delete: otherBotDelete,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'normal',
|
||||||
|
{
|
||||||
|
id: 'normal',
|
||||||
|
author: { id: '999888777' },
|
||||||
|
content: 'normal tracked message',
|
||||||
|
createdTimestamp: Date.now(),
|
||||||
|
delete: normalDelete,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
const client = {
|
||||||
|
user: { id: '999888777' },
|
||||||
|
channels: {
|
||||||
|
fetch: vi.fn().mockResolvedValue({
|
||||||
|
messages: {
|
||||||
|
fetch: vi.fn().mockResolvedValue(messages),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleted = await deleteRecentDiscordMessagesByContent({
|
||||||
|
client: client as any,
|
||||||
|
channelName: 'discord',
|
||||||
|
jid: 'dc:1234567890123456',
|
||||||
|
options: {
|
||||||
|
contentIncludes: '🤖 *모델 구성*',
|
||||||
|
exceptMessageId: 'tracked',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(deleted).toBe(1);
|
||||||
|
expect(duplicateDelete).toHaveBeenCalledTimes(1);
|
||||||
|
expect(keepDelete).not.toHaveBeenCalled();
|
||||||
|
expect(otherBotDelete).not.toHaveBeenCalled();
|
||||||
|
expect(normalDelete).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
81
src/channels/discord-message-cleanup.ts
Normal file
81
src/channels/discord-message-cleanup.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import type { Client, TextChannel } from 'discord.js';
|
||||||
|
|
||||||
|
import { logger } from '../logger.js';
|
||||||
|
import type { DeleteRecentMessagesByContentOptions } from '../types.js';
|
||||||
|
|
||||||
|
export async function deleteRecentDiscordMessagesByContent(args: {
|
||||||
|
client: Client | null;
|
||||||
|
channelName: string;
|
||||||
|
jid: string;
|
||||||
|
options: DeleteRecentMessagesByContentOptions;
|
||||||
|
}): Promise<number> {
|
||||||
|
if (!args.client) return 0;
|
||||||
|
|
||||||
|
const contentIncludes = args.options.contentIncludes.trim();
|
||||||
|
if (!contentIncludes) return 0;
|
||||||
|
|
||||||
|
let deleted = 0;
|
||||||
|
try {
|
||||||
|
const channelId = args.jid.replace(/^dc:/, '');
|
||||||
|
const channel = await args.client.channels.fetch(channelId);
|
||||||
|
if (!channel || !('messages' in channel)) return 0;
|
||||||
|
|
||||||
|
const tc = channel as TextChannel;
|
||||||
|
const messages = await tc.messages.fetch({
|
||||||
|
limit: Math.max(1, Math.min(args.options.limit ?? 100, 100)),
|
||||||
|
});
|
||||||
|
const candidates = messages.filter(
|
||||||
|
(message) =>
|
||||||
|
message.author.id === args.client?.user?.id &&
|
||||||
|
message.id !== args.options.exceptMessageId &&
|
||||||
|
message.content.includes(contentIncludes),
|
||||||
|
);
|
||||||
|
if (candidates.size === 0) return 0;
|
||||||
|
|
||||||
|
const twoWeeksAgo = Date.now() - 14 * 24 * 60 * 60 * 1000;
|
||||||
|
const recent = candidates.filter(
|
||||||
|
(message) => message.createdTimestamp > twoWeeksAgo,
|
||||||
|
);
|
||||||
|
const old = candidates.filter(
|
||||||
|
(message) => message.createdTimestamp <= twoWeeksAgo,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (recent.size >= 2 && 'bulkDelete' in channel) {
|
||||||
|
await tc.bulkDelete(recent);
|
||||||
|
deleted += recent.size;
|
||||||
|
} else {
|
||||||
|
for (const [, message] of recent) {
|
||||||
|
await message.delete();
|
||||||
|
deleted += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [, message] of old) {
|
||||||
|
await message.delete();
|
||||||
|
deleted += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
{
|
||||||
|
jid: args.jid,
|
||||||
|
deleted,
|
||||||
|
exceptMessageId: args.options.exceptMessageId ?? null,
|
||||||
|
channelName: args.channelName,
|
||||||
|
},
|
||||||
|
'Deleted duplicate Discord messages by content marker',
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(
|
||||||
|
{
|
||||||
|
jid: args.jid,
|
||||||
|
err,
|
||||||
|
deleted,
|
||||||
|
exceptMessageId: args.options.exceptMessageId ?? null,
|
||||||
|
channelName: args.channelName,
|
||||||
|
},
|
||||||
|
'Failed to delete duplicate Discord messages by content marker',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return deleted;
|
||||||
|
}
|
||||||
@@ -17,7 +17,12 @@ import { logger } from '../logger.js';
|
|||||||
import { validateOutboundAttachments } from '../outbound-attachments.js';
|
import { validateOutboundAttachments } from '../outbound-attachments.js';
|
||||||
import { formatOutbound } from '../router.js';
|
import { formatOutbound } from '../router.js';
|
||||||
import { hasReviewerLease } from '../service-routing.js';
|
import { hasReviewerLease } from '../service-routing.js';
|
||||||
import type { SendMessageOptions, SendMessageResult } from '../types.js';
|
import type {
|
||||||
|
DeleteRecentMessagesByContentOptions,
|
||||||
|
SendMessageOptions,
|
||||||
|
SendMessageResult,
|
||||||
|
} from '../types.js';
|
||||||
|
import { deleteRecentDiscordMessagesByContent } from './discord-message-cleanup.js';
|
||||||
import { prepareDiscordOutbound } from './discord-outbound.js';
|
import { prepareDiscordOutbound } from './discord-outbound.js';
|
||||||
|
|
||||||
const ATTACHMENTS_DIR = path.join(DATA_DIR, 'attachments');
|
const ATTACHMENTS_DIR = path.join(DATA_DIR, 'attachments');
|
||||||
@@ -760,6 +765,18 @@ export class DiscordChannel implements Channel {
|
|||||||
return deleted;
|
return deleted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async deleteRecentMessagesByContent(
|
||||||
|
jid: string,
|
||||||
|
options: DeleteRecentMessagesByContentOptions,
|
||||||
|
): Promise<number> {
|
||||||
|
return deleteRecentDiscordMessagesByContent({
|
||||||
|
client: this.client,
|
||||||
|
channelName: this.name,
|
||||||
|
jid,
|
||||||
|
options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async editMessage(
|
async editMessage(
|
||||||
jid: string,
|
jid: string,
|
||||||
messageId: string,
|
messageId: string,
|
||||||
|
|||||||
88
src/dashboard-message-cleanup.test.ts
Normal file
88
src/dashboard-message-cleanup.test.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import {
|
||||||
|
cleanupDashboardDuplicateMessages,
|
||||||
|
purgeDashboardMessages,
|
||||||
|
} from './dashboard-message-cleanup.js';
|
||||||
|
|
||||||
|
function makeChannel(deleteRecentMessagesByContent: any) {
|
||||||
|
return {
|
||||||
|
name: 'discord',
|
||||||
|
connect: vi.fn(),
|
||||||
|
sendMessage: vi.fn(),
|
||||||
|
isConnected: () => true,
|
||||||
|
ownsJid: () => true,
|
||||||
|
disconnect: vi.fn(),
|
||||||
|
deleteRecentMessagesByContent,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('cleanupDashboardDuplicateMessages', () => {
|
||||||
|
it('deletes recent dashboard messages except the tracked status message', async () => {
|
||||||
|
const deleteRecentMessagesByContent = vi.fn(async () => 3);
|
||||||
|
const deleted = await cleanupDashboardDuplicateMessages(
|
||||||
|
{
|
||||||
|
statusChannelId: 'status-channel',
|
||||||
|
channels: [makeChannel(deleteRecentMessagesByContent)],
|
||||||
|
},
|
||||||
|
'tracked-message',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(deleted).toBe(3);
|
||||||
|
expect(deleteRecentMessagesByContent).toHaveBeenCalledWith(
|
||||||
|
'dc:status-channel',
|
||||||
|
{
|
||||||
|
contentIncludes: '🤖 *모델 구성*',
|
||||||
|
exceptMessageId: 'tracked-message',
|
||||||
|
limit: 100,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('skips cleanup when there is no tracked status message yet', async () => {
|
||||||
|
const deleteRecentMessagesByContent = vi.fn(async () => 3);
|
||||||
|
const deleted = await cleanupDashboardDuplicateMessages(
|
||||||
|
{
|
||||||
|
statusChannelId: 'status-channel',
|
||||||
|
channels: [makeChannel(deleteRecentMessagesByContent)],
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(deleted).toBe(0);
|
||||||
|
expect(deleteRecentMessagesByContent).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('purgeDashboardMessages', () => {
|
||||||
|
it('deletes dashboard marker messages without purging the whole channel', async () => {
|
||||||
|
const deleteRecentMessagesByContent = vi.fn(async () => 2);
|
||||||
|
const deleted = await purgeDashboardMessages({
|
||||||
|
statusChannelId: 'status-channel',
|
||||||
|
channels: [makeChannel(deleteRecentMessagesByContent)],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(deleted).toBe(2);
|
||||||
|
expect(deleteRecentMessagesByContent).toHaveBeenCalledWith(
|
||||||
|
'dc:status-channel',
|
||||||
|
{
|
||||||
|
contentIncludes: '🤖 *모델 구성*',
|
||||||
|
limit: 100,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('continues deleting dashboard batches until the last batch is partial', async () => {
|
||||||
|
const deleteRecentMessagesByContent = vi
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValueOnce(100)
|
||||||
|
.mockResolvedValueOnce(7);
|
||||||
|
const deleted = await purgeDashboardMessages({
|
||||||
|
statusChannelId: 'status-channel',
|
||||||
|
channels: [makeChannel(deleteRecentMessagesByContent)],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(deleted).toBe(107);
|
||||||
|
expect(deleteRecentMessagesByContent).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
67
src/dashboard-message-cleanup.ts
Normal file
67
src/dashboard-message-cleanup.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { logger } from './logger.js';
|
||||||
|
import type { Channel } from './types.js';
|
||||||
|
|
||||||
|
const DASHBOARD_STATUS_MESSAGE_MARKER = '🤖 *모델 구성*';
|
||||||
|
const DASHBOARD_DUPLICATE_CLEANUP_LIMIT = 100;
|
||||||
|
|
||||||
|
function findDashboardCleanupChannel(opts: {
|
||||||
|
channels: Channel[];
|
||||||
|
statusChannelId: string;
|
||||||
|
}): Channel | undefined {
|
||||||
|
if (!opts.statusChannelId) return undefined;
|
||||||
|
return opts.channels.find(
|
||||||
|
(item) =>
|
||||||
|
item.name.startsWith('discord') &&
|
||||||
|
item.isConnected() &&
|
||||||
|
item.deleteRecentMessagesByContent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function purgeDashboardMessages(opts: {
|
||||||
|
channels: Channel[];
|
||||||
|
statusChannelId: string;
|
||||||
|
}): Promise<number> {
|
||||||
|
const channel = findDashboardCleanupChannel(opts);
|
||||||
|
if (!channel?.deleteRecentMessagesByContent) return 0;
|
||||||
|
|
||||||
|
let total = 0;
|
||||||
|
for (let i = 0; i < 10; i += 1) {
|
||||||
|
const deleted = await channel.deleteRecentMessagesByContent(
|
||||||
|
`dc:${opts.statusChannelId}`,
|
||||||
|
{
|
||||||
|
contentIncludes: DASHBOARD_STATUS_MESSAGE_MARKER,
|
||||||
|
limit: DASHBOARD_DUPLICATE_CLEANUP_LIMIT,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
total += deleted;
|
||||||
|
if (deleted < DASHBOARD_DUPLICATE_CLEANUP_LIMIT) break;
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cleanupDashboardDuplicateMessages(
|
||||||
|
opts: { channels: Channel[]; statusChannelId: string },
|
||||||
|
keepMessageId: string | null,
|
||||||
|
): Promise<number> {
|
||||||
|
if (!opts.statusChannelId || !keepMessageId) return 0;
|
||||||
|
|
||||||
|
const channel = findDashboardCleanupChannel(opts);
|
||||||
|
if (!channel?.deleteRecentMessagesByContent) return 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await channel.deleteRecentMessagesByContent(
|
||||||
|
`dc:${opts.statusChannelId}`,
|
||||||
|
{
|
||||||
|
contentIncludes: DASHBOARD_STATUS_MESSAGE_MARKER,
|
||||||
|
exceptMessageId: keepMessageId,
|
||||||
|
limit: DASHBOARD_DUPLICATE_CLEANUP_LIMIT,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(
|
||||||
|
{ err, keepMessageId },
|
||||||
|
'Dashboard duplicate message cleanup failed',
|
||||||
|
);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/types.ts
10
src/types.ts
@@ -49,6 +49,12 @@ export interface SendMessageResult {
|
|||||||
visible: boolean;
|
visible: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DeleteRecentMessagesByContentOptions {
|
||||||
|
contentIncludes: string;
|
||||||
|
exceptMessageId?: string | null;
|
||||||
|
limit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export type PairedRoomRole = 'owner' | 'reviewer' | 'arbiter';
|
export type PairedRoomRole = 'owner' | 'reviewer' | 'arbiter';
|
||||||
|
|
||||||
export type PairedTaskStatus =
|
export type PairedTaskStatus =
|
||||||
@@ -273,6 +279,10 @@ export interface Channel {
|
|||||||
// Optional: edit/delete messages (used by status dashboard and tracked progress cleanup).
|
// Optional: edit/delete messages (used by status dashboard and tracked progress cleanup).
|
||||||
editMessage?(jid: string, messageId: string, text: string): Promise<void>;
|
editMessage?(jid: string, messageId: string, text: string): Promise<void>;
|
||||||
deleteMessage?(jid: string, messageId: string): Promise<void>;
|
deleteMessage?(jid: string, messageId: string): Promise<void>;
|
||||||
|
deleteRecentMessagesByContent?(
|
||||||
|
jid: string,
|
||||||
|
options: DeleteRecentMessagesByContentOptions,
|
||||||
|
): Promise<number>;
|
||||||
sendAndTrack?(jid: string, text: string): Promise<string | null>;
|
sendAndTrack?(jid: string, text: string): Promise<string | null>;
|
||||||
// Optional: sync group/chat names from the platform.
|
// Optional: sync group/chat names from the platform.
|
||||||
syncGroups?(force: boolean): Promise<void>;
|
syncGroups?(force: boolean): Promise<void>;
|
||||||
|
|||||||
@@ -63,22 +63,16 @@ describe('formatStatusHeader', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('shouldPurgeDashboardChannelOnStart', () => {
|
describe('shouldPurgeDashboardChannelOnStart', () => {
|
||||||
it('keeps the existing status message when a stored message id exists', () => {
|
it('purges on startup when explicitly requested, even with a stored message id', () => {
|
||||||
expect(
|
expect(
|
||||||
shouldPurgeDashboardChannelOnStart({
|
shouldPurgeDashboardChannelOnStart({
|
||||||
purgeOnStart: true,
|
purgeOnStart: true,
|
||||||
storedMessageId: 'status-message-1',
|
storedMessageId: 'status-message-1',
|
||||||
}),
|
}),
|
||||||
).toBe(false);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('purges only when explicitly requested and no stored message id exists', () => {
|
it('does not purge when startup purge is disabled', () => {
|
||||||
expect(
|
|
||||||
shouldPurgeDashboardChannelOnStart({
|
|
||||||
purgeOnStart: true,
|
|
||||||
storedMessageId: null,
|
|
||||||
}),
|
|
||||||
).toBe(true);
|
|
||||||
expect(
|
expect(
|
||||||
shouldPurgeDashboardChannelOnStart({
|
shouldPurgeDashboardChannelOnStart({
|
||||||
purgeOnStart: false,
|
purgeOnStart: false,
|
||||||
|
|||||||
@@ -41,6 +41,10 @@ import {
|
|||||||
type DashboardRoomLine,
|
type DashboardRoomLine,
|
||||||
renderCategorizedRoomSections,
|
renderCategorizedRoomSections,
|
||||||
} from './dashboard-render.js';
|
} from './dashboard-render.js';
|
||||||
|
import {
|
||||||
|
cleanupDashboardDuplicateMessages,
|
||||||
|
purgeDashboardMessages,
|
||||||
|
} from './dashboard-message-cleanup.js';
|
||||||
import {
|
import {
|
||||||
buildClaudeUsageRows,
|
buildClaudeUsageRows,
|
||||||
extractCodexUsageRows,
|
extractCodexUsageRows,
|
||||||
@@ -157,25 +161,14 @@ function findDiscordChannel(channels: Channel[]): Channel | undefined {
|
|||||||
export async function purgeDashboardChannel(
|
export async function purgeDashboardChannel(
|
||||||
opts: Pick<UnifiedDashboardOptions, 'channels' | 'statusChannelId'>,
|
opts: Pick<UnifiedDashboardOptions, 'channels' | 'statusChannelId'>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!opts.statusChannelId) return;
|
await purgeDashboardMessages(opts);
|
||||||
|
|
||||||
const statusJid = `dc:${opts.statusChannelId}`;
|
|
||||||
const channel = opts.channels.find(
|
|
||||||
(item) =>
|
|
||||||
item.name.startsWith('discord') &&
|
|
||||||
item.isConnected() &&
|
|
||||||
item.purgeChannel,
|
|
||||||
);
|
|
||||||
if (channel?.purgeChannel) {
|
|
||||||
await channel.purgeChannel(statusJid);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function shouldPurgeDashboardChannelOnStart(args: {
|
export function shouldPurgeDashboardChannelOnStart(args: {
|
||||||
purgeOnStart?: boolean;
|
purgeOnStart?: boolean;
|
||||||
storedMessageId: string | null;
|
storedMessageId: string | null;
|
||||||
}): boolean {
|
}): boolean {
|
||||||
return args.purgeOnStart === true && !args.storedMessageId;
|
return args.purgeOnStart === true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshChannelMeta(
|
async function refreshChannelMeta(
|
||||||
@@ -729,6 +722,7 @@ export async function startUnifiedDashboard(
|
|||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
await purgeDashboardChannel(opts);
|
await purgeDashboardChannel(opts);
|
||||||
|
statusMessageId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isRenderer) {
|
if (isRenderer) {
|
||||||
@@ -787,6 +781,9 @@ export async function startUnifiedDashboard(
|
|||||||
writeDashboardStatusMessageId(opts.statusChannelId, id);
|
writeDashboardStatusMessageId(opts.statusChannelId, id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (statusMessageId) {
|
||||||
|
await cleanupDashboardDuplicateMessages(opts, statusMessageId);
|
||||||
|
}
|
||||||
if (!dashboardUpdateLogged) {
|
if (!dashboardUpdateLogged) {
|
||||||
logger.info(
|
logger.info(
|
||||||
{ messageId: statusMessageId, contentLength: content.length },
|
{ messageId: statusMessageId, contentLength: content.length },
|
||||||
|
|||||||
Reference in New Issue
Block a user