21 Commits

Author SHA1 Message Date
d7e90614c8 Bump ver 2024-07-07 16:35:32 +01:00
b5dbfe336d Don't set activity in ready event 2024-07-07 16:31:23 +01:00
218be2f0b1 Bump ver 2024-06-18 19:32:47 +01:00
d7515f3611 Don't require View Channel permission 2024-06-18 19:28:53 +01:00
6ae1096d79 Bump ver 2024-06-12 17:44:55 +01:00
1f0d7adae3 Correct service file 2024-06-12 17:21:42 +01:00
fc96ae526f Default permission checks to true 2024-06-10 18:30:55 +01:00
8881ef0f85 Fix DM reminders trying to load guild data 2024-06-06 16:56:19 +01:00
5e82a687f9 Increase watchdog 2024-06-04 22:34:58 +01:00
de4ecf8dd6 QoL
* Made todo added responses ephemeral if /settings ephemeral is on
* Enabled systemd watchdog
* Move metrics to rocket
2024-06-04 18:40:49 +01:00
064efd4386 Bump version 2024-06-04 16:48:26 +01:00
65b8ba3b47 Redirect old dashboard routes to new routes 2024-06-04 16:42:42 +01:00
9d452ed8cb Fix role selector 2024-05-10 17:37:27 +01:00
441419b92b Bump ver 2024-05-04 13:00:30 +01:00
aecf2c15be Store times as local time not UTC 2024-05-04 10:24:20 +01:00
79da56c794 Bump ver 2024-05-03 16:26:52 +01:00
ef10902c1e Fix todo list deletion not working properly 2024-05-03 16:21:27 +01:00
c277f85c2a Bump dependencies 2024-05-03 16:07:34 +01:00
035653c7fa Bump ver 2024-04-29 08:57:47 +01:00
6358bc3deb Partially revert timezone change 2024-04-29 08:49:01 +01:00
9f5066f982 Bump ver 2024-04-29 08:46:36 +01:00
27 changed files with 1315 additions and 1120 deletions

675
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.14" version = "1.7.24"
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"
axum = "0.7" sd-notify = "0.4.1"
[dependencies.extract_derive] [dependencies.extract_derive]
path = "extract_derive" path = "extract_derive"

View File

@ -1,12 +1,22 @@
server { server {
server_name www.reminder-bot.com; server_name www.reminder-bot.com;
return 301 $scheme://reminder-bot.com$request_uri; return 301 https://reminder-bot.com$request_uri;
} }
server { server {
listen 80; listen 80;
server_name reminder-bot.com; server_name beta.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;
} }
@ -25,6 +35,8 @@ 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,6 +7,10 @@ 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));
@ -17,7 +21,7 @@ export const Mentions = ({ 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,10 +45,41 @@ 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);
const {isSuccess, data: guildChannels} = useQuery(fetchGuildChannels(guild)); return (
<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) => ({
@ -67,10 +98,10 @@ export const CreateReminder = () => {
}} }}
/> />
<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

