Compare commits
	
		
			24 Commits
		
	
	
		
			1.7.14
			...
			jude/fix-d
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					9a6b65f3a3 | ||
| 
						 | 
					b6ff149d51 | ||
| 
						 | 
					748e33566b | ||
| 
						 | 
					d7e90614c8 | ||
| 
						 | 
					b5dbfe336d | ||
| 
						 | 
					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]
 | 
					[package]
 | 
				
			||||||
name = "reminder-rs"
 | 
					name = "reminder-rs"
 | 
				
			||||||
version = "1.7.14"
 | 
					version = "1.7.27"
 | 
				
			||||||
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"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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;
 | 
				
			||||||
							
								
								
									
										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 }) => {
 | 
					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}>`,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,8 +1,7 @@
 | 
				
			|||||||
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 { fetchUserInfo } from "../../api";
 | 
					import { fetchGuildInfo, 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";
 | 
				
			||||||
@@ -11,11 +10,12 @@ 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) {
 | 
					    if (!userFetched || !guildFetched) {
 | 
				
			||||||
        return <></>;
 | 
					        return <></>;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -59,7 +59,13 @@ export const Settings = () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            <div class="collapses split-controls">
 | 
					            <div class="collapses split-controls">
 | 
				
			||||||
                <div>
 | 
					                <div>
 | 
				
			||||||
                    <div class={userInfo.patreon ? "patreon-only" : "patreon-only is-locked"}>
 | 
					                    <div
 | 
				
			||||||
 | 
					                        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{" "}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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>
 | 
				
			||||||
        </>
 | 
					        </>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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.guild = m.guild_id
 | 
					            ON guilds.id = m.guild_id
 | 
				
			||||||
        WHERE guild = ?
 | 
					        WHERE guild = ?
 | 
				
			||||||
            AND m.name = ?
 | 
					            AND m.name = ?
 | 
				
			||||||
        ",
 | 
					        ",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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(())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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(())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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(())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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)
 | 
				
			||||||
@@ -58,9 +55,11 @@ To stay up to date on the latest features and fixes, join our [Discord](https://
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        FullEvent::GuildDelete { incomplete, .. } => {
 | 
					        FullEvent::GuildDelete { incomplete, .. } => {
 | 
				
			||||||
            let _ = sqlx::query!("DELETE FROM guilds WHERE guild = ?", incomplete.id.get())
 | 
					            if !incomplete.unavailable {
 | 
				
			||||||
                .execute(&data.database)
 | 
					                let _ = sqlx::query!("DELETE FROM guilds WHERE guild = ?", incomplete.id.get())
 | 
				
			||||||
                .await;
 | 
					                    .execute(&data.database)
 | 
				
			||||||
 | 
					                    .await;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        FullEvent::InteractionCreate { interaction } => {
 | 
					        FullEvent::InteractionCreate { interaction } => {
 | 
				
			||||||
            if let Some(component) = interaction.clone().message_component() {
 | 
					            if let Some(component) = interaction.clone().message_component() {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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 {
 | 
					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 => {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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(®ISTRY.gather());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    res_custom.unwrap_or_else(|e| {
 | 
					 | 
				
			||||||
        warn!("Error encoding metrics: {:?}", e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        String::new()
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)",
 | 
					            "SELECT name, description, commands FROM command_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).unwrap(),
 | 
					            commands: serde_json::from_str(&row.commands.to_string()).unwrap(),
 | 
				
			||||||
        }).collect();
 | 
					        }).collect();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Ok(rows)
 | 
					        Ok(rows)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
									
								
							
							
						
						
									
										1
									
								
								src/web/fairings/mod.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					pub mod metrics;
 | 
				
			||||||
@@ -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,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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() {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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(®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
 | 
					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
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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" />
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user