Add support for server-specific timezone and date format settings

This commit is contained in:
Dragory 2020-08-10 00:24:06 +03:00
parent ddbbc543c2
commit c67a1df11d
No known key found for this signature in database
GPG key ID: 5F387BA66DF8AAC1
51 changed files with 326 additions and 168 deletions

View file

@ -41,7 +41,7 @@ module.exports = {
typeCast(field, next) { typeCast(field, next) {
if (field.type === 'DATETIME') { if (field.type === 'DATETIME') {
const val = field.string(); 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(); return next();

View file

@ -18,12 +18,13 @@ export function initArchives(app: express.Express) {
let body = archive.body; 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) // 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) { 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}`; body += `\n\nLog file generated on ${createdAt}`;
if (archive.expires_at !== null) { 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}`; body += `\nExpires at ${expiresAt}`;
} }
} }

View file

@ -2,35 +2,27 @@ import * as t from "io-ts";
import { guildPlugins } from "./plugins/availablePlugins"; import { guildPlugins } from "./plugins/availablePlugins";
import { decodeAndValidateStrict, StrictValidationError } from "./validatorUtils"; import { decodeAndValidateStrict, StrictValidationError } from "./validatorUtils";
import { ZeppelinPlugin } from "./plugins/ZeppelinPlugin"; import { ZeppelinPlugin } from "./plugins/ZeppelinPlugin";
import { IZeppelinGuildConfig } from "./types"; import { PartialZeppelinGuildConfigSchema, ZeppelinGuildConfig } from "./types";
import { configUtils, ConfigValidationError, PluginOptions } from "knub"; import { configUtils, ConfigValidationError, PluginOptions } from "knub";
import moment from "moment-timezone";
const pluginNameToPlugin = new Map<string, ZeppelinPlugin>(); const pluginNameToPlugin = new Map<string, ZeppelinPlugin>();
for (const plugin of guildPlugins) { for (const plugin of guildPlugins) {
pluginNameToPlugin.set(plugin.name, plugin); 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<string | null> { export async function validateGuildConfig(config: any): Promise<string | null> {
const validationResult = decodeAndValidateStrict(partialGuildConfigRootSchema, config); const validationResult = decodeAndValidateStrict(PartialZeppelinGuildConfigSchema, config);
if (validationResult instanceof StrictValidationError) return validationResult.getErrors(); 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) { if (guildConfig.plugins) {
for (const [pluginName, pluginOptions] of Object.entries(guildConfig.plugins)) { for (const [pluginName, pluginOptions] of Object.entries(guildConfig.plugins)) {

View file

@ -5,7 +5,7 @@ import crypto from "crypto";
import moment from "moment-timezone"; import moment from "moment-timezone";
// tslint:disable-next-line:no-submodule-imports // tslint:disable-next-line:no-submodule-imports
import uuidv4 from "uuid/v4"; import uuidv4 from "uuid/v4";
import { DBDateFormat } from "../utils"; import { DBDateFormat } from "../utils/dateFormats";
export class ApiLogins extends BaseRepository { export class ApiLogins extends BaseRepository {
private apiLogins: Repository<ApiLogin>; private apiLogins: Repository<ApiLogin>;
@ -65,8 +65,9 @@ export class ApiLogins extends BaseRepository {
id: loginId, id: loginId,
token: hashedToken, token: hashedToken,
user_id: userId, user_id: userId,
logged_in_at: moment().format(DBDateFormat), logged_in_at: moment.utc().format(DBDateFormat),
expires_at: moment() expires_at: moment
.utc()
.add(1, "day") .add(1, "day")
.format(DBDateFormat), .format(DBDateFormat),
}); });
@ -81,7 +82,7 @@ export class ApiLogins extends BaseRepository {
return this.apiLogins.update( return this.apiLogins.update(
{ id: loginId }, { id: loginId },
{ {
expires_at: moment().format(DBDateFormat), expires_at: moment.utc().format(DBDateFormat),
}, },
); );
} }

View file

@ -3,7 +3,7 @@ import { ApiUserInfo as ApiUserInfoEntity, ApiUserInfoData } from "./entities/Ap
import { BaseRepository } from "./BaseRepository"; import { BaseRepository } from "./BaseRepository";
import { connection } from "./db"; import { connection } from "./db";
import moment from "moment-timezone"; import moment from "moment-timezone";
import { DBDateFormat } from "../utils"; import { DBDateFormat } from "../utils/dateFormats";
export class ApiUserInfo extends BaseRepository { export class ApiUserInfo extends BaseRepository {
private apiUserInfo: Repository<ApiUserInfoEntity>; private apiUserInfo: Repository<ApiUserInfoEntity>;
@ -26,7 +26,7 @@ export class ApiUserInfo extends BaseRepository {
const repo = entityManager.getRepository(ApiUserInfoEntity); const repo = entityManager.getRepository(ApiUserInfoEntity);
const existingInfo = await repo.findOne({ where: { id } }); const existingInfo = await repo.findOne({ where: { id } });
const updatedAt = moment().format(DBDateFormat); const updatedAt = moment.utc().format(DBDateFormat);
if (existingInfo) { if (existingInfo) {
await repo.update({ id }, { data, updated_at: updatedAt }); await repo.update({ id }, { data, updated_at: updatedAt });

View file

@ -57,8 +57,8 @@
"MEMBER_MUTE_REJOIN": "⚠ Reapplied active mute for {userMention(member)} on rejoin", "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_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 {date} at {time} (UTC), repeated {repeatDetails}", "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}", "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)}", "POSTED_SCHEDULED_MESSAGE": "\uD83D\uDCE8 Posted scheduled message (`{messageId}`) to {channelMention(channel)} as scheduled by {userMention(author)}",

View file

@ -58,7 +58,7 @@ export class GuildArchives extends BaseGuildRepository {
*/ */
async create(body: string, expiresAt: moment.Moment = null): Promise<string> { async create(body: string, expiresAt: moment.Moment = null): Promise<string> {
if (!expiresAt) { if (!expiresAt) {
expiresAt = moment().add(DEFAULT_EXPIRY_DAYS, "days"); expiresAt = moment.utc().add(DEFAULT_EXPIRY_DAYS, "days");
} }
const result = await this.archives.insert({ const result = await this.archives.insert({
@ -78,7 +78,7 @@ export class GuildArchives extends BaseGuildRepository {
const line = await renderTemplate(MESSAGE_ARCHIVE_MESSAGE_FORMAT, { const line = await renderTemplate(MESSAGE_ARCHIVE_MESSAGE_FORMAT, {
id: msg.id, 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, content: msg.data.content,
user, user,
channel, channel,
@ -89,7 +89,9 @@ export class GuildArchives extends BaseGuildRepository {
} }
async createFromSavedMessages(savedMessages: SavedMessage[], guild: Guild, expiresAt = null) { 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 headerStr = await renderTemplate(MESSAGE_ARCHIVE_HEADER_FORMAT, { guild });
const msgLines = await this.renderLinesFromSavedMessages(savedMessages, guild); const msgLines = await this.renderLinesFromSavedMessages(savedMessages, guild);

View file

@ -6,6 +6,7 @@ import { disableLinkPreviews } from "../utils";
import { CaseTypes } from "./CaseTypes"; import { CaseTypes } from "./CaseTypes";
import moment = require("moment-timezone"); import moment = require("moment-timezone");
import { connection } from "./db"; import { connection } from "./db";
import { DBDateFormat } from "../utils/dateFormats";
const CASE_SUMMARY_REASON_MAX_LENGTH = 300; 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) { getSummaryText(theCase: Case) {
const firstNote = theCase.notes[0]; const firstNote = theCase.notes[0];
let reason = firstNote ? firstNote.body : ""; let reason = firstNote ? firstNote.body : "";
@ -172,7 +174,7 @@ export class GuildCases extends BaseGuildRepository {
reason = disableLinkPreviews(reason); 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}`; let line = `\`[${timestamp}]\` \`Case #${theCase.case_number}\` __${CaseTypes[theCase.type]}__ ${reason}`;
if (theCase.notes.length > 1) { if (theCase.notes.length > 1) {
line += ` *(+${theCase.notes.length - 1} ${theCase.notes.length === 2 ? "note" : "notes"})*`; line += ` *(+${theCase.notes.length - 1} ${theCase.notes.length === 2 ? "note" : "notes"})*`;

View file

@ -36,7 +36,8 @@ export class GuildMutes extends BaseGuildRepository {
async addMute(userId, expiryTime): Promise<Mute> { async addMute(userId, expiryTime): Promise<Mute> {
const expiresAt = expiryTime const expiresAt = expiryTime
? moment() ? moment
.utc()
.add(expiryTime, "ms") .add(expiryTime, "ms")
.format("YYYY-MM-DD HH:mm:ss") .format("YYYY-MM-DD HH:mm:ss")
: null; : null;
@ -52,7 +53,8 @@ export class GuildMutes extends BaseGuildRepository {
async updateExpiryTime(userId, newExpiryTime) { async updateExpiryTime(userId, newExpiryTime) {
const expiresAt = newExpiryTime const expiresAt = newExpiryTime
? moment() ? moment
.utc()
.add(newExpiryTime, "ms") .add(newExpiryTime, "ms")
.format("YYYY-MM-DD HH:mm:ss") .format("YYYY-MM-DD HH:mm:ss")
: null; : null;

View file

@ -182,7 +182,7 @@ export class GuildSavedMessages extends BaseGuildRepository {
* If any messages were marked as deleted, also emits the deleteBulk event. * If any messages were marked as deleted, also emits the deleteBulk event.
*/ */
async markBulkAsDeleted(ids) { 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 await this.messages
.createQueryBuilder() .createQueryBuilder()

View file

@ -67,7 +67,8 @@ export class GuildSlowmodes extends BaseGuildRepository {
const slowmode = await this.getChannelSlowmode(channelId); const slowmode = await this.getChannelSlowmode(channelId);
if (!slowmode) return; if (!slowmode) return;
const expiresAt = moment() const expiresAt = moment
.utc()
.add(slowmode.slowmode_seconds, "seconds") .add(slowmode.slowmode_seconds, "seconds")
.format("YYYY-MM-DD HH:mm:ss"); .format("YYYY-MM-DD HH:mm:ss");

View file

@ -2,7 +2,7 @@ import { connection } from "../db";
import { getRepository, In } from "typeorm"; import { getRepository, In } from "typeorm";
import { Config } from "../entities/Config"; import { Config } from "../entities/Config";
import moment from "moment-timezone"; import moment from "moment-timezone";
import { DBDateFormat } from "../../utils"; import { DBDateFormat } from "../../utils/dateFormats";
const CLEAN_PER_LOOP = 50; const CLEAN_PER_LOOP = 50;
@ -13,7 +13,8 @@ export async function cleanupConfigs() {
let rows; let rows;
// >1 month old: 1 config retained per month // >1 month old: 1 config retained per month
const oneMonthCutoff = moment() const oneMonthCutoff = moment
.utc()
.subtract(30, "days") .subtract(30, "days")
.format(DBDateFormat); .format(DBDateFormat);
do { do {
@ -53,7 +54,8 @@ export async function cleanupConfigs() {
} while (rows.length === CLEAN_PER_LOOP); } while (rows.length === CLEAN_PER_LOOP);
// >2 weeks old: 1 config retained per day // >2 weeks old: 1 config retained per day
const twoWeekCutoff = moment() const twoWeekCutoff = moment
.utc()
.subtract(2, "weeks") .subtract(2, "weeks")
.format(DBDateFormat); .format(DBDateFormat);
do { do {

View file

@ -1,8 +1,9 @@
import { DAYS, DBDateFormat, MINUTES } from "../../utils"; import { DAYS, MINUTES } from "../../utils";
import { getRepository, In } from "typeorm"; import { getRepository, In } from "typeorm";
import { SavedMessage } from "../entities/SavedMessage"; import { SavedMessage } from "../entities/SavedMessage";
import moment from "moment-timezone"; import moment from "moment-timezone";
import { connection } from "../db"; import { connection } from "../db";
import { DBDateFormat } from "../../utils/dateFormats";
/** /**
* How long message edits, deletions, etc. will include the original message content. * How long message edits, deletions, etc. will include the original message content.
@ -18,13 +19,16 @@ export async function cleanupMessages(): Promise<number> {
const messagesRepository = getRepository(SavedMessage); const messagesRepository = getRepository(SavedMessage);
const deletedAtThreshold = moment() const deletedAtThreshold = moment
.utc()
.subtract(DELETED_MESSAGE_RETENTION_PERIOD, "ms") .subtract(DELETED_MESSAGE_RETENTION_PERIOD, "ms")
.format(DBDateFormat); .format(DBDateFormat);
const postedAtThreshold = moment() const postedAtThreshold = moment
.utc()
.subtract(RETENTION_PERIOD, "ms") .subtract(RETENTION_PERIOD, "ms")
.format(DBDateFormat); .format(DBDateFormat);
const botPostedAtThreshold = moment() const botPostedAtThreshold = moment
.utc()
.subtract(BOT_MESSAGE_RETENTION_PERIOD, "ms") .subtract(BOT_MESSAGE_RETENTION_PERIOD, "ms")
.format(DBDateFormat); .format(DBDateFormat);

View file

@ -1,8 +1,9 @@
import { getRepository, In } from "typeorm"; import { getRepository, In } from "typeorm";
import moment from "moment-timezone"; import moment from "moment-timezone";
import { NicknameHistoryEntry } from "../entities/NicknameHistoryEntry"; import { NicknameHistoryEntry } from "../entities/NicknameHistoryEntry";
import { DAYS, DBDateFormat } from "../../utils"; import { DAYS } from "../../utils";
import { connection } from "../db"; import { connection } from "../db";
import { DBDateFormat } from "../../utils/dateFormats";
export const NICKNAME_RETENTION_PERIOD = 30 * DAYS; export const NICKNAME_RETENTION_PERIOD = 30 * DAYS;
const CLEAN_PER_LOOP = 500; const CLEAN_PER_LOOP = 500;
@ -11,7 +12,8 @@ export async function cleanupNicknames(): Promise<number> {
let cleaned = 0; let cleaned = 0;
const nicknameHistoryRepository = getRepository(NicknameHistoryEntry); const nicknameHistoryRepository = getRepository(NicknameHistoryEntry);
const dateThreshold = moment() const dateThreshold = moment
.utc()
.subtract(NICKNAME_RETENTION_PERIOD, "ms") .subtract(NICKNAME_RETENTION_PERIOD, "ms")
.format(DBDateFormat); .format(DBDateFormat);

View file

@ -1,8 +1,9 @@
import { getRepository, In } from "typeorm"; import { getRepository, In } from "typeorm";
import moment from "moment-timezone"; import moment from "moment-timezone";
import { UsernameHistoryEntry } from "../entities/UsernameHistoryEntry"; import { UsernameHistoryEntry } from "../entities/UsernameHistoryEntry";
import { DAYS, DBDateFormat } from "../../utils"; import { DAYS } from "../../utils";
import { connection } from "../db"; import { connection } from "../db";
import { DBDateFormat } from "../../utils/dateFormats";
export const USERNAME_RETENTION_PERIOD = 30 * DAYS; export const USERNAME_RETENTION_PERIOD = 30 * DAYS;
const CLEAN_PER_LOOP = 500; const CLEAN_PER_LOOP = 500;
@ -11,7 +12,8 @@ export async function cleanupUsernames(): Promise<number> {
let cleaned = 0; let cleaned = 0;
const usernameHistoryRepository = getRepository(UsernameHistoryEntry); const usernameHistoryRepository = getRepository(UsernameHistoryEntry);
const dateThreshold = moment() const dateThreshold = moment
.utc()
.subtract(USERNAME_RETENTION_PERIOD, "ms") .subtract(USERNAME_RETENTION_PERIOD, "ms")
.format(DBDateFormat); .format(DBDateFormat);

View file

@ -15,7 +15,7 @@ import { baseGuildPlugins, globalPlugins, guildPlugins } from "./plugins/availab
import { errorMessage, isDiscordHTTPError, isDiscordRESTError, MINUTES, successMessage } from "./utils"; import { errorMessage, isDiscordHTTPError, isDiscordRESTError, MINUTES, successMessage } from "./utils";
import { startUptimeCounter } from "./uptime"; import { startUptimeCounter } from "./uptime";
import { AllowedGuilds } from "./data/AllowedGuilds"; import { AllowedGuilds } from "./data/AllowedGuilds";
import { IZeppelinGlobalConfig, IZeppelinGuildConfig } from "./types"; import { ZeppelinGlobalConfig, ZeppelinGuildConfig } from "./types";
import { RecoverablePluginError } from "./RecoverablePluginError"; import { RecoverablePluginError } from "./RecoverablePluginError";
import { GuildLogs } from "./data/GuildLogs"; import { GuildLogs } from "./data/GuildLogs";
import { LogType } from "./data/LogType"; import { LogType } from "./data/LogType";
@ -138,7 +138,7 @@ connect().then(async () => {
const allowedGuilds = new AllowedGuilds(); const allowedGuilds = new AllowedGuilds();
const guildConfigs = new Configs(); const guildConfigs = new Configs();
const bot = new Knub<IZeppelinGuildConfig, IZeppelinGlobalConfig>(client, { const bot = new Knub<ZeppelinGuildConfig, ZeppelinGlobalConfig>(client, {
guildPlugins, guildPlugins,
globalPlugins, globalPlugins,

View file

@ -5,6 +5,8 @@ import { LogType } from "src/data/LogType";
import { stripObjectToScalars, resolveUser } from "src/utils"; import { stripObjectToScalars, resolveUser } from "src/utils";
import { logger } from "src/logger"; import { logger } from "src/logger";
import { scheduleNextDeletion } from "./scheduleNextDeletion"; import { scheduleNextDeletion } from "./scheduleNextDeletion";
import { inGuildTz } from "../../../utils/timezones";
import { getDateFormat } from "../../../utils/dateFormats";
export async function deleteNextItem(pluginData: PluginData<AutoDeletePluginType>) { export async function deleteNextItem(pluginData: PluginData<AutoDeletePluginType>) {
const [itemToDelete] = pluginData.state.deletionQueue.splice(0, 1); const [itemToDelete] = pluginData.state.deletionQueue.splice(0, 1);
@ -17,7 +19,9 @@ export async function deleteNextItem(pluginData: PluginData<AutoDeletePluginType
const user = await resolveUser(pluginData.client, itemToDelete.message.user_id); const user = await resolveUser(pluginData.client, itemToDelete.message.user_id);
const channel = pluginData.guild.channels.get(itemToDelete.message.channel_id); const channel = pluginData.guild.channels.get(itemToDelete.message.channel_id);
const messageDate = moment(itemToDelete.message.data.timestamp, "x").format("YYYY-MM-DD HH:mm:ss"); const messageDate = inGuildTz(pluginData, moment.utc(itemToDelete.message.data.timestamp, "x")).format(
getDateFormat(pluginData, "pretty_datetime"),
);
pluginData.state.guildLogs.log(LogType.MESSAGE_DELETE_AUTO, { pluginData.state.guildLogs.log(LogType.MESSAGE_DELETE_AUTO, {
message: itemToDelete.message, message: itemToDelete.message,

View file

@ -7,6 +7,8 @@ import { CasesPluginType } from "../types";
import { CaseTypeColors } from "../../../data/CaseTypeColors"; import { CaseTypeColors } from "../../../data/CaseTypeColors";
import { resolveCaseId } from "./resolveCaseId"; import { resolveCaseId } from "./resolveCaseId";
import { chunkLines, chunkMessageLines, emptyEmbedValue } from "../../../utils"; import { chunkLines, chunkMessageLines, emptyEmbedValue } from "../../../utils";
import { inGuildTz } from "../../../utils/timezones";
import { getDateFormat } from "../../../utils/dateFormats";
export async function getCaseEmbed( export async function getCaseEmbed(
pluginData: PluginData<CasesPluginType>, pluginData: PluginData<CasesPluginType>,
@ -15,7 +17,7 @@ export async function getCaseEmbed(
const theCase = await pluginData.state.cases.with("notes").find(resolveCaseId(caseOrCaseId)); const theCase = await pluginData.state.cases.with("notes").find(resolveCaseId(caseOrCaseId));
if (!theCase) return null; if (!theCase) return null;
const createdAt = moment(theCase.created_at); const createdAt = moment.utc(theCase.created_at);
const actionTypeStr = CaseTypes[theCase.type].toUpperCase(); const actionTypeStr = CaseTypes[theCase.type].toUpperCase();
let userName = theCase.user_name; let userName = theCase.user_name;
@ -27,7 +29,7 @@ export async function getCaseEmbed(
const embed: any = { const embed: any = {
title: `${actionTypeStr} - Case #${theCase.case_number}`, title: `${actionTypeStr} - Case #${theCase.case_number}`,
footer: { 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: [ fields: [
{ {
@ -57,7 +59,7 @@ export async function getCaseEmbed(
if (theCase.notes.length) { if (theCase.notes.length) {
theCase.notes.forEach((note: any) => { theCase.notes.forEach((note: any) => {
const noteDate = moment(note.created_at); const noteDate = moment.utc(note.created_at);
let noteBody = note.body.trim(); let noteBody = note.body.trim();
if (noteBody === "") { if (noteBody === "") {
noteBody = emptyEmbedValue; noteBody = emptyEmbedValue;
@ -67,8 +69,9 @@ export async function getCaseEmbed(
for (let i = 0; i < chunks.length; i++) { for (let i = 0; i < chunks.length; i++) {
if (i === 0) { if (i === 0) {
const prettyNoteDate = inGuildTz(pluginData, noteDate).format(getDateFormat(pluginData, "pretty_datetime"));
embed.fields.push({ 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], value: chunks[i],
}); });
} else { } else {

View file

@ -4,6 +4,8 @@ import { isOwner, sendErrorMessage } from "src/pluginUtils";
import { confirm, SECONDS, noop } from "src/utils"; import { confirm, SECONDS, noop } from "src/utils";
import moment from "moment-timezone"; import moment from "moment-timezone";
import { rehostAttachment } from "../rehostAttachment"; import { rehostAttachment } from "../rehostAttachment";
import { inGuildTz } from "../../../utils/timezones";
import { getDateFormat } from "../../../utils/dateFormats";
const MAX_ARCHIVED_MESSAGES = 5000; const MAX_ARCHIVED_MESSAGES = 5000;
const MAX_MESSAGES_PER_FETCH = 100; const MAX_MESSAGES_PER_FETCH = 100;
@ -96,7 +98,7 @@ export const ArchiveChannelCmd = channelArchiverCmd({
archiveLines.reverse(); 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}`; let result = `Archived ${archiveLines.length} messages from #${args.channel.name} at ${nowTs}`;
result += `\n\n${archiveLines.join("\n")}\n`; result += `\n\n${archiveLines.join("\n")}\n`;
@ -104,7 +106,7 @@ export const ArchiveChannelCmd = channelArchiverCmd({
progressMsg.delete().catch(noop); progressMsg.delete().catch(noop);
msg.channel.createMessage("Archive created!", { msg.channel.createMessage("Archive created!", {
file: Buffer.from(result), 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`,
}); });
}, },
}); });

View file

@ -21,7 +21,7 @@ export const FollowCmd = locateUserCommand({
async run({ message: msg, args, pluginData }) { async run({ message: msg, args, pluginData }) {
const time = args.duration || 10 * MINUTES; 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 body = args.reminder || "None";
const active = args.active || false; const active = args.active || false;

View file

@ -26,7 +26,7 @@ const defaultOptions: PluginOptions<LogsPluginType> = {
config: { config: {
channels: {}, channels: {},
format: { format: {
timestamp: "YYYY-MM-DD HH:mm:ss", timestamp: "YYYY-MM-DD HH:mm:ss z",
...DefaultLogMessages, ...DefaultLogMessages,
}, },
ping_user: true, ping_user: true,

View file

@ -11,8 +11,8 @@ export const LogsGuildMemberAddEvt = logsEvent({
const pluginData = meta.pluginData; const pluginData = meta.pluginData;
const member = meta.args.member; const member = meta.args.member;
const newThreshold = moment().valueOf() - 1000 * 60 * 60; const newThreshold = moment.utc().valueOf() - 1000 * 60 * 60;
const accountAge = humanizeDuration(moment().valueOf() - member.createdAt, { const accountAge = humanizeDuration(moment.utc().valueOf() - member.createdAt, {
largest: 2, largest: 2,
round: true, round: true,
}); });

View file

@ -13,6 +13,7 @@ import { SavedMessage } from "src/data/entities/SavedMessage";
import { renderTemplate, TemplateParseError } from "src/templateFormatter"; import { renderTemplate, TemplateParseError } from "src/templateFormatter";
import { logger } from "src/logger"; import { logger } from "src/logger";
import moment from "moment-timezone"; import moment from "moment-timezone";
import { inGuildTz } from "../../../utils/timezones";
export async function getLogMessage( export async function getLogMessage(
pluginData: PluginData<LogsPluginType>, pluginData: PluginData<LogsPluginType>,
@ -87,7 +88,7 @@ export async function getLogMessage(
const timestampFormat = config.format.timestamp; const timestampFormat = config.format.timestamp;
if (timestampFormat) { if (timestampFormat) {
const timestamp = moment().format(timestampFormat); const timestamp = inGuildTz(pluginData).format(timestampFormat);
formatted = `\`[${timestamp}]\` ${formatted}`; formatted = `\`[${timestamp}]\` ${formatted}`;
} }
} }

View file

@ -5,6 +5,7 @@ import { LogType } from "src/data/LogType";
import moment from "moment-timezone"; import moment from "moment-timezone";
import { PluginData } from "knub"; import { PluginData } from "knub";
import { LogsPluginType } from "../types"; import { LogsPluginType } from "../types";
import { inGuildTz } from "../../../utils/timezones";
export async function onMessageDelete(pluginData: PluginData<LogsPluginType>, savedMessage: SavedMessage) { export async function onMessageDelete(pluginData: PluginData<LogsPluginType>, savedMessage: SavedMessage) {
const user = await resolveUser(pluginData.client, savedMessage.user_id); const user = await resolveUser(pluginData.client, savedMessage.user_id);
@ -23,7 +24,9 @@ export async function onMessageDelete(pluginData: PluginData<LogsPluginType>, sa
{ {
user: stripObjectToScalars(user), user: stripObjectToScalars(user),
channel: stripObjectToScalars(channel), 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, message: savedMessage,
}, },
savedMessage.id, savedMessage.id,

View file

@ -8,6 +8,8 @@ import { SECONDS, stripObjectToScalars, trimLines } from "../../../utils";
import { LogsPlugin } from "../../Logs/LogsPlugin"; import { LogsPlugin } from "../../Logs/LogsPlugin";
import { LogType } from "../../../data/LogType"; import { LogType } from "../../../data/LogType";
import moment from "moment-timezone"; import moment from "moment-timezone";
import { inGuildTz } from "../../../utils/timezones";
import { getDateFormat } from "../../../utils/dateFormats";
export const DeleteCaseCmd = modActionsCommand({ export const DeleteCaseCmd = modActionsCommand({
trigger: ["delete_case", "deletecase"], trigger: ["delete_case", "deletecase"],
@ -52,7 +54,7 @@ export const DeleteCaseCmd = modActionsCommand({
} }
const deletedByName = `${message.author.username}#${message.author.discriminator}`; 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( await pluginData.state.cases.softDelete(
theCase.id, theCase.id,

View file

@ -1,10 +1,11 @@
import { command } from "knub"; import { command } from "knub";
import { IMuteWithDetails, MutesPluginType } from "../types"; import { IMuteWithDetails, MutesPluginType } from "../types";
import { commandTypeHelpers as ct } from "../../../commandTypes"; 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 moment from "moment-timezone";
import { humanizeDurationShort } from "../../../humanizeDurationShort"; import { humanizeDurationShort } from "../../../humanizeDurationShort";
import { getBaseUrl } from "../../../pluginUtils"; import { getBaseUrl } from "../../../pluginUtils";
import { DBDateFormat } from "../../../utils/dateFormats";
export const MutesCmd = command<MutesPluginType>()({ export const MutesCmd = command<MutesPluginType>()({
trigger: "mutes", trigger: "mutes",
@ -70,7 +71,8 @@ export const MutesCmd = command<MutesPluginType>()({
// Filter: mute age // Filter: mute age
if (args.age) { if (args.age) {
const cutoff = moment() const cutoff = moment
.utc()
.subtract(args.age, "ms") .subtract(args.age, "ms")
.format(DBDateFormat); .format(DBDateFormat);
filteredMutes = filteredMutes.filter(m => m.created_at <= cutoff); filteredMutes = filteredMutes.filter(m => m.created_at <= cutoff);
@ -119,14 +121,14 @@ export const MutesCmd = command<MutesPluginType>()({
let line = `<@!${mute.user_id}> (**${username}**, \`${mute.user_id}\`) 📋 ${caseName}`; let line = `<@!${mute.user_id}> (**${username}**, \`${mute.user_id}\`) 📋 ${caseName}`;
if (mute.expires_at) { 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 }); const humanizedTime = humanizeDurationShort(timeUntilExpiry, { largest: 2, round: true });
line += ` ⏰ Expires in ${humanizedTime}`; line += ` ⏰ Expires in ${humanizedTime}`;
} else { } else {
line += ` ⏰ Indefinite`; 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 }); const humanizedTimeFromMute = humanizeDurationShort(timeFromMute, { largest: 2, round: true });
line += ` 🕒 Muted ${humanizedTimeFromMute} ago`; line += ` 🕒 Muted ${humanizedTimeFromMute} ago`;
@ -184,7 +186,7 @@ export const MutesCmd = command<MutesPluginType>()({
listMessage.edit("No active mutes!"); listMessage.edit("No active mutes!");
} }
} else if (args.export) { } 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 baseUrl = getBaseUrl(pluginData);
const url = await pluginData.state.archives.getUrl(baseUrl, archiveId); const url = await pluginData.state.archives.getUrl(baseUrl, archiveId);

View file

@ -1,6 +1,9 @@
import { postCmd } from "../types"; import { postCmd } from "../types";
import { trimLines, sorter, disableCodeBlocks, deactivateMentions, createChunkedMessage } from "src/utils"; import { trimLines, sorter, disableCodeBlocks, deactivateMentions, createChunkedMessage } from "src/utils";
import humanizeDuration from "humanize-duration"; 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; const SCHEDULED_POST_PREVIEW_TEXT_LENGTH = 50;
@ -28,7 +31,10 @@ export const ScheduledPostsListCmd = postCmd({
.replace(/\s+/g, " ") .replace(/\s+/g, " ")
.slice(0, SCHEDULED_POST_PREVIEW_TEXT_LENGTH); .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.attachments.length) parts.push("*(with attachment)*");
if (p.content.embed) parts.push("*(embed)*"); if (p.content.embed) parts.push("*(embed)*");
if (p.repeat_until) { if (p.repeat_until) {

View file

@ -1,5 +1,5 @@
import { Message, Channel, TextChannel } from "eris"; 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 moment from "moment-timezone";
import { LogType } from "src/data/LogType"; import { LogType } from "src/data/LogType";
import humanizeDuration from "humanize-duration"; import humanizeDuration from "humanize-duration";
@ -8,10 +8,11 @@ import { PluginData } from "knub";
import { PostPluginType } from "../types"; import { PostPluginType } from "../types";
import { parseScheduleTime } from "./parseScheduleTime"; import { parseScheduleTime } from "./parseScheduleTime";
import { postMessage } from "./postMessage"; import { postMessage } from "./postMessage";
import { DBDateFormat, getDateFormat } from "../../../utils/dateFormats";
const MIN_REPEAT_TIME = 5 * MINUTES; const MIN_REPEAT_TIME = 5 * MINUTES;
const MAX_REPEAT_TIME = Math.pow(2, 32); 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( export async function actualPostCmd(
pluginData: PluginData<PostPluginType>, pluginData: PluginData<PostPluginType>,
@ -53,12 +54,12 @@ export async function actualPostCmd(
let postAt; let postAt;
if (opts.schedule) { if (opts.schedule) {
// Schedule the post to be posted later // Schedule the post to be posted later
postAt = parseScheduleTime(opts.schedule); postAt = parseScheduleTime(pluginData, opts.schedule);
if (!postAt) { if (!postAt) {
return sendErrorMessage(pluginData, msg.channel, "Invalid schedule time"); return sendErrorMessage(pluginData, msg.channel, "Invalid schedule time");
} }
} else if (opts.repeat) { } 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 // For repeated posts, make sure repeat-until or repeat-times is specified
@ -67,13 +68,13 @@ export async function actualPostCmd(
let repeatDetailsStr: string = null; let repeatDetailsStr: string = null;
if (opts["repeat-until"]) { if (opts["repeat-until"]) {
repeatUntil = parseScheduleTime(opts["repeat-until"]); repeatUntil = parseScheduleTime(pluginData, opts["repeat-until"]);
// Invalid time // Invalid time
if (!repeatUntil) { if (!repeatUntil) {
return sendErrorMessage(pluginData, msg.channel, "Invalid time specified for -repeat-until"); 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"); return sendErrorMessage(pluginData, msg.channel, "You can't set -repeat-until in the past");
} }
if (repeatUntil.isAfter(MAX_REPEAT_UNTIL)) { if (repeatUntil.isAfter(MAX_REPEAT_UNTIL)) {
@ -110,7 +111,7 @@ export async function actualPostCmd(
// Save schedule/repeat information in DB // Save schedule/repeat information in DB
if (postAt) { 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"); 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, channel_id: targetChannel.id,
content, content,
attachments: msg.attachments, attachments: msg.attachments,
post_at: postAt.format(DBDateFormat), post_at: postAt
.clone()
.tz("Etc/UTC")
.format(DBDateFormat),
enable_mentions: opts["enable-mentions"], enable_mentions: opts["enable-mentions"],
repeat_interval: opts.repeat, 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, repeat_times: repeatTimes ?? null,
}); });
@ -131,8 +140,9 @@ export async function actualPostCmd(
pluginData.state.logs.log(LogType.SCHEDULED_REPEATED_MESSAGE, { pluginData.state.logs.log(LogType.SCHEDULED_REPEATED_MESSAGE, {
author: stripObjectToScalars(msg.author), author: stripObjectToScalars(msg.author),
channel: stripObjectToScalars(targetChannel), channel: stripObjectToScalars(targetChannel),
date: postAt.format("YYYY-MM-DD"), datetime: postAt.format(getDateFormat(pluginData, "pretty_datetime")),
time: postAt.format("HH:mm:ss"), date: postAt.format(getDateFormat(pluginData, "date")),
time: postAt.format(getDateFormat(pluginData, "time")),
repeatInterval: humanizeDuration(opts.repeat), repeatInterval: humanizeDuration(opts.repeat),
repeatDetails: repeatDetailsStr, repeatDetails: repeatDetailsStr,
}); });
@ -140,8 +150,9 @@ export async function actualPostCmd(
pluginData.state.logs.log(LogType.SCHEDULED_MESSAGE, { pluginData.state.logs.log(LogType.SCHEDULED_MESSAGE, {
author: stripObjectToScalars(msg.author), author: stripObjectToScalars(msg.author),
channel: stripObjectToScalars(targetChannel), channel: stripObjectToScalars(targetChannel),
date: postAt.format("YYYY-MM-DD"), datetime: postAt.format(getDateFormat(pluginData, "pretty_datetime")),
time: postAt.format("HH:mm:ss"), 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, { pluginData.state.logs.log(LogType.REPEATED_MESSAGE, {
author: stripObjectToScalars(msg.author), author: stripObjectToScalars(msg.author),
channel: stripObjectToScalars(targetChannel), channel: stripObjectToScalars(targetChannel),
date: postAt.format("YYYY-MM-DD"), datetime: postAt.format(getDateFormat(pluginData, "pretty_datetime")),
time: postAt.format("HH:mm:ss"), date: postAt.format(getDateFormat(pluginData, "date")),
time: postAt.format(getDateFormat(pluginData, "time")),
repeatInterval: humanizeDuration(opts.repeat), repeatInterval: humanizeDuration(opts.repeat),
repeatDetails: repeatDetailsStr, repeatDetails: repeatDetailsStr,
}); });
@ -164,14 +176,16 @@ export async function actualPostCmd(
// Bot reply schenanigans // Bot reply schenanigans
let successMessage = opts.schedule 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}>`; : `Message posted in <#${targetChannel.id}>`;
if (opts.repeat) { if (opts.repeat) {
successMessage += `. Message will be automatically reposted every ${humanizeDuration(opts.repeat)}`; successMessage += `. Message will be automatically reposted every ${humanizeDuration(opts.repeat)}`;
if (repeatUntil) { 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) { } else if (repeatTimes) {
successMessage += `, ${repeatTimes} times in total`; successMessage += `, ${repeatTimes} times in total`;
} }

View file

@ -1,31 +1,36 @@
import moment, { Moment } from "moment-timezone"; import moment, { Moment } from "moment-timezone";
import { convertDelayStringToMS } from "src/utils"; import { convertDelayStringToMS } from "src/utils";
import { PluginData } from "knub";
import { getGuildTz } from "../../../utils/timezones";
export function parseScheduleTime(str): Moment { // TODO: Extract out of the Post plugin, use everywhere with a date input
const dt1 = moment(str, "YYYY-MM-DD HH:mm:ss"); export function parseScheduleTime(pluginData: PluginData<any>, 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; 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; 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; 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 && t1.isValid()) {
if (t1.isBefore(moment())) t1.add(1, "day"); if (t1.isBefore(moment.utc())) t1.add(1, "day");
return t1; return t1;
} }
const t2 = moment(str, "HH:mm"); const t2 = moment.tz(str, "HH:mm", tz);
if (t2 && t2.isValid()) { if (t2 && t2.isValid()) {
if (t2.isBefore(moment())) t2.add(1, "day"); if (t2.isBefore(moment.utc())) t2.add(1, "day");
return t2; return t2;
} }
const delayStringMS = convertDelayStringToMS(str, "m"); const delayStringMS = convertDelayStringToMS(str, "m");
if (delayStringMS) { if (delayStringMS) {
return moment().add(delayStringMS, "ms"); return moment.tz(tz).add(delayStringMS, "ms");
} }
return null; return null;

View file

@ -1,11 +1,12 @@
import { PluginData } from "knub"; import { PluginData } from "knub";
import { PostPluginType } from "../types"; import { PostPluginType } from "../types";
import { logger } from "src/logger"; 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 { LogType } from "src/data/LogType";
import moment from "moment-timezone"; import moment from "moment-timezone";
import { TextChannel, User } from "eris"; import { TextChannel, User } from "eris";
import { postMessage } from "./postMessage"; import { postMessage } from "./postMessage";
import { DBDateFormat } from "../../../utils/dateFormats";
const SCHEDULED_POST_CHECK_INTERVAL = 5 * SECONDS; const SCHEDULED_POST_CHECK_INTERVAL = 5 * SECONDS;
@ -49,10 +50,10 @@ export async function scheduledPostLoop(pluginData: PluginData<PostPluginType>)
let shouldClear = true; let shouldClear = true;
if (post.repeat_interval) { 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) { if (post.repeat_until) {
const repeatUntil = moment(post.repeat_until, DBDateFormat); const repeatUntil = moment.utc(post.repeat_until, DBDateFormat);
if (nextPostAt.isSameOrBefore(repeatUntil)) { if (nextPostAt.isSameOrBefore(repeatUntil)) {
await pluginData.state.scheduledPosts.update(post.id, { await pluginData.state.scheduledPosts.update(post.id, {
post_at: nextPostAt.format(DBDateFormat), post_at: nextPostAt.format(DBDateFormat),

View file

@ -4,6 +4,8 @@ import { convertDelayStringToMS } from "src/utils";
import humanizeDuration from "humanize-duration"; import humanizeDuration from "humanize-duration";
import { sendErrorMessage, sendSuccessMessage } from "src/pluginUtils"; import { sendErrorMessage, sendSuccessMessage } from "src/pluginUtils";
import { remindersCommand } from "../types"; import { remindersCommand } from "../types";
import { getGuildTz, inGuildTz } from "../../../utils/timezones";
import { getDateFormat } from "../../../utils/dateFormats";
export const RemindCmd = remindersCommand({ export const RemindCmd = remindersCommand({
trigger: ["remind", "remindme"], trigger: ["remind", "remindme"],
@ -16,19 +18,20 @@ export const RemindCmd = remindersCommand({
}, },
async run({ message: msg, args, pluginData }) { 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}$/)) { 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 // 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(), hour: now.hour(),
minute: now.minute(), minute: now.minute(),
second: now.second(), second: now.second(),
}); });
} else if (args.time.match(/^\d{4}-\d{1,2}-\d{1,2}T\d{2}:\d{2}$/)) { } 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 // 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 { } else {
// "Delay string" i.e. e.g. "2h30m" // "Delay string" i.e. e.g. "2h30m"
const ms = convertDelayStringToMS(args.time); const ms = convertDelayStringToMS(args.time);
@ -37,7 +40,7 @@ export const RemindCmd = remindersCommand({
return; return;
} }
reminderTime = moment().add(ms, "millisecond"); reminderTime = moment.utc().add(ms, "millisecond");
} }
if (!reminderTime.isValid() || reminderTime.isBefore(now)) { if (!reminderTime.isValid() || reminderTime.isBefore(now)) {
@ -50,17 +53,21 @@ export const RemindCmd = remindersCommand({
await pluginData.state.reminders.add( await pluginData.state.reminders.add(
msg.author.id, msg.author.id,
msg.channel.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, reminderBody,
moment().format("YYYY-MM-DD HH:mm:ss"), moment.utc().format("YYYY-MM-DD HH:mm:ss"),
); );
const msUntilReminder = reminderTime.diff(now); const msUntilReminder = reminderTime.diff(now);
const timeUntilReminder = humanizeDuration(msUntilReminder, { largest: 2, round: true }); const timeUntilReminder = humanizeDuration(msUntilReminder, { largest: 2, round: true });
const prettyReminderTime = inGuildTz(pluginData, reminderTime).format(getDateFormat(pluginData, "pretty_datetime"));
sendSuccessMessage( sendSuccessMessage(
pluginData, pluginData,
msg.channel, msg.channel,
`I will remind you in **${timeUntilReminder}** at **${reminderTime.format("YYYY-MM-DD, HH:mm")}**`, `I will remind you in **${timeUntilReminder}** at **${prettyReminderTime}**`,
); );
}, },
}); });

View file

@ -3,6 +3,8 @@ import { sendErrorMessage } from "src/pluginUtils";
import { createChunkedMessage, sorter } from "src/utils"; import { createChunkedMessage, sorter } from "src/utils";
import moment from "moment-timezone"; import moment from "moment-timezone";
import humanizeDuration from "humanize-duration"; import humanizeDuration from "humanize-duration";
import { inGuildTz } from "../../../utils/timezones";
import { DBDateFormat, getDateFormat } from "../../../utils/dateFormats";
export const RemindersCmd = remindersCommand({ export const RemindersCmd = remindersCommand({
trigger: "reminders", trigger: "reminders",
@ -20,10 +22,13 @@ export const RemindersCmd = remindersCommand({
const lines = Array.from(reminders.entries()).map(([i, reminder]) => { const lines = Array.from(reminders.entries()).map(([i, reminder]) => {
const num = i + 1; const num = i + 1;
const paddedNum = num.toString().padStart(longestNum, " "); const paddedNum = num.toString().padStart(longestNum, " ");
const target = moment(reminder.remind_at, "YYYY-MM-DD HH:mm:ss"); const target = moment.utc(reminder.remind_at, "YYYY-MM-DD HH:mm:ss");
const diff = target.diff(moment()); const diff = target.diff(moment.utc());
const result = humanizeDuration(diff, { largest: 2, round: true }); 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")); createChunkedMessage(msg.channel, lines.join("\n"));

View file

@ -16,9 +16,9 @@ export async function postDueRemindersLoop(pluginData: PluginData<RemindersPlugi
if (channel && channel instanceof TextChannel) { if (channel && channel instanceof TextChannel) {
try { try {
// Only show created at date if one exists // Only show created at date if one exists
if (moment(reminder.created_at).isValid()) { if (moment.utc(reminder.created_at).isValid()) {
const target = moment(); const target = moment.utc();
const diff = target.diff(moment(reminder.created_at, "YYYY-MM-DD HH:mm:ss")); const diff = target.diff(moment.utc(reminder.created_at, "YYYY-MM-DD HH:mm:ss"));
const result = humanizeDuration(diff, { largest: 2, round: true }); const result = humanizeDuration(diff, { largest: 2, round: true });
await channel.createMessage( await channel.createMessage(
disableLinkPreviews( disableLinkPreviews(

View file

@ -14,6 +14,7 @@ import { getRecentActionCount } from "./getRecentActionCount";
import { getRecentActions } from "./getRecentActions"; import { getRecentActions } from "./getRecentActions";
import { clearRecentUserActions } from "./clearRecentUserActions"; import { clearRecentUserActions } from "./clearRecentUserActions";
import { saveSpamArchives } from "./saveSpamArchives"; import { saveSpamArchives } from "./saveSpamArchives";
import { DBDateFormat } from "../../../utils/dateFormats";
export async function logAndDetectMessageSpam( export async function logAndDetectMessageSpam(
pluginData: PluginData<SpamPluginType>, pluginData: PluginData<SpamPluginType>,
@ -36,7 +37,7 @@ export async function logAndDetectMessageSpam(
pluginData.state.spamDetectionQueue = pluginData.state.spamDetectionQueue.then( pluginData.state.spamDetectionQueue = pluginData.state.spamDetectionQueue.then(
async () => { 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); const member = await resolveMember(pluginData.client, pluginData.guild, savedMessage.user_id);
// Log this action... // Log this action...

View file

@ -5,7 +5,7 @@ import { getBaseUrl } from "src/pluginUtils";
const SPAM_ARCHIVE_EXPIRY_DAYS = 90; const SPAM_ARCHIVE_EXPIRY_DAYS = 90;
export async function saveSpamArchives(pluginData, savedMessages: SavedMessage[]) { 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); const archiveId = await pluginData.state.archives.createFromSavedMessages(savedMessages, pluginData.guild, expiresAt);
return pluginData.state.archives.getUrl(getBaseUrl(pluginData), archiveId); return pluginData.state.archives.getUrl(getBaseUrl(pluginData), archiveId);

View file

@ -13,8 +13,6 @@ export async function saveMessageToStarboard(
const channel = pluginData.guild.channels.get(starboard.channel_id); const channel = pluginData.guild.channels.get(starboard.channel_id);
if (!channel) return; if (!channel) return;
const time = moment(msg.timestamp, "x").format("YYYY-MM-DD [at] HH:mm:ss [UTC]");
const embed: Embed = { const embed: Embed = {
footer: { footer: {
text: `#${(msg.channel as GuildChannel).name}`, text: `#${(msg.channel as GuildChannel).name}`,

View file

@ -15,6 +15,7 @@ import { TagSourceCmd } from "./commands/TagSourceCmd";
import moment from "moment-timezone"; import moment from "moment-timezone";
import humanizeDuration from "humanize-duration"; import humanizeDuration from "humanize-duration";
import { convertDelayStringToMS } from "../../utils"; import { convertDelayStringToMS } from "../../utils";
import { getGuildTz, inGuildTz } from "../../utils/timezones";
const defaultOptions: PluginOptions<TagsPluginType> = { const defaultOptions: PluginOptions<TagsPluginType> = {
config: { config: {
@ -76,6 +77,7 @@ export const TagsPlugin = zeppelinPlugin<TagsPluginType>()("tags", {
state.onMessageDeleteFn = msg => onMessageDelete(pluginData, msg); state.onMessageDeleteFn = msg => onMessageDelete(pluginData, msg);
state.savedMessages.events.on("delete", state.onMessageDeleteFn); state.savedMessages.events.on("delete", state.onMessageDeleteFn);
const tz = getGuildTz(pluginData);
state.tagFunctions = { state.tagFunctions = {
parseDateTime(str) { parseDateTime(str) {
if (typeof str === "number") { if (typeof str === "number") {
@ -86,13 +88,13 @@ export const TagsPlugin = zeppelinPlugin<TagsPluginType>()("tags", {
return Date.now(); 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) { 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 ""; if (!target.isValid()) return "";
const diff = target.diff(now); const diff = target.diff(now);
@ -119,7 +121,8 @@ export const TagsPlugin = zeppelinPlugin<TagsPluginType>()("tags", {
} }
const delayMS = convertDelayStringToMS(delay); const delayMS = convertDelayStringToMS(delay);
return moment(reference) return moment
.utc(reference, "x")
.add(delayMS) .add(delayMS)
.valueOf(); .valueOf();
}, },
@ -139,7 +142,8 @@ export const TagsPlugin = zeppelinPlugin<TagsPluginType>()("tags", {
} }
const delayMS = convertDelayStringToMS(delay); const delayMS = convertDelayStringToMS(delay);
return moment(reference) return moment
.utc(reference, "x")
.subtract(delayMS) .subtract(delayMS)
.valueOf(); .valueOf();
}, },
@ -150,13 +154,13 @@ export const TagsPlugin = zeppelinPlugin<TagsPluginType>()("tags", {
formatTime(time, format) { formatTime(time, format) {
const parsed = this.parseDateTime(time); const parsed = this.parseDateTime(time);
return moment(parsed).format(format); return inGuildTz(parsed).format(format);
}, },
discordDateFormat(time) { discordDateFormat(time) {
const parsed = time ? this.parseDateTime(time) : Date.now(); const parsed = time ? this.parseDateTime(time) : Date.now();
return moment(parsed).format("YYYY-MM-DD"); return inGuildTz(parsed).format("YYYY-MM-DD");
}, },
mention: input => { mention: input => {

View file

@ -32,7 +32,7 @@ export const TagSourceCmd = tagsCmd({
return; 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); const url = pluginData.state.archives.getUrl(getBaseUrl(pluginData), archiveId);
msg.channel.createMessage(`Tag source:\n${url}`); msg.channel.createMessage(`Tag source:\n${url}`);

View file

@ -6,6 +6,7 @@ import humanizeDuration from "humanize-duration";
import LCL from "last-commit-log"; import LCL from "last-commit-log";
import path from "path"; import path from "path";
import moment from "moment-timezone"; import moment from "moment-timezone";
import { getGuildTz } from "../../../utils/timezones";
export const AboutCmd = utilityCmd({ export const AboutCmd = utilityCmd({
trigger: "about", trigger: "about",
@ -29,7 +30,7 @@ export const AboutCmd = utilityCmd({
let version; let version;
if (lastCommit) { 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; version = lastCommit.shortHash;
} else { } else {
lastUpdate = "?"; lastUpdate = "?";
@ -49,6 +50,7 @@ export const AboutCmd = utilityCmd({
["Last update", lastUpdate], ["Last update", lastUpdate],
["Version", version], ["Version", version],
["API latency", `${shard.latency}ms`], ["API latency", `${shard.latency}ms`],
["Server timezone", getGuildTz(pluginData)],
]; ];
const loadedPlugins = Array.from( const loadedPlugins = Array.from(

View file

@ -40,7 +40,7 @@ export const SourceCmd = utilityCmd({
const source = `${textSource}\n\nSource:\n\n${fullSource}`; 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 baseUrl = getBaseUrl(pluginData);
const url = pluginData.state.archives.getUrl(baseUrl, archiveId); const url = pluginData.state.archives.getUrl(baseUrl, archiveId);
cmdMessage.channel.createMessage(`Message source: ${url}`); cmdMessage.channel.createMessage(`Message source: ${url}`);

View file

@ -4,6 +4,8 @@ import { Constants, EmbedOptions } from "eris";
import moment from "moment-timezone"; import moment from "moment-timezone";
import humanizeDuration from "humanize-duration"; import humanizeDuration from "humanize-duration";
import { formatNumber, preEmbedPadding, trimLines } from "../../../utils"; import { formatNumber, preEmbedPadding, trimLines } from "../../../utils";
import { inGuildTz } from "../../../utils/timezones";
import { getDateFormat } from "../../../utils/dateFormats";
const TEXT_CHANNEL_ICON = const TEXT_CHANNEL_ICON =
"https://cdn.discordapp.com/attachments/740650744830623756/740656843545772062/text-channel.png"; "https://cdn.discordapp.com/attachments/740650744830623756/740656843545772062/text-channel.png";
@ -55,7 +57,8 @@ export async function getChannelInfoEmbed(
channelName = `#${channel.name}`; 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, { const channelAge = humanizeDuration(Date.now() - channel.createdAt, {
largest: 2, largest: 2,
round: true, round: true,
@ -69,7 +72,7 @@ export async function getChannelInfoEmbed(
value: trimLines(` value: trimLines(`
Name: **${channelName}** Name: **${channelName}**
ID: \`${channel.id}\` ID: \`${channel.id}\`
Created: **${channelAge} ago** (\`${createdAt.format("MMM D, YYYY [at] H:mm [UTC]")}\`) Created: **${channelAge} ago** (\`${prettyCreatedAt}\`)
Type: **${channelType}** Type: **${channelType}**
${showMention ? `Mention: <#${channel.id}>` : ""} ${showMention ? `Mention: <#${channel.id}>` : ""}
`), `),

View file

@ -43,7 +43,7 @@ export async function getInviteInfoEmbed(
} }
const serverCreatedAtTimestamp = snowflakeToTimestamp(invite.guild.id); const serverCreatedAtTimestamp = snowflakeToTimestamp(invite.guild.id);
const serverCreatedAt = moment(serverCreatedAtTimestamp, "x"); const serverCreatedAt = moment.utc(serverCreatedAtTimestamp, "x");
const serverAge = humanizeDuration(Date.now() - serverCreatedAtTimestamp, { const serverAge = humanizeDuration(Date.now() - serverCreatedAtTimestamp, {
largest: 2, largest: 2,
round: true, round: true,
@ -66,7 +66,7 @@ export async function getInviteInfoEmbed(
: `#${invite.channel.name}`; : `#${invite.channel.name}`;
const channelCreatedAtTimestamp = snowflakeToTimestamp(invite.channel.id); const channelCreatedAtTimestamp = snowflakeToTimestamp(invite.channel.id);
const channelCreatedAt = moment(channelCreatedAtTimestamp, "x"); const channelCreatedAt = moment.utc(channelCreatedAtTimestamp, "x");
const channelAge = humanizeDuration(Date.now() - channelCreatedAtTimestamp, { const channelAge = humanizeDuration(Date.now() - channelCreatedAtTimestamp, {
largest: 2, largest: 2,
round: true, round: true,
@ -117,7 +117,7 @@ export async function getInviteInfoEmbed(
} }
const channelCreatedAtTimestamp = snowflakeToTimestamp(invite.channel.id); const channelCreatedAtTimestamp = snowflakeToTimestamp(invite.channel.id);
const channelCreatedAt = moment(channelCreatedAtTimestamp, "x"); const channelCreatedAt = moment.utc(channelCreatedAtTimestamp, "x");
const channelAge = humanizeDuration(Date.now() - channelCreatedAtTimestamp, { const channelAge = humanizeDuration(Date.now() - channelCreatedAtTimestamp, {
largest: 2, largest: 2,
round: true, round: true,

View file

@ -5,6 +5,8 @@ import moment from "moment-timezone";
import humanizeDuration from "humanize-duration"; import humanizeDuration from "humanize-duration";
import { chunkMessageLines, preEmbedPadding, trimEmptyLines, trimLines } from "../../../utils"; import { chunkMessageLines, preEmbedPadding, trimEmptyLines, trimLines } from "../../../utils";
import { getDefaultPrefix } from "knub/dist/commands/commandUtils"; 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"; const MESSAGE_ICON = "https://cdn.discordapp.com/attachments/740650744830623756/740685652152025088/message.png";
@ -27,13 +29,15 @@ export async function getMessageInfoEmbed(
icon_url: MESSAGE_ICON, 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, { const messageAge = humanizeDuration(Date.now() - message.createdAt, {
largest: 2, largest: 2,
round: true, 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 = const editAge =
message.editedTimestamp && message.editedTimestamp &&
humanizeDuration(Date.now() - message.editedTimestamp, { humanizeDuration(Date.now() - message.editedTimestamp, {
@ -62,8 +66,8 @@ export async function getMessageInfoEmbed(
ID: \`${message.id}\` ID: \`${message.id}\`
Channel: <#${message.channel.id}> Channel: <#${message.channel.id}>
Channel ID: \`${message.channel.id}\` Channel ID: \`${message.channel.id}\`
Created: **${messageAge} ago** (\`${createdAt.format("MMM D, YYYY [at] H:mm [UTC]")}\`) Created: **${messageAge} ago** (\`${prettyCreatedAt}\`)
${editedAt ? `Edited at: **${editAge} ago** (\`${editedAt.format("MMM D, YYYY [at] H:mm [UTC]")}\`)` : ""} ${editedAt ? `Edited at: **${editAge} ago** (\`${prettyEditedAt}\`)` : ""}
Type: **${type}** Type: **${type}**
Link: [**Go to message **](https://discord.com/channels/${pluginData.guild.id}/${message.channel.id}/${ Link: [**Go to message **](https://discord.com/channels/${pluginData.guild.id}/${message.channel.id}/${
message.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, { const authorAccountAge = humanizeDuration(Date.now() - message.author.createdAt, {
largest: 2, largest: 2,
round: true, 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 = const authorServerAge =
message.member && message.member &&
humanizeDuration(Date.now() - message.member.joinedAt, { humanizeDuration(Date.now() - message.member.joinedAt, {
@ -91,12 +101,8 @@ export async function getMessageInfoEmbed(
value: trimLines(` value: trimLines(`
Name: **${message.author.username}#${message.author.discriminator}** Name: **${message.author.username}#${message.author.discriminator}**
ID: \`${message.author.id}\` ID: \`${message.author.id}\`
Created: **${authorAccountAge} ago** (\`${authorCreatedAt.format("MMM D, YYYY [at] H:mm [UTC]")}\`) Created: **${authorAccountAge} ago** (\`${prettyAuthorCreatedAt}\`)
${ ${authorJoinedAt ? `Joined: **${authorServerAge} ago** (\`${prettyAuthorJoinedAt}\`)` : ""}
authorJoinedAt
? `Joined: **${authorServerAge} ago** (\`${authorJoinedAt.format("MMM D, YYYY [at] H:mm [UTC]")}\`)`
: ""
}
Mention: <@!${message.author.id}> Mention: <@!${message.author.id}>
`), `),
}); });

View file

@ -5,6 +5,8 @@ import { CategoryChannel, EmbedOptions, Guild, RESTChannelInvite, TextChannel, V
import moment from "moment-timezone"; import moment from "moment-timezone";
import humanizeDuration from "humanize-duration"; import humanizeDuration from "humanize-duration";
import { getGuildPreview } from "./getGuildPreview"; import { getGuildPreview } from "./getGuildPreview";
import { inGuildTz } from "../../../utils/timezones";
import { getDateFormat } from "../../../utils/dateFormats";
export async function getServerInfoEmbed( export async function getServerInfoEmbed(
pluginData: PluginData<UtilityPluginType>, pluginData: PluginData<UtilityPluginType>,
@ -37,14 +39,15 @@ export async function getServerInfoEmbed(
}; };
// BASIC INFORMATION // BASIC INFORMATION
const createdAt = moment((guildPreview || restGuild).createdAt); const createdAt = moment.utc((guildPreview || restGuild).createdAt, "x");
const serverAge = humanizeDuration(moment().valueOf() - createdAt.valueOf(), { const prettyCreatedAt = inGuildTz(pluginData, createdAt).format(getDateFormat(pluginData, "pretty_datetime"));
const serverAge = humanizeDuration(moment.utc().valueOf() - createdAt.valueOf(), {
largest: 2, largest: 2,
round: true, round: true,
}); });
const basicInformation = []; const basicInformation = [];
basicInformation.push(`Created: **${serverAge} ago** (${createdAt.format("YYYY-MM-DD[T]HH:mm:ss")})`); basicInformation.push(`Created: **${serverAge} ago** (${prettyCreatedAt})`);
if (thisServer) { if (thisServer) {
const owner = await resolveUser(pluginData.client, thisServer.ownerID); const owner = await resolveUser(pluginData.client, thisServer.ownerID);

View file

@ -6,6 +6,8 @@ import moment from "moment-timezone";
import { CaseTypes } from "src/data/CaseTypes"; import { CaseTypes } from "src/data/CaseTypes";
import humanizeDuration from "humanize-duration"; import humanizeDuration from "humanize-duration";
import { snowflakeToTimestamp } from "../../../utils/snowflakeToTimestamp"; 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"; const SNOWFLAKE_ICON = "https://cdn.discordapp.com/attachments/740650744830623756/742020790471491668/snowflake.png";
@ -29,7 +31,8 @@ export function getSnowflakeInfoEmbed(
} }
const createdAtMS = snowflakeToTimestamp(snowflake); 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, { const snowflakeAge = humanizeDuration(Date.now() - createdAtMS, {
largest: 2, largest: 2,
round: true, round: true,
@ -37,7 +40,7 @@ export function getSnowflakeInfoEmbed(
embed.fields.push({ embed.fields.push({
name: preEmbedPadding + "Basic information", name: preEmbedPadding + "Basic information",
value: `Created: **${snowflakeAge} ago** (\`${createdAt.format("MMM D, YYYY [at] H:mm [UTC]")}\`)`, value: `Created: **${snowflakeAge} ago** (\`${prettyCreatedAt}\`)`,
}); });
return embed; return embed;

View file

@ -5,6 +5,8 @@ import { UnknownUser, trimLines, embedPadding, resolveMember, resolveUser, preEm
import moment from "moment-timezone"; import moment from "moment-timezone";
import { CaseTypes } from "src/data/CaseTypes"; import { CaseTypes } from "src/data/CaseTypes";
import humanizeDuration from "humanize-duration"; import humanizeDuration from "humanize-duration";
import { inGuildTz } from "../../../utils/timezones";
import { getDateFormat } from "../../../utils/dateFormats";
export async function getUserInfoEmbed( export async function getUserInfoEmbed(
pluginData: PluginData<UtilityPluginType>, pluginData: PluginData<UtilityPluginType>,
@ -29,8 +31,9 @@ export async function getUserInfoEmbed(
const avatarURL = user.avatarURL || user.defaultAvatarURL; const avatarURL = user.avatarURL || user.defaultAvatarURL;
embed.author.icon_url = avatarURL; embed.author.icon_url = avatarURL;
const createdAt = moment(user.createdAt); const createdAt = moment.utc(user.createdAt, "x");
const accountAge = humanizeDuration(moment().valueOf() - user.createdAt, { const prettyCreatedAt = inGuildTz(pluginData, createdAt).format(getDateFormat(pluginData, "pretty_datetime"));
const accountAge = humanizeDuration(moment.utc().valueOf() - user.createdAt, {
largest: 2, largest: 2,
round: true, round: true,
}); });
@ -40,16 +43,17 @@ export async function getUserInfoEmbed(
name: preEmbedPadding + "User information", name: preEmbedPadding + "User information",
value: trimLines(` value: trimLines(`
Profile: <@!${user.id}> Profile: <@!${user.id}>
Created: **${accountAge} ago** (\`${createdAt.format("MMM D, YYYY")}\`) Created: **${accountAge} ago** (\`${prettyCreatedAt}\`)
`), `),
}); });
if (member) { if (member) {
const joinedAt = moment(member.joinedAt); const joinedAt = moment.utc(member.joinedAt, "x");
const joinAge = humanizeDuration(moment().valueOf() - member.joinedAt, { const prettyJoinedAt = inGuildTz(pluginData, joinedAt).format(getDateFormat(pluginData, "pretty_datetime"));
const joinAge = humanizeDuration(moment.utc().valueOf() - member.joinedAt, {
largest: 2, largest: 2,
round: true, round: true,
}); });
embed.fields[0].value += `\nJoined: **${joinAge} ago** (\`${joinedAt.format("MMM D, YYYY")}\`)`; embed.fields[0].value += `\nJoined: **${joinAge} ago** (\`${prettyJoinedAt}\`)`;
} else { } else {
embed.fields.push({ embed.fields.push({
name: preEmbedPadding + "!! NOTE !!", name: preEmbedPadding + "!! NOTE !!",
@ -65,14 +69,15 @@ export async function getUserInfoEmbed(
value: trimLines(` value: trimLines(`
Name: **${user.username}#${user.discriminator}** Name: **${user.username}#${user.discriminator}**
ID: \`${user.id}\` ID: \`${user.id}\`
Created: **${accountAge} ago** (\`${createdAt.format("MMM D, YYYY [at] H:mm [UTC]")}\`) Created: **${accountAge} ago** (\`${prettyCreatedAt}\`)
Mention: <@!${user.id}> Mention: <@!${user.id}>
`), `),
}); });
if (member) { if (member) {
const joinedAt = moment(member.joinedAt); const joinedAt = moment.utc(member.joinedAt, "x");
const joinAge = humanizeDuration(moment().valueOf() - member.joinedAt, { const prettyJoinedAt = inGuildTz(pluginData, joinedAt).format(getDateFormat(pluginData, "pretty_datetime"));
const joinAge = humanizeDuration(moment.utc().valueOf() - member.joinedAt, {
largest: 2, largest: 2,
round: true, round: true,
}); });
@ -81,7 +86,7 @@ export async function getUserInfoEmbed(
embed.fields.push({ embed.fields.push({
name: preEmbedPadding + "Member information", name: preEmbedPadding + "Member information",
value: trimLines(` 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(", ") : ""} ${roles.length > 0 ? "Roles: " + roles.map(r => r.name).join(", ") : ""}
`), `),
}); });

View file

@ -214,7 +214,7 @@ export async function archiveSearch(
${resultList} ${resultList}
`), `),
moment().add(1, "hour"), moment.utc().add(1, "hour"),
); );
const baseUrl = getBaseUrl(pluginData); const baseUrl = getBaseUrl(pluginData);

View file

@ -1,16 +1,47 @@
import { BaseConfig, Knub } from "knub"; import { BaseConfig, Knub } from "knub";
import * as t from "io-ts";
export interface IZeppelinGuildConfig extends BaseConfig<any> { export const DateFormatsSchema = t.type({
date: t.string,
time: t.string,
pretty_datetime: t.string,
});
export type DateFormats = t.TypeOf<typeof DateFormatsSchema>;
export interface ZeppelinGuildConfig extends BaseConfig<any> {
success_emoji?: string; success_emoji?: string;
error_emoji?: string; error_emoji?: string;
timezone?: string;
date_formats?: Partial<DateFormats>;
} }
export interface IZeppelinGlobalConfig extends BaseConfig<any> { 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<any> {
url: string; url: string;
owners?: string[]; owners?: string[];
} }
export type TZeppelinKnub = Knub<IZeppelinGuildConfig, IZeppelinGlobalConfig>; export const ZeppelinGlobalConfigSchema = t.type({
url: t.string,
owners: t.array(t.string),
plugins: t.record(t.string, t.unknown),
});
export type TZeppelinKnub = Knub<ZeppelinGuildConfig, ZeppelinGlobalConfig>;
/** /**
* Wrapper for the string type that indicates the text will be parsed as Markdown later * Wrapper for the string type that indicates the text will be parsed as Markdown later

View file

@ -32,7 +32,7 @@ import https from "https";
import tmp from "tmp"; import tmp from "tmp";
import { helpers } from "knub"; import { helpers } from "knub";
import { SavedMessage } from "./data/entities/SavedMessage"; 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 { either } from "fp-ts/lib/Either";
import moment from "moment-timezone"; import moment from "moment-timezone";
import { SimpleCache } from "./SimpleCache"; import { SimpleCache } from "./SimpleCache";
@ -252,7 +252,7 @@ export const tDateTime = new t.Type<string, string>(
(from, to) => (from, to) =>
either.chain(t.string.validate(from, to), s => { either.chain(t.string.validate(from, to), s => {
const parsed = 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"); return parsed && parsed.isValid() ? t.success(s) : t.failure(from, to, "Invalid datetime");
}), }),
@ -823,8 +823,6 @@ export function noop() {
// IT'S LITERALLY NOTHING // IT'S LITERALLY NOTHING
} }
export const DBDateFormat = "YYYY-MM-DD HH:mm:ss";
export type CustomEmoji = { export type CustomEmoji = {
id: string; id: string;
} & Emoji; } & Emoji;

View file

@ -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<any>, formatName: keyof DateFormats) {
return pluginData.guildConfig.date_formats?.[formatName] || defaultDateFormats[formatName];
}
export const DBDateFormat = "YYYY-MM-DD HH:mm:ss";

View file

@ -0,0 +1,21 @@
import moment from "moment-timezone";
import { PluginData } from "knub";
import { ZeppelinGuildConfig } from "../types";
export function getGuildTz(pluginData: PluginData<any>) {
const guildConfig = pluginData.guildConfig as ZeppelinGuildConfig;
return guildConfig.timezone || "Etc/UTC";
}
export function inGuildTz(pluginData: PluginData<any>, 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));
}