@ -16,28 +16,28 @@ export const TimeInput = ({ defaultValue, onInput }) => {
const ref = useRef(null); const ref = useRef(null);
const [timezone] = useTimezone(); const [timezone] = useTimezone();
const [time, setTime] = useState( const [localTime, setLocalTime] = useState(
defaultValue ? DateTime.fromISO(defaultValue, { zone: "UTC" }) : null, defaultValue ? DateTime.fromISO(defaultValue, { zone: "UTC" }).setZone(timezone) : null,
); );
const updateTime = useCallback( const updateTime = useCallback(
(upd: TimeUpdate) => { (upd: TimeUpdate) => {
if (upd === null) { if (upd === null) {
setTime(null); setLocalTime(null);
} }
let newTime = time; let newTime = localTime;
if (newTime === null) { if (newTime === null) {
newTime = DateTime.now().setZone("UTC"); newTime = DateTime.now().setZone(timezone);
} }
setTime(newTime.setZone(timezone).set(upd).setZone("UTC")); setLocalTime(newTime.set(upd));
}, },
[time, timezone], [localTime, timezone],
); );
useEffect(() => { useEffect(() => {
onInput(time?.setZone("UTC").toFormat("yyyy-LL-dd'T'HH:mm:ss")); onInput(localTime?.setZone("UTC").toFormat("yyyy-LL-dd'T'HH:mm:ss"));
}, [time]); }, [localTime]);
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) {
setTime(dt); setLocalTime(dt);
return; return;
} }
dt = DateTime.fromSQL(pasteValue); dt = DateTime.fromSQL(pasteValue);
if (dt.isValid) { if (dt.isValid) {
setTime(dt); setLocalTime(dt);
return; return;
} }
@ -83,8 +83,8 @@ export const TimeInput = ({ defaultValue, onInput }) => {
maxlength={4} maxlength={4}
placeholder="YYYY" placeholder="YYYY"
value={ value={
time localTime
? time.setZone(timezone).year.toLocaleString("en-US", { ? localTime.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={
time localTime
? time.setZone(timezone).month.toLocaleString("en-US", { ? localTime.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={
time localTime
? time ? localTime.day.toLocaleString("en-US", {
.setZone(timezone) minimumIntegerDigits: 2,
.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={
time localTime
? time ? localTime.hour.toLocaleString("en-US", {
.setZone(timezone) minimumIntegerDigits: 2,
.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={
time localTime
? time.setZone(timezone).minute.toLocaleString("en-US", { ? localTime.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={
time localTime
? time.setZone(timezone).second.toLocaleString("en-US", { ? localTime.second.toLocaleString("en-US", {
minimumIntegerDigits: 2, minimumIntegerDigits: 2,
}) })
: "" : ""
@ -276,15 +276,17 @@ export const TimeInput = ({ defaultValue, onInput }) => {
type="datetime-local" type="datetime-local"
step="1" step="1"
value={ value={
time localTime
? time.toFormat("yyyy-LL-dd'T'HH:mm:ss") ? localTime.toFormat("yyyy-LL-dd'T'HH:mm:ss")
: DateTime.now().toFormat("yyyy-LL-dd'T'HH:mm:ss") : DateTime.now().setZone(timezone).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)
: setTime(DateTime.fromISO(ev.currentTarget.value, { zone: "UTC" })); : setLocalTime(
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, setSelectedZone] = useTimezone(); const [selectedZone] = useTimezone();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { isLoading, isError, data } = useQuery(fetchUserInfo()); const { isLoading, isError, data } = useQuery(fetchUserInfo());
@ -86,36 +86,6 @@ 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,12 +11,7 @@ enum Sort {
} }
export const UserReminders = () => { export const UserReminders = () => {
const { const { isSuccess, isFetching, isFetched, data: reminders } = useQuery(fetchUserReminders());
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);
@ -85,7 +80,7 @@ export const UserReminders = () => {
<div id={"guildReminders"} className={isFetching ? "loading" : ""}> <div id={"guildReminders"} className={isFetching ? "loading" : ""}>
{isSuccess && {isSuccess &&
guildReminders reminders
.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

@ -22,9 +22,9 @@ impl Recordable for Options {
CreateEmbed::new() CreateEmbed::new()
.title("Confirmations ephemeral") .title("Confirmations ephemeral")
.description(concat!( .description(concat!(
"Reminder confirmations will be sent privately, and removed when your client", "Reminder and todo confirmations will be sent privately, and removed when ",
" restarts." "your client 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 confirmations will be sent as regular messages, and won't be ", "Reminder and todo confirmations will be sent as regular messages, and",
"removed automatically." " won't be removed automatically."
)) ))
.color(*THEME_COLOR), .color(*THEME_COLOR),
), ),

View File

@ -1,3 +1,4 @@
use poise::CreateReply;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
@ -33,7 +34,13 @@ impl Recordable for Options {
.await .await
.unwrap(); .unwrap();
ctx.say("Item added to todo list").await?; let ephemeral = ctx
.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,6 +1,8 @@
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,
}; };
@ -26,7 +28,13 @@ impl Recordable for Options {
.await .await
.unwrap(); .unwrap();
ctx.say("Item added to todo list").await?; let ephemeral = ctx
.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,6 +1,8 @@
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,
}; };
@ -27,7 +29,13 @@ impl Recordable for Options {
.await .await
.unwrap(); .unwrap();
ctx.say("Item added to todo list").await?; let ephemeral = ctx
.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,21 +282,52 @@ impl ComponentDataModel {
.await .await
.unwrap(); .unwrap();
let values = sqlx::query!( let values = if let Some(uid) = selector.user_id {
// fucking braindead mysql use <=> instead of = for null comparison sqlx::query!(
" "
SELECT id, value FROM todos WHERE user_id <=> ? AND channel_id <=> ? AND guild_id <=> ? SELECT todos.id, value FROM todos
INNER JOIN users ON todos.user_id = users.id
WHERE users.user = ?
", ",
selector.user_id, uid,
selector.channel_id, )
selector.guild_id, .fetch_all(&data.database)
) .await
.fetch_all(&data.database) .unwrap()
.await .iter()
.unwrap() .map(|row| (row.id as usize, row.value.clone()))
.iter() .collect::<Vec<(usize, String)>>()
.map(|row| (row.id as usize, row.value.clone())) } else if let Some(cid) = selector.channel_id {
.collect::<Vec<(usize, String)>>(); sqlx::query!(
"
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,9 +13,6 @@ 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)

View File

@ -58,6 +58,10 @@ 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) => {
@ -66,42 +70,34 @@ 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()));
let (view_channel, send_messages, embed_links) = ctx if let Some(permissions) = app_permissions {
.channel_id() return if permissions.send_messages()
.to_channel(&ctx) && permissions.embed_links()
.await && manage_webhooks
.ok() {
.and_then(|c| { true
if let Channel::Guild(channel) = c { } else {
let perms = channel.permissions_for_user(&ctx, user_id).ok()?; let _ = ctx
.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;
false Please check the bot's roles, and any channel overrides. Alternatively, giving the bot
\"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,6 +39,7 @@ 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};
@ -212,7 +213,6 @@ 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,8 +284,10 @@ async fn _main(tx: Sender<()>) -> Result<(), Box<dyn StdError + Send + Sync>> {
.options(options) .options(options)
.build(); .build();
let mut client = let mut client = ClientBuilder::new(&discord_token, GatewayIntents::GUILDS)
ClientBuilder::new(&discord_token, GatewayIntents::GUILDS).framework(framework).await?; .framework(framework)
.activity(ActivityData::watching("for /remind"))
.await?;
client.start_autosharded().await?; client.start_autosharded().await?;

View File

@ -1,6 +1,4 @@
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! {
@ -26,21 +24,3 @@ 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

@ -4,6 +4,7 @@ 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,
@ -33,6 +34,15 @@ 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;
@ -42,9 +52,11 @@ 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]);
} }
} }

1
src/web/fairings/mod.rs Normal file
View File

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

View File

@ -2,9 +2,10 @@ 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};
@ -79,7 +80,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},
metrics::MetricProducer, fairings::metrics::MetricProducer,
}; };
type Database = MySql; type Database = MySql;
@ -149,6 +150,7 @@ 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(
@ -177,6 +179,8 @@ 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,6 +697,18 @@ 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,11 +2,14 @@ pub mod dashboard;
pub mod login; pub mod login;
pub mod report; pub mod report;
use std::collections::HashMap; use std::{collections::HashMap, net::IpAddr};
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("/")]
@ -107,3 +110,19 @@ 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,8 +7,9 @@ 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=4 RestartSec=10
Environment="reminder_rs=warn,postman=warn" Environment="RUST_LOG=warn,rocket=info,reminder_rs=debug,postman=debug"
WatchdogSec=120
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

View File

@ -4,6 +4,7 @@
<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" />