3
0
Fork 0
mirror of https://github.com/ZeppelinBot/Zeppelin.git synced 2025-07-06 18:47: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 { commandTypeHelpers as ct } from "../../../commandTypes.js";
import { CaseTypes } from "../../../data/CaseTypes.js";
import { clearExpiringTempban, registerExpiringTempban } from "../../../data/loops/expiringTempbansLoop.js";
import { canActOn, hasPermission, sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils.js";
import {
clearExpiringTempban,
registerExpiringTempban,
} from "../../../data/loops/expiringTempbansLoop.js";
import {
canActOn,
hasPermission,
sendErrorMessage,
sendSuccessMessage,
} from "../../../pluginUtils.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 { waitForButtonConfirm } from "../../../utils/waitForInteraction.js";
import { LogsPlugin } from "../../Logs/LogsPlugin.js";
@ -16,204 +29,247 @@ import { readContactMethodsFromArgs } from "../functions/readContactMethodsFromA
import { modActionsCmd } from "../types.js";
const opts = {
mod: ct.member({ option: true }),
notify: ct.string({ option: true }),
"notify-channel": ct.textChannel({ option: true }),
"delete-days": ct.number({ option: true, shortcut: "d" }),
mod: ct.member({ option: true }),
notify: ct.string({ option: true }),
"notify-channel": ct.textChannel({ option: true }),
"delete-days": ct.number({ option: true, shortcut: "d" }),
};
export const BanCmd = modActionsCmd({
trigger: "ban",
permission: "can_ban",
description: "Ban or Tempban the specified member",
trigger: "ban",
permission: "can_ban",
description: "Ban or Tempban the specified member",
signature: [
{
user: ct.string(),
time: ct.delay(),
reason: ct.string({ required: false, catchAll: true }),
signature: [
{
user: ct.string(),
time: ct.delay(),
reason: ct.string({ required: false, catchAll: true }),
...opts,
},
{
user: ct.string(),
reason: ct.string({ required: false, catchAll: true }),
...opts,
},
{
user: ct.string(),
reason: ct.string({ required: false, catchAll: true }),
...opts,
},
],
...opts,
},
],
async run({ pluginData, message: msg, args }) {
const user = await resolveUser(pluginData.client, args.user);
if (!user.id) {
sendErrorMessage(pluginData, msg.channel, `User not found`);
return;
}
const time = args["time"] ? args["time"] : null;
async run({ pluginData, message: msg, args }) {
const user = await resolveUser(pluginData.client, args.user);
if (!user.id) {
sendErrorMessage(pluginData, msg.channel, `User not found`);
return;
}
const time = args["time"] ? args["time"] : null;
const reason = formatReasonWithAttachments(args.reason, [...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
let mod = msg.member;
if (args.mod) {
if (!(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;
}
const reason = formatReasonWithAttachments(args.reason, [
...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
let mod = msg.member;
if (args.mod) {
if (
!(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;
}
mod = args.mod;
}
mod = args.mod;
}
// acquire a lock because of the needed user-inputs below (if banned/not on server)
const lock = await pluginData.locks.acquire(banLock(user));
let forceban = false;
const existingTempban = await pluginData.state.tempbans.findExistingTempbanForUserId(user.id);
if (!memberToBan) {
const banned = await isBanned(pluginData, user.id);
if (banned) {
// Abort if trying to ban user indefinitely if they are already banned indefinitely
if (!existingTempban && !time) {
sendErrorMessage(pluginData, msg.channel, `User is already banned indefinitely.`);
return;
}
// acquire a lock because of the needed user-inputs below (if banned/not on server)
const lock = await pluginData.locks.acquire(banLock(user));
let forceban = false;
const existingTempban =
await pluginData.state.tempbans.findExistingTempbanForUserId(user.id);
if (!memberToBan) {
const banned = await isBanned(pluginData, user.id);
if (banned) {
// Abort if trying to ban user indefinitely if they are already banned indefinitely
if (!existingTempban && !time) {
sendErrorMessage(
pluginData,
msg.channel,
`User is already banned indefinitely.`,
);
return;
}
// Ask the mod if we should update the existing ban
const reply = await waitForButtonConfirm(
msg.channel,
{ content: "Failed to message the user. Log the warning anyway?" },
{ confirmText: "Yes", cancelText: "No", restrictToId: msg.member.id },
);
if (!reply) {
sendErrorMessage(pluginData, msg.channel, "User already banned, update cancelled by moderator");
lock.unlock();
return;
} else {
// Update or add new tempban / remove old tempban
if (time && time > 0) {
if (existingTempban) {
await pluginData.state.tempbans.updateExpiryTime(user.id, time, mod.id);
} else {
await pluginData.state.tempbans.addTempban(user.id, time, mod.id);
}
const tempban = (await pluginData.state.tempbans.findExistingTempbanForUserId(user.id))!;
registerExpiringTempban(tempban);
} else if (existingTempban) {
clearExpiringTempban(existingTempban);
pluginData.state.tempbans.clear(user.id);
}
// Ask the mod if we should update the existing ban
const reply = await waitForButtonConfirm(
msg.channel,
{ content: "Failed to message the user. Log the warning anyway?" },
{ confirmText: "Yes", cancelText: "No", restrictToId: msg.member.id },
);
if (!reply) {
sendErrorMessage(
pluginData,
msg.channel,
"User already banned, update cancelled by moderator",
);
lock.unlock();
return;
} else {
// Update or add new tempban / remove old tempban
if (time && time > 0) {
if (existingTempban) {
await pluginData.state.tempbans.updateExpiryTime(
user.id,
time,
mod.id,
);
} else {
await pluginData.state.tempbans.addTempban(user.id, time, mod.id);
}
const tempban =
(await pluginData.state.tempbans.findExistingTempbanForUserId(
user.id,
))!;
registerExpiringTempban(tempban);
} else if (existingTempban) {
clearExpiringTempban(existingTempban);
pluginData.state.tempbans.clear(user.id);
}
// Create a new case for the updated ban since we never stored the old case id and log the action
const casesPlugin = pluginData.getPlugin(CasesPlugin);
const createdCase = await casesPlugin.createCase({
modId: mod.id,
type: CaseTypes.Ban,
userId: user.id,
reason,
noteDetails: [`Ban updated to ${time ? humanizeDuration(time) : "indefinite"}`],
});
if (time) {
pluginData.getPlugin(LogsPlugin).logMemberTimedBan({
mod: mod.user,
user,
caseNumber: createdCase.case_number,
reason,
banTime: humanizeDuration(time),
});
} else {
pluginData.getPlugin(LogsPlugin).logMemberBan({
mod: mod.user,
user,
caseNumber: createdCase.case_number,
reason,
});
}
// Create a new case for the updated ban since we never stored the old case id and log the action
const casesPlugin = pluginData.getPlugin(CasesPlugin);
const createdCase = await casesPlugin.createCase({
modId: mod.id,
type: CaseTypes.Ban,
userId: user.id,
reason,
noteDetails: [
`Ban updated to ${time ? `expire ${toRelativeNativeTimestamp(time)}` : "indefinite"}`,
],
});
if (time) {
pluginData.getPlugin(LogsPlugin).logMemberTimedBan({
mod: mod.user,
user,
caseNumber: createdCase.case_number,
reason,
banTime: humanizeDuration(time),
});
} else {
pluginData.getPlugin(LogsPlugin).logMemberBan({
mod: mod.user,
user,
caseNumber: createdCase.case_number,
reason,
});
}
sendSuccessMessage(
pluginData,
msg.channel,
`Ban updated to ${time ? "expire in " + humanizeDuration(time) + " from now" : "indefinite"}`,
);
lock.unlock();
return;
}
} else {
// Ask the mod if we should upgrade to a forceban as the user is not on the server
const reply = await waitForButtonConfirm(
msg.channel,
{ content: "User not on server, forceban instead?" },
{ confirmText: "Yes", cancelText: "No", restrictToId: msg.member.id },
);
if (!reply) {
sendErrorMessage(pluginData, msg.channel, "User not on server, ban cancelled by moderator");
lock.unlock();
return;
} else {
forceban = true;
}
}
}
sendSuccessMessage(
pluginData,
msg.channel,
`Ban updated to ${time ? `expire ${toRelativeNativeTimestamp(time)}` : "indefinite"}`,
);
lock.unlock();
return;
}
} else {
// Ask the mod if we should upgrade to a forceban as the user is not on the server
const reply = await waitForButtonConfirm(
msg.channel,
{ content: "User not on server, forceban instead?" },
{ confirmText: "Yes", cancelText: "No", restrictToId: msg.member.id },
);
if (!reply) {
sendErrorMessage(
pluginData,
msg.channel,
"User not on server, ban cancelled by moderator",
);
lock.unlock();
return;
} else {
forceban = true;
}
}
}
// Make sure we're allowed to ban this member if they are on the server
if (!forceban && !canActOn(pluginData, msg.member, memberToBan!)) {
const ourLevel = getMemberLevel(pluginData, msg.member);
const targetLevel = getMemberLevel(pluginData, memberToBan!);
sendErrorMessage(
pluginData,
msg.channel,
`Cannot ban: target permission level is equal or higher to yours, ${targetLevel} >= ${ourLevel}`,
);
lock.unlock();
return;
}
// Make sure we're allowed to ban this member if they are on the server
if (!forceban && !canActOn(pluginData, msg.member, memberToBan!)) {
const ourLevel = getMemberLevel(pluginData, msg.member);
const targetLevel = getMemberLevel(pluginData, memberToBan!);
sendErrorMessage(
pluginData,
msg.channel,
`Cannot ban: target permission level is equal or higher to yours, ${targetLevel} >= ${ourLevel}`,
);
lock.unlock();
return;
}
let contactMethods;
try {
contactMethods = readContactMethodsFromArgs(args);
} catch (e) {
sendErrorMessage(pluginData, msg.channel, e.message);
lock.unlock();
return;
}
let contactMethods;
try {
contactMethods = readContactMethodsFromArgs(args);
} catch (e) {
sendErrorMessage(pluginData, msg.channel, e.message);
lock.unlock();
return;
}
const deleteMessageDays =
args["delete-days"] ?? (await pluginData.config.getForMessage(msg)).ban_delete_message_days;
const banResult = await banUserId(
pluginData,
user.id,
reason,
{
contactMethods,
caseArgs: {
modId: mod.id,
ppId: mod.id !== msg.author.id ? msg.author.id : undefined,
},
deleteMessageDays,
modId: mod.id,
},
time,
);
const deleteMessageDays =
args["delete-days"] ??
(await pluginData.config.getForMessage(msg)).ban_delete_message_days;
const banResult = await banUserId(
pluginData,
user.id,
reason,
{
contactMethods,
caseArgs: {
modId: mod.id,
ppId: mod.id !== msg.author.id ? msg.author.id : undefined,
},
deleteMessageDays,
modId: mod.id,
},
time,
);
if (banResult.status === "failed") {
sendErrorMessage(pluginData, msg.channel, `Failed to ban member: ${banResult.error}`);
lock.unlock();
return;
}
if (banResult.status === "failed") {
sendErrorMessage(
pluginData,
msg.channel,
`Failed to ban member: ${banResult.error}`,
);
lock.unlock();
return;
}
let forTime = "";
if (time && time > 0) {
forTime = `for ${humanizeDuration(time)} `;
}
let forTime = "";
if (time && time > 0) {
forTime = `for ${humanizeDuration(time)} `;
}
// Confirm the action to the moderator
let response = "";
if (!forceban) {
response = `Banned **${renderUsername(user)}** ${forTime}(Case #${banResult.case.case_number})`;
if (banResult.notifyResult.text) response += ` (${banResult.notifyResult.text})`;
} else {
response = `Member forcebanned ${forTime}(Case #${banResult.case.case_number})`;
}
// Confirm the action to the moderator
let response = "";
if (!forceban) {
response = `Banned **${renderUsername(user)}** ${forTime}(Case #${banResult.case.case_number})`;
if (banResult.notifyResult.text)
response += ` (${banResult.notifyResult.text})`;
} else {
response = `Member forcebanned ${forTime}(Case #${banResult.case.case_number})`;
}
lock.unlock();
sendSuccessMessage(pluginData, msg.channel, response);
},
lock.unlock();
sendSuccessMessage(pluginData, msg.channel, response);
},
});

View file

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

View file

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

View file

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

View file

@ -1,37 +1,37 @@
import humanizeDuration from "humanize-duration";
import moment from "moment-timezone";
import { sendErrorMessage } from "../../../pluginUtils.js";
import { createChunkedMessage, DBDateFormat, sorter } from "../../../utils.js";
import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin.js";
import {
createChunkedMessage,
sorter,
toNativeTimestamp,
toRelativeNativeTimestamp,
} from "../../../utils.js";
import { remindersCmd } from "../types.js";
export const RemindersCmd = remindersCmd({
trigger: "reminders",
permission: "can_use",
trigger: "reminders",
permission: "can_use",
async run({ message: msg, pluginData }) {
const reminders = await pluginData.state.reminders.getRemindersByUserId(msg.author.id);
if (reminders.length === 0) {
sendErrorMessage(pluginData, msg.channel, "No reminders");
return;
}
async run({ message: msg, pluginData }) {
const reminders = await pluginData.state.reminders.getRemindersByUserId(
msg.author.id,
);
if (reminders.length === 0) {
sendErrorMessage(pluginData, msg.channel, "No reminders");
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]) => {
const num = i + 1;
const paddedNum = num.toString().padStart(longestNum, " ");
const target = moment.utc(reminder.remind_at);
const relative = toRelativeNativeTimestamp(target, 0);
const prettyRemindAt = toNativeTimestamp(target);
return `\`${paddedNum}.\` ${prettyRemindAt} (${relative}) ${reminder.body}`;
});
reminders.sort(sorter("remind_at"));
const longestNum = (reminders.length + 1).toString().length;
const lines = Array.from(reminders.entries()).map(([i, reminder]) => {
const num = i + 1;
const paddedNum = num.toString().padStart(longestNum, " ");
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 = timeAndDate
.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 { rootDir } from "../../../paths.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 { utilityCmd } from "../types.js";
@ -31,23 +31,18 @@ export const AboutCmd = utilityCmd({
let version;
if (lastCommit) {
lastUpdate = timeAndDate
.inGuildTz(moment.utc(lastCommit.committer.date, "X"))
.format(pluginData.getPlugin(TimeAndDatePlugin).getDateFormat("pretty_datetime"));
lastUpdate = toRelativeNativeTimestamp(moment.utc(lastCommit.committer.data, "X"), 0);
version = lastCommit.shortHash;
} else {
lastUpdate = "?";
version = "?";
}
const lastReload = humanizeDuration(Date.now() - pluginData.state.lastReload, {
largest: 2,
round: true,
});
const lastReload = toRelativeNativeTimestamp(pluginData.state.lastReload, 0);
const basicInfoRows = [
["Uptime", prettyUptime],
["Last config reload", `${lastReload} ago`],
["Last config reload", lastReload],
["Last bot update", lastUpdate],
["Version", version],
["API latency", `${pluginData.client.ws.ping}ms`],
@ -101,7 +96,9 @@ export const AboutCmd = utilityCmd({
// Use the bot avatar as the embed image
if (pluginData.client.user!.displayAvatarURL()) {
aboutEmbed.thumbnail = { url: pluginData.client.user!.displayAvatarURL()! };
aboutEmbed.thumbnail = {
url: pluginData.client.user!.displayAvatarURL()!,
};
}
msg.channel.send({ embeds: [aboutEmbed] });

View file

@ -446,6 +446,14 @@ export function convertMSToDelayString(ms: number): string {
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>") {
return emoji ? `${emoji} ${str}` : str;
}