Compare commits
	
		
			19 Commits
		
	
	
		
			1.7.14
			...
			218be2f0b1
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 218be2f0b1 | ||
|  | d7515f3611 | ||
|  | 6ae1096d79 | ||
|  | 1f0d7adae3 | ||
|  | fc96ae526f | ||
|  | 8881ef0f85 | ||
|  | 5e82a687f9 | ||
|  | de4ecf8dd6 | ||
|  | 064efd4386 | ||
|  | 65b8ba3b47 | ||
| 9d452ed8cb | |||
|  | 441419b92b | ||
|  | aecf2c15be | ||
|  | 79da56c794 | ||
|  | ef10902c1e | ||
|  | c277f85c2a | ||
|  | 035653c7fa | ||
|  | 6358bc3deb | ||
|  | 9f5066f982 | 
							
								
								
									
										675
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										675
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,6 +1,6 @@ | ||||
| [package] | ||||
| name = "reminder-rs" | ||||
| version = "1.7.14" | ||||
| version = "1.7.23" | ||||
| authors = ["Jude Southworth <judesouthworth@pm.me>"] | ||||
| edition = "2021" | ||||
| 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"] } | ||||
| oauth2 = "4" | ||||
| csv = "1.2" | ||||
| axum = "0.7" | ||||
| sd-notify = "0.4.1" | ||||
|  | ||||
| [dependencies.extract_derive] | ||||
| path = "extract_derive" | ||||
|   | ||||
| @@ -1,12 +1,22 @@ | ||||
| server { | ||||
|     server_name www.reminder-bot.com; | ||||
| 
 | ||||
|     return 301 $scheme://reminder-bot.com$request_uri; | ||||
|     return 301 https://reminder-bot.com$request_uri; | ||||
| } | ||||
| 
 | ||||
