From 4ae8cf85a30ed42a01698fc0f07cd012f570f16e Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 19 Aug 2020 00:19:12 +0300 Subject: [PATCH] Add time_and_date plugin. Use it for timezones and date formats around the bot. --- backend/src/commandTypes.ts | 10 ++++ backend/src/data/ApiLogins.ts | 2 +- backend/src/data/ApiUserInfo.ts | 2 +- backend/src/data/GuildCases.ts | 3 +- backend/src/data/GuildMemberTimezones.ts | 48 ++++++++++++++++++ backend/src/data/cleanup/configs.ts | 2 +- backend/src/data/cleanup/messages.ts | 3 +- backend/src/data/cleanup/nicknames.ts | 3 +- backend/src/data/cleanup/usernames.ts | 3 +- backend/src/data/entities/MemberTimezone.ts | 14 ++++++ ...597109357201-CreateMemberTimezonesTable.ts | 33 ++++++++++++ backend/src/pluginUtils.ts | 14 ++++++ .../plugins/AutoDelete/AutoDeletePlugin.ts | 2 + .../plugins/AutoDelete/util/deleteNextItem.ts | 11 ++-- backend/src/plugins/Cases/CasesPlugin.ts | 16 ++---- .../plugins/Cases/functions/getCaseEmbed.ts | 21 +++++--- .../plugins/Cases/functions/getCaseSummary.ts | 19 +++++-- .../ChannelArchiver/ChannelArchiverPlugin.ts | 3 ++ .../commands/ArchiveChannelCmd.ts | 6 +-- backend/src/plugins/Logs/LogsPlugin.ts | 3 +- .../src/plugins/Logs/util/getLogMessage.ts | 7 ++- .../src/plugins/Logs/util/onMessageDelete.ts | 9 ++-- .../plugins/ModActions/ModActionsPlugin.ts | 4 +- .../plugins/ModActions/commands/CaseCmd.ts | 2 +- .../ModActions/commands/CasesModCmd.ts | 2 +- .../ModActions/commands/CasesUserCmd.ts | 2 +- .../ModActions/commands/DeleteCaseCmd.ts | 7 +-- .../src/plugins/Mutes/commands/MutesCmd.ts | 3 +- backend/src/plugins/Post/PostPlugin.ts | 2 + .../Post/commands/ScheduledPostsListCmd.ts | 19 ++++--- .../src/plugins/Post/util/actualPostCmd.ts | 32 ++++++------ .../plugins/Post/util/parseScheduleTime.ts | 6 +-- .../plugins/Post/util/scheduledPostLoop.ts | 3 +- .../src/plugins/Reminders/RemindersPlugin.ts | 2 + .../plugins/Reminders/commands/RemindCmd.ts | 12 +++-- .../Reminders/commands/RemindersCmd.ts | 13 ++--- .../Spam/util/logAndDetectMessageSpam.ts | 3 +- backend/src/plugins/Tags/TagsPlugin.ts | 10 ++-- .../plugins/TimeAndDate/TimeAndDatePlugin.ts | 50 +++++++++++++++++++ .../TimeAndDate/commands/SetTimezoneCmd.ts | 17 +++++++ .../TimeAndDate/commands/ViewTimezoneCmd.ts | 23 +++++++++ .../plugins/TimeAndDate/defaultDateFormats.ts | 5 ++ .../TimeAndDate/functions/getDateFormat.ts | 6 +++ .../TimeAndDate/functions/getGuildTz.ts | 7 +++ .../TimeAndDate/functions/getMemberTz.ts | 8 +++ .../TimeAndDate/functions/inGuildTz.ts} | 12 ++--- .../TimeAndDate/functions/inMemberTz.ts | 22 ++++++++ backend/src/plugins/TimeAndDate/types.ts | 22 ++++++++ backend/src/plugins/Utility/UtilityPlugin.ts | 2 + .../src/plugins/Utility/commands/AboutCmd.ts | 13 ++--- .../src/plugins/Utility/commands/InfoCmd.ts | 17 ++++--- .../Utility/commands/MessageInfoCmd.ts | 7 ++- .../src/plugins/Utility/commands/ServerCmd.ts | 2 +- .../Utility/commands/SnowflakeInfoCmd.ts | 4 +- .../plugins/Utility/commands/UserInfoCmd.ts | 2 +- .../Utility/functions/getChannelInfoEmbed.ts | 10 ++-- .../Utility/functions/getMessageInfoEmbed.ts | 30 +++++++---- .../Utility/functions/getServerInfoEmbed.ts | 10 ++-- .../functions/getSnowflakeInfoEmbed.ts | 14 ++++-- .../Utility/functions/getUserInfoEmbed.ts | 21 ++++++-- backend/src/plugins/availablePlugins.ts | 3 ++ backend/src/types.ts | 16 +++--- backend/src/utils.ts | 2 + backend/src/utils/dateFormats.ts | 17 ------- backend/src/utils/isValidTimezone.ts | 7 +++ backend/src/utils/tValidTimezone.ts | 13 +++++ backend/src/utils/typeUtils.ts | 2 + 67 files changed, 543 insertions(+), 177 deletions(-) create mode 100644 backend/src/data/GuildMemberTimezones.ts create mode 100644 backend/src/data/entities/MemberTimezone.ts create mode 100644 backend/src/migrations/1597109357201-CreateMemberTimezonesTable.ts create mode 100644 backend/src/plugins/TimeAndDate/TimeAndDatePlugin.ts create mode 100644 backend/src/plugins/TimeAndDate/commands/SetTimezoneCmd.ts create mode 100644 backend/src/plugins/TimeAndDate/commands/ViewTimezoneCmd.ts create mode 100644 backend/src/plugins/TimeAndDate/defaultDateFormats.ts create mode 100644 backend/src/plugins/TimeAndDate/functions/getDateFormat.ts create mode 100644 backend/src/plugins/TimeAndDate/functions/getGuildTz.ts create mode 100644 backend/src/plugins/TimeAndDate/functions/getMemberTz.ts rename backend/src/{utils/timezones.ts => plugins/TimeAndDate/functions/inGuildTz.ts} (52%) create mode 100644 backend/src/plugins/TimeAndDate/functions/inMemberTz.ts create mode 100644 backend/src/plugins/TimeAndDate/types.ts delete mode 100644 backend/src/utils/dateFormats.ts create mode 100644 backend/src/utils/isValidTimezone.ts create mode 100644 backend/src/utils/tValidTimezone.ts create mode 100644 backend/src/utils/typeUtils.ts diff --git a/backend/src/commandTypes.ts b/backend/src/commandTypes.ts index 3c3cb1cb..e36ce831 100644 --- a/backend/src/commandTypes.ts +++ b/backend/src/commandTypes.ts @@ -17,6 +17,7 @@ import { createTypeHelper } from "knub-command-manager"; import { getChannelIdFromMessageId } from "./data/getChannelIdFromMessageId"; import { MessageTarget, resolveMessageTarget } from "./utils/resolveMessageTarget"; import { inputPatternToRegExp } from "./validatorUtils"; +import { isValidTimezone } from "./utils/isValidTimezone"; export const commandTypes = { ...baseTypeConverters, @@ -93,6 +94,14 @@ export const commandTypes = { throw new TypeConversionError(`Could not parse RegExp: \`${disableInlineCode(e.message)}\``); } }, + + timezone(value: string) { + if (!isValidTimezone(value)) { + throw new TypeConversionError(`Invalid timezone: ${disableInlineCode(value)}`); + } + + return value; + }, }; export const commandTypeHelpers = { @@ -105,4 +114,5 @@ export const commandTypeHelpers = { messageTarget: createTypeHelper>(commandTypes.messageTarget), anyId: createTypeHelper>(commandTypes.anyId), regex: createTypeHelper(commandTypes.regex), + timezone: createTypeHelper(commandTypes.timezone), }; diff --git a/backend/src/data/ApiLogins.ts b/backend/src/data/ApiLogins.ts index 62572c78..22ab35e8 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/dateFormats"; +import { DBDateFormat } from "../utils"; export class ApiLogins extends BaseRepository { private apiLogins: Repository; diff --git a/backend/src/data/ApiUserInfo.ts b/backend/src/data/ApiUserInfo.ts index 37e96ebb..09d9df75 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/dateFormats"; +import { DBDateFormat } from "../utils"; export class ApiUserInfo extends BaseRepository { private apiUserInfo: Repository; diff --git a/backend/src/data/GuildCases.ts b/backend/src/data/GuildCases.ts index dc2696b6..37396c3f 100644 --- a/backend/src/data/GuildCases.ts +++ b/backend/src/data/GuildCases.ts @@ -2,11 +2,10 @@ import { Case } from "./entities/Case"; import { CaseNote } from "./entities/CaseNote"; import { BaseGuildRepository } from "./BaseGuildRepository"; import { getRepository, In, Repository } from "typeorm"; -import { disableLinkPreviews } from "../utils"; +import { DBDateFormat, 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; diff --git a/backend/src/data/GuildMemberTimezones.ts b/backend/src/data/GuildMemberTimezones.ts new file mode 100644 index 00000000..df4633aa --- /dev/null +++ b/backend/src/data/GuildMemberTimezones.ts @@ -0,0 +1,48 @@ +import { BaseGuildRepository } from "./BaseGuildRepository"; +import { MemberTimezone } from "./entities/MemberTimezone"; +import { getRepository, Repository } from "typeorm/index"; +import { connection } from "./db"; + +export class GuildMemberTimezones extends BaseGuildRepository { + protected memberTimezones: Repository; + + constructor(guildId: string) { + super(guildId); + this.memberTimezones = getRepository(MemberTimezone); + } + + get(memberId: string) { + return this.memberTimezones.findOne({ + guild_id: this.guildId, + member_id: memberId, + }); + } + + async set(memberId, timezone: string) { + await connection.transaction(async entityManager => { + const repo = entityManager.getRepository(MemberTimezone); + const existingRow = await repo.findOne({ + guild_id: this.guildId, + member_id: memberId, + }); + + if (existingRow) { + await repo.update( + { + guild_id: this.guildId, + member_id: memberId, + }, + { + timezone, + }, + ); + } else { + await repo.insert({ + guild_id: this.guildId, + member_id: memberId, + timezone, + }); + } + }); + } +} diff --git a/backend/src/data/cleanup/configs.ts b/backend/src/data/cleanup/configs.ts index c0c6b8f0..04c76044 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/dateFormats"; +import { DBDateFormat } from "../../utils"; const CLEAN_PER_LOOP = 50; diff --git a/backend/src/data/cleanup/messages.ts b/backend/src/data/cleanup/messages.ts index 4eb3e961..1b9f8427 100644 --- a/backend/src/data/cleanup/messages.ts +++ b/backend/src/data/cleanup/messages.ts @@ -1,9 +1,8 @@ -import { DAYS, MINUTES } from "../../utils"; +import { DAYS, DBDateFormat, 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. diff --git a/backend/src/data/cleanup/nicknames.ts b/backend/src/data/cleanup/nicknames.ts index b62bb287..e48b2670 100644 --- a/backend/src/data/cleanup/nicknames.ts +++ b/backend/src/data/cleanup/nicknames.ts @@ -1,9 +1,8 @@ import { getRepository, In } from "typeorm"; import moment from "moment-timezone"; import { NicknameHistoryEntry } from "../entities/NicknameHistoryEntry"; -import { DAYS } from "../../utils"; +import { DAYS, DBDateFormat } from "../../utils"; import { connection } from "../db"; -import { DBDateFormat } from "../../utils/dateFormats"; export const NICKNAME_RETENTION_PERIOD = 30 * DAYS; const CLEAN_PER_LOOP = 500; diff --git a/backend/src/data/cleanup/usernames.ts b/backend/src/data/cleanup/usernames.ts index b583eb2d..6bcca3d2 100644 --- a/backend/src/data/cleanup/usernames.ts +++ b/backend/src/data/cleanup/usernames.ts @@ -1,9 +1,8 @@ import { getRepository, In } from "typeorm"; import moment from "moment-timezone"; import { UsernameHistoryEntry } from "../entities/UsernameHistoryEntry"; -import { DAYS } from "../../utils"; +import { DAYS, DBDateFormat } from "../../utils"; import { connection } from "../db"; -import { DBDateFormat } from "../../utils/dateFormats"; export const USERNAME_RETENTION_PERIOD = 30 * DAYS; const CLEAN_PER_LOOP = 500; diff --git a/backend/src/data/entities/MemberTimezone.ts b/backend/src/data/entities/MemberTimezone.ts new file mode 100644 index 00000000..c05158d1 --- /dev/null +++ b/backend/src/data/entities/MemberTimezone.ts @@ -0,0 +1,14 @@ +import { Column, Entity, PrimaryColumn } from "typeorm"; + +@Entity("member_timezones") +export class MemberTimezone { + @Column() + @PrimaryColumn() + guild_id: string; + + @Column() + @PrimaryColumn() + member_id: string; + + @Column() timezone: string; +} diff --git a/backend/src/migrations/1597109357201-CreateMemberTimezonesTable.ts b/backend/src/migrations/1597109357201-CreateMemberTimezonesTable.ts new file mode 100644 index 00000000..df4b6cca --- /dev/null +++ b/backend/src/migrations/1597109357201-CreateMemberTimezonesTable.ts @@ -0,0 +1,33 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; +import { Table } from "typeorm/index"; + +export class CreateMemberTimezonesTable1597109357201 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable( + new Table({ + name: "member_timezones", + columns: [ + { + name: "guild_id", + type: "bigint", + isPrimary: true, + }, + { + name: "member_id", + type: "bigint", + isPrimary: true, + }, + { + name: "timezone", + type: "varchar", + length: "255", + }, + ], + }), + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropTable("member_timezones"); + } +} diff --git a/backend/src/pluginUtils.ts b/backend/src/pluginUtils.ts index 9fa34040..8125ca68 100644 --- a/backend/src/pluginUtils.ts +++ b/backend/src/pluginUtils.ts @@ -19,6 +19,7 @@ import { TZeppelinKnub } from "./types"; import { ExtendedMatchParams } from "knub/dist/config/PluginConfigManager"; // TODO: Export from Knub index import * as t from "io-ts"; import { PluginOverrideCriteria } from "knub/dist/config/configTypes"; +import { Tail } from "./utils/typeUtils"; const { getMemberLevel } = helpers; @@ -168,3 +169,16 @@ export function isOwner(pluginData: PluginData, userId: string) { export const isOwnerPreFilter = (_, context: CommandContext) => { return isOwner(context.pluginData, context.message.author.id); }; + +type AnyFn = (...args: any[]) => any; + +/** + * Creates a public plugin function out of a function with pluginData as the first parameter + */ +export function mapToPublicFn(inputFn: T) { + return pluginData => { + return (...args: Tail>): ReturnType => { + return inputFn(pluginData, ...args); + }; + }; +} diff --git a/backend/src/plugins/AutoDelete/AutoDeletePlugin.ts b/backend/src/plugins/AutoDelete/AutoDeletePlugin.ts index 45374fde..7b5d47aa 100644 --- a/backend/src/plugins/AutoDelete/AutoDeletePlugin.ts +++ b/backend/src/plugins/AutoDelete/AutoDeletePlugin.ts @@ -6,6 +6,7 @@ import { GuildLogs } from "src/data/GuildLogs"; import { onMessageCreate } from "./util/onMessageCreate"; import { onMessageDelete } from "./util/onMessageDelete"; import { onMessageDeleteBulk } from "./util/onMessageDeleteBulk"; +import { TimeAndDatePlugin } from "../TimeAndDate/TimeAndDatePlugin"; const defaultOptions: PluginOptions = { config: { @@ -22,6 +23,7 @@ export const AutoDeletePlugin = zeppelinPlugin()("auto_del configurationGuide: "Maximum deletion delay is currently 5 minutes", }, + dependencies: [TimeAndDatePlugin], configSchema: ConfigSchema, defaultOptions, diff --git a/backend/src/plugins/AutoDelete/util/deleteNextItem.ts b/backend/src/plugins/AutoDelete/util/deleteNextItem.ts index 19d20971..68a10eb0 100644 --- a/backend/src/plugins/AutoDelete/util/deleteNextItem.ts +++ b/backend/src/plugins/AutoDelete/util/deleteNextItem.ts @@ -5,13 +5,14 @@ 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"; +import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin"; export async function deleteNextItem(pluginData: PluginData) { const [itemToDelete] = pluginData.state.deletionQueue.splice(0, 1); if (!itemToDelete) return; + const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin); + pluginData.state.guildLogs.ignoreLog(LogType.MESSAGE_DELETE, itemToDelete.message.id); pluginData.client.deleteMessage(itemToDelete.message.channel_id, itemToDelete.message.id).catch(logger.warn); @@ -19,9 +20,9 @@ export async function deleteNextItem(pluginData: PluginData()("cases", { `), }, + dependencies: [TimeAndDatePlugin], configSchema: ConfigSchema, defaultOptions, @@ -61,17 +64,8 @@ export const CasesPlugin = zeppelinPlugin()("cases", { }; }, - getCaseEmbed(pluginData) { - return (caseOrCaseId: Case | number) => { - return getCaseEmbed(pluginData, caseOrCaseId); - }; - }, - - getCaseSummary(pluginData) { - return (caseOrCaseId: Case | number, withLinks = false) => { - return getCaseSummary(pluginData, caseOrCaseId, withLinks); - }; - }, + getCaseEmbed: mapToPublicFn(getCaseEmbed), + getCaseSummary: mapToPublicFn(getCaseSummary), }, onLoad(pluginData) { diff --git a/backend/src/plugins/Cases/functions/getCaseEmbed.ts b/backend/src/plugins/Cases/functions/getCaseEmbed.ts index 385c6720..4e7ea817 100644 --- a/backend/src/plugins/Cases/functions/getCaseEmbed.ts +++ b/backend/src/plugins/Cases/functions/getCaseEmbed.ts @@ -6,17 +6,19 @@ import { PluginData, helpers } from "knub"; import { CasesPluginType } from "../types"; import { resolveCaseId } from "./resolveCaseId"; import { chunkLines, chunkMessageLines, emptyEmbedValue, messageLink } from "../../../utils"; -import { inGuildTz } from "../../../utils/timezones"; -import { getDateFormat } from "../../../utils/dateFormats"; import { getCaseColor } from "./getCaseColor"; +import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin"; export async function getCaseEmbed( pluginData: PluginData, caseOrCaseId: Case | number, + requestMemberId?: string, ): Promise { const theCase = await pluginData.state.cases.with("notes").find(resolveCaseId(caseOrCaseId)); if (!theCase) return null; + const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin); + const createdAt = moment.utc(theCase.created_at); const actionTypeStr = CaseTypes[theCase.type].toUpperCase(); @@ -26,10 +28,14 @@ export async function getCaseEmbed( let modName = theCase.mod_name; if (theCase.mod_id) modName += `\n<@!${theCase.mod_id}>`; + const createdAtWithTz = requestMemberId + ? await timeAndDate.inMemberTz(requestMemberId, createdAt) + : timeAndDate.inGuildTz(createdAt); + const embed: any = { title: `${actionTypeStr} - Case #${theCase.case_number}`, footer: { - text: `Case created on ${inGuildTz(pluginData, createdAt).format(getDateFormat(pluginData, "pretty_datetime"))}`, + text: `Case created on ${createdAtWithTz.format(timeAndDate.getDateFormat("pretty_datetime"))}`, }, fields: [ { @@ -56,7 +62,7 @@ export async function getCaseEmbed( embed.color = getCaseColor(pluginData, theCase.type); if (theCase.notes.length) { - theCase.notes.forEach((note: any) => { + for (const note of theCase.notes) { const noteDate = moment.utc(note.created_at); let noteBody = note.body.trim(); if (noteBody === "") { @@ -67,7 +73,10 @@ 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")); + const noteDateWithTz = requestMemberId + ? await timeAndDate.inMemberTz(requestMemberId, noteDate) + : timeAndDate.inGuildTz(noteDate); + const prettyNoteDate = noteDateWithTz.format(timeAndDate.getDateFormat("pretty_datetime")); embed.fields.push({ name: `${note.mod_name} at ${prettyNoteDate}:`, value: chunks[i], @@ -79,7 +88,7 @@ export async function getCaseEmbed( }); } } - }); + } } else { embed.fields.push({ name: "!!! THIS CASE HAS NO NOTES !!!", diff --git a/backend/src/plugins/Cases/functions/getCaseSummary.ts b/backend/src/plugins/Cases/functions/getCaseSummary.ts index 4c7fc322..18034e8e 100644 --- a/backend/src/plugins/Cases/functions/getCaseSummary.ts +++ b/backend/src/plugins/Cases/functions/getCaseSummary.ts @@ -1,15 +1,21 @@ import { PluginData } from "knub"; import { CasesPluginType } from "../types"; -import { convertDelayStringToMS, DAYS, disableLinkPreviews, emptyEmbedValue, messageLink } from "../../../utils"; -import { DBDateFormat, getDateFormat } from "../../../utils/dateFormats"; +import { + convertDelayStringToMS, + DAYS, + DBDateFormat, + disableLinkPreviews, + emptyEmbedValue, + messageLink, +} from "../../../utils"; import { CaseTypes, CaseTypeToName } from "../../../data/CaseTypes"; import moment from "moment-timezone"; import { Case } from "../../../data/entities/Case"; -import { inGuildTz } from "../../../utils/timezones"; import humanizeDuration from "humanize-duration"; import { humanizeDurationShort } from "../../../humanizeDurationShort"; import { caseAbbreviations } from "../caseAbbreviations"; import { getCaseIcon } from "./getCaseIcon"; +import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin"; const CASE_SUMMARY_REASON_MAX_LENGTH = 300; const INCLUDE_MORE_NOTES_THRESHOLD = 20; @@ -21,8 +27,10 @@ export async function getCaseSummary( pluginData: PluginData, caseOrCaseId: Case | number, withLinks = false, + requestMemberId?: string, ) { const config = pluginData.config.get(); + const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin); const caseId = caseOrCaseId instanceof Case ? caseOrCaseId.id : caseOrCaseId; const theCase = await pluginData.state.cases.with("notes").find(caseId); @@ -50,9 +58,12 @@ export async function getCaseSummary( const timestamp = moment.utc(theCase.created_at, DBDateFormat); const relativeTimeCutoff = convertDelayStringToMS(config.relative_time_cutoff); const useRelativeTime = config.show_relative_times && Date.now() - timestamp.valueOf() < relativeTimeCutoff; + const timestampWithTz = requestMemberId + ? await timeAndDate.inMemberTz(requestMemberId, timestamp) + : timeAndDate.inGuildTz(timestamp); const prettyTimestamp = useRelativeTime ? moment.utc().to(timestamp) - : inGuildTz(pluginData, timestamp).format(getDateFormat(pluginData, "date")); + : timestampWithTz.format(timeAndDate.getDateFormat("date")); const icon = getCaseIcon(pluginData, theCase.type); diff --git a/backend/src/plugins/ChannelArchiver/ChannelArchiverPlugin.ts b/backend/src/plugins/ChannelArchiver/ChannelArchiverPlugin.ts index e7012134..9c34054d 100644 --- a/backend/src/plugins/ChannelArchiver/ChannelArchiverPlugin.ts +++ b/backend/src/plugins/ChannelArchiver/ChannelArchiverPlugin.ts @@ -2,9 +2,12 @@ import { zeppelinPlugin } from "../ZeppelinPluginBlueprint"; import { ChannelArchiverPluginType } from "./types"; import { ArchiveChannelCmd } from "./commands/ArchiveChannelCmd"; import * as t from "io-ts"; +import { TimeAndDatePlugin } from "../TimeAndDate/TimeAndDatePlugin"; export const ChannelArchiverPlugin = zeppelinPlugin()("channel_archiver", { showInDocs: false, + + dependencies: [TimeAndDatePlugin], configSchema: t.type({}), // prettier-ignore diff --git a/backend/src/plugins/ChannelArchiver/commands/ArchiveChannelCmd.ts b/backend/src/plugins/ChannelArchiver/commands/ArchiveChannelCmd.ts index 38297182..4f779feb 100644 --- a/backend/src/plugins/ChannelArchiver/commands/ArchiveChannelCmd.ts +++ b/backend/src/plugins/ChannelArchiver/commands/ArchiveChannelCmd.ts @@ -4,8 +4,7 @@ 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"; +import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin"; const MAX_ARCHIVED_MESSAGES = 5000; const MAX_MESSAGES_PER_FETCH = 100; @@ -98,7 +97,8 @@ export const ArchiveChannelCmd = channelArchiverCmd({ archiveLines.reverse(); - const nowTs = inGuildTz(pluginData).format(getDateFormat(pluginData, "pretty_datetime")); + const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin); + const nowTs = timeAndDate.inGuildTz().format(timeAndDate.getDateFormat("pretty_datetime")); let result = `Archived ${archiveLines.length} messages from #${args.channel.name} at ${nowTs}`; result += `\n\n${archiveLines.join("\n")}\n`; diff --git a/backend/src/plugins/Logs/LogsPlugin.ts b/backend/src/plugins/Logs/LogsPlugin.ts index b7ef5132..a678b3e9 100644 --- a/backend/src/plugins/Logs/LogsPlugin.ts +++ b/backend/src/plugins/Logs/LogsPlugin.ts @@ -22,6 +22,7 @@ import { discardRegExpRunner, getRegExpRunner } from "../../regExpRunners"; import { disableCodeBlocks } from "../../utils"; import { logger } from "../../logger"; import { CasesPlugin } from "../Cases/CasesPlugin"; +import { TimeAndDatePlugin } from "../TimeAndDate/TimeAndDatePlugin"; const defaultOptions: PluginOptions = { config: { @@ -49,7 +50,7 @@ export const LogsPlugin = zeppelinPlugin()("logs", { prettyName: "Logs", }, - dependencies: [CasesPlugin], + dependencies: [TimeAndDatePlugin, CasesPlugin], configSchema: ConfigSchema, defaultOptions, diff --git a/backend/src/plugins/Logs/util/getLogMessage.ts b/backend/src/plugins/Logs/util/getLogMessage.ts index 527f3fe1..92cc073b 100644 --- a/backend/src/plugins/Logs/util/getLogMessage.ts +++ b/backend/src/plugins/Logs/util/getLogMessage.ts @@ -13,7 +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"; +import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin"; export async function getLogMessage( pluginData: PluginData, @@ -88,7 +88,10 @@ export async function getLogMessage( const timestampFormat = config.format.timestamp; if (timestampFormat) { - const timestamp = inGuildTz(pluginData).format(timestampFormat); + const timestamp = pluginData + .getPlugin(TimeAndDatePlugin) + .inGuildTz() + .format(timestampFormat); formatted = `\`[${timestamp}]\` ${formatted}`; } } diff --git a/backend/src/plugins/Logs/util/onMessageDelete.ts b/backend/src/plugins/Logs/util/onMessageDelete.ts index 0ff8de5c..80112e22 100644 --- a/backend/src/plugins/Logs/util/onMessageDelete.ts +++ b/backend/src/plugins/Logs/util/onMessageDelete.ts @@ -5,7 +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"; +import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin"; export async function onMessageDelete(pluginData: PluginData, savedMessage: SavedMessage) { const user = await resolveUser(pluginData.client, savedMessage.user_id); @@ -24,9 +24,10 @@ export async function onMessageDelete(pluginData: PluginData, sa { user: stripObjectToScalars(user), channel: stripObjectToScalars(channel), - messageDate: inGuildTz(pluginData, moment.utc(savedMessage.data.timestamp, "x")).format( - pluginData.config.get().format.timestamp, - ), + messageDate: pluginData + .getPlugin(TimeAndDatePlugin) + .inGuildTz(moment.utc(savedMessage.data.timestamp, "x")) + .format(pluginData.config.get().format.timestamp), message: savedMessage, }, savedMessage.id, diff --git a/backend/src/plugins/ModActions/ModActionsPlugin.ts b/backend/src/plugins/ModActions/ModActionsPlugin.ts index d6abe1f4..1714d7b1 100644 --- a/backend/src/plugins/ModActions/ModActionsPlugin.ts +++ b/backend/src/plugins/ModActions/ModActionsPlugin.ts @@ -35,6 +35,7 @@ import { banUserId } from "./functions/banUserId"; import { MassmuteCmd } from "./commands/MassmuteCmd"; import { trimPluginDescription } from "../../utils"; import { DeleteCaseCmd } from "./commands/DeleteCaseCmd"; +import { TimeAndDatePlugin } from "../TimeAndDate/TimeAndDatePlugin"; const defaultOptions = { config: { @@ -103,11 +104,10 @@ export const ModActionsPlugin = zeppelinPlugin()("mod_acti `), }, + dependencies: [TimeAndDatePlugin, CasesPlugin, MutesPlugin], configSchema: ConfigSchema, defaultOptions, - dependencies: [CasesPlugin, MutesPlugin], - events: [ CreateBanCaseOnManualBanEvt, CreateUnbanCaseOnManualUnbanEvt, diff --git a/backend/src/plugins/ModActions/commands/CaseCmd.ts b/backend/src/plugins/ModActions/commands/CaseCmd.ts index ddb07ee1..36d6a86b 100644 --- a/backend/src/plugins/ModActions/commands/CaseCmd.ts +++ b/backend/src/plugins/ModActions/commands/CaseCmd.ts @@ -23,7 +23,7 @@ export const CaseCmd = modActionsCommand({ } const casesPlugin = pluginData.getPlugin(CasesPlugin); - const embed = await casesPlugin.getCaseEmbed(theCase.id); + const embed = await casesPlugin.getCaseEmbed(theCase.id, msg.author.id); msg.channel.createMessage(embed); }, }); diff --git a/backend/src/plugins/ModActions/commands/CasesModCmd.ts b/backend/src/plugins/ModActions/commands/CasesModCmd.ts index 67fbfd0a..f518d92a 100644 --- a/backend/src/plugins/ModActions/commands/CasesModCmd.ts +++ b/backend/src/plugins/ModActions/commands/CasesModCmd.ts @@ -36,7 +36,7 @@ export const CasesModCmd = modActionsCommand({ sendErrorMessage(pluginData, msg.channel, `No cases by **${modName}**`); } else { const casesPlugin = pluginData.getPlugin(CasesPlugin); - const lines = await asyncMap(recentCases, c => casesPlugin.getCaseSummary(c, true)); + const lines = await asyncMap(recentCases, c => casesPlugin.getCaseSummary(c, true, msg.author.id)); const prefix = getGuildPrefix(pluginData); const embed: EmbedOptions = { author: { diff --git a/backend/src/plugins/ModActions/commands/CasesUserCmd.ts b/backend/src/plugins/ModActions/commands/CasesUserCmd.ts index 61e51b32..366ae28e 100644 --- a/backend/src/plugins/ModActions/commands/CasesUserCmd.ts +++ b/backend/src/plugins/ModActions/commands/CasesUserCmd.ts @@ -67,7 +67,7 @@ export const CasesUserCmd = modActionsCommand({ } else { // Compact view (= regular message with a preview of each case) const casesPlugin = pluginData.getPlugin(CasesPlugin); - const lines = await asyncMap(casesToDisplay, c => casesPlugin.getCaseSummary(c, true)); + const lines = await asyncMap(casesToDisplay, c => casesPlugin.getCaseSummary(c, true, msg.author.id)); const prefix = getGuildPrefix(pluginData); const linesPerChunk = 15; diff --git a/backend/src/plugins/ModActions/commands/DeleteCaseCmd.ts b/backend/src/plugins/ModActions/commands/DeleteCaseCmd.ts index 3c96577e..ce02c85a 100644 --- a/backend/src/plugins/ModActions/commands/DeleteCaseCmd.ts +++ b/backend/src/plugins/ModActions/commands/DeleteCaseCmd.ts @@ -8,8 +8,7 @@ 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"; +import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin"; export const DeleteCaseCmd = modActionsCommand({ trigger: ["delete_case", "deletecase"], @@ -54,7 +53,9 @@ export const DeleteCaseCmd = modActionsCommand({ } const deletedByName = `${message.author.username}#${message.author.discriminator}`; - const deletedAt = inGuildTz(pluginData).format(getDateFormat(pluginData, "pretty_datetime")); + + const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin); + const deletedAt = timeAndDate.inGuildTz().format(timeAndDate.getDateFormat("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 0ed7ce68..81f06d8d 100644 --- a/backend/src/plugins/Mutes/commands/MutesCmd.ts +++ b/backend/src/plugins/Mutes/commands/MutesCmd.ts @@ -1,11 +1,10 @@ import { command } from "knub"; import { IMuteWithDetails, MutesPluginType } from "../types"; import { commandTypeHelpers as ct } from "../../../commandTypes"; -import { isFullMessage, MINUTES, noop, resolveMember } from "../../../utils"; +import { DBDateFormat, 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", diff --git a/backend/src/plugins/Post/PostPlugin.ts b/backend/src/plugins/Post/PostPlugin.ts index 6d30dce5..93d46605 100644 --- a/backend/src/plugins/Post/PostPlugin.ts +++ b/backend/src/plugins/Post/PostPlugin.ts @@ -12,6 +12,7 @@ import { ScheduledPostsShowCmd } from "./commands/ScheduledPostsShowCmd"; import { ScheduledPostsListCmd } from "./commands/ScheduledPostsListCmd"; import { ScheduledPostsDeleteCmd } from "./commands/SchedluedPostsDeleteCmd"; import { scheduledPostLoop } from "./util/scheduledPostLoop"; +import { TimeAndDatePlugin } from "../TimeAndDate/TimeAndDatePlugin"; const defaultOptions: PluginOptions = { config: { @@ -33,6 +34,7 @@ export const PostPlugin = zeppelinPlugin()("post", { prettyName: "Post", }, + dependencies: [TimeAndDatePlugin], configSchema: ConfigSchema, defaultOptions, diff --git a/backend/src/plugins/Post/commands/ScheduledPostsListCmd.ts b/backend/src/plugins/Post/commands/ScheduledPostsListCmd.ts index 82abf191..28b123bc 100644 --- a/backend/src/plugins/Post/commands/ScheduledPostsListCmd.ts +++ b/backend/src/plugins/Post/commands/ScheduledPostsListCmd.ts @@ -1,9 +1,15 @@ import { postCmd } from "../types"; -import { trimLines, sorter, disableCodeBlocks, deactivateMentions, createChunkedMessage } from "src/utils"; +import { + trimLines, + sorter, + disableCodeBlocks, + deactivateMentions, + createChunkedMessage, + DBDateFormat, +} from "src/utils"; import humanizeDuration from "humanize-duration"; import moment from "moment-timezone"; -import { inGuildTz } from "../../../utils/timezones"; -import { DBDateFormat, getDateFormat } from "../../../utils/dateFormats"; +import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin"; const SCHEDULED_POST_PREVIEW_TEXT_LENGTH = 50; @@ -31,9 +37,10 @@ export const ScheduledPostsListCmd = postCmd({ .replace(/\s+/g, " ") .slice(0, SCHEDULED_POST_PREVIEW_TEXT_LENGTH); - const prettyPostAt = inGuildTz(pluginData, moment.utc(p.post_at, DBDateFormat)).format( - getDateFormat(pluginData, "pretty_datetime"), - ); + const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin); + const prettyPostAt = timeAndDate + .inGuildTz(moment.utc(p.post_at, DBDateFormat)) + .format(timeAndDate.getDateFormat("pretty_datetime")); const parts = [`\`#${i++}\` \`[${prettyPostAt}]\` ${previewText}${isTruncated ? "..." : ""}`]; if (p.attachments.length) parts.push("*(with attachment)*"); if (p.content.embed) parts.push("*(embed)*"); diff --git a/backend/src/plugins/Post/util/actualPostCmd.ts b/backend/src/plugins/Post/util/actualPostCmd.ts index 4090eed1..885e32dd 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, stripObjectToScalars, MINUTES } from "src/utils"; +import { StrictMessageContent, errorMessage, stripObjectToScalars, MINUTES, DBDateFormat } from "src/utils"; import moment from "moment-timezone"; import { LogType } from "src/data/LogType"; import humanizeDuration from "humanize-duration"; @@ -8,7 +8,7 @@ import { PluginData } from "knub"; import { PostPluginType } from "../types"; import { parseScheduleTime } from "./parseScheduleTime"; import { postMessage } from "./postMessage"; -import { DBDateFormat, getDateFormat } from "../../../utils/dateFormats"; +import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin"; const MIN_REPEAT_TIME = 5 * MINUTES; const MAX_REPEAT_TIME = Math.pow(2, 32); @@ -54,7 +54,7 @@ export async function actualPostCmd( let postAt; if (opts.schedule) { // Schedule the post to be posted later - postAt = parseScheduleTime(pluginData, opts.schedule); + postAt = await parseScheduleTime(pluginData, msg.author.id, opts.schedule); if (!postAt) { return sendErrorMessage(pluginData, msg.channel, "Invalid schedule time"); } @@ -68,7 +68,7 @@ export async function actualPostCmd( let repeatDetailsStr: string = null; if (opts["repeat-until"]) { - repeatUntil = parseScheduleTime(pluginData, opts["repeat-until"]); + repeatUntil = await parseScheduleTime(pluginData, msg.author.id, opts["repeat-until"]); // Invalid time if (!repeatUntil) { @@ -109,6 +109,8 @@ export async function actualPostCmd( : `every ${humanizeDuration(opts.repeat)}, ${repeatTimes} times in total`; } + const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin); + // Save schedule/repeat information in DB if (postAt) { if (postAt < moment.utc()) { @@ -140,9 +142,9 @@ export async function actualPostCmd( pluginData.state.logs.log(LogType.SCHEDULED_REPEATED_MESSAGE, { author: stripObjectToScalars(msg.author), channel: stripObjectToScalars(targetChannel), - datetime: postAt.format(getDateFormat(pluginData, "pretty_datetime")), - date: postAt.format(getDateFormat(pluginData, "date")), - time: postAt.format(getDateFormat(pluginData, "time")), + datetime: postAt.format(timeAndDate.getDateFormat("pretty_datetime")), + date: postAt.format(timeAndDate.getDateFormat("date")), + time: postAt.format(timeAndDate.getDateFormat("time")), repeatInterval: humanizeDuration(opts.repeat), repeatDetails: repeatDetailsStr, }); @@ -150,9 +152,9 @@ export async function actualPostCmd( pluginData.state.logs.log(LogType.SCHEDULED_MESSAGE, { author: stripObjectToScalars(msg.author), channel: stripObjectToScalars(targetChannel), - datetime: postAt.format(getDateFormat(pluginData, "pretty_datetime")), - date: postAt.format(getDateFormat(pluginData, "date")), - time: postAt.format(getDateFormat(pluginData, "time")), + datetime: postAt.format(timeAndDate.getDateFormat("pretty_datetime")), + date: postAt.format(timeAndDate.getDateFormat("date")), + time: postAt.format(timeAndDate.getDateFormat("time")), }); } } @@ -166,9 +168,9 @@ export async function actualPostCmd( pluginData.state.logs.log(LogType.REPEATED_MESSAGE, { author: stripObjectToScalars(msg.author), channel: stripObjectToScalars(targetChannel), - datetime: postAt.format(getDateFormat(pluginData, "pretty_datetime")), - date: postAt.format(getDateFormat(pluginData, "date")), - time: postAt.format(getDateFormat(pluginData, "time")), + datetime: postAt.format(timeAndDate.getDateFormat("pretty_datetime")), + date: postAt.format(timeAndDate.getDateFormat("date")), + time: postAt.format(timeAndDate.getDateFormat("time")), repeatInterval: humanizeDuration(opts.repeat), repeatDetails: repeatDetailsStr, }); @@ -177,7 +179,7 @@ export async function actualPostCmd( // Bot reply schenanigans let successMessage = opts.schedule ? `Message scheduled to be posted in <#${targetChannel.id}> on ${postAt.format( - getDateFormat(pluginData, "pretty_datetime"), + timeAndDate.getDateFormat("pretty_datetime"), )}` : `Message posted in <#${targetChannel.id}>`; @@ -185,7 +187,7 @@ export async function actualPostCmd( successMessage += `. Message will be automatically reposted every ${humanizeDuration(opts.repeat)}`; if (repeatUntil) { - successMessage += ` until ${repeatUntil.format(getDateFormat(pluginData, "pretty_datetime"))}`; + successMessage += ` until ${repeatUntil.format(timeAndDate.getDateFormat("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 3729df2e..6db95ed0 100644 --- a/backend/src/plugins/Post/util/parseScheduleTime.ts +++ b/backend/src/plugins/Post/util/parseScheduleTime.ts @@ -1,11 +1,11 @@ import moment, { Moment } from "moment-timezone"; import { convertDelayStringToMS } from "src/utils"; import { PluginData } from "knub"; -import { getGuildTz } from "../../../utils/timezones"; +import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin"; // 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); +export async function parseScheduleTime(pluginData: PluginData, memberId: string, str: string): Promise { + const tz = await pluginData.getPlugin(TimeAndDatePlugin).getMemberTz(memberId); const dt1 = moment.tz(str, "YYYY-MM-DD HH:mm:ss", tz); if (dt1 && dt1.isValid()) return dt1; diff --git a/backend/src/plugins/Post/util/scheduledPostLoop.ts b/backend/src/plugins/Post/util/scheduledPostLoop.ts index 104c61a6..58e0d545 100644 --- a/backend/src/plugins/Post/util/scheduledPostLoop.ts +++ b/backend/src/plugins/Post/util/scheduledPostLoop.ts @@ -1,12 +1,11 @@ import { PluginData } from "knub"; import { PostPluginType } from "../types"; import { logger } from "src/logger"; -import { stripObjectToScalars, SECONDS } from "src/utils"; +import { stripObjectToScalars, SECONDS, DBDateFormat } 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; diff --git a/backend/src/plugins/Reminders/RemindersPlugin.ts b/backend/src/plugins/Reminders/RemindersPlugin.ts index c0841f85..80100578 100644 --- a/backend/src/plugins/Reminders/RemindersPlugin.ts +++ b/backend/src/plugins/Reminders/RemindersPlugin.ts @@ -6,6 +6,7 @@ import { postDueRemindersLoop } from "./utils/postDueRemindersLoop"; import { RemindCmd } from "./commands/RemindCmd"; import { RemindersCmd } from "./commands/RemindersCmd"; import { RemindersDeleteCmd } from "./commands/RemindersDeleteCmd"; +import { TimeAndDatePlugin } from "../TimeAndDate/TimeAndDatePlugin"; const defaultOptions: PluginOptions = { config: { @@ -27,6 +28,7 @@ export const RemindersPlugin = zeppelinPlugin()("reminders" prettyName: "Reminders", }, + dependencies: [TimeAndDatePlugin], configSchema: ConfigSchema, defaultOptions, diff --git a/backend/src/plugins/Reminders/commands/RemindCmd.ts b/backend/src/plugins/Reminders/commands/RemindCmd.ts index e58a9987..326b9ac3 100644 --- a/backend/src/plugins/Reminders/commands/RemindCmd.ts +++ b/backend/src/plugins/Reminders/commands/RemindCmd.ts @@ -4,8 +4,7 @@ import { convertDelayStringToMS, messageLink } 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"; +import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin"; export const RemindCmd = remindersCommand({ trigger: ["remind", "remindme"], @@ -18,8 +17,10 @@ export const RemindCmd = remindersCommand({ }, async run({ message: msg, args, pluginData }) { + const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin); + const now = moment.utc(); - const tz = getGuildTz(pluginData); + const tz = await timeAndDate.getMemberTz(msg.author.id); let reminderTime: moment.Moment; if (args.time.match(/^\d{4}-\d{1,2}-\d{1,2}$/)) { @@ -62,7 +63,10 @@ export const RemindCmd = remindersCommand({ const msUntilReminder = reminderTime.diff(now); const timeUntilReminder = humanizeDuration(msUntilReminder, { largest: 2, round: true }); - const prettyReminderTime = inGuildTz(pluginData, reminderTime).format(getDateFormat(pluginData, "pretty_datetime")); + const prettyReminderTime = (await timeAndDate.inMemberTz(msg.author.id, reminderTime)).format( + pluginData.getPlugin(TimeAndDatePlugin).getDateFormat("pretty_datetime"), + ); + sendSuccessMessage( pluginData, msg.channel, diff --git a/backend/src/plugins/Reminders/commands/RemindersCmd.ts b/backend/src/plugins/Reminders/commands/RemindersCmd.ts index 54c36f1d..1c7b3470 100644 --- a/backend/src/plugins/Reminders/commands/RemindersCmd.ts +++ b/backend/src/plugins/Reminders/commands/RemindersCmd.ts @@ -1,10 +1,9 @@ import { remindersCommand } from "../types"; import { sendErrorMessage } from "src/pluginUtils"; -import { createChunkedMessage, sorter } from "src/utils"; +import { createChunkedMessage, DBDateFormat, 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"; +import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin"; export const RemindersCmd = remindersCommand({ trigger: "reminders", @@ -17,6 +16,8 @@ export const RemindersCmd = remindersCommand({ return; } + const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin); + reminders.sort(sorter("remind_at")); const longestNum = (reminders.length + 1).toString().length; const lines = Array.from(reminders.entries()).map(([i, reminder]) => { @@ -25,9 +26,9 @@ export const RemindersCmd = remindersCommand({ 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 }); - const prettyRemindAt = inGuildTz(pluginData, moment.utc(reminder.remind_at, DBDateFormat)).format( - getDateFormat(pluginData, "pretty_datetime"), - ); + const prettyRemindAt = timeAndDate + .inGuildTz(moment.utc(reminder.remind_at, DBDateFormat)) + .format(timeAndDate.getDateFormat("pretty_datetime")); return `\`${paddedNum}.\` \`${prettyRemindAt} (${result})\` ${reminder.body}`; }); diff --git a/backend/src/plugins/Spam/util/logAndDetectMessageSpam.ts b/backend/src/plugins/Spam/util/logAndDetectMessageSpam.ts index bff73c2d..801b8510 100644 --- a/backend/src/plugins/Spam/util/logAndDetectMessageSpam.ts +++ b/backend/src/plugins/Spam/util/logAndDetectMessageSpam.ts @@ -2,7 +2,7 @@ import { SavedMessage } from "src/data/entities/SavedMessage"; import { RecentActionType, TBaseSingleSpamConfig, SpamPluginType } from "../types"; import moment from "moment-timezone"; import { MuteResult } from "src/plugins/Mutes/types"; -import { convertDelayStringToMS, trimLines, stripObjectToScalars, resolveMember, noop } from "src/utils"; +import { convertDelayStringToMS, trimLines, stripObjectToScalars, resolveMember, noop, DBDateFormat } from "src/utils"; import { LogType } from "src/data/LogType"; import { CaseTypes } from "src/data/CaseTypes"; import { logger } from "src/logger"; @@ -14,7 +14,6 @@ import { getRecentActionCount } from "./getRecentActionCount"; import { getRecentActions } from "./getRecentActions"; import { clearRecentUserActions } from "./clearRecentUserActions"; import { saveSpamArchives } from "./saveSpamArchives"; -import { DBDateFormat } from "../../../utils/dateFormats"; export async function logAndDetectMessageSpam( pluginData: PluginData, diff --git a/backend/src/plugins/Tags/TagsPlugin.ts b/backend/src/plugins/Tags/TagsPlugin.ts index 65499b90..8497a23b 100644 --- a/backend/src/plugins/Tags/TagsPlugin.ts +++ b/backend/src/plugins/Tags/TagsPlugin.ts @@ -15,7 +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"; +import { TimeAndDatePlugin } from "../TimeAndDate/TimeAndDatePlugin"; const defaultOptions: PluginOptions = { config: { @@ -77,7 +77,9 @@ export const TagsPlugin = zeppelinPlugin()("tags", { state.onMessageDeleteFn = msg => onMessageDelete(pluginData, msg); state.savedMessages.events.on("delete", state.onMessageDeleteFn); - const tz = getGuildTz(pluginData); + const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin); + + const tz = timeAndDate.getGuildTz(); state.tagFunctions = { parseDateTime(str) { if (typeof str === "number") { @@ -154,13 +156,13 @@ export const TagsPlugin = zeppelinPlugin()("tags", { formatTime(time, format) { const parsed = this.parseDateTime(time); - return inGuildTz(parsed).format(format); + return timeAndDate.inGuildTz(parsed).format(format); }, discordDateFormat(time) { const parsed = time ? this.parseDateTime(time) : Date.now(); - return inGuildTz(parsed).format("YYYY-MM-DD"); + return timeAndDate.inGuildTz(parsed).format("YYYY-MM-DD"); }, mention: input => { diff --git a/backend/src/plugins/TimeAndDate/TimeAndDatePlugin.ts b/backend/src/plugins/TimeAndDate/TimeAndDatePlugin.ts new file mode 100644 index 00000000..d9e2e05f --- /dev/null +++ b/backend/src/plugins/TimeAndDate/TimeAndDatePlugin.ts @@ -0,0 +1,50 @@ +import { zeppelinPlugin } from "../ZeppelinPluginBlueprint"; +import { ConfigSchema, TimeAndDatePluginType } from "./types"; +import { GuildMemberTimezones } from "../../data/GuildMemberTimezones"; +import { PluginOptions } from "knub"; +import { SetTimezoneCmd } from "./commands/SetTimezoneCmd"; +import { ViewTimezoneCmd } from "./commands/ViewTimezoneCmd"; +import { defaultDateFormats } from "./defaultDateFormats"; +import { Tail } from "../../utils/typeUtils"; +import { inGuildTz } from "./functions/inGuildTz"; +import { mapToPublicFn } from "../../pluginUtils"; +import { getGuildTz } from "./functions/getGuildTz"; +import { getMemberTz } from "./functions/getMemberTz"; +import { getDateFormat } from "./functions/getDateFormat"; +import { inMemberTz } from "./functions/inMemberTz"; + +const defaultOptions: PluginOptions = { + config: { + timezone: "Etc/UTC", + can_set_timezone: false, + date_formats: defaultDateFormats, + }, + + overrides: [ + { + level: ">=50", + config: { + can_set_timezone: true, + }, + }, + ], +}; + +export const TimeAndDatePlugin = zeppelinPlugin()("time_and_date", { + configSchema: ConfigSchema, + defaultOptions, + + commands: [SetTimezoneCmd, ViewTimezoneCmd], + + public: { + getGuildTz: mapToPublicFn(getGuildTz), + inGuildTz: mapToPublicFn(inGuildTz), + getMemberTz: mapToPublicFn(getMemberTz), + inMemberTz: mapToPublicFn(inMemberTz), + getDateFormat: mapToPublicFn(getDateFormat), + }, + + onLoad(pluginData) { + pluginData.state.memberTimezones = GuildMemberTimezones.getGuildInstance(pluginData.guild.id); + }, +}); diff --git a/backend/src/plugins/TimeAndDate/commands/SetTimezoneCmd.ts b/backend/src/plugins/TimeAndDate/commands/SetTimezoneCmd.ts new file mode 100644 index 00000000..e802f6c5 --- /dev/null +++ b/backend/src/plugins/TimeAndDate/commands/SetTimezoneCmd.ts @@ -0,0 +1,17 @@ +import { timeAndDateCmd } from "../types"; +import { commandTypeHelpers as ct } from "../../../commandTypes"; +import { sendSuccessMessage } from "../../../pluginUtils"; + +export const SetTimezoneCmd = timeAndDateCmd({ + trigger: "timezone", + permission: "can_set_timezone", + + signature: { + timezone: ct.timezone(), + }, + + async run({ pluginData, message, args }) { + await pluginData.state.memberTimezones.set(message.author.id, args.timezone); + sendSuccessMessage(pluginData, message.channel, `Your timezone is now set to **${args.timezone}**`); + }, +}); diff --git a/backend/src/plugins/TimeAndDate/commands/ViewTimezoneCmd.ts b/backend/src/plugins/TimeAndDate/commands/ViewTimezoneCmd.ts new file mode 100644 index 00000000..292f46de --- /dev/null +++ b/backend/src/plugins/TimeAndDate/commands/ViewTimezoneCmd.ts @@ -0,0 +1,23 @@ +import { timeAndDateCmd } from "../types"; +import { commandTypeHelpers as ct } from "../../../commandTypes"; +import { sendSuccessMessage } from "../../../pluginUtils"; +import { getMemberTz } from "../functions/getMemberTz"; +import { getGuildTz } from "../functions/getGuildTz"; + +export const ViewTimezoneCmd = timeAndDateCmd({ + trigger: "timezone", + permission: "can_set_timezone", + + signature: {}, + + async run({ pluginData, message, args }) { + const memberTimezone = await pluginData.state.memberTimezones.get(message.author.id); + if (memberTimezone) { + message.channel.createMessage(`Your timezone is currently set to **${memberTimezone.timezone}**`); + return; + } + + const serverTimezone = getGuildTz(pluginData); + message.channel.createMessage(`Your timezone is currently set to **${serverTimezone}** (server default)`); + }, +}); diff --git a/backend/src/plugins/TimeAndDate/defaultDateFormats.ts b/backend/src/plugins/TimeAndDate/defaultDateFormats.ts new file mode 100644 index 00000000..bb361c3d --- /dev/null +++ b/backend/src/plugins/TimeAndDate/defaultDateFormats.ts @@ -0,0 +1,5 @@ +export const defaultDateFormats = { + date: "MMM D, YYYY", + time: "H:mm", + pretty_datetime: "MMM D, YYYY [at] H:mm z", +}; diff --git a/backend/src/plugins/TimeAndDate/functions/getDateFormat.ts b/backend/src/plugins/TimeAndDate/functions/getDateFormat.ts new file mode 100644 index 00000000..d8e9ab65 --- /dev/null +++ b/backend/src/plugins/TimeAndDate/functions/getDateFormat.ts @@ -0,0 +1,6 @@ +import { PluginData } from "knub"; +import { defaultDateFormats } from "../defaultDateFormats"; + +export function getDateFormat(pluginData: PluginData, formatName: keyof typeof defaultDateFormats) { + return pluginData.config.get().date_formats?.[formatName] || defaultDateFormats[formatName]; +} diff --git a/backend/src/plugins/TimeAndDate/functions/getGuildTz.ts b/backend/src/plugins/TimeAndDate/functions/getGuildTz.ts new file mode 100644 index 00000000..d619a6fb --- /dev/null +++ b/backend/src/plugins/TimeAndDate/functions/getGuildTz.ts @@ -0,0 +1,7 @@ +import { PluginData } from "knub"; +import { ZeppelinGuildConfig } from "../../../types"; +import { TimeAndDatePluginType } from "../types"; + +export function getGuildTz(pluginData: PluginData) { + return pluginData.config.get().timezone; +} diff --git a/backend/src/plugins/TimeAndDate/functions/getMemberTz.ts b/backend/src/plugins/TimeAndDate/functions/getMemberTz.ts new file mode 100644 index 00000000..6f8b1e70 --- /dev/null +++ b/backend/src/plugins/TimeAndDate/functions/getMemberTz.ts @@ -0,0 +1,8 @@ +import { PluginData } from "knub"; +import { TimeAndDatePluginType } from "../types"; +import { getGuildTz } from "./getGuildTz"; + +export async function getMemberTz(pluginData: PluginData, memberId: string) { + const memberTz = await pluginData.state.memberTimezones.get(memberId); + return memberTz?.timezone || getGuildTz(pluginData); +} diff --git a/backend/src/utils/timezones.ts b/backend/src/plugins/TimeAndDate/functions/inGuildTz.ts similarity index 52% rename from backend/src/utils/timezones.ts rename to backend/src/plugins/TimeAndDate/functions/inGuildTz.ts index 0fe7c372..51485d05 100644 --- a/backend/src/utils/timezones.ts +++ b/backend/src/plugins/TimeAndDate/functions/inGuildTz.ts @@ -1,13 +1,9 @@ -import moment from "moment-timezone"; import { PluginData } from "knub"; -import { ZeppelinGuildConfig } from "../types"; +import { TimeAndDatePluginType } from "../types"; +import moment from "moment-timezone"; +import { getGuildTz } from "./getGuildTz"; -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) { +export function inGuildTz(pluginData: PluginData, input?: moment.Moment | number) { let momentObj: moment.Moment; if (typeof input === "number") { momentObj = moment.utc(input, "x"); diff --git a/backend/src/plugins/TimeAndDate/functions/inMemberTz.ts b/backend/src/plugins/TimeAndDate/functions/inMemberTz.ts new file mode 100644 index 00000000..ad082e80 --- /dev/null +++ b/backend/src/plugins/TimeAndDate/functions/inMemberTz.ts @@ -0,0 +1,22 @@ +import { PluginData } from "knub"; +import { TimeAndDatePluginType } from "../types"; +import moment from "moment-timezone"; +import { getGuildTz } from "./getGuildTz"; +import { getMemberTz } from "./getMemberTz"; + +export async function inMemberTz( + pluginData: PluginData, + memberId: string, + 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(await getMemberTz(pluginData, memberId)); +} diff --git a/backend/src/plugins/TimeAndDate/types.ts b/backend/src/plugins/TimeAndDate/types.ts new file mode 100644 index 00000000..f38ad34a --- /dev/null +++ b/backend/src/plugins/TimeAndDate/types.ts @@ -0,0 +1,22 @@ +import * as t from "io-ts"; +import { tNullable, tPartialDictionary } from "../../utils"; +import { BasePluginType, command } from "knub"; +import { GuildMemberTimezones } from "../../data/GuildMemberTimezones"; +import { tValidTimezone } from "../../utils/tValidTimezone"; +import { defaultDateFormats } from "./defaultDateFormats"; + +export const ConfigSchema = t.type({ + timezone: tValidTimezone, + date_formats: tNullable(tPartialDictionary(t.keyof(defaultDateFormats), t.string)), + can_set_timezone: t.boolean, +}); +export type TConfigSchema = t.TypeOf; + +export interface TimeAndDatePluginType extends BasePluginType { + config: TConfigSchema; + state: { + memberTimezones: GuildMemberTimezones; + }; +} + +export const timeAndDateCmd = command(); diff --git a/backend/src/plugins/Utility/UtilityPlugin.ts b/backend/src/plugins/Utility/UtilityPlugin.ts index b54e7c23..301137c7 100644 --- a/backend/src/plugins/Utility/UtilityPlugin.ts +++ b/backend/src/plugins/Utility/UtilityPlugin.ts @@ -33,6 +33,7 @@ import { MessageInfoCmd } from "./commands/MessageInfoCmd"; import { InfoCmd } from "./commands/InfoCmd"; import { SnowflakeInfoCmd } from "./commands/SnowflakeInfoCmd"; import { discardRegExpRunner, getRegExpRunner } from "../../regExpRunners"; +import { TimeAndDatePlugin } from "../TimeAndDate/TimeAndDatePlugin"; const defaultOptions: PluginOptions = { config: { @@ -101,6 +102,7 @@ export const UtilityPlugin = zeppelinPlugin()("utility", { prettyName: "Utility", }, + dependencies: [TimeAndDatePlugin], configSchema: ConfigSchema, defaultOptions, diff --git a/backend/src/plugins/Utility/commands/AboutCmd.ts b/backend/src/plugins/Utility/commands/AboutCmd.ts index 7f6fd354..95bcd372 100644 --- a/backend/src/plugins/Utility/commands/AboutCmd.ts +++ b/backend/src/plugins/Utility/commands/AboutCmd.ts @@ -6,9 +6,8 @@ import humanizeDuration from "humanize-duration"; import LCL from "last-commit-log"; import path from "path"; import moment from "moment-timezone"; -import { getGuildTz, inGuildTz } from "../../../utils/timezones"; import { rootDir } from "../../../paths"; -import { getDateFormat } from "../../../utils/dateFormats"; +import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin"; export const AboutCmd = utilityCmd({ trigger: "about", @@ -16,6 +15,8 @@ export const AboutCmd = utilityCmd({ permission: "can_about", async run({ message: msg, pluginData }) { + const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin); + const uptime = getCurrentUptime(); const prettyUptime = humanizeDuration(uptime, { largest: 2, round: true }); @@ -30,9 +31,9 @@ export const AboutCmd = utilityCmd({ let version; if (lastCommit) { - lastUpdate = inGuildTz(pluginData, moment.utc(lastCommit.committer.date, "X")).format( - getDateFormat(pluginData, "pretty_datetime"), - ); + lastUpdate = timeAndDate + .inGuildTz(moment.utc(lastCommit.committer.date, "X")) + .format(pluginData.getPlugin(TimeAndDatePlugin).getDateFormat("pretty_datetime")); version = lastCommit.shortHash; } else { lastUpdate = "?"; @@ -52,7 +53,7 @@ export const AboutCmd = utilityCmd({ ["Last update", lastUpdate], ["Version", version], ["API latency", `${shard.latency}ms`], - ["Server timezone", getGuildTz(pluginData)], + ["Server timezone", timeAndDate.getGuildTz()], ]; const loadedPlugins = Array.from( diff --git a/backend/src/plugins/Utility/commands/InfoCmd.ts b/backend/src/plugins/Utility/commands/InfoCmd.ts index bc2b1e77..2fd2256a 100644 --- a/backend/src/plugins/Utility/commands/InfoCmd.ts +++ b/backend/src/plugins/Utility/commands/InfoCmd.ts @@ -32,7 +32,7 @@ export const InfoCmd = utilityCmd({ const channelId = getChannelId(value); const channel = channelId && pluginData.guild.channels.get(channelId); if (channel) { - const embed = await getChannelInfoEmbed(pluginData, channelId); + const embed = await getChannelInfoEmbed(pluginData, channelId, message.author.id); if (embed) { message.channel.createMessage({ embed }); return; @@ -42,7 +42,7 @@ export const InfoCmd = utilityCmd({ // 2. Server const guild = pluginData.client.guilds.get(value); if (guild) { - const embed = await getServerInfoEmbed(pluginData, value); + const embed = await getServerInfoEmbed(pluginData, value, message.author.id); if (embed) { message.channel.createMessage({ embed }); return; @@ -52,7 +52,7 @@ export const InfoCmd = utilityCmd({ // 3. User const user = await resolveUser(pluginData.client, value); if (user) { - const embed = await getUserInfoEmbed(pluginData, user.id, Boolean(args.compact)); + const embed = await getUserInfoEmbed(pluginData, user.id, Boolean(args.compact), message.author.id); if (embed) { message.channel.createMessage({ embed }); return; @@ -63,7 +63,12 @@ export const InfoCmd = utilityCmd({ const messageTarget = await resolveMessageTarget(pluginData, value); if (messageTarget) { if (canReadChannel(messageTarget.channel, message.member)) { - const embed = await getMessageInfoEmbed(pluginData, messageTarget.channel.id, messageTarget.messageId); + const embed = await getMessageInfoEmbed( + pluginData, + messageTarget.channel.id, + messageTarget.messageId, + message.author.id, + ); if (embed) { message.channel.createMessage({ embed }); return; @@ -87,7 +92,7 @@ export const InfoCmd = utilityCmd({ // 6. Server again (fallback for discovery servers) const serverPreview = getGuildPreview(pluginData.client, value).catch(() => null); if (serverPreview) { - const embed = await getServerInfoEmbed(pluginData, value); + const embed = await getServerInfoEmbed(pluginData, value, message.author.id); if (embed) { message.channel.createMessage({ embed }); return; @@ -96,7 +101,7 @@ export const InfoCmd = utilityCmd({ // 7. Arbitrary ID if (isValidSnowflake(value)) { - const embed = getSnowflakeInfoEmbed(pluginData, value, true); + const embed = await getSnowflakeInfoEmbed(pluginData, value, true, message.author.id); message.channel.createMessage({ embed }); return; } diff --git a/backend/src/plugins/Utility/commands/MessageInfoCmd.ts b/backend/src/plugins/Utility/commands/MessageInfoCmd.ts index 582bc731..2d694d24 100644 --- a/backend/src/plugins/Utility/commands/MessageInfoCmd.ts +++ b/backend/src/plugins/Utility/commands/MessageInfoCmd.ts @@ -20,7 +20,12 @@ export const MessageInfoCmd = utilityCmd({ return; } - const embed = await getMessageInfoEmbed(pluginData, args.message.channel.id, args.message.messageId); + const embed = await getMessageInfoEmbed( + pluginData, + args.message.channel.id, + args.message.messageId, + message.author.id, + ); if (!embed) { sendErrorMessage(pluginData, message.channel, "Unknown message"); return; diff --git a/backend/src/plugins/Utility/commands/ServerCmd.ts b/backend/src/plugins/Utility/commands/ServerCmd.ts index adc15062..2792b821 100644 --- a/backend/src/plugins/Utility/commands/ServerCmd.ts +++ b/backend/src/plugins/Utility/commands/ServerCmd.ts @@ -15,7 +15,7 @@ export const ServerCmd = utilityCmd({ async run({ message, pluginData, args }) { const serverId = args.serverId || pluginData.guild.id; - const serverInfoEmbed = await getServerInfoEmbed(pluginData, serverId); + const serverInfoEmbed = await getServerInfoEmbed(pluginData, serverId, message.author.id); if (!serverInfoEmbed) { sendErrorMessage(pluginData, message.channel, "Could not find information for that server"); return; diff --git a/backend/src/plugins/Utility/commands/SnowflakeInfoCmd.ts b/backend/src/plugins/Utility/commands/SnowflakeInfoCmd.ts index b327dfc0..ee03044e 100644 --- a/backend/src/plugins/Utility/commands/SnowflakeInfoCmd.ts +++ b/backend/src/plugins/Utility/commands/SnowflakeInfoCmd.ts @@ -14,8 +14,8 @@ export const SnowflakeInfoCmd = utilityCmd({ id: ct.anyId(), }, - run({ message, args, pluginData }) { - const embed = getSnowflakeInfoEmbed(pluginData, args.id); + async run({ message, args, pluginData }) { + const embed = await getSnowflakeInfoEmbed(pluginData, args.id, false, message.author.id); message.channel.createMessage({ embed }); }, }); diff --git a/backend/src/plugins/Utility/commands/UserInfoCmd.ts b/backend/src/plugins/Utility/commands/UserInfoCmd.ts index c28f6109..ffe658c1 100644 --- a/backend/src/plugins/Utility/commands/UserInfoCmd.ts +++ b/backend/src/plugins/Utility/commands/UserInfoCmd.ts @@ -17,7 +17,7 @@ export const UserInfoCmd = utilityCmd({ async run({ message, args, pluginData }) { const userId = args.user?.id || message.author.id; - const embed = await getUserInfoEmbed(pluginData, userId, args.compact); + const embed = await getUserInfoEmbed(pluginData, userId, args.compact, message.author.id); if (!embed) { sendErrorMessage(pluginData, message.channel, "User not found"); return; diff --git a/backend/src/plugins/Utility/functions/getChannelInfoEmbed.ts b/backend/src/plugins/Utility/functions/getChannelInfoEmbed.ts index a6e513f1..b837cca1 100644 --- a/backend/src/plugins/Utility/functions/getChannelInfoEmbed.ts +++ b/backend/src/plugins/Utility/functions/getChannelInfoEmbed.ts @@ -4,8 +4,7 @@ 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"; +import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin"; const TEXT_CHANNEL_ICON = "https://cdn.discordapp.com/attachments/740650744830623756/740656843545772062/text-channel.png"; @@ -17,6 +16,7 @@ const ANNOUNCEMENT_CHANNEL_ICON = export async function getChannelInfoEmbed( pluginData: PluginData, channelId: string, + requestMemberId?: string, ): Promise { const channel = pluginData.guild.channels.get(channelId); if (!channel) { @@ -58,7 +58,11 @@ export async function getChannelInfoEmbed( } const createdAt = moment.utc(channel.createdAt, "x"); - const prettyCreatedAt = inGuildTz(pluginData, createdAt).format(getDateFormat(pluginData, "pretty_datetime")); + const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin); + const tzCreatedAt = requestMemberId + ? await timeAndDate.inMemberTz(requestMemberId, createdAt) + : timeAndDate.inGuildTz(createdAt); + const prettyCreatedAt = tzCreatedAt.format(timeAndDate.getDateFormat("pretty_datetime")); const channelAge = humanizeDuration(Date.now() - channel.createdAt, { largest: 2, round: true, diff --git a/backend/src/plugins/Utility/functions/getMessageInfoEmbed.ts b/backend/src/plugins/Utility/functions/getMessageInfoEmbed.ts index d2d6a7ce..bde02308 100644 --- a/backend/src/plugins/Utility/functions/getMessageInfoEmbed.ts +++ b/backend/src/plugins/Utility/functions/getMessageInfoEmbed.ts @@ -5,8 +5,7 @@ import moment from "moment-timezone"; import humanizeDuration from "humanize-duration"; import { chunkMessageLines, messageLink, preEmbedPadding, trimEmptyLines, trimLines } from "../../../utils"; import { getDefaultPrefix } from "knub/dist/commands/commandUtils"; -import { inGuildTz } from "../../../utils/timezones"; -import { getDateFormat } from "../../../utils/dateFormats"; +import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin"; const MESSAGE_ICON = "https://cdn.discordapp.com/attachments/740650744830623756/740685652152025088/message.png"; @@ -14,12 +13,15 @@ export async function getMessageInfoEmbed( pluginData: PluginData, channelId: string, messageId: string, + requestMemberId?: string, ): Promise { const message = await pluginData.client.getMessage(channelId, messageId).catch(() => null); if (!message) { return null; } + const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin); + const embed: EmbedOptions = { fields: [], }; @@ -30,14 +32,20 @@ export async function getMessageInfoEmbed( }; const createdAt = moment.utc(message.createdAt, "x"); - const prettyCreatedAt = inGuildTz(pluginData, createdAt).format(getDateFormat(pluginData, "pretty_datetime")); + const tzCreatedAt = requestMemberId + ? await timeAndDate.inMemberTz(requestMemberId, createdAt) + : timeAndDate.inGuildTz(createdAt); + const prettyCreatedAt = tzCreatedAt.format(timeAndDate.getDateFormat("pretty_datetime")); const messageAge = humanizeDuration(Date.now() - message.createdAt, { largest: 2, round: true, }); const editedAt = message.editedTimestamp && moment.utc(message.editedTimestamp, "x"); - const prettyEditedAt = inGuildTz(pluginData, editedAt).format(getDateFormat(pluginData, "pretty_datetime")); + const tzEditedAt = requestMemberId + ? await timeAndDate.inMemberTz(requestMemberId, editedAt) + : timeAndDate.inGuildTz(editedAt); + const prettyEditedAt = tzEditedAt.format(timeAndDate.getDateFormat("pretty_datetime")); const editAge = message.editedTimestamp && humanizeDuration(Date.now() - message.editedTimestamp, { @@ -75,18 +83,20 @@ export async function getMessageInfoEmbed( }); const authorCreatedAt = moment.utc(message.author.createdAt, "x"); - const prettyAuthorCreatedAt = inGuildTz(pluginData, authorCreatedAt).format( - getDateFormat(pluginData, "pretty_datetime"), - ); + const tzAuthorCreatedAt = requestMemberId + ? await timeAndDate.inMemberTz(requestMemberId, authorCreatedAt) + : timeAndDate.inGuildTz(authorCreatedAt); + const prettyAuthorCreatedAt = tzAuthorCreatedAt.format(timeAndDate.getDateFormat("pretty_datetime")); const authorAccountAge = humanizeDuration(Date.now() - message.author.createdAt, { largest: 2, round: true, }); const authorJoinedAt = message.member && moment.utc(message.member.joinedAt, "x"); - const prettyAuthorJoinedAt = inGuildTz(pluginData, authorJoinedAt).format( - getDateFormat(pluginData, "pretty_datetime"), - ); + const tzAuthorJoinedAt = requestMemberId + ? await timeAndDate.inMemberTz(requestMemberId, authorJoinedAt) + : timeAndDate.inGuildTz(authorJoinedAt); + const prettyAuthorJoinedAt = tzAuthorJoinedAt.format(timeAndDate.getDateFormat("pretty_datetime")); const authorServerAge = message.member && humanizeDuration(Date.now() - message.member.joinedAt, { diff --git a/backend/src/plugins/Utility/functions/getServerInfoEmbed.ts b/backend/src/plugins/Utility/functions/getServerInfoEmbed.ts index 6adf8d30..a5117a43 100644 --- a/backend/src/plugins/Utility/functions/getServerInfoEmbed.ts +++ b/backend/src/plugins/Utility/functions/getServerInfoEmbed.ts @@ -5,12 +5,12 @@ 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"; +import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin"; export async function getServerInfoEmbed( pluginData: PluginData, serverId: string, + requestMemberId?: string, ): Promise { const thisServer = serverId === pluginData.guild.id ? pluginData.guild : null; const [restGuild, guildPreview] = await Promise.all([ @@ -39,8 +39,12 @@ export async function getServerInfoEmbed( }; // BASIC INFORMATION + const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin); const createdAt = moment.utc((guildPreview || restGuild).createdAt, "x"); - const prettyCreatedAt = inGuildTz(pluginData, createdAt).format(getDateFormat(pluginData, "pretty_datetime")); + const tzCreatedAt = requestMemberId + ? await timeAndDate.inMemberTz(requestMemberId, createdAt) + : timeAndDate.inGuildTz(createdAt); + const prettyCreatedAt = tzCreatedAt.format(timeAndDate.getDateFormat("pretty_datetime")); const serverAge = humanizeDuration(moment.utc().valueOf() - createdAt.valueOf(), { largest: 2, round: true, diff --git a/backend/src/plugins/Utility/functions/getSnowflakeInfoEmbed.ts b/backend/src/plugins/Utility/functions/getSnowflakeInfoEmbed.ts index 7ca674a4..d4561348 100644 --- a/backend/src/plugins/Utility/functions/getSnowflakeInfoEmbed.ts +++ b/backend/src/plugins/Utility/functions/getSnowflakeInfoEmbed.ts @@ -6,16 +6,16 @@ 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"; +import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin"; const SNOWFLAKE_ICON = "https://cdn.discordapp.com/attachments/740650744830623756/742020790471491668/snowflake.png"; -export function getSnowflakeInfoEmbed( +export async function getSnowflakeInfoEmbed( pluginData: PluginData, snowflake: string, showUnknownWarning = false, -): EmbedOptions { + requestMemberId?: string, +): Promise { const embed: EmbedOptions = { fields: [], }; @@ -30,9 +30,13 @@ export function getSnowflakeInfoEmbed( "This is a valid [snowflake ID](https://discord.com/developers/docs/reference#snowflakes), but I don't know what it's for."; } + const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin); const createdAtMS = snowflakeToTimestamp(snowflake); const createdAt = moment.utc(createdAtMS, "x"); - const prettyCreatedAt = inGuildTz(pluginData, createdAt).format(getDateFormat(pluginData, "pretty_datetime")); + const tzCreatedAt = requestMemberId + ? await timeAndDate.inMemberTz(requestMemberId, createdAt) + : timeAndDate.inGuildTz(createdAt); + const prettyCreatedAt = tzCreatedAt.format(timeAndDate.getDateFormat("pretty_datetime")); const snowflakeAge = humanizeDuration(Date.now() - createdAtMS, { largest: 2, round: true, diff --git a/backend/src/plugins/Utility/functions/getUserInfoEmbed.ts b/backend/src/plugins/Utility/functions/getUserInfoEmbed.ts index 01af5803..6a127206 100644 --- a/backend/src/plugins/Utility/functions/getUserInfoEmbed.ts +++ b/backend/src/plugins/Utility/functions/getUserInfoEmbed.ts @@ -14,13 +14,13 @@ import { 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"; +import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin"; export async function getUserInfoEmbed( pluginData: PluginData, userId: string, compact = false, + requestMemberId?: string, ): Promise { const user = await resolveUser(pluginData.client, userId); if (!user || user instanceof UnknownUser) { @@ -33,6 +33,8 @@ export async function getUserInfoEmbed( fields: [], }; + const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin); + embed.author = { name: `User: ${user.username}#${user.discriminator}`, }; @@ -41,7 +43,10 @@ export async function getUserInfoEmbed( embed.author.icon_url = avatarURL; const createdAt = moment.utc(user.createdAt, "x"); - const prettyCreatedAt = inGuildTz(pluginData, createdAt).format(getDateFormat(pluginData, "pretty_datetime")); + const tzCreatedAt = requestMemberId + ? await timeAndDate.inMemberTz(requestMemberId, createdAt) + : timeAndDate.inGuildTz(createdAt); + const prettyCreatedAt = tzCreatedAt.format(timeAndDate.getDateFormat("pretty_datetime")); const accountAge = humanizeDuration(moment.utc().valueOf() - user.createdAt, { largest: 2, round: true, @@ -57,7 +62,10 @@ export async function getUserInfoEmbed( }); if (member) { const joinedAt = moment.utc(member.joinedAt, "x"); - const prettyJoinedAt = inGuildTz(pluginData, joinedAt).format(getDateFormat(pluginData, "pretty_datetime")); + const tzJoinedAt = requestMemberId + ? await timeAndDate.inMemberTz(requestMemberId, joinedAt) + : timeAndDate.inGuildTz(joinedAt); + const prettyJoinedAt = tzJoinedAt.format(timeAndDate.getDateFormat("pretty_datetime")); const joinAge = humanizeDuration(moment.utc().valueOf() - member.joinedAt, { largest: 2, round: true, @@ -85,7 +93,10 @@ export async function getUserInfoEmbed( if (member) { const joinedAt = moment.utc(member.joinedAt, "x"); - const prettyJoinedAt = inGuildTz(pluginData, joinedAt).format(getDateFormat(pluginData, "pretty_datetime")); + const tzJoinedAt = requestMemberId + ? await timeAndDate.inMemberTz(requestMemberId, joinedAt) + : timeAndDate.inGuildTz(joinedAt); + const prettyJoinedAt = tzJoinedAt.format(timeAndDate.getDateFormat("pretty_datetime")); const joinAge = humanizeDuration(moment.utc().valueOf() - member.joinedAt, { largest: 2, round: true, diff --git a/backend/src/plugins/availablePlugins.ts b/backend/src/plugins/availablePlugins.ts index f7c31251..149d9d2a 100644 --- a/backend/src/plugins/availablePlugins.ts +++ b/backend/src/plugins/availablePlugins.ts @@ -31,6 +31,7 @@ import { CompanionChannelsPlugin } from "./CompanionChannels/CompanionChannelsPl import { CustomEventsPlugin } from "./CustomEvents/CustomEventsPlugin"; import { BotControlPlugin } from "./BotControl/BotControlPlugin"; import { GuildAccessMonitorPlugin } from "./GuildAccessMonitor/GuildAccessMonitorPlugin"; +import { TimeAndDatePlugin } from "./TimeAndDate/TimeAndDatePlugin"; // prettier-ignore export const guildPlugins: Array> = [ @@ -63,6 +64,7 @@ export const guildPlugins: Array> = [ AutomodPlugin, CompanionChannelsPlugin, CustomEventsPlugin, + TimeAndDatePlugin, ]; // prettier-ignore @@ -79,5 +81,6 @@ export const baseGuildPlugins: Array> = [ NameHistoryPlugin, CasesPlugin, MutesPlugin, + TimeAndDatePlugin, // TODO: Replace these with proper dependencies ]; diff --git a/backend/src/types.ts b/backend/src/types.ts index 3d3f64ca..5e0d8b8a 100644 --- a/backend/src/types.ts +++ b/backend/src/types.ts @@ -1,19 +1,13 @@ import { BaseConfig, Knub } from "knub"; import * as t from "io-ts"; -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; + + // Deprecated timezone?: string; - date_formats?: Partial; + date_formats?: any; } export const ZeppelinGuildConfigSchema = t.type({ @@ -25,8 +19,10 @@ export const ZeppelinGuildConfigSchema = t.type({ // From ZeppelinGuildConfig success_emoji: t.string, error_emoji: t.string, + + // Deprecated timezone: t.string, - date_formats: t.partial(DateFormatsSchema.props), + date_formats: t.unknown, }); export const PartialZeppelinGuildConfigSchema = t.partial(ZeppelinGuildConfigSchema.props); diff --git a/backend/src/utils.ts b/backend/src/utils.ts index 9be52c74..fb2875e5 100644 --- a/backend/src/utils.ts +++ b/backend/src/utils.ts @@ -1298,3 +1298,5 @@ export function asyncMap(arr: T[], fn: (item: T) => Promise): Promise(arr: T[]): T[] { return Array.from(new Set(arr)); } + +export const DBDateFormat = "YYYY-MM-DD HH:mm:ss"; diff --git a/backend/src/utils/dateFormats.ts b/backend/src/utils/dateFormats.ts deleted file mode 100644 index b429f2a5..00000000 --- a/backend/src/utils/dateFormats.ts +++ /dev/null @@ -1,17 +0,0 @@ -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/isValidTimezone.ts b/backend/src/utils/isValidTimezone.ts new file mode 100644 index 00000000..9aeb3eea --- /dev/null +++ b/backend/src/utils/isValidTimezone.ts @@ -0,0 +1,7 @@ +import moment from "moment-timezone"; + +const validTimezones = moment.tz.names(); + +export function isValidTimezone(input: string) { + return validTimezones.includes(input); +} diff --git a/backend/src/utils/tValidTimezone.ts b/backend/src/utils/tValidTimezone.ts new file mode 100644 index 00000000..9922bd96 --- /dev/null +++ b/backend/src/utils/tValidTimezone.ts @@ -0,0 +1,13 @@ +import * as t from "io-ts"; +import { either } from "fp-ts/lib/Either"; +import { isValidTimezone } from "./isValidTimezone"; + +export const tValidTimezone = new t.Type( + "tValidTimezone", + (s): s is string => typeof s === "string", + (from, to) => + either.chain(t.string.validate(from, to), input => { + return isValidTimezone(input) ? t.success(input) : t.failure(from, to, `Invalid timezone: ${input}`); + }), + s => s, +); diff --git a/backend/src/utils/typeUtils.ts b/backend/src/utils/typeUtils.ts new file mode 100644 index 00000000..fe327f8e --- /dev/null +++ b/backend/src/utils/typeUtils.ts @@ -0,0 +1,2 @@ +// From https://stackoverflow.com/a/56370310/316944 +export type Tail = ((...t: T) => void) extends (h: any, ...r: infer R) => void ? R : never;