From c67a1df11deae8018ab077d0ff6256611bdd7e04 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 10 Aug 2020 00:24:06 +0300 Subject: [PATCH] Add support for server-specific timezone and date format settings --- backend/ormconfig.js | 2 +- backend/src/api/archives.ts | 5 +- backend/src/configValidator.ts | 30 +++++------- backend/src/data/ApiLogins.ts | 9 ++-- backend/src/data/ApiUserInfo.ts | 4 +- backend/src/data/DefaultLogMessages.json | 4 +- backend/src/data/GuildArchives.ts | 8 ++-- backend/src/data/GuildCases.ts | 4 +- backend/src/data/GuildMutes.ts | 6 ++- backend/src/data/GuildSavedMessages.ts | 2 +- backend/src/data/GuildSlowmodes.ts | 3 +- backend/src/data/cleanup/configs.ts | 8 ++-- backend/src/data/cleanup/messages.ts | 12 +++-- backend/src/data/cleanup/nicknames.ts | 6 ++- backend/src/data/cleanup/usernames.ts | 6 ++- backend/src/index.ts | 4 +- .../plugins/AutoDelete/util/deleteNextItem.ts | 6 ++- .../plugins/Cases/functions/getCaseEmbed.ts | 11 +++-- .../commands/ArchiveChannelCmd.ts | 6 ++- .../plugins/LocateUser/commands/FollowCmd.ts | 2 +- backend/src/plugins/Logs/LogsPlugin.ts | 2 +- .../Logs/events/LogsGuildMemberAddEvt.ts | 4 +- .../src/plugins/Logs/util/getLogMessage.ts | 3 +- .../src/plugins/Logs/util/onMessageDelete.ts | 5 +- .../ModActions/commands/DeleteCaseCmd.ts | 4 +- .../src/plugins/Mutes/commands/MutesCmd.ts | 12 +++-- .../Post/commands/ScheduledPostsListCmd.ts | 8 +++- .../src/plugins/Post/util/actualPostCmd.ts | 48 ++++++++++++------- .../plugins/Post/util/parseScheduleTime.ts | 23 +++++---- .../plugins/Post/util/scheduledPostLoop.ts | 7 +-- .../plugins/Reminders/commands/RemindCmd.ts | 23 +++++---- .../Reminders/commands/RemindersCmd.ts | 11 +++-- .../Reminders/utils/postDueRemindersLoop.ts | 6 +-- .../Spam/util/logAndDetectMessageSpam.ts | 3 +- .../src/plugins/Spam/util/saveSpamArchives.ts | 2 +- .../Starboard/util/saveMessageToStarboard.ts | 2 - backend/src/plugins/Tags/TagsPlugin.ts | 18 ++++--- .../src/plugins/Tags/commands/TagSourceCmd.ts | 2 +- .../src/plugins/Utility/commands/AboutCmd.ts | 4 +- .../src/plugins/Utility/commands/SourceCmd.ts | 2 +- .../Utility/functions/getChannelInfoEmbed.ts | 7 ++- .../Utility/functions/getInviteInfoEmbed.ts | 6 +-- .../Utility/functions/getMessageInfoEmbed.ts | 30 +++++++----- .../Utility/functions/getServerInfoEmbed.ts | 9 ++-- .../functions/getSnowflakeInfoEmbed.ts | 7 ++- .../Utility/functions/getUserInfoEmbed.ts | 25 ++++++---- backend/src/plugins/Utility/search.ts | 2 +- backend/src/types.ts | 37 ++++++++++++-- backend/src/utils.ts | 6 +-- backend/src/utils/dateFormats.ts | 17 +++++++ backend/src/utils/timezones.ts | 21 ++++++++ 51 files changed, 326 insertions(+), 168 deletions(-) create mode 100644 backend/src/utils/dateFormats.ts create mode 100644 backend/src/utils/timezones.ts diff --git a/backend/ormconfig.js b/backend/ormconfig.js index b7ffe433..d9b4770b 100644 --- a/backend/ormconfig.js +++ b/backend/ormconfig.js @@ -41,7 +41,7 @@ module.exports = { typeCast(field, next) { if (field.type === 'DATETIME') { const val = field.string(); - return val != null ? moment(val).format('YYYY-MM-DD HH:mm:ss') : null; + return val != null ? moment.utc(val).format('YYYY-MM-DD HH:mm:ss') : null; } return next(); diff --git a/backend/src/api/archives.ts b/backend/src/api/archives.ts index 0193ca9a..83fdbf83 100644 --- a/backend/src/api/archives.ts +++ b/backend/src/api/archives.ts @@ -18,12 +18,13 @@ export function initArchives(app: express.Express) { let body = archive.body; // Add some metadata at the end of the log file (but only if it doesn't already have it directly in the body) + // TODO: Use server timezone / date formats if (archive.body.indexOf("Log file generated on") === -1) { - const createdAt = moment(archive.created_at).format("YYYY-MM-DD [at] HH:mm:ss [(+00:00)]"); + const createdAt = moment.utc(archive.created_at).format("YYYY-MM-DD [at] HH:mm:ss [(+00:00)]"); body += `\n\nLog file generated on ${createdAt}`; if (archive.expires_at !== null) { - const expiresAt = moment(archive.expires_at).format("YYYY-MM-DD [at] HH:mm:ss [(+00:00)]"); + const expiresAt = moment.utc(archive.expires_at).format("YYYY-MM-DD [at] HH:mm:ss [(+00:00)]"); body += `\nExpires at ${expiresAt}`; } } diff --git a/backend/src/configValidator.ts b/backend/src/configValidator.ts index 9a8b3c8b..9d2d4c07 100644 --- a/backend/src/configValidator.ts +++ b/backend/src/configValidator.ts @@ -2,35 +2,27 @@ import * as t from "io-ts"; import { guildPlugins } from "./plugins/availablePlugins"; import { decodeAndValidateStrict, StrictValidationError } from "./validatorUtils"; import { ZeppelinPlugin } from "./plugins/ZeppelinPlugin"; -import { IZeppelinGuildConfig } from "./types"; +import { PartialZeppelinGuildConfigSchema, ZeppelinGuildConfig } from "./types"; import { configUtils, ConfigValidationError, PluginOptions } from "knub"; +import moment from "moment-timezone"; const pluginNameToPlugin = new Map(); for (const plugin of guildPlugins) { pluginNameToPlugin.set(plugin.name, plugin); } -const guildConfigRootSchema = t.type({ - prefix: t.string, - levels: t.record(t.string, t.number), - success_emoji: t.string, - plugins: t.record(t.string, t.unknown), -}); -const partialGuildConfigRootSchema = t.partial(guildConfigRootSchema.props); - -const globalConfigRootSchema = t.type({ - url: t.string, - owners: t.array(t.string), - plugins: t.record(t.string, t.unknown), -}); - -const partialMegaTest = t.partial({ name: t.string }); - export async function validateGuildConfig(config: any): Promise { - const validationResult = decodeAndValidateStrict(partialGuildConfigRootSchema, config); + const validationResult = decodeAndValidateStrict(PartialZeppelinGuildConfigSchema, config); if (validationResult instanceof StrictValidationError) return validationResult.getErrors(); - const guildConfig = config as IZeppelinGuildConfig; + const guildConfig = config as ZeppelinGuildConfig; + + if (guildConfig.timezone) { + const validTimezones = moment.tz.names(); + if (!validTimezones.includes(guildConfig.timezone)) { + return `Invalid timezone: ${guildConfig.timezone}`; + } + } if (guildConfig.plugins) { for (const [pluginName, pluginOptions] of Object.entries(guildConfig.plugins)) { diff --git a/backend/src/data/ApiLogins.ts b/backend/src/data/ApiLogins.ts index f08e1ebd..62572c78 100644 --- a/backend/src/data/ApiLogins.ts +++ b/backend/src/data/ApiLogins.ts @@ -5,7 +5,7 @@ import crypto from "crypto"; import moment from "moment-timezone"; // tslint:disable-next-line:no-submodule-imports import uuidv4 from "uuid/v4"; -import { DBDateFormat } from "../utils"; +import { DBDateFormat } from "../utils/dateFormats"; export class ApiLogins extends BaseRepository { private apiLogins: Repository; @@ -65,8 +65,9 @@ export class ApiLogins extends BaseRepository { id: loginId, token: hashedToken, user_id: userId, - logged_in_at: moment().format(DBDateFormat), - expires_at: moment() + logged_in_at: moment.utc().format(DBDateFormat), + expires_at: moment + .utc() .add(1, "day") .format(DBDateFormat), }); @@ -81,7 +82,7 @@ export class ApiLogins extends BaseRepository { return this.apiLogins.update( { id: loginId }, { - expires_at: moment().format(DBDateFormat), + expires_at: moment.utc().format(DBDateFormat), }, ); } diff --git a/backend/src/data/ApiUserInfo.ts b/backend/src/data/ApiUserInfo.ts index c7ef64d2..37e96ebb 100644 --- a/backend/src/data/ApiUserInfo.ts +++ b/backend/src/data/ApiUserInfo.ts @@ -3,7 +3,7 @@ import { ApiUserInfo as ApiUserInfoEntity, ApiUserInfoData } from "./entities/Ap import { BaseRepository } from "./BaseRepository"; import { connection } from "./db"; import moment from "moment-timezone"; -import { DBDateFormat } from "../utils"; +import { DBDateFormat } from "../utils/dateFormats"; export class ApiUserInfo extends BaseRepository { private apiUserInfo: Repository; @@ -26,7 +26,7 @@ export class ApiUserInfo extends BaseRepository { const repo = entityManager.getRepository(ApiUserInfoEntity); const existingInfo = await repo.findOne({ where: { id } }); - const updatedAt = moment().format(DBDateFormat); + const updatedAt = moment.utc().format(DBDateFormat); if (existingInfo) { await repo.update({ id }, { data, updated_at: updatedAt }); diff --git a/backend/src/data/DefaultLogMessages.json b/backend/src/data/DefaultLogMessages.json index 20f957d2..868318a8 100644 --- a/backend/src/data/DefaultLogMessages.json +++ b/backend/src/data/DefaultLogMessages.json @@ -57,8 +57,8 @@ "MEMBER_MUTE_REJOIN": "⚠ Reapplied active mute for {userMention(member)} on rejoin", - "SCHEDULED_MESSAGE": "⏰ {userMention(author)} scheduled a message to be posted to {channelMention(channel)} on {date} at {time} (UTC)", - "SCHEDULED_REPEATED_MESSAGE": "⏰ {userMention(author)} scheduled a message to be posted to {channelMention(channel)} on {date} at {time} (UTC), repeated {repeatDetails}", + "SCHEDULED_MESSAGE": "⏰ {userMention(author)} scheduled a message to be posted to {channelMention(channel)} on {datetime}", + "SCHEDULED_REPEATED_MESSAGE": "⏰ {userMention(author)} scheduled a message to be posted to {channelMention(channel)} on {datetime}, repeated {repeatDetails}", "REPEATED_MESSAGE": "⏰ {userMention(author)} scheduled a message to be posted to {channelMention(channel)} {repeatDetails}", "POSTED_SCHEDULED_MESSAGE": "\uD83D\uDCE8 Posted scheduled message (`{messageId}`) to {channelMention(channel)} as scheduled by {userMention(author)}", diff --git a/backend/src/data/GuildArchives.ts b/backend/src/data/GuildArchives.ts index 206abd0e..31ca854f 100644 --- a/backend/src/data/GuildArchives.ts +++ b/backend/src/data/GuildArchives.ts @@ -58,7 +58,7 @@ export class GuildArchives extends BaseGuildRepository { */ async create(body: string, expiresAt: moment.Moment = null): Promise { if (!expiresAt) { - expiresAt = moment().add(DEFAULT_EXPIRY_DAYS, "days"); + expiresAt = moment.utc().add(DEFAULT_EXPIRY_DAYS, "days"); } const result = await this.archives.insert({ @@ -78,7 +78,7 @@ export class GuildArchives extends BaseGuildRepository { const line = await renderTemplate(MESSAGE_ARCHIVE_MESSAGE_FORMAT, { id: msg.id, - timestamp: moment(msg.posted_at).format("YYYY-MM-DD HH:mm:ss"), + timestamp: moment.utc(msg.posted_at).format("YYYY-MM-DD HH:mm:ss"), content: msg.data.content, user, channel, @@ -89,7 +89,9 @@ export class GuildArchives extends BaseGuildRepository { } async createFromSavedMessages(savedMessages: SavedMessage[], guild: Guild, expiresAt = null) { - if (expiresAt == null) expiresAt = moment().add(DEFAULT_EXPIRY_DAYS, "days"); + if (expiresAt == null) { + expiresAt = moment.utc().add(DEFAULT_EXPIRY_DAYS, "days"); + } const headerStr = await renderTemplate(MESSAGE_ARCHIVE_HEADER_FORMAT, { guild }); const msgLines = await this.renderLinesFromSavedMessages(savedMessages, guild); diff --git a/backend/src/data/GuildCases.ts b/backend/src/data/GuildCases.ts index 8e4c2400..8e660311 100644 --- a/backend/src/data/GuildCases.ts +++ b/backend/src/data/GuildCases.ts @@ -6,6 +6,7 @@ import { disableLinkPreviews } from "../utils"; import { CaseTypes } from "./CaseTypes"; import moment = require("moment-timezone"); import { connection } from "./db"; +import { DBDateFormat } from "../utils/dateFormats"; const CASE_SUMMARY_REASON_MAX_LENGTH = 300; @@ -158,6 +159,7 @@ export class GuildCases extends BaseGuildRepository { }); } + // TODO: Move this to the cases plugin, use server timezone + date formats getSummaryText(theCase: Case) { const firstNote = theCase.notes[0]; let reason = firstNote ? firstNote.body : ""; @@ -172,7 +174,7 @@ export class GuildCases extends BaseGuildRepository { reason = disableLinkPreviews(reason); - const timestamp = moment(theCase.created_at).format("YYYY-MM-DD"); + const timestamp = moment.utc(theCase.created_at, DBDateFormat).format("YYYY-MM-DD"); let line = `\`[${timestamp}]\` \`Case #${theCase.case_number}\` __${CaseTypes[theCase.type]}__ ${reason}`; if (theCase.notes.length > 1) { line += ` *(+${theCase.notes.length - 1} ${theCase.notes.length === 2 ? "note" : "notes"})*`; diff --git a/backend/src/data/GuildMutes.ts b/backend/src/data/GuildMutes.ts index 697a0e26..9f0ca9e3 100644 --- a/backend/src/data/GuildMutes.ts +++ b/backend/src/data/GuildMutes.ts @@ -36,7 +36,8 @@ export class GuildMutes extends BaseGuildRepository { async addMute(userId, expiryTime): Promise { const expiresAt = expiryTime - ? moment() + ? moment + .utc() .add(expiryTime, "ms") .format("YYYY-MM-DD HH:mm:ss") : null; @@ -52,7 +53,8 @@ export class GuildMutes extends BaseGuildRepository { async updateExpiryTime(userId, newExpiryTime) { const expiresAt = newExpiryTime - ? moment() + ? moment + .utc() .add(newExpiryTime, "ms") .format("YYYY-MM-DD HH:mm:ss") : null; diff --git a/backend/src/data/GuildSavedMessages.ts b/backend/src/data/GuildSavedMessages.ts index 474f8003..67d0e4e5 100644 --- a/backend/src/data/GuildSavedMessages.ts +++ b/backend/src/data/GuildSavedMessages.ts @@ -182,7 +182,7 @@ export class GuildSavedMessages extends BaseGuildRepository { * If any messages were marked as deleted, also emits the deleteBulk event. */ async markBulkAsDeleted(ids) { - const deletedAt = moment().format("YYYY-MM-DD HH:mm:ss"); + const deletedAt = moment.utc().format("YYYY-MM-DD HH:mm:ss"); await this.messages .createQueryBuilder() diff --git a/backend/src/data/GuildSlowmodes.ts b/backend/src/data/GuildSlowmodes.ts index 80132235..ed042021 100644 --- a/backend/src/data/GuildSlowmodes.ts +++ b/backend/src/data/GuildSlowmodes.ts @@ -67,7 +67,8 @@ export class GuildSlowmodes extends BaseGuildRepository { const slowmode = await this.getChannelSlowmode(channelId); if (!slowmode) return; - const expiresAt = moment() + const expiresAt = moment + .utc() .add(slowmode.slowmode_seconds, "seconds") .format("YYYY-MM-DD HH:mm:ss"); diff --git a/backend/src/data/cleanup/configs.ts b/backend/src/data/cleanup/configs.ts index 4775b74f..c0c6b8f0 100644 --- a/backend/src/data/cleanup/configs.ts +++ b/backend/src/data/cleanup/configs.ts @@ -2,7 +2,7 @@ import { connection } from "../db"; import { getRepository, In } from "typeorm"; import { Config } from "../entities/Config"; import moment from "moment-timezone"; -import { DBDateFormat } from "../../utils"; +import { DBDateFormat } from "../../utils/dateFormats"; const CLEAN_PER_LOOP = 50; @@ -13,7 +13,8 @@ export async function cleanupConfigs() { let rows; // >1 month old: 1 config retained per month - const oneMonthCutoff = moment() + const oneMonthCutoff = moment + .utc() .subtract(30, "days") .format(DBDateFormat); do { @@ -53,7 +54,8 @@ export async function cleanupConfigs() { } while (rows.length === CLEAN_PER_LOOP); // >2 weeks old: 1 config retained per day - const twoWeekCutoff = moment() + const twoWeekCutoff = moment + .utc() .subtract(2, "weeks") .format(DBDateFormat); do { diff --git a/backend/src/data/cleanup/messages.ts b/backend/src/data/cleanup/messages.ts index ddb42232..4eb3e961 100644 --- a/backend/src/data/cleanup/messages.ts +++ b/backend/src/data/cleanup/messages.ts @@ -1,8 +1,9 @@ -import { DAYS, DBDateFormat, MINUTES } from "../../utils"; +import { DAYS, MINUTES } from "../../utils"; import { getRepository, In } from "typeorm"; import { SavedMessage } from "../entities/SavedMessage"; import moment from "moment-timezone"; import { connection } from "../db"; +import { DBDateFormat } from "../../utils/dateFormats"; /** * How long message edits, deletions, etc. will include the original message content. @@ -18,13 +19,16 @@ export async function cleanupMessages(): Promise { const messagesRepository = getRepository(SavedMessage); - const deletedAtThreshold = moment() + const deletedAtThreshold = moment + .utc() .subtract(DELETED_MESSAGE_RETENTION_PERIOD, "ms") .format(DBDateFormat); - const postedAtThreshold = moment() + const postedAtThreshold = moment + .utc() .subtract(RETENTION_PERIOD, "ms") .format(DBDateFormat); - const botPostedAtThreshold = moment() + const botPostedAtThreshold = moment + .utc() .subtract(BOT_MESSAGE_RETENTION_PERIOD, "ms") .format(DBDateFormat); diff --git a/backend/src/data/cleanup/nicknames.ts b/backend/src/data/cleanup/nicknames.ts index 3f41084d..b62bb287 100644 --- a/backend/src/data/cleanup/nicknames.ts +++ b/backend/src/data/cleanup/nicknames.ts @@ -1,8 +1,9 @@ import { getRepository, In } from "typeorm"; import moment from "moment-timezone"; import { NicknameHistoryEntry } from "../entities/NicknameHistoryEntry"; -import { DAYS, DBDateFormat } from "../../utils"; +import { DAYS } from "../../utils"; import { connection } from "../db"; +import { DBDateFormat } from "../../utils/dateFormats"; export const NICKNAME_RETENTION_PERIOD = 30 * DAYS; const CLEAN_PER_LOOP = 500; @@ -11,7 +12,8 @@ export async function cleanupNicknames(): Promise { let cleaned = 0; const nicknameHistoryRepository = getRepository(NicknameHistoryEntry); - const dateThreshold = moment() + const dateThreshold = moment + .utc() .subtract(NICKNAME_RETENTION_PERIOD, "ms") .format(DBDateFormat); diff --git a/backend/src/data/cleanup/usernames.ts b/backend/src/data/cleanup/usernames.ts index 71afcfbc..b583eb2d 100644 --- a/backend/src/data/cleanup/usernames.ts +++ b/backend/src/data/cleanup/usernames.ts @@ -1,8 +1,9 @@ import { getRepository, In } from "typeorm"; import moment from "moment-timezone"; import { UsernameHistoryEntry } from "../entities/UsernameHistoryEntry"; -import { DAYS, DBDateFormat } from "../../utils"; +import { DAYS } from "../../utils"; import { connection } from "../db"; +import { DBDateFormat } from "../../utils/dateFormats"; export const USERNAME_RETENTION_PERIOD = 30 * DAYS; const CLEAN_PER_LOOP = 500; @@ -11,7 +12,8 @@ export async function cleanupUsernames(): Promise { let cleaned = 0; const usernameHistoryRepository = getRepository(UsernameHistoryEntry); - const dateThreshold = moment() + const dateThreshold = moment + .utc() .subtract(USERNAME_RETENTION_PERIOD, "ms") .format(DBDateFormat); diff --git a/backend/src/index.ts b/backend/src/index.ts index 6057d587..e9f057ac 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -15,7 +15,7 @@ import { baseGuildPlugins, globalPlugins, guildPlugins } from "./plugins/availab import { errorMessage, isDiscordHTTPError, isDiscordRESTError, MINUTES, successMessage } from "./utils"; import { startUptimeCounter } from "./uptime"; import { AllowedGuilds } from "./data/AllowedGuilds"; -import { IZeppelinGlobalConfig, IZeppelinGuildConfig } from "./types"; +import { ZeppelinGlobalConfig, ZeppelinGuildConfig } from "./types"; import { RecoverablePluginError } from "./RecoverablePluginError"; import { GuildLogs } from "./data/GuildLogs"; import { LogType } from "./data/LogType"; @@ -138,7 +138,7 @@ connect().then(async () => { const allowedGuilds = new AllowedGuilds(); const guildConfigs = new Configs(); - const bot = new Knub(client, { + const bot = new Knub(client, { guildPlugins, globalPlugins, diff --git a/backend/src/plugins/AutoDelete/util/deleteNextItem.ts b/backend/src/plugins/AutoDelete/util/deleteNextItem.ts index 593914df..19d20971 100644 --- a/backend/src/plugins/AutoDelete/util/deleteNextItem.ts +++ b/backend/src/plugins/AutoDelete/util/deleteNextItem.ts @@ -5,6 +5,8 @@ import { LogType } from "src/data/LogType"; import { stripObjectToScalars, resolveUser } from "src/utils"; import { logger } from "src/logger"; import { scheduleNextDeletion } from "./scheduleNextDeletion"; +import { inGuildTz } from "../../../utils/timezones"; +import { getDateFormat } from "../../../utils/dateFormats"; export async function deleteNextItem(pluginData: PluginData) { const [itemToDelete] = pluginData.state.deletionQueue.splice(0, 1); @@ -17,7 +19,9 @@ export async function deleteNextItem(pluginData: PluginData, @@ -15,7 +17,7 @@ export async function getCaseEmbed( const theCase = await pluginData.state.cases.with("notes").find(resolveCaseId(caseOrCaseId)); if (!theCase) return null; - const createdAt = moment(theCase.created_at); + const createdAt = moment.utc(theCase.created_at); const actionTypeStr = CaseTypes[theCase.type].toUpperCase(); let userName = theCase.user_name; @@ -27,7 +29,7 @@ export async function getCaseEmbed( const embed: any = { title: `${actionTypeStr} - Case #${theCase.case_number}`, footer: { - text: `Case created at ${createdAt.format("YYYY-MM-DD [at] HH:mm")}`, + text: `Case created on ${inGuildTz(pluginData, createdAt).format(getDateFormat(pluginData, "pretty_datetime"))}`, }, fields: [ { @@ -57,7 +59,7 @@ export async function getCaseEmbed( if (theCase.notes.length) { theCase.notes.forEach((note: any) => { - const noteDate = moment(note.created_at); + const noteDate = moment.utc(note.created_at); let noteBody = note.body.trim(); if (noteBody === "") { noteBody = emptyEmbedValue; @@ -67,8 +69,9 @@ export async function getCaseEmbed( for (let i = 0; i < chunks.length; i++) { if (i === 0) { + const prettyNoteDate = inGuildTz(pluginData, noteDate).format(getDateFormat(pluginData, "pretty_datetime")); embed.fields.push({ - name: `${note.mod_name} at ${noteDate.format("YYYY-MM-DD [at] HH:mm")}:`, + name: `${note.mod_name} at ${prettyNoteDate}:`, value: chunks[i], }); } else { diff --git a/backend/src/plugins/ChannelArchiver/commands/ArchiveChannelCmd.ts b/backend/src/plugins/ChannelArchiver/commands/ArchiveChannelCmd.ts index 61eff881..38297182 100644 --- a/backend/src/plugins/ChannelArchiver/commands/ArchiveChannelCmd.ts +++ b/backend/src/plugins/ChannelArchiver/commands/ArchiveChannelCmd.ts @@ -4,6 +4,8 @@ import { isOwner, sendErrorMessage } from "src/pluginUtils"; import { confirm, SECONDS, noop } from "src/utils"; import moment from "moment-timezone"; import { rehostAttachment } from "../rehostAttachment"; +import { inGuildTz } from "../../../utils/timezones"; +import { getDateFormat } from "../../../utils/dateFormats"; const MAX_ARCHIVED_MESSAGES = 5000; const MAX_MESSAGES_PER_FETCH = 100; @@ -96,7 +98,7 @@ export const ArchiveChannelCmd = channelArchiverCmd({ archiveLines.reverse(); - const nowTs = moment().format("YYYY-MM-DD HH:mm:ss"); + const nowTs = inGuildTz(pluginData).format(getDateFormat(pluginData, "pretty_datetime")); let result = `Archived ${archiveLines.length} messages from #${args.channel.name} at ${nowTs}`; result += `\n\n${archiveLines.join("\n")}\n`; @@ -104,7 +106,7 @@ export const ArchiveChannelCmd = channelArchiverCmd({ progressMsg.delete().catch(noop); msg.channel.createMessage("Archive created!", { file: Buffer.from(result), - name: `archive-${args.channel.name}-${moment().format("YYYY-MM-DD-HH-mm-ss")}.txt`, + name: `archive-${args.channel.name}-${moment.utc().format("YYYY-MM-DD-HH-mm-ss")}.txt`, }); }, }); diff --git a/backend/src/plugins/LocateUser/commands/FollowCmd.ts b/backend/src/plugins/LocateUser/commands/FollowCmd.ts index bcbd253d..1f3bd7db 100644 --- a/backend/src/plugins/LocateUser/commands/FollowCmd.ts +++ b/backend/src/plugins/LocateUser/commands/FollowCmd.ts @@ -21,7 +21,7 @@ export const FollowCmd = locateUserCommand({ async run({ message: msg, args, pluginData }) { const time = args.duration || 10 * MINUTES; - const alertTime = moment().add(time, "millisecond"); + const alertTime = moment.utc().add(time, "millisecond"); const body = args.reminder || "None"; const active = args.active || false; diff --git a/backend/src/plugins/Logs/LogsPlugin.ts b/backend/src/plugins/Logs/LogsPlugin.ts index 51ba8d0a..0fb577ac 100644 --- a/backend/src/plugins/Logs/LogsPlugin.ts +++ b/backend/src/plugins/Logs/LogsPlugin.ts @@ -26,7 +26,7 @@ const defaultOptions: PluginOptions = { config: { channels: {}, format: { - timestamp: "YYYY-MM-DD HH:mm:ss", + timestamp: "YYYY-MM-DD HH:mm:ss z", ...DefaultLogMessages, }, ping_user: true, diff --git a/backend/src/plugins/Logs/events/LogsGuildMemberAddEvt.ts b/backend/src/plugins/Logs/events/LogsGuildMemberAddEvt.ts index 16c542a7..55146a30 100644 --- a/backend/src/plugins/Logs/events/LogsGuildMemberAddEvt.ts +++ b/backend/src/plugins/Logs/events/LogsGuildMemberAddEvt.ts @@ -11,8 +11,8 @@ export const LogsGuildMemberAddEvt = logsEvent({ const pluginData = meta.pluginData; const member = meta.args.member; - const newThreshold = moment().valueOf() - 1000 * 60 * 60; - const accountAge = humanizeDuration(moment().valueOf() - member.createdAt, { + const newThreshold = moment.utc().valueOf() - 1000 * 60 * 60; + const accountAge = humanizeDuration(moment.utc().valueOf() - member.createdAt, { largest: 2, round: true, }); diff --git a/backend/src/plugins/Logs/util/getLogMessage.ts b/backend/src/plugins/Logs/util/getLogMessage.ts index d926db79..527f3fe1 100644 --- a/backend/src/plugins/Logs/util/getLogMessage.ts +++ b/backend/src/plugins/Logs/util/getLogMessage.ts @@ -13,6 +13,7 @@ import { SavedMessage } from "src/data/entities/SavedMessage"; import { renderTemplate, TemplateParseError } from "src/templateFormatter"; import { logger } from "src/logger"; import moment from "moment-timezone"; +import { inGuildTz } from "../../../utils/timezones"; export async function getLogMessage( pluginData: PluginData, @@ -87,7 +88,7 @@ export async function getLogMessage( const timestampFormat = config.format.timestamp; if (timestampFormat) { - const timestamp = moment().format(timestampFormat); + const timestamp = inGuildTz(pluginData).format(timestampFormat); formatted = `\`[${timestamp}]\` ${formatted}`; } } diff --git a/backend/src/plugins/Logs/util/onMessageDelete.ts b/backend/src/plugins/Logs/util/onMessageDelete.ts index 70e2b65c..0ff8de5c 100644 --- a/backend/src/plugins/Logs/util/onMessageDelete.ts +++ b/backend/src/plugins/Logs/util/onMessageDelete.ts @@ -5,6 +5,7 @@ import { LogType } from "src/data/LogType"; import moment from "moment-timezone"; import { PluginData } from "knub"; import { LogsPluginType } from "../types"; +import { inGuildTz } from "../../../utils/timezones"; export async function onMessageDelete(pluginData: PluginData, savedMessage: SavedMessage) { const user = await resolveUser(pluginData.client, savedMessage.user_id); @@ -23,7 +24,9 @@ export async function onMessageDelete(pluginData: PluginData, sa { user: stripObjectToScalars(user), channel: stripObjectToScalars(channel), - messageDate: moment(savedMessage.data.timestamp, "x").format(pluginData.config.get().format.timestamp), + messageDate: inGuildTz(pluginData, moment.utc(savedMessage.data.timestamp, "x")).format( + pluginData.config.get().format.timestamp, + ), message: savedMessage, }, savedMessage.id, diff --git a/backend/src/plugins/ModActions/commands/DeleteCaseCmd.ts b/backend/src/plugins/ModActions/commands/DeleteCaseCmd.ts index 5d6241fb..3c96577e 100644 --- a/backend/src/plugins/ModActions/commands/DeleteCaseCmd.ts +++ b/backend/src/plugins/ModActions/commands/DeleteCaseCmd.ts @@ -8,6 +8,8 @@ import { SECONDS, stripObjectToScalars, trimLines } from "../../../utils"; import { LogsPlugin } from "../../Logs/LogsPlugin"; import { LogType } from "../../../data/LogType"; import moment from "moment-timezone"; +import { inGuildTz } from "../../../utils/timezones"; +import { getDateFormat } from "../../../utils/dateFormats"; export const DeleteCaseCmd = modActionsCommand({ trigger: ["delete_case", "deletecase"], @@ -52,7 +54,7 @@ export const DeleteCaseCmd = modActionsCommand({ } const deletedByName = `${message.author.username}#${message.author.discriminator}`; - const deletedAt = moment().format(`MMM D, YYYY [at] H:mm [UTC]`); + const deletedAt = inGuildTz(pluginData).format(getDateFormat(pluginData, "pretty_datetime")); await pluginData.state.cases.softDelete( theCase.id, diff --git a/backend/src/plugins/Mutes/commands/MutesCmd.ts b/backend/src/plugins/Mutes/commands/MutesCmd.ts index 6a643da7..0ed7ce68 100644 --- a/backend/src/plugins/Mutes/commands/MutesCmd.ts +++ b/backend/src/plugins/Mutes/commands/MutesCmd.ts @@ -1,10 +1,11 @@ import { command } from "knub"; import { IMuteWithDetails, MutesPluginType } from "../types"; import { commandTypeHelpers as ct } from "../../../commandTypes"; -import { DBDateFormat, isFullMessage, MINUTES, noop, resolveMember } from "../../../utils"; +import { isFullMessage, MINUTES, noop, resolveMember } from "../../../utils"; import moment from "moment-timezone"; import { humanizeDurationShort } from "../../../humanizeDurationShort"; import { getBaseUrl } from "../../../pluginUtils"; +import { DBDateFormat } from "../../../utils/dateFormats"; export const MutesCmd = command()({ trigger: "mutes", @@ -70,7 +71,8 @@ export const MutesCmd = command()({ // Filter: mute age if (args.age) { - const cutoff = moment() + const cutoff = moment + .utc() .subtract(args.age, "ms") .format(DBDateFormat); filteredMutes = filteredMutes.filter(m => m.created_at <= cutoff); @@ -119,14 +121,14 @@ export const MutesCmd = command()({ let line = `<@!${mute.user_id}> (**${username}**, \`${mute.user_id}\`) 📋 ${caseName}`; if (mute.expires_at) { - const timeUntilExpiry = moment().diff(moment(mute.expires_at, DBDateFormat)); + const timeUntilExpiry = moment.utc().diff(moment.utc(mute.expires_at, DBDateFormat)); const humanizedTime = humanizeDurationShort(timeUntilExpiry, { largest: 2, round: true }); line += ` ⏰ Expires in ${humanizedTime}`; } else { line += ` ⏰ Indefinite`; } - const timeFromMute = moment(mute.created_at, DBDateFormat).diff(moment()); + const timeFromMute = moment.utc(mute.created_at, DBDateFormat).diff(moment.utc()); const humanizedTimeFromMute = humanizeDurationShort(timeFromMute, { largest: 2, round: true }); line += ` 🕒 Muted ${humanizedTimeFromMute} ago`; @@ -184,7 +186,7 @@ export const MutesCmd = command()({ listMessage.edit("No active mutes!"); } } else if (args.export) { - const archiveId = await pluginData.state.archives.create(lines.join("\n"), moment().add(1, "hour")); + const archiveId = await pluginData.state.archives.create(lines.join("\n"), moment.utc().add(1, "hour")); const baseUrl = getBaseUrl(pluginData); const url = await pluginData.state.archives.getUrl(baseUrl, archiveId); diff --git a/backend/src/plugins/Post/commands/ScheduledPostsListCmd.ts b/backend/src/plugins/Post/commands/ScheduledPostsListCmd.ts index 85d3f950..82abf191 100644 --- a/backend/src/plugins/Post/commands/ScheduledPostsListCmd.ts +++ b/backend/src/plugins/Post/commands/ScheduledPostsListCmd.ts @@ -1,6 +1,9 @@ import { postCmd } from "../types"; import { trimLines, sorter, disableCodeBlocks, deactivateMentions, createChunkedMessage } from "src/utils"; import humanizeDuration from "humanize-duration"; +import moment from "moment-timezone"; +import { inGuildTz } from "../../../utils/timezones"; +import { DBDateFormat, getDateFormat } from "../../../utils/dateFormats"; const SCHEDULED_POST_PREVIEW_TEXT_LENGTH = 50; @@ -28,7 +31,10 @@ export const ScheduledPostsListCmd = postCmd({ .replace(/\s+/g, " ") .slice(0, SCHEDULED_POST_PREVIEW_TEXT_LENGTH); - const parts = [`\`#${i++}\` \`[${p.post_at}]\` ${previewText}${isTruncated ? "..." : ""}`]; + const prettyPostAt = inGuildTz(pluginData, moment.utc(p.post_at, DBDateFormat)).format( + getDateFormat(pluginData, "pretty_datetime"), + ); + const parts = [`\`#${i++}\` \`[${prettyPostAt}]\` ${previewText}${isTruncated ? "..." : ""}`]; if (p.attachments.length) parts.push("*(with attachment)*"); if (p.content.embed) parts.push("*(embed)*"); if (p.repeat_until) { diff --git a/backend/src/plugins/Post/util/actualPostCmd.ts b/backend/src/plugins/Post/util/actualPostCmd.ts index 556d05a0..4090eed1 100644 --- a/backend/src/plugins/Post/util/actualPostCmd.ts +++ b/backend/src/plugins/Post/util/actualPostCmd.ts @@ -1,5 +1,5 @@ import { Message, Channel, TextChannel } from "eris"; -import { StrictMessageContent, errorMessage, DBDateFormat, stripObjectToScalars, MINUTES } from "src/utils"; +import { StrictMessageContent, errorMessage, stripObjectToScalars, MINUTES } from "src/utils"; import moment from "moment-timezone"; import { LogType } from "src/data/LogType"; import humanizeDuration from "humanize-duration"; @@ -8,10 +8,11 @@ import { PluginData } from "knub"; import { PostPluginType } from "../types"; import { parseScheduleTime } from "./parseScheduleTime"; import { postMessage } from "./postMessage"; +import { DBDateFormat, getDateFormat } from "../../../utils/dateFormats"; const MIN_REPEAT_TIME = 5 * MINUTES; const MAX_REPEAT_TIME = Math.pow(2, 32); -const MAX_REPEAT_UNTIL = moment().add(100, "years"); +const MAX_REPEAT_UNTIL = moment.utc().add(100, "years"); export async function actualPostCmd( pluginData: PluginData, @@ -53,12 +54,12 @@ export async function actualPostCmd( let postAt; if (opts.schedule) { // Schedule the post to be posted later - postAt = parseScheduleTime(opts.schedule); + postAt = parseScheduleTime(pluginData, opts.schedule); if (!postAt) { return sendErrorMessage(pluginData, msg.channel, "Invalid schedule time"); } } else if (opts.repeat) { - postAt = moment().add(opts.repeat, "ms"); + postAt = moment.utc().add(opts.repeat, "ms"); } // For repeated posts, make sure repeat-until or repeat-times is specified @@ -67,13 +68,13 @@ export async function actualPostCmd( let repeatDetailsStr: string = null; if (opts["repeat-until"]) { - repeatUntil = parseScheduleTime(opts["repeat-until"]); + repeatUntil = parseScheduleTime(pluginData, opts["repeat-until"]); // Invalid time if (!repeatUntil) { return sendErrorMessage(pluginData, msg.channel, "Invalid time specified for -repeat-until"); } - if (repeatUntil.isBefore(moment())) { + if (repeatUntil.isBefore(moment.utc())) { return sendErrorMessage(pluginData, msg.channel, "You can't set -repeat-until in the past"); } if (repeatUntil.isAfter(MAX_REPEAT_UNTIL)) { @@ -110,7 +111,7 @@ export async function actualPostCmd( // Save schedule/repeat information in DB if (postAt) { - if (postAt < moment()) { + if (postAt < moment.utc()) { return sendErrorMessage(pluginData, msg.channel, "Post can't be scheduled to be posted in the past"); } @@ -120,10 +121,18 @@ export async function actualPostCmd( channel_id: targetChannel.id, content, attachments: msg.attachments, - post_at: postAt.format(DBDateFormat), + post_at: postAt + .clone() + .tz("Etc/UTC") + .format(DBDateFormat), enable_mentions: opts["enable-mentions"], repeat_interval: opts.repeat, - repeat_until: repeatUntil ? repeatUntil.format(DBDateFormat) : null, + repeat_until: repeatUntil + ? repeatUntil + .clone() + .tz("Etc/UTC") + .format(DBDateFormat) + : null, repeat_times: repeatTimes ?? null, }); @@ -131,8 +140,9 @@ export async function actualPostCmd( pluginData.state.logs.log(LogType.SCHEDULED_REPEATED_MESSAGE, { author: stripObjectToScalars(msg.author), channel: stripObjectToScalars(targetChannel), - date: postAt.format("YYYY-MM-DD"), - time: postAt.format("HH:mm:ss"), + datetime: postAt.format(getDateFormat(pluginData, "pretty_datetime")), + date: postAt.format(getDateFormat(pluginData, "date")), + time: postAt.format(getDateFormat(pluginData, "time")), repeatInterval: humanizeDuration(opts.repeat), repeatDetails: repeatDetailsStr, }); @@ -140,8 +150,9 @@ export async function actualPostCmd( pluginData.state.logs.log(LogType.SCHEDULED_MESSAGE, { author: stripObjectToScalars(msg.author), channel: stripObjectToScalars(targetChannel), - date: postAt.format("YYYY-MM-DD"), - time: postAt.format("HH:mm:ss"), + datetime: postAt.format(getDateFormat(pluginData, "pretty_datetime")), + date: postAt.format(getDateFormat(pluginData, "date")), + time: postAt.format(getDateFormat(pluginData, "time")), }); } } @@ -155,8 +166,9 @@ export async function actualPostCmd( pluginData.state.logs.log(LogType.REPEATED_MESSAGE, { author: stripObjectToScalars(msg.author), channel: stripObjectToScalars(targetChannel), - date: postAt.format("YYYY-MM-DD"), - time: postAt.format("HH:mm:ss"), + datetime: postAt.format(getDateFormat(pluginData, "pretty_datetime")), + date: postAt.format(getDateFormat(pluginData, "date")), + time: postAt.format(getDateFormat(pluginData, "time")), repeatInterval: humanizeDuration(opts.repeat), repeatDetails: repeatDetailsStr, }); @@ -164,14 +176,16 @@ export async function actualPostCmd( // Bot reply schenanigans let successMessage = opts.schedule - ? `Message scheduled to be posted in <#${targetChannel.id}> on ${postAt.format("YYYY-MM-DD [at] HH:mm:ss")} (UTC)` + ? `Message scheduled to be posted in <#${targetChannel.id}> on ${postAt.format( + getDateFormat(pluginData, "pretty_datetime"), + )}` : `Message posted in <#${targetChannel.id}>`; if (opts.repeat) { successMessage += `. Message will be automatically reposted every ${humanizeDuration(opts.repeat)}`; if (repeatUntil) { - successMessage += ` until ${repeatUntil.format("YYYY-MM-DD [at] HH:mm:ss")} (UTC)`; + successMessage += ` until ${repeatUntil.format(getDateFormat(pluginData, "pretty_datetime"))}`; } else if (repeatTimes) { successMessage += `, ${repeatTimes} times in total`; } diff --git a/backend/src/plugins/Post/util/parseScheduleTime.ts b/backend/src/plugins/Post/util/parseScheduleTime.ts index c4d57231..3729df2e 100644 --- a/backend/src/plugins/Post/util/parseScheduleTime.ts +++ b/backend/src/plugins/Post/util/parseScheduleTime.ts @@ -1,31 +1,36 @@ import moment, { Moment } from "moment-timezone"; import { convertDelayStringToMS } from "src/utils"; +import { PluginData } from "knub"; +import { getGuildTz } from "../../../utils/timezones"; -export function parseScheduleTime(str): Moment { - const dt1 = moment(str, "YYYY-MM-DD HH:mm:ss"); +// TODO: Extract out of the Post plugin, use everywhere with a date input +export function parseScheduleTime(pluginData: PluginData, str: string): Moment { + const tz = getGuildTz(pluginData); + + const dt1 = moment.tz(str, "YYYY-MM-DD HH:mm:ss", tz); if (dt1 && dt1.isValid()) return dt1; - const dt2 = moment(str, "YYYY-MM-DD HH:mm"); + const dt2 = moment.tz(str, "YYYY-MM-DD HH:mm", tz); if (dt2 && dt2.isValid()) return dt2; - const date = moment(str, "YYYY-MM-DD"); + const date = moment.tz(str, "YYYY-MM-DD", tz); if (date && date.isValid()) return date; - const t1 = moment(str, "HH:mm:ss"); + const t1 = moment.tz(str, "HH:mm:ss", tz); if (t1 && t1.isValid()) { - if (t1.isBefore(moment())) t1.add(1, "day"); + if (t1.isBefore(moment.utc())) t1.add(1, "day"); return t1; } - const t2 = moment(str, "HH:mm"); + const t2 = moment.tz(str, "HH:mm", tz); if (t2 && t2.isValid()) { - if (t2.isBefore(moment())) t2.add(1, "day"); + if (t2.isBefore(moment.utc())) t2.add(1, "day"); return t2; } const delayStringMS = convertDelayStringToMS(str, "m"); if (delayStringMS) { - return moment().add(delayStringMS, "ms"); + return moment.tz(tz).add(delayStringMS, "ms"); } return null; diff --git a/backend/src/plugins/Post/util/scheduledPostLoop.ts b/backend/src/plugins/Post/util/scheduledPostLoop.ts index 124af05b..104c61a6 100644 --- a/backend/src/plugins/Post/util/scheduledPostLoop.ts +++ b/backend/src/plugins/Post/util/scheduledPostLoop.ts @@ -1,11 +1,12 @@ import { PluginData } from "knub"; import { PostPluginType } from "../types"; import { logger } from "src/logger"; -import { stripObjectToScalars, DBDateFormat, SECONDS } from "src/utils"; +import { stripObjectToScalars, SECONDS } from "src/utils"; import { LogType } from "src/data/LogType"; import moment from "moment-timezone"; import { TextChannel, User } from "eris"; import { postMessage } from "./postMessage"; +import { DBDateFormat } from "../../../utils/dateFormats"; const SCHEDULED_POST_CHECK_INTERVAL = 5 * SECONDS; @@ -49,10 +50,10 @@ export async function scheduledPostLoop(pluginData: PluginData) let shouldClear = true; if (post.repeat_interval) { - const nextPostAt = moment().add(post.repeat_interval, "ms"); + const nextPostAt = moment.utc().add(post.repeat_interval, "ms"); if (post.repeat_until) { - const repeatUntil = moment(post.repeat_until, DBDateFormat); + const repeatUntil = moment.utc(post.repeat_until, DBDateFormat); if (nextPostAt.isSameOrBefore(repeatUntil)) { await pluginData.state.scheduledPosts.update(post.id, { post_at: nextPostAt.format(DBDateFormat), diff --git a/backend/src/plugins/Reminders/commands/RemindCmd.ts b/backend/src/plugins/Reminders/commands/RemindCmd.ts index e2dfab86..6f2e4eae 100644 --- a/backend/src/plugins/Reminders/commands/RemindCmd.ts +++ b/backend/src/plugins/Reminders/commands/RemindCmd.ts @@ -4,6 +4,8 @@ import { convertDelayStringToMS } from "src/utils"; import humanizeDuration from "humanize-duration"; import { sendErrorMessage, sendSuccessMessage } from "src/pluginUtils"; import { remindersCommand } from "../types"; +import { getGuildTz, inGuildTz } from "../../../utils/timezones"; +import { getDateFormat } from "../../../utils/dateFormats"; export const RemindCmd = remindersCommand({ trigger: ["remind", "remindme"], @@ -16,19 +18,20 @@ export const RemindCmd = remindersCommand({ }, async run({ message: msg, args, pluginData }) { - const now = moment(); + const now = moment.utc(); + const tz = getGuildTz(pluginData); - let reminderTime; + let reminderTime: moment.Moment; if (args.time.match(/^\d{4}-\d{1,2}-\d{1,2}$/)) { // Date in YYYY-MM-DD format, remind at current time on that date - reminderTime = moment(args.time, "YYYY-M-D").set({ + reminderTime = moment.tz(args.time, "YYYY-M-D", tz).set({ hour: now.hour(), minute: now.minute(), second: now.second(), }); } else if (args.time.match(/^\d{4}-\d{1,2}-\d{1,2}T\d{2}:\d{2}$/)) { // Date and time in YYYY-MM-DD[T]HH:mm format - reminderTime = moment(args.time, "YYYY-M-D[T]HH:mm").second(0); + reminderTime = moment.tz(args.time, "YYYY-M-D[T]HH:mm", tz).second(0); } else { // "Delay string" i.e. e.g. "2h30m" const ms = convertDelayStringToMS(args.time); @@ -37,7 +40,7 @@ export const RemindCmd = remindersCommand({ return; } - reminderTime = moment().add(ms, "millisecond"); + reminderTime = moment.utc().add(ms, "millisecond"); } if (!reminderTime.isValid() || reminderTime.isBefore(now)) { @@ -50,17 +53,21 @@ export const RemindCmd = remindersCommand({ await pluginData.state.reminders.add( msg.author.id, msg.channel.id, - reminderTime.format("YYYY-MM-DD HH:mm:ss"), + reminderTime + .clone() + .tz("Etc/UTC") + .format("YYYY-MM-DD HH:mm:ss"), reminderBody, - moment().format("YYYY-MM-DD HH:mm:ss"), + moment.utc().format("YYYY-MM-DD HH:mm:ss"), ); const msUntilReminder = reminderTime.diff(now); const timeUntilReminder = humanizeDuration(msUntilReminder, { largest: 2, round: true }); + const prettyReminderTime = inGuildTz(pluginData, reminderTime).format(getDateFormat(pluginData, "pretty_datetime")); sendSuccessMessage( pluginData, msg.channel, - `I will remind you in **${timeUntilReminder}** at **${reminderTime.format("YYYY-MM-DD, HH:mm")}**`, + `I will remind you in **${timeUntilReminder}** at **${prettyReminderTime}**`, ); }, }); diff --git a/backend/src/plugins/Reminders/commands/RemindersCmd.ts b/backend/src/plugins/Reminders/commands/RemindersCmd.ts index 82cceaea..54c36f1d 100644 --- a/backend/src/plugins/Reminders/commands/RemindersCmd.ts +++ b/backend/src/plugins/Reminders/commands/RemindersCmd.ts @@ -3,6 +3,8 @@ import { sendErrorMessage } from "src/pluginUtils"; import { createChunkedMessage, sorter } from "src/utils"; import moment from "moment-timezone"; import humanizeDuration from "humanize-duration"; +import { inGuildTz } from "../../../utils/timezones"; +import { DBDateFormat, getDateFormat } from "../../../utils/dateFormats"; export const RemindersCmd = remindersCommand({ trigger: "reminders", @@ -20,10 +22,13 @@ export const RemindersCmd = remindersCommand({ const lines = Array.from(reminders.entries()).map(([i, reminder]) => { const num = i + 1; const paddedNum = num.toString().padStart(longestNum, " "); - const target = moment(reminder.remind_at, "YYYY-MM-DD HH:mm:ss"); - const diff = target.diff(moment()); + const target = moment.utc(reminder.remind_at, "YYYY-MM-DD HH:mm:ss"); + const diff = target.diff(moment.utc()); const result = humanizeDuration(diff, { largest: 2, round: true }); - return `\`${paddedNum}.\` \`${reminder.remind_at} (${result})\` ${reminder.body}`; + const prettyRemindAt = inGuildTz(pluginData, moment.utc(reminder.remind_at, DBDateFormat)).format( + getDateFormat(pluginData, "pretty_datetime"), + ); + return `\`${paddedNum}.\` \`${prettyRemindAt} (${result})\` ${reminder.body}`; }); createChunkedMessage(msg.channel, lines.join("\n")); diff --git a/backend/src/plugins/Reminders/utils/postDueRemindersLoop.ts b/backend/src/plugins/Reminders/utils/postDueRemindersLoop.ts index 18d94a2b..b4d972a6 100644 --- a/backend/src/plugins/Reminders/utils/postDueRemindersLoop.ts +++ b/backend/src/plugins/Reminders/utils/postDueRemindersLoop.ts @@ -16,9 +16,9 @@ export async function postDueRemindersLoop(pluginData: PluginData, @@ -36,7 +37,7 @@ export async function logAndDetectMessageSpam( pluginData.state.spamDetectionQueue = pluginData.state.spamDetectionQueue.then( async () => { - const timestamp = moment(savedMessage.posted_at).valueOf(); + const timestamp = moment.utc(savedMessage.posted_at, DBDateFormat).valueOf(); const member = await resolveMember(pluginData.client, pluginData.guild, savedMessage.user_id); // Log this action... diff --git a/backend/src/plugins/Spam/util/saveSpamArchives.ts b/backend/src/plugins/Spam/util/saveSpamArchives.ts index deee6c98..0e2a425e 100644 --- a/backend/src/plugins/Spam/util/saveSpamArchives.ts +++ b/backend/src/plugins/Spam/util/saveSpamArchives.ts @@ -5,7 +5,7 @@ import { getBaseUrl } from "src/pluginUtils"; const SPAM_ARCHIVE_EXPIRY_DAYS = 90; export async function saveSpamArchives(pluginData, savedMessages: SavedMessage[]) { - const expiresAt = moment().add(SPAM_ARCHIVE_EXPIRY_DAYS, "days"); + const expiresAt = moment.utc().add(SPAM_ARCHIVE_EXPIRY_DAYS, "days"); const archiveId = await pluginData.state.archives.createFromSavedMessages(savedMessages, pluginData.guild, expiresAt); return pluginData.state.archives.getUrl(getBaseUrl(pluginData), archiveId); diff --git a/backend/src/plugins/Starboard/util/saveMessageToStarboard.ts b/backend/src/plugins/Starboard/util/saveMessageToStarboard.ts index c060f8b5..8fc2be24 100644 --- a/backend/src/plugins/Starboard/util/saveMessageToStarboard.ts +++ b/backend/src/plugins/Starboard/util/saveMessageToStarboard.ts @@ -13,8 +13,6 @@ export async function saveMessageToStarboard( const channel = pluginData.guild.channels.get(starboard.channel_id); if (!channel) return; - const time = moment(msg.timestamp, "x").format("YYYY-MM-DD [at] HH:mm:ss [UTC]"); - const embed: Embed = { footer: { text: `#${(msg.channel as GuildChannel).name}`, diff --git a/backend/src/plugins/Tags/TagsPlugin.ts b/backend/src/plugins/Tags/TagsPlugin.ts index b98ed082..65499b90 100644 --- a/backend/src/plugins/Tags/TagsPlugin.ts +++ b/backend/src/plugins/Tags/TagsPlugin.ts @@ -15,6 +15,7 @@ import { TagSourceCmd } from "./commands/TagSourceCmd"; import moment from "moment-timezone"; import humanizeDuration from "humanize-duration"; import { convertDelayStringToMS } from "../../utils"; +import { getGuildTz, inGuildTz } from "../../utils/timezones"; const defaultOptions: PluginOptions = { config: { @@ -76,6 +77,7 @@ export const TagsPlugin = zeppelinPlugin()("tags", { state.onMessageDeleteFn = msg => onMessageDelete(pluginData, msg); state.savedMessages.events.on("delete", state.onMessageDeleteFn); + const tz = getGuildTz(pluginData); state.tagFunctions = { parseDateTime(str) { if (typeof str === "number") { @@ -86,13 +88,13 @@ export const TagsPlugin = zeppelinPlugin()("tags", { return Date.now(); } - return moment(str, "YYYY-MM-DD HH:mm:ss").valueOf(); + return moment.tz(str, "YYYY-MM-DD HH:mm:ss", tz).valueOf(); }, countdown(toDate) { - const target = moment(this.parseDateTime(toDate)); + const target = moment.utc(this.parseDateTime(toDate), "x"); - const now = moment(); + const now = moment.utc(); if (!target.isValid()) return ""; const diff = target.diff(now); @@ -119,7 +121,8 @@ export const TagsPlugin = zeppelinPlugin()("tags", { } const delayMS = convertDelayStringToMS(delay); - return moment(reference) + return moment + .utc(reference, "x") .add(delayMS) .valueOf(); }, @@ -139,7 +142,8 @@ export const TagsPlugin = zeppelinPlugin()("tags", { } const delayMS = convertDelayStringToMS(delay); - return moment(reference) + return moment + .utc(reference, "x") .subtract(delayMS) .valueOf(); }, @@ -150,13 +154,13 @@ export const TagsPlugin = zeppelinPlugin()("tags", { formatTime(time, format) { const parsed = this.parseDateTime(time); - return moment(parsed).format(format); + return inGuildTz(parsed).format(format); }, discordDateFormat(time) { const parsed = time ? this.parseDateTime(time) : Date.now(); - return moment(parsed).format("YYYY-MM-DD"); + return inGuildTz(parsed).format("YYYY-MM-DD"); }, mention: input => { diff --git a/backend/src/plugins/Tags/commands/TagSourceCmd.ts b/backend/src/plugins/Tags/commands/TagSourceCmd.ts index c4a8f914..27fe79e3 100644 --- a/backend/src/plugins/Tags/commands/TagSourceCmd.ts +++ b/backend/src/plugins/Tags/commands/TagSourceCmd.ts @@ -32,7 +32,7 @@ export const TagSourceCmd = tagsCmd({ return; } - const archiveId = await pluginData.state.archives.create(tag.body, moment().add(10, "minutes")); + const archiveId = await pluginData.state.archives.create(tag.body, moment.utc().add(10, "minutes")); const url = pluginData.state.archives.getUrl(getBaseUrl(pluginData), archiveId); msg.channel.createMessage(`Tag source:\n${url}`); diff --git a/backend/src/plugins/Utility/commands/AboutCmd.ts b/backend/src/plugins/Utility/commands/AboutCmd.ts index 119b4123..592dce23 100644 --- a/backend/src/plugins/Utility/commands/AboutCmd.ts +++ b/backend/src/plugins/Utility/commands/AboutCmd.ts @@ -6,6 +6,7 @@ import humanizeDuration from "humanize-duration"; import LCL from "last-commit-log"; import path from "path"; import moment from "moment-timezone"; +import { getGuildTz } from "../../../utils/timezones"; export const AboutCmd = utilityCmd({ trigger: "about", @@ -29,7 +30,7 @@ export const AboutCmd = utilityCmd({ let version; if (lastCommit) { - lastUpdate = moment(lastCommit.committer.date, "X").format("LL [at] H:mm [(UTC)]"); + lastUpdate = moment.utc(lastCommit.committer.date, "X").format("LL [at] H:mm z"); version = lastCommit.shortHash; } else { lastUpdate = "?"; @@ -49,6 +50,7 @@ export const AboutCmd = utilityCmd({ ["Last update", lastUpdate], ["Version", version], ["API latency", `${shard.latency}ms`], + ["Server timezone", getGuildTz(pluginData)], ]; const loadedPlugins = Array.from( diff --git a/backend/src/plugins/Utility/commands/SourceCmd.ts b/backend/src/plugins/Utility/commands/SourceCmd.ts index c3698c94..0f574530 100644 --- a/backend/src/plugins/Utility/commands/SourceCmd.ts +++ b/backend/src/plugins/Utility/commands/SourceCmd.ts @@ -40,7 +40,7 @@ export const SourceCmd = utilityCmd({ const source = `${textSource}\n\nSource:\n\n${fullSource}`; - const archiveId = await pluginData.state.archives.create(source, moment().add(1, "hour")); + const archiveId = await pluginData.state.archives.create(source, moment.utc().add(1, "hour")); const baseUrl = getBaseUrl(pluginData); const url = pluginData.state.archives.getUrl(baseUrl, archiveId); cmdMessage.channel.createMessage(`Message source: ${url}`); diff --git a/backend/src/plugins/Utility/functions/getChannelInfoEmbed.ts b/backend/src/plugins/Utility/functions/getChannelInfoEmbed.ts index 6206dc5a..a6e513f1 100644 --- a/backend/src/plugins/Utility/functions/getChannelInfoEmbed.ts +++ b/backend/src/plugins/Utility/functions/getChannelInfoEmbed.ts @@ -4,6 +4,8 @@ import { Constants, EmbedOptions } from "eris"; import moment from "moment-timezone"; import humanizeDuration from "humanize-duration"; import { formatNumber, preEmbedPadding, trimLines } from "../../../utils"; +import { inGuildTz } from "../../../utils/timezones"; +import { getDateFormat } from "../../../utils/dateFormats"; const TEXT_CHANNEL_ICON = "https://cdn.discordapp.com/attachments/740650744830623756/740656843545772062/text-channel.png"; @@ -55,7 +57,8 @@ export async function getChannelInfoEmbed( channelName = `#${channel.name}`; } - const createdAt = moment(channel.createdAt, "x"); + const createdAt = moment.utc(channel.createdAt, "x"); + const prettyCreatedAt = inGuildTz(pluginData, createdAt).format(getDateFormat(pluginData, "pretty_datetime")); const channelAge = humanizeDuration(Date.now() - channel.createdAt, { largest: 2, round: true, @@ -69,7 +72,7 @@ export async function getChannelInfoEmbed( value: trimLines(` Name: **${channelName}** ID: \`${channel.id}\` - Created: **${channelAge} ago** (\`${createdAt.format("MMM D, YYYY [at] H:mm [UTC]")}\`) + Created: **${channelAge} ago** (\`${prettyCreatedAt}\`) Type: **${channelType}** ${showMention ? `Mention: <#${channel.id}>` : ""} `), diff --git a/backend/src/plugins/Utility/functions/getInviteInfoEmbed.ts b/backend/src/plugins/Utility/functions/getInviteInfoEmbed.ts index 90946d24..4f3f47a1 100644 --- a/backend/src/plugins/Utility/functions/getInviteInfoEmbed.ts +++ b/backend/src/plugins/Utility/functions/getInviteInfoEmbed.ts @@ -43,7 +43,7 @@ export async function getInviteInfoEmbed( } const serverCreatedAtTimestamp = snowflakeToTimestamp(invite.guild.id); - const serverCreatedAt = moment(serverCreatedAtTimestamp, "x"); + const serverCreatedAt = moment.utc(serverCreatedAtTimestamp, "x"); const serverAge = humanizeDuration(Date.now() - serverCreatedAtTimestamp, { largest: 2, round: true, @@ -66,7 +66,7 @@ export async function getInviteInfoEmbed( : `#${invite.channel.name}`; const channelCreatedAtTimestamp = snowflakeToTimestamp(invite.channel.id); - const channelCreatedAt = moment(channelCreatedAtTimestamp, "x"); + const channelCreatedAt = moment.utc(channelCreatedAtTimestamp, "x"); const channelAge = humanizeDuration(Date.now() - channelCreatedAtTimestamp, { largest: 2, round: true, @@ -117,7 +117,7 @@ export async function getInviteInfoEmbed( } const channelCreatedAtTimestamp = snowflakeToTimestamp(invite.channel.id); - const channelCreatedAt = moment(channelCreatedAtTimestamp, "x"); + const channelCreatedAt = moment.utc(channelCreatedAtTimestamp, "x"); const channelAge = humanizeDuration(Date.now() - channelCreatedAtTimestamp, { largest: 2, round: true, diff --git a/backend/src/plugins/Utility/functions/getMessageInfoEmbed.ts b/backend/src/plugins/Utility/functions/getMessageInfoEmbed.ts index 083f6cfd..1379bf82 100644 --- a/backend/src/plugins/Utility/functions/getMessageInfoEmbed.ts +++ b/backend/src/plugins/Utility/functions/getMessageInfoEmbed.ts @@ -5,6 +5,8 @@ import moment from "moment-timezone"; import humanizeDuration from "humanize-duration"; import { chunkMessageLines, preEmbedPadding, trimEmptyLines, trimLines } from "../../../utils"; import { getDefaultPrefix } from "knub/dist/commands/commandUtils"; +import { inGuildTz } from "../../../utils/timezones"; +import { getDateFormat } from "../../../utils/dateFormats"; const MESSAGE_ICON = "https://cdn.discordapp.com/attachments/740650744830623756/740685652152025088/message.png"; @@ -27,13 +29,15 @@ export async function getMessageInfoEmbed( icon_url: MESSAGE_ICON, }; - const createdAt = moment(message.createdAt, "x"); + const createdAt = moment.utc(message.createdAt, "x"); + const prettyCreatedAt = inGuildTz(pluginData, createdAt).format(getDateFormat(pluginData, "pretty_datetime")); const messageAge = humanizeDuration(Date.now() - message.createdAt, { largest: 2, round: true, }); - const editedAt = message.editedTimestamp && moment(message.editedTimestamp, "x"); + const editedAt = message.editedTimestamp && moment.utc(message.editedTimestamp, "x"); + const prettyEditedAt = inGuildTz(pluginData, editedAt).format(getDateFormat(pluginData, "pretty_datetime")); const editAge = message.editedTimestamp && humanizeDuration(Date.now() - message.editedTimestamp, { @@ -62,8 +66,8 @@ export async function getMessageInfoEmbed( ID: \`${message.id}\` Channel: <#${message.channel.id}> Channel ID: \`${message.channel.id}\` - Created: **${messageAge} ago** (\`${createdAt.format("MMM D, YYYY [at] H:mm [UTC]")}\`) - ${editedAt ? `Edited at: **${editAge} ago** (\`${editedAt.format("MMM D, YYYY [at] H:mm [UTC]")}\`)` : ""} + Created: **${messageAge} ago** (\`${prettyCreatedAt}\`) + ${editedAt ? `Edited at: **${editAge} ago** (\`${prettyEditedAt}\`)` : ""} Type: **${type}** Link: [**Go to message ➔**](https://discord.com/channels/${pluginData.guild.id}/${message.channel.id}/${ message.id @@ -72,13 +76,19 @@ export async function getMessageInfoEmbed( ), }); - const authorCreatedAt = moment(message.author.createdAt); + const authorCreatedAt = moment.utc(message.author.createdAt, "x"); + const prettyAuthorCreatedAt = inGuildTz(pluginData, authorCreatedAt).format( + getDateFormat(pluginData, "pretty_datetime"), + ); const authorAccountAge = humanizeDuration(Date.now() - message.author.createdAt, { largest: 2, round: true, }); - const authorJoinedAt = message.member && moment(message.member.joinedAt); + const authorJoinedAt = message.member && moment.utc(message.member.joinedAt, "x"); + const prettyAuthorJoinedAt = inGuildTz(pluginData, authorJoinedAt).format( + getDateFormat(pluginData, "pretty_datetime"), + ); const authorServerAge = message.member && humanizeDuration(Date.now() - message.member.joinedAt, { @@ -91,12 +101,8 @@ export async function getMessageInfoEmbed( value: trimLines(` Name: **${message.author.username}#${message.author.discriminator}** ID: \`${message.author.id}\` - Created: **${authorAccountAge} ago** (\`${authorCreatedAt.format("MMM D, YYYY [at] H:mm [UTC]")}\`) - ${ - authorJoinedAt - ? `Joined: **${authorServerAge} ago** (\`${authorJoinedAt.format("MMM D, YYYY [at] H:mm [UTC]")}\`)` - : "" - } + Created: **${authorAccountAge} ago** (\`${prettyAuthorCreatedAt}\`) + ${authorJoinedAt ? `Joined: **${authorServerAge} ago** (\`${prettyAuthorJoinedAt}\`)` : ""} Mention: <@!${message.author.id}> `), }); diff --git a/backend/src/plugins/Utility/functions/getServerInfoEmbed.ts b/backend/src/plugins/Utility/functions/getServerInfoEmbed.ts index bc7f17f8..6adf8d30 100644 --- a/backend/src/plugins/Utility/functions/getServerInfoEmbed.ts +++ b/backend/src/plugins/Utility/functions/getServerInfoEmbed.ts @@ -5,6 +5,8 @@ import { CategoryChannel, EmbedOptions, Guild, RESTChannelInvite, TextChannel, V import moment from "moment-timezone"; import humanizeDuration from "humanize-duration"; import { getGuildPreview } from "./getGuildPreview"; +import { inGuildTz } from "../../../utils/timezones"; +import { getDateFormat } from "../../../utils/dateFormats"; export async function getServerInfoEmbed( pluginData: PluginData, @@ -37,14 +39,15 @@ export async function getServerInfoEmbed( }; // BASIC INFORMATION - const createdAt = moment((guildPreview || restGuild).createdAt); - const serverAge = humanizeDuration(moment().valueOf() - createdAt.valueOf(), { + const createdAt = moment.utc((guildPreview || restGuild).createdAt, "x"); + const prettyCreatedAt = inGuildTz(pluginData, createdAt).format(getDateFormat(pluginData, "pretty_datetime")); + const serverAge = humanizeDuration(moment.utc().valueOf() - createdAt.valueOf(), { largest: 2, round: true, }); const basicInformation = []; - basicInformation.push(`Created: **${serverAge} ago** (${createdAt.format("YYYY-MM-DD[T]HH:mm:ss")})`); + basicInformation.push(`Created: **${serverAge} ago** (${prettyCreatedAt})`); if (thisServer) { const owner = await resolveUser(pluginData.client, thisServer.ownerID); diff --git a/backend/src/plugins/Utility/functions/getSnowflakeInfoEmbed.ts b/backend/src/plugins/Utility/functions/getSnowflakeInfoEmbed.ts index 68b4ce82..7ca674a4 100644 --- a/backend/src/plugins/Utility/functions/getSnowflakeInfoEmbed.ts +++ b/backend/src/plugins/Utility/functions/getSnowflakeInfoEmbed.ts @@ -6,6 +6,8 @@ import moment from "moment-timezone"; import { CaseTypes } from "src/data/CaseTypes"; import humanizeDuration from "humanize-duration"; import { snowflakeToTimestamp } from "../../../utils/snowflakeToTimestamp"; +import { inGuildTz } from "../../../utils/timezones"; +import { getDateFormat } from "../../../utils/dateFormats"; const SNOWFLAKE_ICON = "https://cdn.discordapp.com/attachments/740650744830623756/742020790471491668/snowflake.png"; @@ -29,7 +31,8 @@ export function getSnowflakeInfoEmbed( } const createdAtMS = snowflakeToTimestamp(snowflake); - const createdAt = moment(createdAtMS, "x"); + const createdAt = moment.utc(createdAtMS, "x"); + const prettyCreatedAt = inGuildTz(pluginData, createdAt).format(getDateFormat(pluginData, "pretty_datetime")); const snowflakeAge = humanizeDuration(Date.now() - createdAtMS, { largest: 2, round: true, @@ -37,7 +40,7 @@ export function getSnowflakeInfoEmbed( embed.fields.push({ name: preEmbedPadding + "Basic information", - value: `Created: **${snowflakeAge} ago** (\`${createdAt.format("MMM D, YYYY [at] H:mm [UTC]")}\`)`, + value: `Created: **${snowflakeAge} ago** (\`${prettyCreatedAt}\`)`, }); return embed; diff --git a/backend/src/plugins/Utility/functions/getUserInfoEmbed.ts b/backend/src/plugins/Utility/functions/getUserInfoEmbed.ts index 76686ed6..cf851f41 100644 --- a/backend/src/plugins/Utility/functions/getUserInfoEmbed.ts +++ b/backend/src/plugins/Utility/functions/getUserInfoEmbed.ts @@ -5,6 +5,8 @@ import { UnknownUser, trimLines, embedPadding, resolveMember, resolveUser, preEm import moment from "moment-timezone"; import { CaseTypes } from "src/data/CaseTypes"; import humanizeDuration from "humanize-duration"; +import { inGuildTz } from "../../../utils/timezones"; +import { getDateFormat } from "../../../utils/dateFormats"; export async function getUserInfoEmbed( pluginData: PluginData, @@ -29,8 +31,9 @@ export async function getUserInfoEmbed( const avatarURL = user.avatarURL || user.defaultAvatarURL; embed.author.icon_url = avatarURL; - const createdAt = moment(user.createdAt); - const accountAge = humanizeDuration(moment().valueOf() - user.createdAt, { + const createdAt = moment.utc(user.createdAt, "x"); + const prettyCreatedAt = inGuildTz(pluginData, createdAt).format(getDateFormat(pluginData, "pretty_datetime")); + const accountAge = humanizeDuration(moment.utc().valueOf() - user.createdAt, { largest: 2, round: true, }); @@ -40,16 +43,17 @@ export async function getUserInfoEmbed( name: preEmbedPadding + "User information", value: trimLines(` Profile: <@!${user.id}> - Created: **${accountAge} ago** (\`${createdAt.format("MMM D, YYYY")}\`) + Created: **${accountAge} ago** (\`${prettyCreatedAt}\`) `), }); if (member) { - const joinedAt = moment(member.joinedAt); - const joinAge = humanizeDuration(moment().valueOf() - member.joinedAt, { + const joinedAt = moment.utc(member.joinedAt, "x"); + const prettyJoinedAt = inGuildTz(pluginData, joinedAt).format(getDateFormat(pluginData, "pretty_datetime")); + const joinAge = humanizeDuration(moment.utc().valueOf() - member.joinedAt, { largest: 2, round: true, }); - embed.fields[0].value += `\nJoined: **${joinAge} ago** (\`${joinedAt.format("MMM D, YYYY")}\`)`; + embed.fields[0].value += `\nJoined: **${joinAge} ago** (\`${prettyJoinedAt}\`)`; } else { embed.fields.push({ name: preEmbedPadding + "!! NOTE !!", @@ -65,14 +69,15 @@ export async function getUserInfoEmbed( value: trimLines(` Name: **${user.username}#${user.discriminator}** ID: \`${user.id}\` - Created: **${accountAge} ago** (\`${createdAt.format("MMM D, YYYY [at] H:mm [UTC]")}\`) + Created: **${accountAge} ago** (\`${prettyCreatedAt}\`) Mention: <@!${user.id}> `), }); if (member) { - const joinedAt = moment(member.joinedAt); - const joinAge = humanizeDuration(moment().valueOf() - member.joinedAt, { + const joinedAt = moment.utc(member.joinedAt, "x"); + const prettyJoinedAt = inGuildTz(pluginData, joinedAt).format(getDateFormat(pluginData, "pretty_datetime")); + const joinAge = humanizeDuration(moment.utc().valueOf() - member.joinedAt, { largest: 2, round: true, }); @@ -81,7 +86,7 @@ export async function getUserInfoEmbed( embed.fields.push({ name: preEmbedPadding + "Member information", value: trimLines(` - Joined: **${joinAge} ago** (\`${joinedAt.format("MMM D, YYYY [at] H:mm [UTC]")}\`) + Joined: **${joinAge} ago** (\`${prettyJoinedAt}\`) ${roles.length > 0 ? "Roles: " + roles.map(r => r.name).join(", ") : ""} `), }); diff --git a/backend/src/plugins/Utility/search.ts b/backend/src/plugins/Utility/search.ts index 069b7cd8..3463c8d6 100644 --- a/backend/src/plugins/Utility/search.ts +++ b/backend/src/plugins/Utility/search.ts @@ -214,7 +214,7 @@ export async function archiveSearch( ${resultList} `), - moment().add(1, "hour"), + moment.utc().add(1, "hour"), ); const baseUrl = getBaseUrl(pluginData); diff --git a/backend/src/types.ts b/backend/src/types.ts index bdb7ae8c..3d3f64ca 100644 --- a/backend/src/types.ts +++ b/backend/src/types.ts @@ -1,16 +1,47 @@ import { BaseConfig, Knub } from "knub"; +import * as t from "io-ts"; -export interface IZeppelinGuildConfig extends BaseConfig { +export const DateFormatsSchema = t.type({ + date: t.string, + time: t.string, + pretty_datetime: t.string, +}); + +export type DateFormats = t.TypeOf; + +export interface ZeppelinGuildConfig extends BaseConfig { success_emoji?: string; error_emoji?: string; + timezone?: string; + date_formats?: Partial; } -export interface IZeppelinGlobalConfig extends BaseConfig { +export const ZeppelinGuildConfigSchema = t.type({ + // From BaseConfig + prefix: t.string, + levels: t.record(t.string, t.number), + plugins: t.record(t.string, t.unknown), + + // From ZeppelinGuildConfig + success_emoji: t.string, + error_emoji: t.string, + timezone: t.string, + date_formats: t.partial(DateFormatsSchema.props), +}); +export const PartialZeppelinGuildConfigSchema = t.partial(ZeppelinGuildConfigSchema.props); + +export interface ZeppelinGlobalConfig extends BaseConfig { url: string; owners?: string[]; } -export type TZeppelinKnub = Knub; +export const ZeppelinGlobalConfigSchema = t.type({ + url: t.string, + owners: t.array(t.string), + plugins: t.record(t.string, t.unknown), +}); + +export type TZeppelinKnub = Knub; /** * Wrapper for the string type that indicates the text will be parsed as Markdown later diff --git a/backend/src/utils.ts b/backend/src/utils.ts index af4c96f7..6a86c87a 100644 --- a/backend/src/utils.ts +++ b/backend/src/utils.ts @@ -32,7 +32,7 @@ import https from "https"; import tmp from "tmp"; import { helpers } from "knub"; import { SavedMessage } from "./data/entities/SavedMessage"; -import { decodeAndValidateStrict, StrictValidationError, validate } from "./validatorUtils"; +import { decodeAndValidateStrict, StrictValidationError } from "./validatorUtils"; import { either } from "fp-ts/lib/Either"; import moment from "moment-timezone"; import { SimpleCache } from "./SimpleCache"; @@ -252,7 +252,7 @@ export const tDateTime = new t.Type( (from, to) => either.chain(t.string.validate(from, to), s => { const parsed = - s.length === 10 ? moment(s, "YYYY-MM-DD") : s.length === 19 ? moment(s, "YYYY-MM-DD HH:mm:ss") : null; + s.length === 10 ? moment.utc(s, "YYYY-MM-DD") : s.length === 19 ? moment.utc(s, "YYYY-MM-DD HH:mm:ss") : null; return parsed && parsed.isValid() ? t.success(s) : t.failure(from, to, "Invalid datetime"); }), @@ -823,8 +823,6 @@ export function noop() { // IT'S LITERALLY NOTHING } -export const DBDateFormat = "YYYY-MM-DD HH:mm:ss"; - export type CustomEmoji = { id: string; } & Emoji; diff --git a/backend/src/utils/dateFormats.ts b/backend/src/utils/dateFormats.ts new file mode 100644 index 00000000..b429f2a5 --- /dev/null +++ b/backend/src/utils/dateFormats.ts @@ -0,0 +1,17 @@ +import { PluginData } from "knub"; +import { DateFormats } from "../types"; + +const defaultDateFormats: DateFormats = { + date: "MMM D, YYYY", + time: "H:mm", + pretty_datetime: "MMM D, YYYY [at] H:mm z", +}; + +/** + * Returns the guild-specific date format, falling back to the defaults if one has not been specified + */ +export function getDateFormat(pluginData: PluginData, formatName: keyof DateFormats) { + return pluginData.guildConfig.date_formats?.[formatName] || defaultDateFormats[formatName]; +} + +export const DBDateFormat = "YYYY-MM-DD HH:mm:ss"; diff --git a/backend/src/utils/timezones.ts b/backend/src/utils/timezones.ts new file mode 100644 index 00000000..0fe7c372 --- /dev/null +++ b/backend/src/utils/timezones.ts @@ -0,0 +1,21 @@ +import moment from "moment-timezone"; +import { PluginData } from "knub"; +import { ZeppelinGuildConfig } from "../types"; + +export function getGuildTz(pluginData: PluginData) { + const guildConfig = pluginData.guildConfig as ZeppelinGuildConfig; + return guildConfig.timezone || "Etc/UTC"; +} + +export function inGuildTz(pluginData: PluginData, input?: moment.Moment | number) { + let momentObj: moment.Moment; + if (typeof input === "number") { + momentObj = moment.utc(input, "x"); + } else if (moment.isMoment(input)) { + momentObj = input.clone(); + } else { + momentObj = moment.utc(); + } + + return momentObj.tz(getGuildTz(pluginData)); +}