3
0
Fork 0
mirror of https://github.com/ZeppelinBot/Zeppelin.git synced 2025-07-07 02:57:20 +00:00

feat: native timestamps

This commit is contained in:
Rei Star 2025-03-13 07:04:42 +04:00
parent eb5fda8d19
commit 6006504459
No known key found for this signature in database
7 changed files with 305 additions and 243 deletions

View file

@ -2,10 +2,23 @@ import humanizeDuration from "humanize-duration";
import { getMemberLevel } from "knub/helpers"; import { getMemberLevel } from "knub/helpers";
import { commandTypeHelpers as ct } from "../../../commandTypes.js"; import { commandTypeHelpers as ct } from "../../../commandTypes.js";
import { CaseTypes } from "../../../data/CaseTypes.js"; import { CaseTypes } from "../../../data/CaseTypes.js";
import { clearExpiringTempban, registerExpiringTempban } from "../../../data/loops/expiringTempbansLoop.js"; import {
import { canActOn, hasPermission, sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils.js"; clearExpiringTempban,
registerExpiringTempban,
} from "../../../data/loops/expiringTempbansLoop.js";
import {
canActOn,
hasPermission,
sendErrorMessage,
sendSuccessMessage,
} from "../../../pluginUtils.js";
import { CasesPlugin } from "../../../plugins/Cases/CasesPlugin.js"; import { CasesPlugin } from "../../../plugins/Cases/CasesPlugin.js";
import { renderUsername, resolveMember, resolveUser } from "../../../utils.js"; import {
renderUsername,
resolveMember,
resolveUser,
toRelativeNativeTimestamp,
} from "../../../utils.js";
import { banLock } from "../../../utils/lockNameHelpers.js"; import { banLock } from "../../../utils/lockNameHelpers.js";
import { waitForButtonConfirm } from "../../../utils/waitForInteraction.js"; import { waitForButtonConfirm } from "../../../utils/waitForInteraction.js";
import { LogsPlugin } from "../../Logs/LogsPlugin.js"; import { LogsPlugin } from "../../Logs/LogsPlugin.js";
@ -51,13 +64,28 @@ export const BanCmd = modActionsCmd({
} }
const time = args["time"] ? args["time"] : null; const time = args["time"] ? args["time"] : null;
const reason = formatReasonWithAttachments(args.reason, [...msg.attachments.values()]); const reason = formatReasonWithAttachments(args.reason, [
const memberToBan = await resolveMember(pluginData.client, pluginData.guild, user.id); ...msg.attachments.values(),
]);
const memberToBan = await resolveMember(
pluginData.client,
pluginData.guild,
user.id,
);
// The moderator who did the action is the message author or, if used, the specified -mod // The moderator who did the action is the message author or, if used, the specified -mod
let mod = msg.member; let mod = msg.member;
if (args.mod) { if (args.mod) {
if (!(await hasPermission(pluginData, "can_act_as_other", { message: msg, channelId: msg.channel.id }))) { if (
sendErrorMessage(pluginData, msg.channel, "You don't have permission to use -mod"); !(await hasPermission(pluginData, "can_act_as_other", {
message: msg,
channelId: msg.channel.id,
}))
) {
sendErrorMessage(
pluginData,
msg.channel,
"You don't have permission to use -mod",
);
return; return;
} }
@ -67,13 +95,18 @@ export const BanCmd = modActionsCmd({
// acquire a lock because of the needed user-inputs below (if banned/not on server) // acquire a lock because of the needed user-inputs below (if banned/not on server)
const lock = await pluginData.locks.acquire(banLock(user)); const lock = await pluginData.locks.acquire(banLock(user));
let forceban = false; let forceban = false;
const existingTempban = await pluginData.state.tempbans.findExistingTempbanForUserId(user.id); const existingTempban =
await pluginData.state.tempbans.findExistingTempbanForUserId(user.id);
if (!memberToBan) { if (!memberToBan) {
const banned = await isBanned(pluginData, user.id); const banned = await isBanned(pluginData, user.id);
if (banned) { if (banned) {
// Abort if trying to ban user indefinitely if they are already banned indefinitely // Abort if trying to ban user indefinitely if they are already banned indefinitely
if (!existingTempban && !time) { if (!existingTempban && !time) {
sendErrorMessage(pluginData, msg.channel, `User is already banned indefinitely.`); sendErrorMessage(
pluginData,
msg.channel,
`User is already banned indefinitely.`,
);
return; return;
} }
@ -84,18 +117,29 @@ export const BanCmd = modActionsCmd({
{ confirmText: "Yes", cancelText: "No", restrictToId: msg.member.id }, { confirmText: "Yes", cancelText: "No", restrictToId: msg.member.id },
); );
if (!reply) { if (!reply) {
sendErrorMessage(pluginData, msg.channel, "User already banned, update cancelled by moderator"); sendErrorMessage(
pluginData,
msg.channel,
"User already banned, update cancelled by moderator",
);
lock.unlock(); lock.unlock();
return; return;
} else { } else {
// Update or add new tempban / remove old tempban // Update or add new tempban / remove old tempban
if (time && time > 0) { if (time && time > 0) {
if (existingTempban) { if (existingTempban) {
await pluginData.state.tempbans.updateExpiryTime(user.id, time, mod.id); await pluginData.state.tempbans.updateExpiryTime(
user.id,
time,
mod.id,
);
} else { } else {
await pluginData.state.tempbans.addTempban(user.id, time, mod.id); await pluginData.state.tempbans.addTempban(user.id, time, mod.id);
} }
const tempban = (await pluginData.state.tempbans.findExistingTempbanForUserId(user.id))!; const tempban =
(await pluginData.state.tempbans.findExistingTempbanForUserId(
user.id,
))!;
registerExpiringTempban(tempban); registerExpiringTempban(tempban);
} else if (existingTempban) { } else if (existingTempban) {
clearExpiringTempban(existingTempban); clearExpiringTempban(existingTempban);
@ -109,7 +153,9 @@ export const BanCmd = modActionsCmd({
type: CaseTypes.Ban, type: CaseTypes.Ban,
userId: user.id, userId: user.id,
reason, reason,
noteDetails: [`Ban updated to ${time ? humanizeDuration(time) : "indefinite"}`], noteDetails: [
`Ban updated to ${time ? `expire ${toRelativeNativeTimestamp(time)}` : "indefinite"}`,
],
}); });
if (time) { if (time) {
pluginData.getPlugin(LogsPlugin).logMemberTimedBan({ pluginData.getPlugin(LogsPlugin).logMemberTimedBan({
@ -131,7 +177,7 @@ export const BanCmd = modActionsCmd({
sendSuccessMessage( sendSuccessMessage(
pluginData, pluginData,
msg.channel, msg.channel,
`Ban updated to ${time ? "expire in " + humanizeDuration(time) + " from now" : "indefinite"}`, `Ban updated to ${time ? `expire ${toRelativeNativeTimestamp(time)}` : "indefinite"}`,
); );
lock.unlock(); lock.unlock();
return; return;
@ -144,7 +190,11 @@ export const BanCmd = modActionsCmd({
{ confirmText: "Yes", cancelText: "No", restrictToId: msg.member.id }, { confirmText: "Yes", cancelText: "No", restrictToId: msg.member.id },
); );
if (!reply) { if (!reply) {
sendErrorMessage(pluginData, msg.channel, "User not on server, ban cancelled by moderator"); sendErrorMessage(
pluginData,
msg.channel,
"User not on server, ban cancelled by moderator",
);
lock.unlock(); lock.unlock();
return; return;
} else { } else {
@ -176,7 +226,8 @@ export const BanCmd = modActionsCmd({
} }
const deleteMessageDays = const deleteMessageDays =
args["delete-days"] ?? (await pluginData.config.getForMessage(msg)).ban_delete_message_days; args["delete-days"] ??
(await pluginData.config.getForMessage(msg)).ban_delete_message_days;
const banResult = await banUserId( const banResult = await banUserId(
pluginData, pluginData,
user.id, user.id,
@ -194,7 +245,11 @@ export const BanCmd = modActionsCmd({
); );
if (banResult.status === "failed") { if (banResult.status === "failed") {
sendErrorMessage(pluginData, msg.channel, `Failed to ban member: ${banResult.error}`); sendErrorMessage(
pluginData,
msg.channel,
`Failed to ban member: ${banResult.error}`,
);
lock.unlock(); lock.unlock();
return; return;
} }
@ -208,7 +263,8 @@ export const BanCmd = modActionsCmd({
let response = ""; let response = "";
if (!forceban) { if (!forceban) {
response = `Banned **${renderUsername(user)}** ${forTime}(Case #${banResult.case.case_number})`; response = `Banned **${renderUsername(user)}** ${forTime}(Case #${banResult.case.case_number})`;
if (banResult.notifyResult.text) response += ` (${banResult.notifyResult.text})`; if (banResult.notifyResult.text)
response += ` (${banResult.notifyResult.text})`;
} else { } else {
response = `Member forcebanned ${forTime}(Case #${banResult.case.case_number})`; response = `Member forcebanned ${forTime}(Case #${banResult.case.case_number})`;
} }

View file

@ -14,6 +14,7 @@ import {
notifyUser, notifyUser,
resolveMember, resolveMember,
resolveUser, resolveUser,
toRelativeNativeTimestamp,
ucfirst, ucfirst,
} from "../../../utils.js"; } from "../../../utils.js";
import { userToTemplateSafeUser } from "../../../utils/templateSafeObjects.js"; import { userToTemplateSafeUser } from "../../../utils/templateSafeObjects.js";
@ -172,7 +173,7 @@ export async function banUserId(
user, user,
caseNumber: createdCase.case_number, caseNumber: createdCase.case_number,
reason: reason ?? "", reason: reason ?? "",
banTime: humanizeDuration(banTime), banTime: toRelativeNativeTimestamp(banTime),
}); });
} else { } else {
pluginData.getPlugin(LogsPlugin).logMemberBan({ pluginData.getPlugin(LogsPlugin).logMemberBan({

View file

@ -10,7 +10,7 @@ import moment from "moment-timezone";
import { commandTypeHelpers as ct } from "../../../commandTypes.js"; import { commandTypeHelpers as ct } from "../../../commandTypes.js";
import { humanizeDurationShort } from "../../../humanizeDurationShort.js"; import { humanizeDurationShort } from "../../../humanizeDurationShort.js";
import { getBaseUrl } from "../../../pluginUtils.js"; import { getBaseUrl } from "../../../pluginUtils.js";
import { DBDateFormat, MINUTES, renderUsername, resolveMember } from "../../../utils.js"; import { DBDateFormat, MINUTES, renderUsername, resolveMember, toRelativeNativeTimestamp } from "../../../utils.js";
import { IMuteWithDetails, mutesCmd } from "../types.js"; import { IMuteWithDetails, mutesCmd } from "../types.js";
export const MutesCmd = mutesCmd({ export const MutesCmd = mutesCmd({
@ -131,15 +131,15 @@ export const MutesCmd = mutesCmd({
if (mute.expires_at) { if (mute.expires_at) {
const timeUntilExpiry = moment.utc().diff(moment.utc(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 = toRelativeNativeTimestamp(timeUntilExpiry, 0);
line += ` ⏰ Expires in ${humanizedTime}`; line += ` ⏰ Expires ${humanizedTime}`;
} else { } else {
line += ` ⏰ Indefinite`; line += ` ⏰ Indefinite`;
} }
const timeFromMute = moment.utc(mute.created_at, DBDateFormat).diff(moment.utc()); const timeFromMute = moment.utc(mute.created_at, DBDateFormat);
const humanizedTimeFromMute = humanizeDurationShort(timeFromMute, { largest: 2, round: true }); const humanizedTimeFromMute = toRelativeNativeTimestamp(timeFromMute);
line += ` 🕒 Muted ${humanizedTimeFromMute} ago`; line += ` 🕒 Muted ${humanizedTimeFromMute}`;
if (mute.banned) { if (mute.banned) {
line += ` 🔨 Banned`; line += ` 🔨 Banned`;
@ -207,12 +207,17 @@ export const MutesCmd = mutesCmd({
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(buttons); const row = new ActionRowBuilder<ButtonBuilder>().addComponents(buttons);
await listMessage.edit({ components: [row] }); await listMessage.edit({ components: [row] });
const collector = listMessage.createMessageComponentCollector({ time: stopCollectionDebounce }); const collector = listMessage.createMessageComponentCollector({
time: stopCollectionDebounce,
});
collector.on("collect", async (interaction: MessageComponentInteraction) => { collector.on("collect", async (interaction: MessageComponentInteraction) => {
if (msg.author.id !== interaction.user.id) { if (msg.author.id !== interaction.user.id) {
interaction interaction
.reply({ content: `You are not permitted to use these buttons.`, ephemeral: true }) .reply({
content: `You are not permitted to use these buttons.`,
ephemeral: true,
})
// tslint:disable-next-line no-console // tslint:disable-next-line no-console
.catch((err) => console.trace(err.message)); .catch((err) => console.trace(err.message));
} else { } else {
@ -228,7 +233,10 @@ export const MutesCmd = mutesCmd({
stopCollectionFn = async () => { stopCollectionFn = async () => {
collector.stop(); collector.stop();
await listMessage.edit({ content: listMessage.content, components: [] }); await listMessage.edit({
content: listMessage.content,
components: [],
});
}; };
bumpCollectionTimeout(); bumpCollectionTimeout();
} }

View file

@ -1,9 +1,8 @@
import humanizeDuration from "humanize-duration";
import moment from "moment-timezone"; import moment from "moment-timezone";
import { commandTypeHelpers as ct } from "../../../commandTypes.js"; import { commandTypeHelpers as ct } from "../../../commandTypes.js";
import { registerUpcomingReminder } from "../../../data/loops/upcomingRemindersLoop.js"; import { registerUpcomingReminder } from "../../../data/loops/upcomingRemindersLoop.js";
import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils.js"; import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils.js";
import { convertDelayStringToMS, messageLink } from "../../../utils.js"; import { convertDelayStringToMS, toNativeTimestamp, toRelativeNativeTimestamp, messageLink } from "../../../utils.js";
import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin.js"; import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin.js";
import { remindersCmd } from "../types.js"; import { remindersCmd } from "../types.js";
@ -61,16 +60,9 @@ export const RemindCmd = remindersCmd({
registerUpcomingReminder(reminder); registerUpcomingReminder(reminder);
const msUntilReminder = reminderTime.diff(now); const timeUntilReminder = toRelativeNativeTimestamp(reminderTime, 0);
const timeUntilReminder = humanizeDuration(msUntilReminder, { largest: 2, round: true }); const prettyReminderTime = toNativeTimestamp(reminderTime);
const prettyReminderTime = (await timeAndDate.inMemberTz(msg.author.id, reminderTime)).format(
pluginData.getPlugin(TimeAndDatePlugin).getDateFormat("pretty_datetime"),
);
sendSuccessMessage( sendSuccessMessage(pluginData, msg.channel, `I will remind you ${timeUntilReminder} at ${prettyReminderTime}`);
pluginData,
msg.channel,
`I will remind you in **${timeUntilReminder}** at **${prettyReminderTime}**`,
);
}, },
}); });

View file

@ -1,8 +1,11 @@
import humanizeDuration from "humanize-duration";
import moment from "moment-timezone"; import moment from "moment-timezone";
import { sendErrorMessage } from "../../../pluginUtils.js"; import { sendErrorMessage } from "../../../pluginUtils.js";
import { createChunkedMessage, DBDateFormat, sorter } from "../../../utils.js"; import {
import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin.js"; createChunkedMessage,
sorter,
toNativeTimestamp,
toRelativeNativeTimestamp,
} from "../../../utils.js";
import { remindersCmd } from "../types.js"; import { remindersCmd } from "../types.js";
export const RemindersCmd = remindersCmd({ export const RemindersCmd = remindersCmd({
@ -10,26 +13,23 @@ export const RemindersCmd = remindersCmd({
permission: "can_use", permission: "can_use",
async run({ message: msg, pluginData }) { async run({ message: msg, pluginData }) {
const reminders = await pluginData.state.reminders.getRemindersByUserId(msg.author.id); const reminders = await pluginData.state.reminders.getRemindersByUserId(
msg.author.id,
);
if (reminders.length === 0) { if (reminders.length === 0) {
sendErrorMessage(pluginData, msg.channel, "No reminders"); sendErrorMessage(pluginData, msg.channel, "No reminders");
return; return;
} }
const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin);
reminders.sort(sorter("remind_at")); reminders.sort(sorter("remind_at"));
const longestNum = (reminders.length + 1).toString().length; const longestNum = (reminders.length + 1).toString().length;
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.utc(reminder.remind_at, "YYYY-MM-DD HH:mm:ss"); const target = moment.utc(reminder.remind_at);
const diff = target.diff(moment.utc()); const relative = toRelativeNativeTimestamp(target, 0);
const result = humanizeDuration(diff, { largest: 2, round: true }); const prettyRemindAt = toNativeTimestamp(target);
const prettyRemindAt = timeAndDate return `\`${paddedNum}.\` ${prettyRemindAt} (${relative}) ${reminder.body}`;
.inGuildTz(moment.utc(reminder.remind_at, DBDateFormat))
.format(timeAndDate.getDateFormat("pretty_datetime"));
return `\`${paddedNum}.\` \`${prettyRemindAt} (${result})\` ${reminder.body}`;
}); });
createChunkedMessage(msg.channel, lines.join("\n")); createChunkedMessage(msg.channel, lines.join("\n"));

View file

@ -5,7 +5,7 @@ import shuffle from "lodash/shuffle.js";
import moment from "moment-timezone"; import moment from "moment-timezone";
import { rootDir } from "../../../paths.js"; import { rootDir } from "../../../paths.js";
import { getCurrentUptime } from "../../../uptime.js"; import { getCurrentUptime } from "../../../uptime.js";
import { resolveMember, sorter } from "../../../utils.js"; import { resolveMember, sorter, toRelativeNativeTimestamp } from "../../../utils.js";
import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin.js"; import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin.js";
import { utilityCmd } from "../types.js"; import { utilityCmd } from "../types.js";
@ -31,23 +31,18 @@ export const AboutCmd = utilityCmd({
let version; let version;
if (lastCommit) { if (lastCommit) {
lastUpdate = timeAndDate lastUpdate = toRelativeNativeTimestamp(moment.utc(lastCommit.committer.data, "X"), 0);
.inGuildTz(moment.utc(lastCommit.committer.date, "X"))
.format(pluginData.getPlugin(TimeAndDatePlugin).getDateFormat("pretty_datetime"));
version = lastCommit.shortHash; version = lastCommit.shortHash;
} else { } else {
lastUpdate = "?"; lastUpdate = "?";
version = "?"; version = "?";
} }
const lastReload = humanizeDuration(Date.now() - pluginData.state.lastReload, { const lastReload = toRelativeNativeTimestamp(pluginData.state.lastReload, 0);
largest: 2,
round: true,
});
const basicInfoRows = [ const basicInfoRows = [
["Uptime", prettyUptime], ["Uptime", prettyUptime],
["Last config reload", `${lastReload} ago`], ["Last config reload", lastReload],
["Last bot update", lastUpdate], ["Last bot update", lastUpdate],
["Version", version], ["Version", version],
["API latency", `${pluginData.client.ws.ping}ms`], ["API latency", `${pluginData.client.ws.ping}ms`],
@ -101,7 +96,9 @@ export const AboutCmd = utilityCmd({
// Use the bot avatar as the embed image // Use the bot avatar as the embed image
if (pluginData.client.user!.displayAvatarURL()) { if (pluginData.client.user!.displayAvatarURL()) {
aboutEmbed.thumbnail = { url: pluginData.client.user!.displayAvatarURL()! }; aboutEmbed.thumbnail = {
url: pluginData.client.user!.displayAvatarURL()!,
};
} }
msg.channel.send({ embeds: [aboutEmbed] }); msg.channel.send({ embeds: [aboutEmbed] });

View file

@ -446,6 +446,14 @@ export function convertMSToDelayString(ms: number): string {
return result; return result;
} }
export function toNativeTimestamp(time, flag = "f") {
return `<t:${Math.round(time / 1000)}:${flag}>`;
}
export function toRelativeNativeTimestamp(ms, offset = Date.now()) {
return `<t:${Math.round((offset + ms) / 1000)}:R>`;
}
export function successMessage(str: string, emoji = "<:zep_check:906897402101891093>") { export function successMessage(str: string, emoji = "<:zep_check:906897402101891093>") {
return emoji ? `${emoji} ${str}` : str; return emoji ? `${emoji} ${str}` : str;
} }