Add time_and_date plugin. Use it for timezones and date formats around the bot.

This commit is contained in:
Dragory 2020-08-19 00:19:12 +03:00
parent cffb0dbd6b
commit 4ae8cf85a3
No known key found for this signature in database
GPG key ID: 5F387BA66DF8AAC1
67 changed files with 543 additions and 177 deletions

View file

@ -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<Promise<MessageTarget>>(commandTypes.messageTarget),
anyId: createTypeHelper<Promise<string>>(commandTypes.anyId),
regex: createTypeHelper<RegExp>(commandTypes.regex),
timezone: createTypeHelper<string>(commandTypes.timezone),
};

View file

@ -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<ApiLogin>;

View file

@ -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<ApiUserInfoEntity>;

View file

@ -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;

View file

@ -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<MemberTimezone>;
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,
});
}
});
}
}

View file

@ -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;

View file

@ -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.

View file

@ -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;

View file

@ -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;

View file

@ -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;
}

View file

@ -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<any> {
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<any> {
await queryRunner.dropTable("member_timezones");
}
}

View file

@ -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<any>, userId: string) {
export const isOwnerPreFilter = (_, context: CommandContext<any>) => {
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<T extends AnyFn>(inputFn: T) {
return pluginData => {
return (...args: Tail<Parameters<typeof inputFn>>): ReturnType<typeof inputFn> => {
return inputFn(pluginData, ...args);
};
};
}

View file

@ -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<AutoDeletePluginType> = {
config: {
@ -22,6 +23,7 @@ export const AutoDeletePlugin = zeppelinPlugin<AutoDeletePluginType>()("auto_del
configurationGuide: "Maximum deletion delay is currently 5 minutes",
},
dependencies: [TimeAndDatePlugin],
configSchema: ConfigSchema,
defaultOptions,

View file

@ -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<AutoDeletePluginType>) {
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<AutoDeletePluginType
const user = await resolveUser(pluginData.client, itemToDelete.message.user_id);
const channel = pluginData.guild.channels.get(itemToDelete.message.channel_id);
const messageDate = inGuildTz(pluginData, moment.utc(itemToDelete.message.data.timestamp, "x")).format(
getDateFormat(pluginData, "pretty_datetime"),
);
const messageDate = timeAndDate
.inGuildTz(moment.utc(itemToDelete.message.data.timestamp, "x"))
.format(timeAndDate.getDateFormat("pretty_datetime"));
pluginData.state.guildLogs.log(LogType.MESSAGE_DELETE_AUTO, {
message: itemToDelete.message,

View file

@ -12,6 +12,8 @@ import { getCaseTypeAmountForUserId } from "./functions/getCaseTypeAmountForUser
import { getCaseEmbed } from "./functions/getCaseEmbed";
import { trimPluginDescription } from "../../utils";
import { getCaseSummary } from "./functions/getCaseSummary";
import { TimeAndDatePlugin } from "../TimeAndDate/TimeAndDatePlugin";
import { mapToPublicFn } from "../../pluginUtils";
const defaultOptions = {
config: {
@ -33,6 +35,7 @@ export const CasesPlugin = zeppelinPlugin<CasesPluginType>()("cases", {
`),
},
dependencies: [TimeAndDatePlugin],
configSchema: ConfigSchema,
defaultOptions,
@ -61,17 +64,8 @@ export const CasesPlugin = zeppelinPlugin<CasesPluginType>()("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) {

View file

@ -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<CasesPluginType>,
caseOrCaseId: Case | number,
requestMemberId?: string,
): Promise<AdvancedMessageContent> {
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 !!!",

View file

@ -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<CasesPluginType>,
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);

View file

@ -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<ChannelArchiverPluginType>()("channel_archiver", {
showInDocs: false,
dependencies: [TimeAndDatePlugin],
configSchema: t.type({}),
// prettier-ignore

View file

@ -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`;

View file

@ -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<LogsPluginType> = {
config: {
@ -49,7 +50,7 @@ export const LogsPlugin = zeppelinPlugin<LogsPluginType>()("logs", {
prettyName: "Logs",
},
dependencies: [CasesPlugin],
dependencies: [TimeAndDatePlugin, CasesPlugin],
configSchema: ConfigSchema,
defaultOptions,

View file

@ -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<LogsPluginType>,
@ -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}`;
}
}

View file

@ -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<LogsPluginType>, savedMessage: SavedMessage) {
const user = await resolveUser(pluginData.client, savedMessage.user_id);
@ -24,9 +24,10 @@ export async function onMessageDelete(pluginData: PluginData<LogsPluginType>, 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,

View file

@ -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<ModActionsPluginType>()("mod_acti
`),
},
dependencies: [TimeAndDatePlugin, CasesPlugin, MutesPlugin],
configSchema: ConfigSchema,
defaultOptions,
dependencies: [CasesPlugin, MutesPlugin],
events: [
CreateBanCaseOnManualBanEvt,
CreateUnbanCaseOnManualUnbanEvt,

View file

@ -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);
},
});

View file

@ -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: {

View file

@ -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;

View file

@ -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,

View file

@ -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<MutesPluginType>()({
trigger: "mutes",

View file

@ -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<PostPluginType> = {
config: {
@ -33,6 +34,7 @@ export const PostPlugin = zeppelinPlugin<PostPluginType>()("post", {
prettyName: "Post",
},
dependencies: [TimeAndDatePlugin],
configSchema: ConfigSchema,
defaultOptions,

View file

@ -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)*");

View file

@ -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`;
}

View file

@ -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<any>, str: string): Moment {
const tz = getGuildTz(pluginData);
export async function parseScheduleTime(pluginData: PluginData<any>, memberId: string, str: string): Promise<Moment> {
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;

View file

@ -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;

View file

@ -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<RemindersPluginType> = {
config: {
@ -27,6 +28,7 @@ export const RemindersPlugin = zeppelinPlugin<RemindersPluginType>()("reminders"
prettyName: "Reminders",
},
dependencies: [TimeAndDatePlugin],
configSchema: ConfigSchema,
defaultOptions,

View file

@ -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,

View file

@ -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}`;
});

View file

@ -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<SpamPluginType>,

View file

@ -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<TagsPluginType> = {
config: {
@ -77,7 +77,9 @@ export const TagsPlugin = zeppelinPlugin<TagsPluginType>()("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<TagsPluginType>()("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 => {

View file

@ -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<TimeAndDatePluginType> = {
config: {
timezone: "Etc/UTC",
can_set_timezone: false,
date_formats: defaultDateFormats,
},
overrides: [
{
level: ">=50",
config: {
can_set_timezone: true,
},
},
],
};
export const TimeAndDatePlugin = zeppelinPlugin<TimeAndDatePluginType>()("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);
},
});

View file

@ -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}**`);
},
});

View file

@ -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)`);
},
});

View file

@ -0,0 +1,5 @@
export const defaultDateFormats = {
date: "MMM D, YYYY",
time: "H:mm",
pretty_datetime: "MMM D, YYYY [at] H:mm z",
};

View file

@ -0,0 +1,6 @@
import { PluginData } from "knub";
import { defaultDateFormats } from "../defaultDateFormats";
export function getDateFormat(pluginData: PluginData<any>, formatName: keyof typeof defaultDateFormats) {
return pluginData.config.get().date_formats?.[formatName] || defaultDateFormats[formatName];
}

View file

@ -0,0 +1,7 @@
import { PluginData } from "knub";
import { ZeppelinGuildConfig } from "../../../types";
import { TimeAndDatePluginType } from "../types";
export function getGuildTz(pluginData: PluginData<TimeAndDatePluginType>) {
return pluginData.config.get().timezone;
}

View file

@ -0,0 +1,8 @@
import { PluginData } from "knub";
import { TimeAndDatePluginType } from "../types";
import { getGuildTz } from "./getGuildTz";
export async function getMemberTz(pluginData: PluginData<TimeAndDatePluginType>, memberId: string) {
const memberTz = await pluginData.state.memberTimezones.get(memberId);
return memberTz?.timezone || getGuildTz(pluginData);
}

View file

@ -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<any>) {
const guildConfig = pluginData.guildConfig as ZeppelinGuildConfig;
return guildConfig.timezone || "Etc/UTC";
}
export function inGuildTz(pluginData: PluginData<any>, input?: moment.Moment | number) {
export function inGuildTz(pluginData: PluginData<TimeAndDatePluginType>, input?: moment.Moment | number) {
let momentObj: moment.Moment;
if (typeof input === "number") {
momentObj = moment.utc(input, "x");

View file

@ -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<TimeAndDatePluginType>,
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));
}

View file

@ -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<typeof ConfigSchema>;
export interface TimeAndDatePluginType extends BasePluginType {
config: TConfigSchema;
state: {
memberTimezones: GuildMemberTimezones;
};
}
export const timeAndDateCmd = command<TimeAndDatePluginType>();

View file

@ -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<UtilityPluginType> = {
config: {
@ -101,6 +102,7 @@ export const UtilityPlugin = zeppelinPlugin<UtilityPluginType>()("utility", {
prettyName: "Utility",
},
dependencies: [TimeAndDatePlugin],
configSchema: ConfigSchema,
defaultOptions,

View file

@ -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(

View file

@ -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;
}

View file

@ -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;

View file

@ -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;

View file

@ -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 });
},
});

View file

@ -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;

View file

@ -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<UtilityPluginType>,
channelId: string,
requestMemberId?: string,
): Promise<EmbedOptions | null> {
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,

View file

@ -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<UtilityPluginType>,
channelId: string,
messageId: string,
requestMemberId?: string,
): Promise<EmbedOptions | null> {
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, {

View file

@ -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<UtilityPluginType>,
serverId: string,
requestMemberId?: string,
): Promise<EmbedOptions> {
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,

View file

@ -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<UtilityPluginType>,
snowflake: string,
showUnknownWarning = false,
): EmbedOptions {
requestMemberId?: string,
): Promise<EmbedOptions> {
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,

View file

@ -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<UtilityPluginType>,
userId: string,
compact = false,
requestMemberId?: string,
): Promise<EmbedOptions | null> {
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,

View file

@ -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<ZeppelinPluginBlueprint<any>> = [
@ -63,6 +64,7 @@ export const guildPlugins: Array<ZeppelinPluginBlueprint<any>> = [
AutomodPlugin,
CompanionChannelsPlugin,
CustomEventsPlugin,
TimeAndDatePlugin,
];
// prettier-ignore
@ -79,5 +81,6 @@ export const baseGuildPlugins: Array<ZeppelinPluginBlueprint<any>> = [
NameHistoryPlugin,
CasesPlugin,
MutesPlugin,
TimeAndDatePlugin,
// TODO: Replace these with proper dependencies
];

View file

@ -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<typeof DateFormatsSchema>;
export interface ZeppelinGuildConfig extends BaseConfig<any> {
success_emoji?: string;
error_emoji?: string;
// Deprecated
timezone?: string;
date_formats?: Partial<DateFormats>;
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);

View file

@ -1298,3 +1298,5 @@ export function asyncMap<T, R>(arr: T[], fn: (item: T) => Promise<R>): Promise<R
export function unique<T>(arr: T[]): T[] {
return Array.from(new Set(arr));
}
export const DBDateFormat = "YYYY-MM-DD HH:mm:ss";

View file

@ -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<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,7 @@
import moment from "moment-timezone";
const validTimezones = moment.tz.names();
export function isValidTimezone(input: string) {
return validTimezones.includes(input);
}

View file

@ -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<string, string>(
"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,
);

View file

@ -0,0 +1,2 @@
// From https://stackoverflow.com/a/56370310/316944
export type Tail<T extends any[]> = ((...t: T) => void) extends (h: any, ...r: infer R) => void ? R : never;