| server { | ||||
|     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; | ||||
| } | ||||
| @@ -25,6 +35,8 @@ server { | ||||
|     proxy_buffers 4 256k; | ||||
|     proxy_busy_buffers_size 256k; | ||||
| 
 | ||||
|     client_max_body_size 10M; | ||||
| 
 | ||||
|     location / { | ||||
|         proxy_pass http://localhost:18920; | ||||
|         proxy_redirect off; | ||||
							
								
								
									
										1321
									
								
								reminder-dashboard/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1321
									
								
								reminder-dashboard/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -7,6 +7,10 @@ import { useGuild } from "./useGuild"; | ||||
| export const Mentions = ({ input }) => { | ||||
|     const guild = useGuild(); | ||||
|  | ||||
|     return <>{guild && <_Mentions guild={guild} input={input} />}</>; | ||||
| }; | ||||
|  | ||||
| const _Mentions = ({ guild, input }) => { | ||||
|     const { data: roles } = useQuery(fetchGuildRoles(guild)); | ||||
|     const { data: channels } = useQuery(fetchGuildChannels(guild)); | ||||
|     const { data: emojis } = useQuery(fetchGuildEmojis(guild)); | ||||
| @@ -17,7 +21,7 @@ export const Mentions = ({ input }) => { | ||||
|                 { | ||||
|                     trigger: "@", | ||||
|                     values: (roles || []) | ||||
|                         .filter((role) => role.name === "@everyone") | ||||
|                         .filter((role) => role.name !== "@everyone") | ||||
|                         .map(({ id, name }) => ({ key: name, value: id })), | ||||
|                     allowSpaces: true, | ||||
|                     selectTemplate: (item) => `<@&${item.original.value}>`, | ||||
|   | ||||
| @@ -1,15 +1,15 @@ | ||||
| import {useState} from "preact/hooks"; | ||||
| import {fetchGuildChannels, Reminder} from "../../api"; | ||||
| import {DateTime} from "luxon"; | ||||
| import {CreateButtonRow} from "./ButtonRow/CreateButtonRow"; | ||||
| import {TopBar} from "./TopBar"; | ||||
| import {Message} from "./Message"; | ||||
| import {Settings} from "./Settings"; | ||||
| import {ReminderContext} from "./ReminderContext"; | ||||
| import {useQuery} from "react-query"; | ||||
| import { useState } from "preact/hooks"; | ||||
| import { fetchGuildChannels, Reminder } from "../../api"; | ||||
| import { DateTime } from "luxon"; | ||||
| import { CreateButtonRow } from "./ButtonRow/CreateButtonRow"; | ||||
| import { TopBar } from "./TopBar"; | ||||
| import { Message } from "./Message"; | ||||
| import { Settings } from "./Settings"; | ||||
| import { ReminderContext } from "./ReminderContext"; | ||||
| import { useQuery } from "react-query"; | ||||
| import "./styles.scss"; | ||||
| import {useGuild} from "../App/useGuild"; | ||||
| import {DEFAULT_COLOR} from "./Embed"; | ||||
| import { useGuild } from "../App/useGuild"; | ||||
| import { DEFAULT_COLOR } from "./Embed"; | ||||
|  | ||||
| function defaultReminder(): Reminder { | ||||
|     return { | ||||
| @@ -45,10 +45,41 @@ function defaultReminder(): Reminder { | ||||
| export const CreateReminder = () => { | ||||
|     const guild = useGuild(); | ||||
|  | ||||
|     if (guild) { | ||||
|         return <_Guild guild={guild} />; | ||||
|     } else { | ||||
|         return <_User />; | ||||
|     } | ||||
| }; | ||||
|  | ||||
| const _User = () => { | ||||
|     const [reminder, setReminder] = useState(defaultReminder()); | ||||
|     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) { | ||||
|         setReminder((reminder) => ({ | ||||
| @@ -67,10 +98,10 @@ export const CreateReminder = () => { | ||||
|                     }} | ||||
|                 /> | ||||
|                 <div class="columns reminder-settings"> | ||||
|                     <Message/> | ||||
|                     <Settings/> | ||||
|                     <Message /> | ||||
|                     <Settings /> | ||||
|                 </div> | ||||
|                 <CreateButtonRow/> | ||||
|                 <CreateButtonRow /> | ||||
|             </div> | ||||
|         </ReminderContext.Provider> | ||||
|     ); | ||||
|   | ||||
| @@ -16,28 +16,28 @@ export const TimeInput = ({ defaultValue, onInput }) => { | ||||
|     const ref = useRef(null); | ||||
|  | ||||
|     const [timezone] = useTimezone(); | ||||
|     const [time, setTime] = useState( | ||||
|         defaultValue ? DateTime.fromISO(defaultValue, { zone: "UTC" }) : null, | ||||
|     const [localTime, setLocalTime] = useState( | ||||
|         defaultValue ? DateTime.fromISO(defaultValue, { zone: "UTC" }).setZone(timezone) : null, | ||||
|     ); | ||||
|  | ||||
|     const updateTime = useCallback( | ||||
|         (upd: TimeUpdate) => { | ||||
|             if (upd === null) { | ||||
|                 setTime(null); | ||||
|                 setLocalTime(null); | ||||
|             } | ||||
|  | ||||
|             let newTime = time; | ||||
|             let newTime = localTime; | ||||
|             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(() => { | ||||
|         onInput(time?.setZone("UTC").toFormat("yyyy-LL-dd'T'HH:mm:ss")); | ||||
|     }, [time]); | ||||
|         onInput(localTime?.setZone("UTC").toFormat("yyyy-LL-dd'T'HH:mm:ss")); | ||||
|     }, [localTime]); | ||||
|  | ||||
|     const flash = useFlash(); | ||||
|  | ||||
| @@ -51,14 +51,14 @@ export const TimeInput = ({ defaultValue, onInput }) => { | ||||
|                     let dt = DateTime.fromISO(pasteValue, { zone: timezone }); | ||||
|  | ||||
|                     if (dt.isValid) { | ||||
|                         setTime(dt); | ||||
|                         setLocalTime(dt); | ||||
|                         return; | ||||
|                     } | ||||
|  | ||||
|                     dt = DateTime.fromSQL(pasteValue); | ||||
|  | ||||
|                     if (dt.isValid) { | ||||
|                         setTime(dt); | ||||
|                         setLocalTime(dt); | ||||
|                         return; | ||||
|                     } | ||||
|  | ||||
| @@ -83,8 +83,8 @@ export const TimeInput = ({ defaultValue, onInput }) => { | ||||
|                             maxlength={4} | ||||
|                             placeholder="YYYY" | ||||
|                             value={ | ||||
|                                 time | ||||
|                                     ? time.setZone(timezone).year.toLocaleString("en-US", { | ||||
|                                 localTime | ||||
|                                     ? localTime.year.toLocaleString("en-US", { | ||||
|                                           minimumIntegerDigits: 4, | ||||
|                                           useGrouping: false, | ||||
|                                       }) | ||||
| @@ -114,8 +114,8 @@ export const TimeInput = ({ defaultValue, onInput }) => { | ||||
|                             maxlength={2} | ||||
|                             placeholder="MM" | ||||
|                             value={ | ||||
|                                 time | ||||
|                                     ? time.setZone(timezone).month.toLocaleString("en-US", { | ||||
|                                 localTime | ||||
|                                     ? localTime.month.toLocaleString("en-US", { | ||||
|                                           minimumIntegerDigits: 2, | ||||
|                                       }) | ||||
|                                     : "" | ||||
| @@ -144,10 +144,10 @@ export const TimeInput = ({ defaultValue, onInput }) => { | ||||
|                             maxlength={2} | ||||
|                             placeholder="DD" | ||||
|                             value={ | ||||
|                                 time | ||||
|                                     ? time | ||||
|                                           .setZone(timezone) | ||||
|                                           .day.toLocaleString("en-US", { minimumIntegerDigits: 2 }) | ||||
|                                 localTime | ||||
|                                     ? localTime.day.toLocaleString("en-US", { | ||||
|                                           minimumIntegerDigits: 2, | ||||
|                                       }) | ||||
|                                     : "" | ||||
|                             } | ||||
|                             onBlur={(ev) => { | ||||
| @@ -173,10 +173,10 @@ export const TimeInput = ({ defaultValue, onInput }) => { | ||||
|                             maxlength={2} | ||||
|                             placeholder="hh" | ||||
|                             value={ | ||||
|                                 time | ||||
|                                     ? time | ||||
|                                           .setZone(timezone) | ||||
|                                           .hour.toLocaleString("en-US", { minimumIntegerDigits: 2 }) | ||||
|                                 localTime | ||||
|                                     ? localTime.hour.toLocaleString("en-US", { | ||||
|                                           minimumIntegerDigits: 2, | ||||
|                                       }) | ||||
|                                     : "" | ||||
|                             } | ||||
|                             onBlur={(ev) => { | ||||
| @@ -203,8 +203,8 @@ export const TimeInput = ({ defaultValue, onInput }) => { | ||||
|                             maxlength={2} | ||||
|                             placeholder="mm" | ||||
|                             value={ | ||||
|                                 time | ||||
|                                     ? time.setZone(timezone).minute.toLocaleString("en-US", { | ||||
|                                 localTime | ||||
|                                     ? localTime.minute.toLocaleString("en-US", { | ||||
|                                           minimumIntegerDigits: 2, | ||||
|                                       }) | ||||
|                                     : "" | ||||
| @@ -233,8 +233,8 @@ export const TimeInput = ({ defaultValue, onInput }) => { | ||||
|                             maxlength={2} | ||||
|                             placeholder="ss" | ||||
|                             value={ | ||||
|                                 time | ||||
|                                     ? time.setZone(timezone).second.toLocaleString("en-US", { | ||||
|                                 localTime | ||||
|                                     ? localTime.second.toLocaleString("en-US", { | ||||
|                                           minimumIntegerDigits: 2, | ||||
|                                       }) | ||||
|                                     : "" | ||||
| @@ -276,15 +276,17 @@ export const TimeInput = ({ defaultValue, onInput }) => { | ||||
|                 type="datetime-local" | ||||
|                 step="1" | ||||
|                 value={ | ||||
|                     time | ||||
|                         ? time.toFormat("yyyy-LL-dd'T'HH:mm:ss") | ||||
|                         : DateTime.now().toFormat("yyyy-LL-dd'T'HH:mm:ss") | ||||
|                     localTime | ||||
|                         ? localTime.toFormat("yyyy-LL-dd'T'HH:mm:ss") | ||||
|                         : DateTime.now().setZone(timezone).toFormat("yyyy-LL-dd'T'HH:mm:ss") | ||||
|                 } | ||||
|                 ref={ref} | ||||
|                 onInput={(ev) => { | ||||
|                     ev.currentTarget.value === "" | ||||
|                         ? updateTime(null) | ||||
|                         : setTime(DateTime.fromISO(ev.currentTarget.value, { zone: "UTC" })); | ||||
|                         : setLocalTime( | ||||
|                               DateTime.fromISO(ev.currentTarget.value, { zone: timezone }), | ||||
|                           ); | ||||
|                 }} | ||||
|             ></input> | ||||
|         </> | ||||
|   | ||||
| @@ -53,7 +53,7 @@ export const TimezonePicker = () => { | ||||
|  | ||||
| const TimezoneModal = ({ setModalOpen }) => { | ||||
|     const browserTimezone = DateTime.now().zoneName; | ||||
|     const [selectedZone, setSelectedZone] = useTimezone(); | ||||
|     const [selectedZone] = useTimezone(); | ||||
|  | ||||
|     const queryClient = useQueryClient(); | ||||
|     const { isLoading, isError, data } = useQuery(fetchUserInfo()); | ||||
| @@ -86,36 +86,6 @@ const TimezoneModal = ({ setModalOpen }) => { | ||||
|             </p> | ||||
|             <br></br> | ||||
|             <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 | ||||
|                     class="button is-success is-outlined" | ||||
|                     id="update-bot-timezone" | ||||
|   | ||||
| @@ -11,12 +11,7 @@ enum Sort { | ||||
| } | ||||
|  | ||||
| export const UserReminders = () => { | ||||
|     const { | ||||
|         isSuccess, | ||||
|         isFetching, | ||||
|         isFetched, | ||||
|         data: guildReminders, | ||||
|     } = useQuery(fetchUserReminders()); | ||||
|     const { isSuccess, isFetching, isFetched, data: reminders } = useQuery(fetchUserReminders()); | ||||
|  | ||||
|     const [collapsed, setCollapsed] = useState(false); | ||||
|     const [sort, setSort] = useState(Sort.Time); | ||||
| @@ -85,7 +80,7 @@ export const UserReminders = () => { | ||||
|  | ||||
|                 <div id={"guildReminders"} className={isFetching ? "loading" : ""}> | ||||
|                     {isSuccess && | ||||
|                         guildReminders | ||||
|                         reminders | ||||
|                             .sort((r1, r2) => { | ||||
|                                 if (sort === Sort.Time) { | ||||
|                                     return r1.utc_time > r2.utc_time ? 1 : -1; | ||||
|   | ||||
| @@ -22,9 +22,9 @@ impl Recordable for Options { | ||||
|                 CreateEmbed::new() | ||||
|                     .title("Confirmations ephemeral") | ||||
|                     .description(concat!( | ||||
|                     "Reminder confirmations will be sent privately, and removed when your client", | ||||
|                     " restarts." | ||||
|                 )) | ||||
|                         "Reminder and todo confirmations will be sent privately, and removed when ", | ||||
|                         "your client restarts." | ||||
|                     )) | ||||
|                     .color(*THEME_COLOR), | ||||
|             ), | ||||
|         ) | ||||
|   | ||||
| @@ -22,8 +22,8 @@ impl Recordable for Options { | ||||
|                 CreateEmbed::new() | ||||
|                     .title("Confirmations public") | ||||
|                     .description(concat!( | ||||
|                         "Reminder confirmations will be sent as regular messages, and won't be ", | ||||
|                         "removed automatically." | ||||
|                         "Reminder and todo confirmations will be sent as regular messages, and", | ||||
|                         " won't be removed automatically." | ||||
|                     )) | ||||
|                     .color(*THEME_COLOR), | ||||
|             ), | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| use poise::CreateReply; | ||||
| use serde::{Deserialize, Serialize}; | ||||
|  | ||||
| use crate::{ | ||||
| @@ -33,7 +34,13 @@ impl Recordable for Options { | ||||
|         .await | ||||
|         .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(()) | ||||
|     } | ||||
|   | ||||
| @@ -1,6 +1,8 @@ | ||||
| use poise::CreateReply; | ||||
| use serde::{Deserialize, Serialize}; | ||||
|  | ||||
| use crate::{ | ||||
|     models::CtxData, | ||||
|     utils::{Extract, Recordable}, | ||||
|     Context, Error, | ||||
| }; | ||||
| @@ -26,7 +28,13 @@ impl Recordable for Options { | ||||
|         .await | ||||
|         .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(()) | ||||
|     } | ||||
|   | ||||
| @@ -1,6 +1,8 @@ | ||||
| use poise::CreateReply; | ||||
| use serde::{Deserialize, Serialize}; | ||||
|  | ||||
| use crate::{ | ||||
|     models::CtxData, | ||||
|     utils::{Extract, Recordable}, | ||||
|     Context, Error, | ||||
| }; | ||||
| @@ -27,7 +29,13 @@ impl Recordable for Options { | ||||
|         .await | ||||
|         .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(()) | ||||
|     } | ||||
|   | ||||
| @@ -282,21 +282,52 @@ impl ComponentDataModel { | ||||
|                         .await | ||||
|                         .unwrap(); | ||||
|  | ||||
|                         let values = sqlx::query!( | ||||
|                             // fucking braindead mysql use <=> instead of = for null comparison | ||||
|                             " | ||||
|                             SELECT id, value FROM todos WHERE user_id <=> ? AND channel_id <=> ? AND guild_id <=> ? | ||||
|                         let values = if let Some(uid) = selector.user_id { | ||||
|                             sqlx::query!( | ||||
|                                 " | ||||
|                             SELECT todos.id, value FROM todos | ||||
|                             INNER JOIN users ON todos.user_id = users.id | ||||
|                             WHERE users.user = ? | ||||
|                             ", | ||||
|                             selector.user_id, | ||||
|                             selector.channel_id, | ||||
|                             selector.guild_id, | ||||
|                         ) | ||||
|                         .fetch_all(&data.database) | ||||
|                         .await | ||||
|                         .unwrap() | ||||
|                         .iter() | ||||
|                         .map(|row| (row.id as usize, row.value.clone())) | ||||
|                         .collect::<Vec<(usize, String)>>(); | ||||
|                                 uid, | ||||
|                             ) | ||||
|                             .fetch_all(&data.database) | ||||
|                             .await | ||||
|                             .unwrap() | ||||
|                             .iter() | ||||
|                             .map(|row| (row.id as usize, row.value.clone())) | ||||
|                             .collect::<Vec<(usize, String)>>() | ||||
|                         } else if let Some(cid) = selector.channel_id { | ||||
|                             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( | ||||
|                             &values, | ||||
|   | ||||
							
								
								
									
										58
									
								
								src/hooks.rs
									
									
									
									
									
								
							
							
						
						
									
										58
									
								
								src/hooks.rs
									
									
									
									
									
								
							| @@ -58,6 +58,10 @@ async fn macro_check(ctx: Context<'_>) -> bool { | ||||
|  | ||||
| async fn check_self_permissions(ctx: Context<'_>) -> bool { | ||||
|     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()) { | ||||
|         Some(guild) => { | ||||
| @@ -66,42 +70,34 @@ async fn check_self_permissions(ctx: Context<'_>) -> bool { | ||||
|                 .await | ||||
|                 .map_or(false, |m| m.permissions(&ctx).map_or(false, |p| p.manage_webhooks())); | ||||
|  | ||||
|             let (view_channel, send_messages, embed_links) = ctx | ||||
|                 .channel_id() | ||||
|                 .to_channel(&ctx) | ||||
|                 .await | ||||
|                 .ok() | ||||
|                 .and_then(|c| { | ||||
|                     if let Channel::Guild(channel) = c { | ||||
|                         let perms = channel.permissions_for_user(&ctx, user_id).ok()?; | ||||
|             if let Some(permissions) = app_permissions { | ||||
|                 return if permissions.send_messages() | ||||
|                     && permissions.embed_links() | ||||
|                     && manage_webhooks | ||||
|                 { | ||||
|                     true | ||||
|                 } else { | ||||
|                     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** | ||||
| {}     **Embed Links** | ||||
| {}     **Manage Webhooks**", | ||||
|                         if view_channel { "✅" } else { "❌" }, | ||||
|                         if send_messages { "✅" } else { "❌" }, | ||||
|                         if embed_links { "✅" } else { "❌" }, | ||||
|                         if manage_webhooks { "✅" } else { "❌" }, | ||||
|                     ))) | ||||
|                     .await; | ||||
| {}     **Manage Webhooks** | ||||
|  | ||||
|                 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 => { | ||||
|   | ||||
| @@ -212,7 +212,6 @@ async fn _main(tx: Sender<()>) -> Result<(), Box<dyn StdError + Send + Sync>> { | ||||
|  | ||||
|     // Start metrics | ||||
|     init_metrics(); | ||||
|     tokio::spawn(async { metrics::serve().await }); | ||||
|  | ||||
|     let database = | ||||
|         Pool::connect(&env::var("DATABASE_URL").expect("No database URL provided")).await.unwrap(); | ||||
|   | ||||
| @@ -1,6 +1,4 @@ | ||||
| use axum::{routing::get, Router}; | ||||
| use lazy_static::lazy_static; | ||||
| use log::warn; | ||||
| use prometheus::{IntCounterVec, Opts, Registry}; | ||||
|  | ||||
| lazy_static! { | ||||
| @@ -26,21 +24,3 @@ pub fn init_metrics() { | ||||
|     REGISTRY.register(Box::new(REMINDER_FAIL_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(®ISTRY.gather()); | ||||
|  | ||||
|     res_custom.unwrap_or_else(|e| { | ||||
|         warn!("Error encoding metrics: {:?}", e); | ||||
|  | ||||
|         String::new() | ||||
|     }) | ||||
| } | ||||
|   | ||||
| @@ -4,6 +4,7 @@ use std::env; | ||||
|  | ||||
| use log::{info, warn}; | ||||
| use poise::serenity_prelude::client::Context; | ||||
| use sd_notify::{self, NotifyState}; | ||||
| use sqlx::{Executor, MySql}; | ||||
| use tokio::{ | ||||
|     sync::broadcast::Receiver, | ||||
| @@ -33,6 +34,15 @@ async fn _initialize(ctx: Context, pool: impl Executor<'_, Database = Database> | ||||
|         .flatten() | ||||
|         .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 { | ||||
|         let sleep_to = Instant::now() + Duration::from_secs(remind_interval); | ||||
|         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 { | ||||
|                 reminder.send(pool, ctx.clone()).await; | ||||
|                 let _ = sd_notify::notify(false, &[NotifyState::Watchdog]); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         sleep_until(sleep_to).await; | ||||
|         let _ = sd_notify::notify(false, &[NotifyState::Watchdog]); | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										1
									
								
								src/web/fairings/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/web/fairings/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| pub mod metrics; | ||||
| @@ -2,9 +2,10 @@ mod consts; | ||||
| #[macro_use] | ||||
| mod macros; | ||||
| mod catchers; | ||||
| mod fairings; | ||||
| mod guards; | ||||
| mod metrics; | ||||
| mod routes; | ||||
|  | ||||
| pub mod string { | ||||
|     use std::{fmt::Display, str::FromStr}; | ||||
|  | ||||
| @@ -79,7 +80,7 @@ use sqlx::{MySql, Pool}; | ||||
|  | ||||
| use crate::web::{ | ||||
|     consts::{CNC_GUILD, DISCORD_OAUTH_AUTHORIZE, DISCORD_OAUTH_TOKEN, SUBSCRIPTION_ROLES}, | ||||
|     metrics::MetricProducer, | ||||
|     fairings::metrics::MetricProducer, | ||||
| }; | ||||
|  | ||||
| type Database = MySql; | ||||
| @@ -149,6 +150,7 @@ pub async fn initialize( | ||||
|                 routes::report::report_error, | ||||
|                 routes::return_to_same_site, | ||||
|                 routes::terms, | ||||
|                 routes::metrics, | ||||
|             ], | ||||
|         ) | ||||
|         .mount( | ||||
| @@ -177,6 +179,8 @@ pub async fn initialize( | ||||
|         .mount( | ||||
|             "/dashboard", | ||||
|             routes![ | ||||
|                 routes::dashboard::reminders_redirect, | ||||
|                 routes::dashboard::todos_redirect, | ||||
|                 routes::dashboard::dashboard, | ||||
|                 routes::dashboard::dashboard_home, | ||||
|                 routes::dashboard::api::delete_reminder, | ||||
|   | ||||
| @@ -697,6 +697,18 @@ pub enum DashboardPage { | ||||
|     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("/")] | ||||
| pub async fn dashboard_home(cookies: &CookieJar<'_>) -> DashboardPage { | ||||
|     if cookies.get_private("userid").is_some() { | ||||
|   | ||||
| @@ -2,11 +2,14 @@ pub mod dashboard; | ||||
| pub mod login; | ||||
| 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_dyn_templates::Template; | ||||
|  | ||||
| use crate::metrics::REGISTRY; | ||||
|  | ||||
| pub type JsonResult = Result<JsonValue, JsonValue>; | ||||
|  | ||||
| #[get("/")] | ||||
| @@ -107,3 +110,19 @@ pub async fn help_iemanager() -> Template { | ||||
|     let map: HashMap<&str, String> = HashMap::new(); | ||||
|     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(®ISTRY.gather()); | ||||
|  | ||||
|         res_custom.unwrap_or_else(|e| { | ||||
|             warn!("Error encoding metrics: {:?}", e); | ||||
|  | ||||
|             String::new() | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -7,8 +7,9 @@ Type=simple | ||||
| ExecStart=/usr/bin/reminder-rs | ||||
| WorkingDirectory=/etc/reminder-rs | ||||
| Restart=always | ||||
| RestartSec=4 | ||||
| Environment="reminder_rs=warn,postman=warn" | ||||
| RestartSec=10 | ||||
| Environment="RUST_LOG=warn,rocket=info,reminder_rs=debug,postman=debug" | ||||
| WatchdogSec=120 | ||||
|  | ||||
| [Install] | ||||
| WantedBy=multi-user.target | ||||
|   | ||||
| @@ -4,6 +4,7 @@ | ||||
| <html lang="EN"> | ||||
| <head> | ||||
|     <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 charset="UTF-8"> | ||||
|     <meta name="yandex-verification" content="bb77b8681eb64a90" /> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user