1 Commits

Author SHA1 Message Date
3fc27b466a Bump ver 2024-04-20 22:14:49 +01:00
30 changed files with 1118 additions and 1321 deletions

669
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[package] [package]
name = "reminder-rs" name = "reminder-rs"
version = "1.7.27" version = "1.7.14"
authors = ["Jude Southworth <judesouthworth@pm.me>"] authors = ["Jude Southworth <judesouthworth@pm.me>"]
edition = "2021" edition = "2021"
license = "AGPL-3.0 only" license = "AGPL-3.0 only"
@ -34,7 +34,7 @@ rocket_dyn_templates = { version = "0.1.0", features = ["tera"] }
serenity = { version = "0.12", default-features = false, features = ["builder", "cache", "client", "gateway", "http", "model", "utils", "rustls_backend"] } serenity = { version = "0.12", default-features = false, features = ["builder", "cache", "client", "gateway", "http", "model", "utils", "rustls_backend"] }
oauth2 = "4" oauth2 = "4"
csv = "1.2" csv = "1.2"
sd-notify = "0.4.1" axum = "0.7"
[dependencies.extract_derive] [dependencies.extract_derive]
path = "extract_derive" path = "extract_derive"

View File

@ -1,22 +1,12 @@
server { server {
server_name www.reminder-bot.com; server_name www.reminder-bot.com;
return 301 https://reminder-bot.com$request_uri; return 301 $scheme://reminder-bot.com$request_uri;
} }
server { server {
listen 80; listen 80;
server_name beta.reminder-bot.com; server_name reminder-bot.com;
return 301 https://reminder-bot.com$request_uri;
}
server {
listen 443 ssl;
server_name beta.reminder-bot.com;
ssl_certificate /etc/letsencrypt/live/beta.reminder-bot.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/beta.reminder-bot.com/privkey.pem;
return 301 https://reminder-bot.com$request_uri; return 301 https://reminder-bot.com$request_uri;
} }
@ -35,8 +25,6 @@ server {
proxy_buffers 4 256k; proxy_buffers 4 256k;
proxy_busy_buffers_size 256k; proxy_busy_buffers_size 256k;
client_max_body_size 10M;
location / { location / {
proxy_pass http://localhost:18920; proxy_pass http://localhost:18920;
proxy_redirect off; proxy_redirect off;

File diff suppressed because it is too large Load Diff

View File

@ -7,10 +7,6 @@ import { useGuild } from "./useGuild";
export const Mentions = ({ input }) => { export const Mentions = ({ input }) => {
const guild = useGuild(); const guild = useGuild();
return <>{guild && <_Mentions guild={guild} input={input} />}</>;
};
const _Mentions = ({ guild, input }) => {
const { data: roles } = useQuery(fetchGuildRoles(guild)); const { data: roles } = useQuery(fetchGuildRoles(guild));
const { data: channels } = useQuery(fetchGuildChannels(guild)); const { data: channels } = useQuery(fetchGuildChannels(guild));
const { data: emojis } = useQuery(fetchGuildEmojis(guild)); const { data: emojis } = useQuery(fetchGuildEmojis(guild));
@ -21,7 +17,7 @@ const _Mentions = ({ guild, input }) => {
{ {
trigger: "@", trigger: "@",
values: (roles || []) values: (roles || [])
.filter((role) => role.name !== "@everyone") .filter((role) => role.name === "@everyone")
.map(({ id, name }) => ({ key: name, value: id })), .map(({ id, name }) => ({ key: name, value: id })),
allowSpaces: true, allowSpaces: true,
selectTemplate: (item) => `<@&${item.original.value}>`, selectTemplate: (item) => `<@&${item.original.value}>`,

View File

@ -1,15 +1,15 @@
import { useState } from "preact/hooks"; import {useState} from "preact/hooks";
import { fetchGuildChannels, Reminder } from "../../api"; import {fetchGuildChannels, Reminder} from "../../api";
import { DateTime } from "luxon"; import {DateTime} from "luxon";
import { CreateButtonRow } from "./ButtonRow/CreateButtonRow"; import {CreateButtonRow} from "./ButtonRow/CreateButtonRow";
import { TopBar } from "./TopBar"; import {TopBar} from "./TopBar";
import { Message } from "./Message"; import {Message} from "./Message";
import { Settings } from "./Settings"; import {Settings} from "./Settings";
import { ReminderContext } from "./ReminderContext"; import {ReminderContext} from "./ReminderContext";
import { useQuery } from "react-query"; import {useQuery} from "react-query";
import "./styles.scss"; import "./styles.scss";
import { useGuild } from "../App/useGuild"; import {useGuild} from "../App/useGuild";
import { DEFAULT_COLOR } from "./Embed"; import {DEFAULT_COLOR} from "./Embed";
function defaultReminder(): Reminder { function defaultReminder(): Reminder {
return { return {
@ -45,41 +45,10 @@ function defaultReminder(): Reminder {
export const CreateReminder = () => { export const CreateReminder = () => {
const guild = useGuild(); const guild = useGuild();
if (guild) {
return <_Guild guild={guild} />;
} else {
return <_User />;
}
};
const _User = () => {
const [reminder, setReminder] = useState(defaultReminder()); const [reminder, setReminder] = useState(defaultReminder());
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
return ( const {isSuccess, data: guildChannels} = useQuery(fetchGuildChannels(guild));
<ReminderContext.Provider value={[reminder, setReminder]}>
<div class={collapsed ? "reminderContent is-collapsed" : "reminderContent"}>
<TopBar
isCreating={true}
toggleCollapsed={() => {
setCollapsed(!collapsed);
}}
/>
<div class="columns reminder-settings">
<Message />
<Settings />
</div>
<CreateButtonRow />
</div>
</ReminderContext.Provider>
);
};
const _Guild = ({ guild }) => {
const [reminder, setReminder] = useState(defaultReminder());
const [collapsed, setCollapsed] = useState(false);
const { isSuccess, data: guildChannels } = useQuery(fetchGuildChannels(guild));
if (isSuccess && reminder.channel === null) { if (isSuccess && reminder.channel === null) {
setReminder((reminder) => ({ setReminder((reminder) => ({
@ -98,10 +67,10 @@ const _Guild = ({ guild }) => {
}} }}
/> />
<div class="columns reminder-settings"> <div class="columns reminder-settings">
<Message /> <Message/>
<Settings /> <Settings/>
</div> </div>
<CreateButtonRow /> <CreateButtonRow/>
</div> </div>
</ReminderContext.Provider> </ReminderContext.Provider>
); );

View File

@ -1,7 +1,8 @@
import { ChannelSelector } from "./ChannelSelector"; import { ChannelSelector } from "./ChannelSelector";
import { DateTime } from "luxon";
import { IntervalSelector } from "./IntervalSelector"; import { IntervalSelector } from "./IntervalSelector";
import { useQuery } from "react-query"; import { useQuery } from "react-query";
import { fetchGuildInfo, fetchUserInfo } from "../../api"; import { fetchUserInfo } from "../../api";
import { useReminder } from "./ReminderContext"; import { useReminder } from "./ReminderContext";
import { Attachment } from "./Attachment"; import { Attachment } from "./Attachment";
import { TTS } from "./TTS"; import { TTS } from "./TTS";
@ -10,12 +11,11 @@ import { useGuild } from "../App/useGuild";
export const Settings = () => { export const Settings = () => {
const guild = useGuild(); const guild = useGuild();
const { isSuccess: userFetched, data: userInfo } = useQuery({ ...fetchUserInfo() }); const { isSuccess: userFetched, data: userInfo } = useQuery(fetchUserInfo());
const { isSuccess: guildFetched, data: guildInfo } = useQuery({ ...fetchGuildInfo(guild) });
const [reminder, setReminder] = useReminder(); const [reminder, setReminder] = useReminder();
if (!userFetched || !guildFetched) { if (!userFetched) {
return <></>; return <></>;
} }
@ -59,13 +59,7 @@ export const Settings = () => {
<div class="collapses split-controls"> <div class="collapses split-controls">
<div> <div>
<div <div class={userInfo.patreon ? "patreon-only" : "patreon-only is-locked"}>
class={
userInfo.patreon || guildInfo.patreon
? "patreon-only"
: "patreon-only is-locked"
}
>
<div class="patreon-invert foreground"> <div class="patreon-invert foreground">
Intervals available on <a href="https://patreon.com/jellywx">Patreon</a>{" "} Intervals available on <a href="https://patreon.com/jellywx">Patreon</a>{" "}
or{" "} or{" "}

View File

@ -16,28 +16,28 @@ export const TimeInput = ({ defaultValue, onInput }) => {
const ref = useRef(null); const ref = useRef(null);
const [timezone] = useTimezone(); const [timezone] = useTimezone();
const [localTime, setLocalTime] = useState( const [time, setTime] = useState(
defaultValue ? DateTime.fromISO(defaultValue, { zone: "UTC" }).setZone(timezone) : null, defaultValue ? DateTime.fromISO(defaultValue, { zone: "UTC" }) : null,
); );
const updateTime = useCallback( const updateTime = useCallback(
(upd: TimeUpdate) => { (upd: TimeUpdate) => {
if (upd === null) { if (upd === null) {
setLocalTime(null); setTime(null);
} }
let newTime = localTime; let newTime = time;
if (newTime === null) { if (newTime === null) {
newTime = DateTime.now().setZone(timezone); newTime = DateTime.now().setZone("UTC");
} }
setLocalTime(newTime.set(upd)); setTime(newTime.setZone(timezone).set(upd).setZone("UTC"));
}, },
[localTime, timezone], [time, timezone],
); );
useEffect(() => { useEffect(() => {
onInput(localTime?.setZone("UTC").toFormat("yyyy-LL-dd'T'HH:mm:ss")); onInput(time?.setZone("UTC").toFormat("yyyy-LL-dd'T'HH:mm:ss"));
}, [localTime]); }, [time]);
const flash = useFlash(); const flash = useFlash();
@ -51,14 +51,14 @@ export const TimeInput = ({ defaultValue, onInput }) => {
let dt = DateTime.fromISO(pasteValue, { zone: timezone }); let dt = DateTime.fromISO(pasteValue, { zone: timezone });
if (dt.isValid) { if (dt.isValid) {
setLocalTime(dt); setTime(dt);
return; return;
} }
dt = DateTime.fromSQL(pasteValue); dt = DateTime.fromSQL(pasteValue);
if (dt.isValid) { if (dt.isValid) {
setLocalTime(dt); setTime(dt);
return; return;
} }
@ -83,8 +83,8 @@ export const TimeInput = ({ defaultValue, onInput }) => {
maxlength={4} maxlength={4}
placeholder="YYYY" placeholder="YYYY"
value={ value={
localTime time
? localTime.year.toLocaleString("en-US", { ? time.setZone(timezone).year.toLocaleString("en-US", {
minimumIntegerDigits: 4, minimumIntegerDigits: 4,
useGrouping: false, useGrouping: false,
}) })
@ -114,8 +114,8 @@ export const TimeInput = ({ defaultValue, onInput }) => {
maxlength={2} maxlength={2}
placeholder="MM" placeholder="MM"
value={ value={
localTime time
? localTime.month.toLocaleString("en-US", { ? time.setZone(timezone).month.toLocaleString("en-US", {
minimumIntegerDigits: 2, minimumIntegerDigits: 2,
}) })
: "" : ""
@ -144,10 +144,10 @@ export const TimeInput = ({ defaultValue, onInput }) => {
maxlength={2} maxlength={2}
placeholder="DD" placeholder="DD"
value={ value={
localTime time
? localTime.day.toLocaleString("en-US", { ? time
minimumIntegerDigits: 2, .setZone(timezone)
}) .day.toLocaleString("en-US", { minimumIntegerDigits: 2 })
: "" : ""
} }
onBlur={(ev) => { onBlur={(ev) => {
@ -173,10 +173,10 @@ export const TimeInput = ({ defaultValue, onInput }) => {
maxlength={2} maxlength={2}
placeholder="hh" placeholder="hh"
value={ value={
localTime time
? localTime.hour.toLocaleString("en-US", { ? time
minimumIntegerDigits: 2, .setZone(timezone)
}) .hour.toLocaleString("en-US", { minimumIntegerDigits: 2 })
: "" : ""
} }
onBlur={(ev) => { onBlur={(ev) => {
@ -203,8 +203,8 @@ export const TimeInput = ({ defaultValue, onInput }) => {
maxlength={2} maxlength={2}
placeholder="mm" placeholder="mm"
value={ value={
localTime time
? localTime.minute.toLocaleString("en-US", { ? time.setZone(timezone).minute.toLocaleString("en-US", {
minimumIntegerDigits: 2, minimumIntegerDigits: 2,
}) })
: "" : ""
@ -233,8 +233,8 @@ export const TimeInput = ({ defaultValue, onInput }) => {
maxlength={2} maxlength={2}
placeholder="ss" placeholder="ss"
value={ value={
localTime time
? localTime.second.toLocaleString("en-US", { ? time.setZone(timezone).second.toLocaleString("en-US", {
minimumIntegerDigits: 2, minimumIntegerDigits: 2,
}) })
: "" : ""
@ -276,17 +276,15 @@ export const TimeInput = ({ defaultValue, onInput }) => {
type="datetime-local" type="datetime-local"
step="1" step="1"
value={ value={
localTime time
? localTime.toFormat("yyyy-LL-dd'T'HH:mm:ss") ? time.toFormat("yyyy-LL-dd'T'HH:mm:ss")
: DateTime.now().setZone(timezone).toFormat("yyyy-LL-dd'T'HH:mm:ss") : DateTime.now().toFormat("yyyy-LL-dd'T'HH:mm:ss")
} }
ref={ref} ref={ref}
onInput={(ev) => { onInput={(ev) => {
ev.currentTarget.value === "" ev.currentTarget.value === ""
? updateTime(null) ? updateTime(null)
: setLocalTime( : setTime(DateTime.fromISO(ev.currentTarget.value, { zone: "UTC" }));
DateTime.fromISO(ev.currentTarget.value, { zone: timezone }),
);
}} }}
></input> ></input>
</> </>

View File

@ -53,7 +53,7 @@ export const TimezonePicker = () => {
const TimezoneModal = ({ setModalOpen }) => { const TimezoneModal = ({ setModalOpen }) => {
const browserTimezone = DateTime.now().zoneName; const browserTimezone = DateTime.now().zoneName;
const [selectedZone] = useTimezone(); const [selectedZone, setSelectedZone] = useTimezone();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { isLoading, isError, data } = useQuery(fetchUserInfo()); const { isLoading, isError, data } = useQuery(fetchUserInfo());
@ -86,6 +86,36 @@ const TimezoneModal = ({ setModalOpen }) => {
</p> </p>
<br></br> <br></br>
<div class="has-text-centered"> <div class="has-text-centered">
<button
class="button is-success"
style={{
margin: "2px",
}}
id="set-browser-timezone"
onClick={() => {
setSelectedZone(browserTimezone);
}}
>
<span>Use Browser Timezone</span>{" "}
<span class="icon">
<i class="fab fa-firefox-browser"></i>
</span>
</button>
<button
class="button is-success"
id="set-bot-timezone"
style={{
margin: "2px",
}}
onClick={() => {
setSelectedZone(data.timezone);
}}
>
<span>Use Bot Timezone</span>{" "}
<span class="icon">
<i class="fab fa-discord"></i>
</span>
</button>
<button <button
class="button is-success is-outlined" class="button is-success is-outlined"
id="update-bot-timezone" id="update-bot-timezone"

View File

@ -11,7 +11,12 @@ enum Sort {
} }
export const UserReminders = () => { export const UserReminders = () => {
const { isSuccess, isFetching, isFetched, data: reminders } = useQuery(fetchUserReminders()); const {
isSuccess,
isFetching,
isFetched,
data: guildReminders,
} = useQuery(fetchUserReminders());
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
const [sort, setSort] = useState(Sort.Time); const [sort, setSort] = useState(Sort.Time);
@ -80,7 +85,7 @@ export const UserReminders = () => {
<div id={"guildReminders"} className={isFetching ? "loading" : ""}> <div id={"guildReminders"} className={isFetching ? "loading" : ""}>
{isSuccess && {isSuccess &&
reminders guildReminders
.sort((r1, r2) => { .sort((r1, r2) => {
if (sort === Sort.Time) { if (sort === Sort.Time) {
return r1.utc_time > r2.utc_time ? 1 : -1; return r1.utc_time > r2.utc_time ? 1 : -1;

View File

@ -20,7 +20,7 @@ pub async fn delete_macro(
SELECT m.id SELECT m.id
FROM command_macro m FROM command_macro m
INNER JOIN guilds INNER JOIN guilds
ON guilds.id = m.guild_id ON guilds.guild = m.guild_id
WHERE guild = ? WHERE guild = ?
AND m.name = ? AND m.name = ?
", ",

View File

@ -22,9 +22,9 @@ impl Recordable for Options {
CreateEmbed::new() CreateEmbed::new()
.title("Confirmations ephemeral") .title("Confirmations ephemeral")
.description(concat!( .description(concat!(
"Reminder and todo confirmations will be sent privately, and removed when ", "Reminder confirmations will be sent privately, and removed when your client",
"your client restarts." " restarts."
)) ))
.color(*THEME_COLOR), .color(*THEME_COLOR),
), ),
) )

View File

@ -22,8 +22,8 @@ impl Recordable for Options {
CreateEmbed::new() CreateEmbed::new()
.title("Confirmations public") .title("Confirmations public")
.description(concat!( .description(concat!(
"Reminder and todo confirmations will be sent as regular messages, and", "Reminder confirmations will be sent as regular messages, and won't be ",
" won't be removed automatically." "removed automatically."
)) ))
.color(*THEME_COLOR), .color(*THEME_COLOR),
), ),

View File

@ -1,4 +1,3 @@
use poise::CreateReply;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
@ -34,13 +33,7 @@ impl Recordable for Options {
.await .await
.unwrap(); .unwrap();
let ephemeral = ctx ctx.say("Item added to todo list").await?;
.guild_data()
.await
.map_or(false, |gr| gr.map_or(false, |g| g.ephemeral_confirmations));
ctx.send(CreateReply::default().content("Item added to todo list").ephemeral(ephemeral))
.await?;
Ok(()) Ok(())
} }

View File

@ -1,8 +1,6 @@
use poise::CreateReply;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
models::CtxData,
utils::{Extract, Recordable}, utils::{Extract, Recordable},
Context, Error, Context, Error,
}; };
@ -28,13 +26,7 @@ impl Recordable for Options {
.await .await
.unwrap(); .unwrap();
let ephemeral = ctx ctx.say("Item added to todo list").await?;
.guild_data()
.await
.map_or(false, |gr| gr.map_or(false, |g| g.ephemeral_confirmations));
ctx.send(CreateReply::default().content("Item added to todo list").ephemeral(ephemeral))
.await?;
Ok(()) Ok(())
} }

View File

@ -1,8 +1,6 @@
use poise::CreateReply;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
models::CtxData,
utils::{Extract, Recordable}, utils::{Extract, Recordable},
Context, Error, Context, Error,
}; };
@ -29,13 +27,7 @@ impl Recordable for Options {
.await .await
.unwrap(); .unwrap();
let ephemeral = ctx ctx.say("Item added to todo list").await?;
.guild_data()
.await
.map_or(false, |gr| gr.map_or(false, |g| g.ephemeral_confirmations));
ctx.send(CreateReply::default().content("Item added to todo list").ephemeral(ephemeral))
.await?;
Ok(()) Ok(())
} }

View File

@ -282,52 +282,21 @@ impl ComponentDataModel {
.await .await
.unwrap(); .unwrap();
let values = if let Some(uid) = selector.user_id { let values = sqlx::query!(
sqlx::query!( // fucking braindead mysql use <=> instead of = for null comparison
" "
SELECT todos.id, value FROM todos SELECT id, value FROM todos WHERE user_id <=> ? AND channel_id <=> ? AND guild_id <=> ?
INNER JOIN users ON todos.user_id = users.id
WHERE users.user = ?
", ",
uid, selector.user_id,
) selector.channel_id,
.fetch_all(&data.database) selector.guild_id,
.await )
.unwrap() .fetch_all(&data.database)
.iter() .await
.map(|row| (row.id as usize, row.value.clone())) .unwrap()
.collect::<Vec<(usize, String)>>() .iter()
} else if let Some(cid) = selector.channel_id { .map(|row| (row.id as usize, row.value.clone()))
sqlx::query!( .collect::<Vec<(usize, String)>>();
"
SELECT todos.id, value FROM todos
INNER JOIN channels ON todos.channel_id = channels.id
WHERE channels.channel = ?
",
cid,
)
.fetch_all(&data.database)
.await
.unwrap()
.iter()
.map(|row| (row.id as usize, row.value.clone()))
.collect::<Vec<(usize, String)>>()
} else {
sqlx::query!(
"
SELECT todos.id, value FROM todos
INNER JOIN guilds ON todos.guild_id = guilds.id
WHERE guilds.guild = ?
",
selector.guild_id,
)
.fetch_all(&data.database)
.await
.unwrap()
.iter()
.map(|row| (row.id as usize, row.value.clone()))
.collect::<Vec<(usize, String)>>()
};
let resp = show_todo_page( let resp = show_todo_page(
&values, &values,

View File

@ -13,6 +13,9 @@ pub async fn listener(
data: &Data, data: &Data,
) -> Result<(), Error> { ) -> Result<(), Error> {
match event { match event {
FullEvent::Ready { .. } => {
ctx.set_activity(Some(ActivityData::watching("for /remind")));
}
FullEvent::ChannelDelete { channel, .. } => { FullEvent::ChannelDelete { channel, .. } => {
sqlx::query!("DELETE FROM channels WHERE channel = ?", channel.id.get()) sqlx::query!("DELETE FROM channels WHERE channel = ?", channel.id.get())
.execute(&data.database) .execute(&data.database)
@ -55,11 +58,9 @@ To stay up to date on the latest features and fixes, join our [Discord](https://
} }
} }
FullEvent::GuildDelete { incomplete, .. } => { FullEvent::GuildDelete { incomplete, .. } => {
if !incomplete.unavailable { let _ = sqlx::query!("DELETE FROM guilds WHERE guild = ?", incomplete.id.get())
let _ = sqlx::query!("DELETE FROM guilds WHERE guild = ?", incomplete.id.get()) .execute(&data.database)
.execute(&data.database) .await;
.await;
}
} }
FullEvent::InteractionCreate { interaction } => { FullEvent::InteractionCreate { interaction } => {
if let Some(component) = interaction.clone().message_component() { if let Some(component) = interaction.clone().message_component() {

View File

@ -58,10 +58,6 @@ async fn macro_check(ctx: Context<'_>) -> bool {
async fn check_self_permissions(ctx: Context<'_>) -> bool { async fn check_self_permissions(ctx: Context<'_>) -> bool {
let user_id = ctx.serenity_context().cache.current_user().id; let user_id = ctx.serenity_context().cache.current_user().id;
let app_permissions = match ctx {
Context::Application(app_ctx) => app_ctx.interaction.app_permissions,
_ => None,
};
match ctx.guild().map(|g| g.to_owned()) { match ctx.guild().map(|g| g.to_owned()) {
Some(guild) => { Some(guild) => {
@ -70,34 +66,42 @@ async fn check_self_permissions(ctx: Context<'_>) -> bool {
.await .await
.map_or(false, |m| m.permissions(&ctx).map_or(false, |p| p.manage_webhooks())); .map_or(false, |m| m.permissions(&ctx).map_or(false, |p| p.manage_webhooks()));
if let Some(permissions) = app_permissions { let (view_channel, send_messages, embed_links) = ctx
return if permissions.send_messages() .channel_id()
&& permissions.embed_links() .to_channel(&ctx)
&& manage_webhooks .await
{ .ok()
true .and_then(|c| {
} else { if let Channel::Guild(channel) = c {
let _ = ctx let perms = channel.permissions_for_user(&ctx, user_id).ok()?;
.send(CreateReply::default().content(format!(
"The bot appears to be missing some permissions:
Some((perms.view_channel(), perms.send_messages(), perms.embed_links()))
} else {
None
}
})
.unwrap_or((false, false, false));
if manage_webhooks && send_messages && embed_links {
true
} else {
let _ = ctx
.send(CreateReply::default().content(format!(
"Please ensure the bot has the correct permissions:
{} **View Channel**
{} **Send Message** {} **Send Message**
{} **Embed Links** {} **Embed Links**
{} **Manage Webhooks** {} **Manage Webhooks**",
if view_channel { "" } else { "" },
if send_messages { "" } else { "" },
if embed_links { "" } else { "" },
if manage_webhooks { "" } else { "" },
)))
.await;
Please check the bot's roles, and any channel overrides. Alternatively, giving the bot false
\"Administrator\" will bypass permission checks",
if permissions.send_messages() { "" } else { "" },
if permissions.embed_links() { "" } else { "" },
if manage_webhooks { "" } else { "" },
)))
.await;
false
};
} }
manage_webhooks
} }
None => { None => {

View File

@ -39,7 +39,6 @@ use poise::serenity_prelude::{
}, },
ClientBuilder, ClientBuilder,
}; };
use serenity::all::ActivityData;
use sqlx::{MySql, Pool}; use sqlx::{MySql, Pool};
use tokio::sync::{broadcast, broadcast::Sender, RwLock}; use tokio::sync::{broadcast, broadcast::Sender, RwLock};
@ -213,6 +212,7 @@ async fn _main(tx: Sender<()>) -> Result<(), Box<dyn StdError + Send + Sync>> {
// Start metrics // Start metrics
init_metrics(); init_metrics();
tokio::spawn(async { metrics::serve().await });
let database = let database =
Pool::connect(&env::var("DATABASE_URL").expect("No database URL provided")).await.unwrap(); Pool::connect(&env::var("DATABASE_URL").expect("No database URL provided")).await.unwrap();
@ -284,10 +284,8 @@ async fn _main(tx: Sender<()>) -> Result<(), Box<dyn StdError + Send + Sync>> {
.options(options) .options(options)
.build(); .build();
let mut client = ClientBuilder::new(&discord_token, GatewayIntents::GUILDS) let mut client =
.framework(framework) ClientBuilder::new(&discord_token, GatewayIntents::GUILDS).framework(framework).await?;
.activity(ActivityData::watching("for /remind"))
.await?;
client.start_autosharded().await?; client.start_autosharded().await?;

View File

@ -1,4 +1,6 @@
use axum::{routing::get, Router};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use log::warn;
use prometheus::{IntCounterVec, Opts, Registry}; use prometheus::{IntCounterVec, Opts, Registry};
lazy_static! { lazy_static! {
@ -24,3 +26,21 @@ pub fn init_metrics() {
REGISTRY.register(Box::new(REMINDER_FAIL_COUNTER.clone())).unwrap(); REGISTRY.register(Box::new(REMINDER_FAIL_COUNTER.clone())).unwrap();
REGISTRY.register(Box::new(COMMAND_COUNTER.clone())).unwrap(); REGISTRY.register(Box::new(COMMAND_COUNTER.clone())).unwrap();
} }
pub async fn serve() {
let app = Router::new().route("/metrics", get(metrics));
let listener = tokio::net::TcpListener::bind("localhost:31756").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn metrics() -> String {
let encoder = prometheus::TextEncoder::new();
let res_custom = encoder.encode_to_string(&REGISTRY.gather());
res_custom.unwrap_or_else(|e| {
warn!("Error encoding metrics: {:?}", e);
String::new()
})
}

View File

@ -68,7 +68,7 @@ impl Data {
guild_id: GuildId, guild_id: GuildId,
) -> Result<Vec<CommandMacro>, Error> { ) -> Result<Vec<CommandMacro>, Error> {
let rows = sqlx::query!( let rows = sqlx::query!(
"SELECT name, description, commands FROM command_macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)", "SELECT name, description, commands FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)",
guild_id.get() guild_id.get()
) )
.fetch_all(&self.database) .fetch_all(&self.database)
@ -76,7 +76,7 @@ impl Data {
guild_id, guild_id,
name: row.name.clone(), name: row.name.clone(),
description: row.description.clone(), description: row.description.clone(),
commands: serde_json::from_str(&row.commands.to_string()).unwrap(), commands: serde_json::from_str(&row.commands).unwrap(),
}).collect(); }).collect();
Ok(rows) Ok(rows)

View File

@ -4,7 +4,6 @@ use std::env;
use log::{info, warn}; use log::{info, warn};
use poise::serenity_prelude::client::Context; use poise::serenity_prelude::client::Context;
use sd_notify::{self, NotifyState};
use sqlx::{Executor, MySql}; use sqlx::{Executor, MySql};
use tokio::{ use tokio::{
sync::broadcast::Receiver, sync::broadcast::Receiver,
@ -34,15 +33,6 @@ async fn _initialize(ctx: Context, pool: impl Executor<'_, Database = Database>
.flatten() .flatten()
.unwrap_or(10); .unwrap_or(10);
let mut watchdog_interval = 0;
let watchdog = sd_notify::watchdog_enabled(false, &mut watchdog_interval);
if watchdog {
warn!("Watchdog enabled. Don't die!");
} else {
warn!("No watchdog running")
}
loop { loop {
let sleep_to = Instant::now() + Duration::from_secs(remind_interval); let sleep_to = Instant::now() + Duration::from_secs(remind_interval);
let reminders = sender::Reminder::fetch_reminders(pool).await; let reminders = sender::Reminder::fetch_reminders(pool).await;
@ -52,11 +42,9 @@ async fn _initialize(ctx: Context, pool: impl Executor<'_, Database = Database>
for reminder in reminders { for reminder in reminders {
reminder.send(pool, ctx.clone()).await; reminder.send(pool, ctx.clone()).await;
let _ = sd_notify::notify(false, &[NotifyState::Watchdog]);
} }
} }
sleep_until(sleep_to).await; sleep_until(sleep_to).await;
let _ = sd_notify::notify(false, &[NotifyState::Watchdog]);
} }
} }

View File

@ -1 +0,0 @@
pub mod metrics;

View File

@ -2,10 +2,9 @@ mod consts;
#[macro_use] #[macro_use]
mod macros; mod macros;
mod catchers; mod catchers;
mod fairings;
mod guards; mod guards;
mod metrics;
mod routes; mod routes;
pub mod string { pub mod string {
use std::{fmt::Display, str::FromStr}; use std::{fmt::Display, str::FromStr};
@ -80,7 +79,7 @@ use sqlx::{MySql, Pool};
use crate::web::{ use crate::web::{
consts::{CNC_GUILD, DISCORD_OAUTH_AUTHORIZE, DISCORD_OAUTH_TOKEN, SUBSCRIPTION_ROLES}, consts::{CNC_GUILD, DISCORD_OAUTH_AUTHORIZE, DISCORD_OAUTH_TOKEN, SUBSCRIPTION_ROLES},
fairings::metrics::MetricProducer, metrics::MetricProducer,
}; };
type Database = MySql; type Database = MySql;
@ -150,7 +149,6 @@ pub async fn initialize(
routes::report::report_error, routes::report::report_error,
routes::return_to_same_site, routes::return_to_same_site,
routes::terms, routes::terms,
routes::metrics,
], ],
) )
.mount( .mount(
@ -179,8 +177,6 @@ pub async fn initialize(
.mount( .mount(
"/dashboard", "/dashboard",
routes![ routes![
routes::dashboard::reminders_redirect,
routes::dashboard::todos_redirect,
routes::dashboard::dashboard, routes::dashboard::dashboard,
routes::dashboard::dashboard_home, routes::dashboard::dashboard_home,
routes::dashboard::api::delete_reminder, routes::dashboard::api::delete_reminder,

View File

@ -697,18 +697,6 @@ pub enum DashboardPage {
NotConfigured(Template), NotConfigured(Template),
} }
// Legacy route to maintain compatibility with old dashboard routing
#[get("/?<id>")]
pub async fn reminders_redirect(id: &str) -> Redirect {
Redirect::to(format!("/dashboard/{}/reminders", id))
}
// Legacy route to maintain compatibility with old dashboard routing
#[get("/todo?<id>")]
pub async fn todos_redirect(id: &str) -> Redirect {
Redirect::to(format!("/dashboard/{}/todos", id))
}
#[get("/")] #[get("/")]
pub async fn dashboard_home(cookies: &CookieJar<'_>) -> DashboardPage { pub async fn dashboard_home(cookies: &CookieJar<'_>) -> DashboardPage {
if cookies.get_private("userid").is_some() { if cookies.get_private("userid").is_some() {

View File

@ -2,14 +2,11 @@ pub mod dashboard;
pub mod login; pub mod login;
pub mod report; pub mod report;
use std::{collections::HashMap, net::IpAddr}; use std::collections::HashMap;
use log::warn;
use rocket::{get, request::FlashMessage, serde::json::Value as JsonValue}; use rocket::{get, request::FlashMessage, serde::json::Value as JsonValue};
use rocket_dyn_templates::Template; use rocket_dyn_templates::Template;
use crate::metrics::REGISTRY;
pub type JsonResult = Result<JsonValue, JsonValue>; pub type JsonResult = Result<JsonValue, JsonValue>;
#[get("/")] #[get("/")]
@ -110,19 +107,3 @@ pub async fn help_iemanager() -> Template {
let map: HashMap<&str, String> = HashMap::new(); let map: HashMap<&str, String> = HashMap::new();
Template::render("support/iemanager", &map) Template::render("support/iemanager", &map)
} }
#[get("/metrics")]
pub async fn metrics(client_ip: IpAddr) -> String {
if !client_ip.is_loopback() {
String::new()
} else {
let encoder = prometheus::TextEncoder::new();
let res_custom = encoder.encode_to_string(&REGISTRY.gather());
res_custom.unwrap_or_else(|e| {
warn!("Error encoding metrics: {:?}", e);
String::new()
})
}
}

View File

@ -7,9 +7,8 @@ Type=simple
ExecStart=/usr/bin/reminder-rs ExecStart=/usr/bin/reminder-rs
WorkingDirectory=/etc/reminder-rs WorkingDirectory=/etc/reminder-rs
Restart=always Restart=always
RestartSec=10 RestartSec=4
Environment="RUST_LOG=warn,rocket=info,reminder_rs=debug,postman=debug" Environment="reminder_rs=warn,postman=warn"
WatchdogSec=120
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

View File

@ -4,7 +4,6 @@
<html lang="EN"> <html lang="EN">
<head> <head>
<meta name="description" content="The most powerful Discord Reminders Bot"> <meta name="description" content="The most powerful Discord Reminders Bot">
<meta name="keywords" content="discord,discord bot,reminders,reminders bot,discord reminders,discord automation,discord messages">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="yandex-verification" content="bb77b8681eb64a90" /> <meta name="yandex-verification" content="bb77b8681eb64a90" />