Skip to content
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
3 changes: 2 additions & 1 deletion src/utils/forecastGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ export function generateForecast(options: ForecastOptions): {

for (let i = 1; i <= 24; i++) {
const hourIndex = (currentHour + i) % 24;
const rotation = rotations[hourIndex];
const rotation = rotations.find((r) => r.hour === hourIndex);
if (!rotation) continue;
const timestamp = nextRotationTs + (i - 1) * 3600;
const timeLabel = `<t:${timestamp}:R>`;

Expand Down
8 changes: 4 additions & 4 deletions src/utils/mapRotationCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,11 @@ export async function updateCacheIfChanged(newRotations: MapRotation[]): Promise
const newHash = computeRotationsHash(newRotations);
const storedHash = await mapRotationRepo.getRotationsHash();

// Always invalidate in-memory cache on sync to ensure freshness
invalidateCache();

if (storedHash === newHash) {
logger.debug("Rotations hash unchanged, skipping update");
logger.debug("Rotations hash unchanged, skipping database update");
return { updated: false, changes: [] };
}

Expand All @@ -117,9 +120,6 @@ export async function updateCacheIfChanged(newRotations: MapRotation[]): Promise
await mapRotationRepo.upsertRotations(newRotations);
await mapRotationRepo.setRotationsHash(newHash);

// Invalidate cache so next read fetches fresh data
invalidateCache();

logger.info(
`Map rotations updated, new hash: ${newHash.substring(0, 8)}..., ${changes.length} changes detected`,
);
Expand Down
9 changes: 6 additions & 3 deletions src/utils/messageManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
import {
CONDITION_COLORS,
CONDITION_EMOJIS,
getAllRotations,
getCurrentRotation,
getNextRotationTimestamp,
MAP_ROTATIONS,
} from "../config/mapRotation";
import type { MapRotation, ServerConfigEntry } from "../types";
import { getT, translateEvent } from "./i18n";
Expand Down Expand Up @@ -55,6 +55,7 @@
}> {
const t = getT(locale);
const current = options?.rotation ?? (await getCurrentRotation());
const rotations = await getAllRotations();
const nextTimestamp = getNextRotationTimestamp();

const primaryColor =
Expand Down Expand Up @@ -166,7 +167,8 @@
let forecastText = "";
for (let i = 1; i <= 6; i++) {
const hourIndex = (currentHour + i) % 24;
const rotation = MAP_ROTATIONS[hourIndex];
const rotation = rotations.find((r) => r.hour === hourIndex);
if (!rotation) continue;
const timestamp = nextTimestamp + (i - 1) * 3600;
const timeLabel = `<t:${timestamp}:R>`;

Expand Down Expand Up @@ -217,7 +219,8 @@

for (let i = 1; i <= 6; i++) {
const hourIndex = (currentHour + i) % 24;
const rotation = MAP_ROTATIONS[hourIndex];
const rotation = rotations.find((r) => r.hour === hourIndex);
if (!rotation) continue;
const timestamp = nextTimestamp + (i - 1) * 3600;
const timeLabel = `<t:${timestamp}:R>`;

Expand Down Expand Up @@ -324,159 +327,159 @@
* @param localeOverride Optional locale to use instead of the config locale.
* @param configOverride Optional server config to avoid refetching.
*/
export async function postOrUpdateInChannel(
client: Client,
guildId: string,
channelId: string,
existingMessageId?: string,
localeOverride?: string,
configOverride?: ServerConfigEntry,
): Promise<void> {
try {
// Try to resolve from cache first, then fetch if missing
let channel = client.channels.resolve(channelId) as TextChannel;
if (!channel) {
channel = (await client.channels.fetch(channelId)) as TextChannel;
}

if (!channel || !channel.isTextBased()) {
logger.warn(`Invalid or non-text channel: ${channelId}`);
return;
}
logVerbose({ guildId, channelId }, "Resolved text channel for update");

const config = configOverride ?? (await getServerConfig(guildId));
const mobileFriendly = config?.mobileFriendly ?? false;
const notificationMethod = config?.notificationMethod ?? "pin-edit";
// Use override if provided, otherwise fetch from config
const locale = localeOverride || config?.locale || channel.guild?.preferredLocale || "en";
logVerbose(
{ guildId, channelId, notificationMethod, locale, mobileFriendly },
"Loaded server configuration for update",
);

const imageContext = await prepareImageCacheContext(locale);
const { embed, files, components } = await createMapRotationEmbed(mobileFriendly, locale, {
rotation: imageContext.rotation,
imageUrl: imageContext.cachedUrl,
});
let message: Message;
const payload = {
embeds: [embed],
components,
files: files.length > 0 ? files : undefined,
};
const logOperationResult = (action: string, startedAt: number) => {
logger.debug(
{
guildId,
channelId,
notificationMethod,
durationMs: Date.now() - startedAt,
cachedImage: Boolean(imageContext.cachedUrl),
},
action,
);
};

// Handle different notification methods
if (notificationMethod === "pin-edit") {
// Option 1: Pin and Edit (current behavior)
if (
existingMessageId != null &&
typeof existingMessageId === "string" &&
existingMessageId.trim() !== ""
) {
try {
logVerbose(
{ guildId, channelId, existingMessageId },
"Attempting to edit existing pinned message",
);
const opStart = Date.now();
message = await withRetry(() => channel.messages.edit(existingMessageId, payload), {
shouldAbort: (error) => error instanceof DiscordAPIError && error.code === 10008,
onRetry: (attempt, error, delayMs) => {
logger.warn(
{
guildId,
channelId,
existingMessageId,
attempt,
nextRetryMs: delayMs,
error: formatDiscordError(error),
},
`Edit failed, retrying in ${delayMs}ms`,
);
},
});
logOperationResult(`Updated pinned message in ${channelId}`, opStart);
} catch (error) {
logger.warn(
{
guildId,
channelId,
existingMessageId,
error: formatDiscordError(error),
},
`Message edit failed in ${channelId}, creating a new one.`,
);
const opStart = Date.now();
message = await channel.send(payload);
await message.pin().catch(catchPinError);
logOperationResult(`Created and pinned a new message in ${channelId}`, opStart);
}
} else {
const opStart = Date.now();
message = await channel.send(payload);
await message.pin().catch(catchPinError);
logOperationResult(`Created and pinned a new message in ${channelId}`, opStart);
}
await setServerMessageState(guildId, message.id, new Date().toISOString());
} else if (notificationMethod === "post-delete") {
// Option 2: Post new and delete old
if (
existingMessageId != null &&
typeof existingMessageId === "string" &&
existingMessageId.trim() !== ""
) {
try {
logVerbose(
{ guildId, channelId, existingMessageId },
"Attempting to delete previous message before posting new one",
);
await channel.messages.delete(existingMessageId);
} catch (_error) {
logger.warn(`Old message ${existingMessageId} not found, skipping deletion`);
}
}

const opStart = Date.now();
message = await channel.send(payload);
logOperationResult(`Posted new message in ${channelId} (post-delete mode)`, opStart);
await setServerMessageState(guildId, message.id, new Date().toISOString());
} else if (notificationMethod === "post-keep") {
// Option 3: Post new and keep history
const opStart = Date.now();
message = await channel.send(payload);
logOperationResult(`Posted new message in ${channelId} (post-keep mode)`, opStart);
// Don't store message ID for post-keep mode, or set to null
await setServerMessageState(guildId, message.id, new Date().toISOString());
}

if (!imageContext.cachedUrl && message) {
cacheImageUrlFromMessage(message, imageContext.cacheKey);
}
} catch (error) {
logger.error(
{
guildId,
channelId,
error: formatDiscordError(error),
},
`Error processing channel ${channelId}`,
);
}
}

Check notice on line 482 in src/utils/messageManager.ts

View check run for this annotation

codefactor.io / CodeFactor

src/utils/messageManager.ts#L330-L482

Complex Method

/**
* Iterates through all configured servers and updates their map rotation messages.
Expand Down