Compare commits

..
Author SHA1 Message Date
jude 4c98265657 Performance improvements 2025-12-03 21:55:35 +00:00
42 changed files with 361 additions and 688 deletions

No files matched your search

-1
View File
@@ -28,4 +28,3 @@ dist-ssr
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
.junie
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\n UPDATE channels\n SET\n name = ?,\n nudge = ?,\n blacklisted = ?,\n webhook_id = ?,\n webhook_token = ?,\n paused = ?,\n paused_until = ?\n WHERE id = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 8
},
"nullable": []
},
"hash": "1bd6f70d57d5e913f97025cf6fc5be795a1302485ef1c07fe13eb246f184a1d1"
}
@@ -1,6 +1,6 @@
{ {
"db_name": "MySQL", "db_name": "MySQL",
"query": "\n SELECT id, channel, nudge, webhook_id, webhook_token, paused,\n paused_until\n FROM channels\n WHERE channel = ?\n ", "query": "\n SELECT id, channel, name, nudge, blacklisted, webhook_id, webhook_token, paused, paused_until\n FROM channels\n WHERE channel = ?\n ",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -23,6 +23,15 @@
}, },
{ {
"ordinal": 2, "ordinal": 2,
"name": "name",
"type_info": {
"type": "VarString",
"flags": "",
"max_size": 400
}
},
{
"ordinal": 3,
"name": "nudge", "name": "nudge",
"type_info": { "type_info": {
"type": "Short", "type": "Short",
@@ -31,7 +40,16 @@
} }
}, },
{ {
"ordinal": 3, "ordinal": 4,
"name": "blacklisted",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"max_size": 1
}
},
{
"ordinal": 5,
"name": "webhook_id", "name": "webhook_id",
"type_info": { "type_info": {
"type": "LongLong", "type": "LongLong",
@@ -40,7 +58,7 @@
} }
}, },
{ {
"ordinal": 4, "ordinal": 6,
"name": "webhook_token", "name": "webhook_token",
"type_info": { "type_info": {
"type": "Blob", "type": "Blob",
@@ -49,7 +67,7 @@
} }
}, },
{ {
"ordinal": 5, "ordinal": 7,
"name": "paused", "name": "paused",
"type_info": { "type_info": {
"type": "Tiny", "type": "Tiny",
@@ -58,7 +76,7 @@
} }
}, },
{ {
"ordinal": 6, "ordinal": 8,
"name": "paused_until", "name": "paused_until",
"type_info": { "type_info": {
"type": "Timestamp", "type": "Timestamp",
@@ -73,6 +91,8 @@
"nullable": [ "nullable": [
false, false,
false, false,
true,
false,
false, false,
true, true,
true, true,
@@ -80,5 +100,5 @@
true true
] ]
}, },
"hash": "017b3f73101926a3fe029ed75c412ec902aa74fee334c23fd75fe0cde1e324ca" "hash": "4d26e92088234552f9159b876c7fdd3bb557a7f918820e289bd39742de553883"
} }
@@ -1,12 +0,0 @@
{
"db_name": "MySQL",
"query": "INSERT IGNORE INTO channels (channel, guild_id) VALUES (?, (SELECT id FROM guilds WHERE guild = ?))",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "5d7a787e3a6f487c3fe00bd39d308c9350c82e58b0ad1aae0b92824375786f02"
}
@@ -1,12 +0,0 @@
{
"db_name": "MySQL",
"query": "\n UPDATE channels\n SET\n nudge = ?,\n webhook_id = ?,\n webhook_token = ?,\n paused = ?,\n paused_until = ?\n WHERE id = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 6
},
"nullable": []
},
"hash": "af08f3524c316f438feb62a24c67b96197e3655a87efe128914ed386be20d900"
}
@@ -1,6 +1,6 @@
{ {
"db_name": "MySQL", "db_name": "MySQL",
"query": "\n SELECT id, channel, nudge, webhook_id, webhook_token, paused, paused_until\n FROM channels\n WHERE channel = ?\n ", "query": "\n SELECT id, channel, name, nudge, blacklisted, webhook_id, webhook_token, paused,\n paused_until\n FROM channels\n WHERE channel = ?\n ",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -23,6 +23,15 @@
}, },
{ {
"ordinal": 2, "ordinal": 2,
"name": "name",
"type_info": {
"type": "VarString",
"flags": "",
"max_size": 400
}
},
{
"ordinal": 3,
"name": "nudge", "name": "nudge",
"type_info": { "type_info": {
"type": "Short", "type": "Short",
@@ -31,7 +40,16 @@
} }
}, },
{ {
"ordinal": 3, "ordinal": 4,
"name": "blacklisted",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"max_size": 1
}
},
{
"ordinal": 5,
"name": "webhook_id", "name": "webhook_id",
"type_info": { "type_info": {
"type": "LongLong", "type": "LongLong",
@@ -40,7 +58,7 @@
} }
}, },
{ {
"ordinal": 4, "ordinal": 6,
"name": "webhook_token", "name": "webhook_token",
"type_info": { "type_info": {
"type": "Blob", "type": "Blob",
@@ -49,7 +67,7 @@
} }
}, },
{ {
"ordinal": 5, "ordinal": 7,
"name": "paused", "name": "paused",
"type_info": { "type_info": {
"type": "Tiny", "type": "Tiny",
@@ -58,7 +76,7 @@
} }
}, },
{ {
"ordinal": 6, "ordinal": 8,
"name": "paused_until", "name": "paused_until",
"type_info": { "type_info": {
"type": "Timestamp", "type": "Timestamp",
@@ -73,6 +91,8 @@
"nullable": [ "nullable": [
false, false,
false, false,
true,
false,
false, false,
true, true,
true, true,
@@ -80,5 +100,5 @@
true true
] ]
}, },
"hash": "21b660ecbeffcac31ef283f59e420360a28b344759407024a11dc4a04da9341d" "hash": "d3e41b7c9df5823c418181f46aa78999159afbd458dcd05ffb9e032182ec493b"
} }
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "INSERT IGNORE INTO channels (channel, name, guild_id) VALUES (?, ?, (SELECT id FROM guilds WHERE guild = ?))",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "f8e99a39fb87d47bcdf70f38e00c4dbe4d6832b1b073efddfa79a8cd756d8dc6"
}
Generated
+1 -1
View File
@@ -2623,7 +2623,7 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]] [[package]]
name = "reminder-rs" name = "reminder-rs"
version = "1.7.45" version = "1.7.41"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"chrono", "chrono",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "reminder-rs" name = "reminder-rs"
version = "1.7.45" version = "1.7.41"
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"
+3
View File
@@ -25,6 +25,9 @@ COPY ./Cargo.lock ./
COPY ./Cargo.toml ./ COPY ./Cargo.toml ./
COPY ./dp.py ./ COPY ./dp.py ./
# Build dashboard assets explicitly to ensure dist exists
RUN npm ci --prefix reminder-dashboard && npm run build --prefix reminder-dashboard
# Build and install the Rust binary # Build and install the Rust binary
RUN cargo install --path . RUN cargo install --path .
-22
View File
@@ -1,22 +0,0 @@
.DEFAULT_GOAL := build
CARGO ?= cargo
PODMAN ?= podman
IMAGE ?= reminder-rs
.PHONY: build prepare test image
# Keep the stages ordered even when make is run with -j.
build: image
$(PODMAN) run --rm -v "$(CURDIR):/mnt" -w /mnt \
-e SQLX_OFFLINE=true "$(IMAGE)" cargo deb
image: test
$(PODMAN) build -t "$(IMAGE)" .
test: prepare
SQLX_OFFLINE=true $(CARGO) test --locked
# Include test-only queries in the metadata used by offline compilation.
prepare:
SQLX_OFFLINE=false $(CARGO) sqlx prepare -- --all-targets --locked
+6 -18
View File
@@ -18,26 +18,14 @@ Recommended method.
By default, this builds targeting Ubuntu 20.04. Modify the Containerfile if you wish to target a By default, this builds targeting Ubuntu 20.04. Modify the Containerfile if you wish to target a
different platform. These instructions are written using `podman`, but `docker` should work too. different platform. These instructions are written using `podman`, but `docker` should work too.
1. Install container software and Make: `sudo apt install podman make`. 1. Install container software: `sudo apt install podman`.
2. Install database server: `sudo apt install mysql-server-8.0`. Create a database called 2. Install database server: `sudo apt install mysql-server-8.0`. Create a database called
`reminders` `reminders`
3. Install SQLx CLI matching the project's SQLx version: `cargo install sqlx-cli --version 0.8.2 --locked`. 3. Install SQLx CLI: `cargo install sqlx-cli`
4. Install the local build requirements (Rust, Node.js/npm, GCC, CMake, pkg-config, and OpenSSL 4. From the source code directory, execute `sqlx migrate run`
development headers), then install dashboard dependencies: `npm ci --prefix reminder-dashboard`. 5. Build container image: `podman build -t reminder-rs .`
5. Export the URL of your build/test database: 6. Build with podman:
`export DATABASE_URL="mysql://user[:password]@localhost/reminders"`. `podman run --rm --network=host -v "$PWD":/mnt -w /mnt -e "DATABASE_URL=mysql://user@localhost/reminders" reminder-rs cargo deb`
6. From the source code directory, execute `sqlx migrate run`.
7. Run `make` (or `make build`). This prepares SQLx query metadata in `.sqlx`, runs the Rust tests,
builds the `reminder-rs` container image, and runs `cargo deb` inside it. The resulting package is
written to `target/debian/`.
Preparation and database-dependent tests use the local database. The container packaging step uses
the prepared `.sqlx` metadata with `SQLX_OFFLINE=true`, so it does not need database access, including
when a `.env` file is present in the mounted source directory.
The stages can also be run separately: `make prepare`, `make test` (includes preparation), and
`make image` (includes preparation and tests). Override `IMAGE` to change the container image tag,
or `PODMAN` to use another container command, for example `make PODMAN=docker`.
### Compiling for other target ### Compiling for other target
+3 -10
View File
@@ -8,26 +8,19 @@ fn main() {
.arg("run") .arg("run")
.arg("build") .arg("build")
.current_dir(Path::new("reminder-dashboard")) .current_dir(Path::new("reminder-dashboard"))
.env("VITE_VERSION", env!("CARGO_PKG_VERSION"))
.spawn() .spawn()
.expect("Failed to build NPM") .expect("Failed to build NPM");
.wait()
.expect("Failed to wait for NPM build");
Command::new("cp") Command::new("cp")
.arg("reminder-dashboard/dist/index.html") .arg("reminder-dashboard/dist/index.html")
.arg("static/index.html") .arg("static/index.html")
.spawn() .spawn()
.expect("Failed to copy index.html") .expect("Failed to copy index.html");
.wait()
.expect("Failed to wait for index.html copy");
Command::new("cp") Command::new("cp")
.arg("-r") .arg("-r")
.arg("reminder-dashboard/dist/static/assets") .arg("reminder-dashboard/dist/static/assets")
.arg("static/") .arg("static/")
.spawn() .spawn()
.expect("Failed to copy assets") .expect("Failed to copy assets");
.wait()
.expect("Failed to wait for assets copy");
} }
@@ -1,13 +0,0 @@
SET foreign_key_checks = 0;
-- Drop all old tables
DROP TABLE IF EXISTS users_old;
DROP TABLE IF EXISTS messages;
DROP TABLE IF EXISTS embeds;
DROP TABLE IF EXISTS embed_fields;
DROP TABLE IF EXISTS command_aliases;
DROP TABLE IF EXISTS macro;
DROP TABLE IF EXISTS roles;
DROP TABLE IF EXISTS command_restrictions;
SET foreign_key_checks = 1;
+1 -2
View File
@@ -5,8 +5,7 @@
"scripts": { "scripts": {
"dev": "vite build --watch --mode development", "dev": "vite build --watch --mode development",
"build": "vite build", "build": "vite build",
"preview": "vite preview", "preview": "vite preview"
"prettier": "prettier -w src/"
}, },
"dependencies": { "dependencies": {
"axios": "^1.5.1", "axios": "^1.5.1",
+3 -3
View File
@@ -6,7 +6,7 @@ enum ColorScheme {
Light = "light", Light = "light",
} }
type UserInfo = { export type UserInfo = {
name: string; name: string;
patreon: boolean; patreon: boolean;
preferences: { preferences: {
@@ -116,7 +116,7 @@ type Template = {
embed_fields: EmbedField[] | null; embed_fields: EmbedField[] | null;
}; };
const USER_INFO_STALE_TIME = 86_400_000; const USER_INFO_STALE_TIME = 120_000;
const GUILD_INFO_STALE_TIME = 300_000; const GUILD_INFO_STALE_TIME = 300_000;
const OTHER_STALE_TIME = 120_000; const OTHER_STALE_TIME = 120_000;
@@ -236,7 +236,7 @@ export const fetchGuildTodos = (guild: string) => ({
}); });
export const patchGuildTodo = (guild: string) => ({ export const patchGuildTodo = (guild: string) => ({
mutationFn: ({id, todo}) => axios.patch(`/dashboard/api/guild/${guild}/todos/${id}`, todo), mutationFn: ({ id, todo }) => axios.patch(`/dashboard/api/guild/${guild}/todos/${id}`, todo),
}); });
export const postGuildTodo = (guild: string) => ({ export const postGuildTodo = (guild: string) => ({
@@ -1,43 +0,0 @@
import { createContext } from "preact";
import { useContext } from "preact/compat";
import { useEffect, useState } from "preact/hooks";
import { fetchUserInfo } from "../../api";
import { useQuery } from "react-query";
type TColorScheme = "light" | "dark";
type TColorSchemeContext = {
colorScheme: TColorScheme;
};
const ColorSchemeContext = createContext({ colorScheme: "light" } as TColorSchemeContext);
export const ColorSchemeProvider = ({ children }) => {
const { data } = useQuery({ ...fetchUserInfo() });
const [activeScheme, setActiveScheme] = useState<TColorScheme>("light");
useEffect(() => {
const preference = data?.preferences?.dashboard_color_scheme || "system";
if (preference === "system") {
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
const handleChange = () => {
setActiveScheme(mediaQuery.matches ? "dark" : "light");
};
handleChange();
mediaQuery.addEventListener("change", handleChange);
return () => mediaQuery.removeEventListener("change", handleChange);
} else {
setActiveScheme(preference as TColorScheme);
}
}, [data]);
return (
<ColorSchemeContext.Provider value={{ colorScheme: activeScheme }}>
{children}
</ColorSchemeContext.Provider>
);
};
export const useColorScheme = () => useContext(ColorSchemeContext);
+52 -59
View File
@@ -1,66 +1,59 @@
import {Sidebar} from "../Sidebar"; import { Sidebar } from "../Sidebar";
import {QueryClient, QueryClientProvider} from "react-query"; import { QueryClient, QueryClientProvider } from "react-query";
import {Route, Router, Switch} from "wouter"; import { Route, Router, Switch } from "wouter";
import {Welcome} from "../Welcome"; import { Welcome } from "../Welcome";
import {Guild} from "../Guild"; import { Guild } from "../Guild";
import {FlashProvider} from "./FlashProvider"; import { FlashProvider } from "./FlashProvider";
import {TimezoneProvider} from "./TimezoneProvider"; import { TimezoneProvider } from "./TimezoneProvider";
import {User} from "../User"; import { User } from "../User";
import {GuildReminders} from "../Guild/GuildReminders"; import { GuildReminders } from "../Guild/GuildReminders";
import {GuildTodos} from "../Guild/GuildTodos"; import { GuildTodos } from "../Guild/GuildTodos";
import {ColorSchemeProvider, useColorScheme} from "./ColorSchemeProvider";
import {useEffect} from "preact/hooks";
const InnerApp = () => {
const {colorScheme} = useColorScheme();
useEffect(() => {
const body = document.querySelector("body");
body.className = body.className.replace(/scheme-\w+/g, "");
body.classList.add(`scheme-${colorScheme}`);
}, [colorScheme]);
return (
<Router base={"/dashboard"}>
<div class={`columns is-gapless dashboard-frame scheme-${colorScheme}`}>
<Sidebar/>
<div class="column is-main-content">
<div style={{margin: "0 12px 12px 12px"}}>
<Switch>
<Route path={"/@me/reminders"} component={User}></Route>
<Route path={"/:guild/reminders"}>
<Guild>
<GuildReminders/>
</Guild>
</Route>
<Route path={"/:guild/todos"}>
<Guild>
<GuildTodos/>
</Guild>
</Route>
<Route>
<Welcome/>
</Route>
</Switch>
</div>
</div>
</div>
</Router>
);
};
const queryClient = new QueryClient();
export function App() { export function App() {
const queryClient = new QueryClient();
let scheme = "light";
// if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
// scheme = "dark";
// }
return ( return (
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<ColorSchemeProvider> <TimezoneProvider>
<TimezoneProvider> <FlashProvider>
<FlashProvider> <Router base={"/dashboard"}>
<InnerApp/> <div class={`columns is-gapless dashboard-frame scheme-${scheme}`}>
</FlashProvider> <Sidebar />
</TimezoneProvider> <div class="column is-main-content">
</ColorSchemeProvider> <div style={{ margin: "0 12px 12px 12px" }}>
<Switch>
<Route path={"/@me/reminders"} component={User}></Route>
<Route
path={"/:guild/reminders"}
component={() => (
<Guild>
<GuildReminders />
</Guild>
)}
></Route>
<Route
path={"/:guild/todos"}
component={() => (
<Guild>
<GuildTodos />
</Guild>
)}
></Route>
<Route>
<Welcome />
</Route>
</Switch>
</div>
</div>
</div>
</Router>
</FlashProvider>
</TimezoneProvider>
</QueryClientProvider> </QueryClientProvider>
); );
} }
@@ -1,8 +1,8 @@
import { useQuery, useQueryClient } from "react-query"; import { useQuery, useQueryClient } from "react-query";
import { fetchGuildChannels, fetchGuildReminders } from "../../api"; import { fetchGuildChannels, fetchGuildInfo, fetchGuildReminders, fetchUserInfo } from "../../api";
import { EditReminder } from "../Reminder/EditReminder"; import { EditReminder } from "../Reminder/EditReminder";
import { CreateReminder } from "../Reminder/CreateReminder"; import { CreateReminder } from "../Reminder/CreateReminder";
import { useCallback, useState } from "preact/hooks"; import { useCallback, useMemo, useState } from "preact/hooks";
import { Loader } from "../Loader"; import { Loader } from "../Loader";
import { useGuild } from "../App/useGuild"; import { useGuild } from "../App/useGuild";
@@ -22,6 +22,11 @@ export const GuildReminders = () => {
data: guildReminders, data: guildReminders,
} = useQuery(fetchGuildReminders(guild)); } = useQuery(fetchGuildReminders(guild));
const { data: channels } = useQuery(fetchGuildChannels(guild)); const { data: channels } = useQuery(fetchGuildChannels(guild));
const channelNames = useMemo(() => {
return new Map(channels.map((ch) => [ch.id, ch.name]));
}, [channels]);
const { isSuccess: guildFetched, data: guildInfo } = useQuery({ ...fetchGuildInfo(guild) });
const { isSuccess: userFetched, data: userInfo } = useQuery({ ...fetchUserInfo() });
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
const [sort, _setSort] = useState(Sort.Time); const [sort, _setSort] = useState(Sort.Time);
@@ -34,9 +39,49 @@ export const GuildReminders = () => {
_setSort(sort); _setSort(sort);
}, []); }, []);
const sorted = useMemo(
() =>
[...guildReminders]
.sort((r1, r2) => {
if (sort === Sort.Time) {
return r1.utc_time > r2.utc_time ? 1 : -1;
} else if (sort === Sort.Name) {
return r1.name > r2.name ? 1 : -1;
} else {
return r1.channel > r2.channel ? 1 : -1;
}
})
.map((reminder) => {
let breaker = <></>;
if (sort === Sort.Channel && channels) {
if (prevReminder === null || prevReminder.channel !== reminder.channel) {
breaker = (
<div class={"channel-tag"}>#{channelNames[reminder.channel]}</div>
);
}
}
prevReminder = reminder;
return (
<>
{breaker}
<EditReminder
key={reminder.uid}
reminder={reminder}
guildInfo={guildInfo}
userInfo={userInfo}
globalCollapse={collapsed}
/>
</>
);
}),
[guildReminders, sort, channelNames, collapsed],
);
return ( return (
<> <>
{!isFetched && <Loader />} {!(userFetched && guildFetched && isFetched) && <Loader />}
<strong>Create Reminder</strong> <strong>Create Reminder</strong>
<div id={"reminderCreator"}> <div id={"reminderCreator"}>
@@ -100,44 +145,7 @@ export const GuildReminders = () => {
</div> </div>
<div id={"guildReminders"} className={isFetching ? "loading" : ""}> <div id={"guildReminders"} className={isFetching ? "loading" : ""}>
{isSuccess && {isSuccess && sorted}
guildReminders
.sort((r1, r2) => {
if (sort === Sort.Time) {
return r1.utc_time > r2.utc_time ? 1 : -1;
} else if (sort === Sort.Name) {
return r1.name > r2.name ? 1 : -1;
} else {
return r1.channel > r2.channel ? 1 : -1;
}
})
.map((reminder) => {
let breaker = <></>;
if (sort === Sort.Channel && channels) {
if (
prevReminder === null ||
prevReminder.channel !== reminder.channel
) {
const channel = channels.find(
(ch) => ch.id === reminder.channel,
);
breaker = <div class={"channel-tag"}>#{channel.name}</div>;
}
}
prevReminder = reminder;
return (
<>
{breaker}
<EditReminder
key={reminder.uid}
reminder={reminder}
globalCollapse={collapsed}
/>
</>
);
})}
</div> </div>
</> </>
); );
@@ -2,4 +2,4 @@
> * { > * {
margin: 2px; margin: 2px;
} }
} }
@@ -1,5 +1,5 @@
import { JSX } from "preact"; import {JSX} from "preact";
import { createPortal } from "preact/compat"; import {createPortal} from "preact/compat";
type Props = { type Props = {
setModalOpen: (open: boolean) => never; setModalOpen: (open: boolean) => never;
@@ -9,7 +9,7 @@ type Props = {
children: string | JSX.Element | JSX.Element[] | (() => JSX.Element); children: string | JSX.Element | JSX.Element[] | (() => JSX.Element);
}; };
export const Modal = ({ setModalOpen, title, onSubmit, onSubmitText, children }: Props) => { export const Modal = ({setModalOpen, title, onSubmit, onSubmitText, children}: Props) => {
const body = document.querySelector("body"); const body = document.querySelector("body");
return createPortal( return createPortal(
@@ -1,5 +1,5 @@
import { useRef, useState } from "preact/hooks"; import { useRef, useState } from "preact/hooks";
import { useMutation, useQueryClient } from "react-query"; import { useMutation } from "react-query";
import { patchGuildReminder, patchUserReminder } from "../../../api"; import { patchGuildReminder, patchUserReminder } from "../../../api";
import { ICON_FLASH_TIME } from "../../../consts"; import { ICON_FLASH_TIME } from "../../../consts";
import { DeleteButton } from "./DeleteButton"; import { DeleteButton } from "./DeleteButton";
@@ -1,4 +1,4 @@
import { Reminder } from "../../api"; import { Reminder, UserInfo, GuildInfo } from "../../api";
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import { EditButtonRow } from "./ButtonRow/EditButtonRow"; import { EditButtonRow } from "./ButtonRow/EditButtonRow";
import { Message } from "./Message"; import { Message } from "./Message";
@@ -10,9 +10,16 @@ import "./styles.scss";
type Props = { type Props = {
reminder: Reminder; reminder: Reminder;
globalCollapse: boolean; globalCollapse: boolean;
userInfo: UserInfo;
guildInfo: GuildInfo;
}; };
export const EditReminder = ({ reminder: initialReminder, globalCollapse }: Props) => { export const EditReminder = ({
reminder: initialReminder,
globalCollapse,
userInfo,
guildInfo,
}: Props) => {
const [propReminder, setPropReminder] = useState(initialReminder); const [propReminder, setPropReminder] = useState(initialReminder);
const [reminder, setReminder] = useState(initialReminder); const [reminder, setReminder] = useState(initialReminder);
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
@@ -39,11 +46,15 @@ export const EditReminder = ({ reminder: initialReminder, globalCollapse }: Prop
setCollapsed(!collapsed); setCollapsed(!collapsed);
}} }}
/> />
<div class="columns reminder-settings"> {!collapsed && (
<Message /> <>
<Settings /> <div class="columns reminder-settings">
</div> <Message />
<EditButtonRow /> <Settings userInfo={userInfo} guildInfo={guildInfo} />
</div>
<EditButtonRow />
</>
)}
</div> </div>
</ReminderContext.Provider> </ReminderContext.Provider>
); );
@@ -1,24 +1,21 @@
import { ChannelSelector } from "./ChannelSelector"; import { ChannelSelector } from "./ChannelSelector";
import { IntervalSelector } from "./IntervalSelector"; import { IntervalSelector } from "./IntervalSelector";
import { useQuery } from "react-query";
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";
import { TimeInput } from "./TimeInput"; import { TimeInput } from "./TimeInput";
import { useGuild } from "../App/useGuild"; import { useGuild } from "../App/useGuild";
import { GuildInfo, UserInfo } from "../../api";
export const Settings = () => { type Props = {
userInfo: UserInfo;
guildInfo: GuildInfo;
};
export const Settings = ({ guildInfo, userInfo }: Props) => {
const guild = useGuild(); const guild = useGuild();
const { isSuccess: userFetched, data: userInfo } = useQuery({ ...fetchUserInfo() });
const { isSuccess: guildFetched, data: guildInfo } = useQuery({ ...fetchGuildInfo(guild) });
const [reminder, setReminder] = useReminder(); const [reminder, setReminder] = useReminder();
if (!userFetched || !guildFetched) {
return <></>;
}
return ( return (
<div class="column settings"> <div class="column settings">
{guild && ( {guild && (
@@ -11,7 +11,7 @@
border-radius: 8px; border-radius: 8px;
margin: 4px; margin: 4px;
padding: 4px; padding: 4px;
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.75); box-shadow: 0 0 5px 0 rgba(0,0,0,0.75);
.highlight { .highlight {
background-color: #35373c; background-color: #35373c;
@@ -66,7 +66,7 @@ aside.menu {
padding-left: 4px; padding-left: 4px;
} }
@media screen and (max-width: 1023px), print { @media screen and (max-width: 1023px),print {
.columns:not(.is-desktop) { .columns:not(.is-desktop) {
flex-direction: column; flex-direction: column;
} }
@@ -5,12 +5,6 @@ import { useRef, useState } from "preact/hooks";
import { ICON_FLASH_TIME } from "../../consts"; import { ICON_FLASH_TIME } from "../../consts";
import { useFlash } from "../App/FlashContext"; import { useFlash } from "../App/FlashContext";
enum ColorScheme {
System = "system",
Dark = "dark",
Light = "light",
}
export const UserPreferences = () => { export const UserPreferences = () => {
const [modalOpen, setModalOpen] = useState(false); const [modalOpen, setModalOpen] = useState(false);
@@ -102,40 +96,6 @@ const PreferencesModal = ({ setModalOpen }) => {
</label> </label>
</div> </div>
<br></br> <br></br>
<div style={{ display: "flex", flexDirection: "row", alignContent: "center" }}>
<label>
<div class={"is-inline-block"} style={{ marginRight: "6px" }}>
Dashboard Color Scheme:
</div>
<div class={"control"}>
<div class={"is-inline-block select"}>
{isLoading && <i class={"fa fa-spinner"} />}
{isSuccess && (
<select
class={"channel-selector"}
value={
updatedSettings.dashboard_color_scheme === undefined
? data.preferences.dashboard_color_scheme
: updatedSettings.dashboard_color_scheme
}
onChange={(e) =>
setUpdatedSettings((s) => ({
...s,
dashboard_color_scheme: (e.target as HTMLSelectElement)
.value as ColorScheme,
}))
}
>
<option value={ColorScheme.System}>System default</option>
<option value={ColorScheme.Light}>Light</option>
<option value={ColorScheme.Dark}>Dark</option>
</select>
)}
</div>
</div>
</label>
</div>
<br></br>
<div class="has-text-centered"> <div class="has-text-centered">
<button <button
class="button is-success is-outlined" class="button is-success is-outlined"
@@ -14,9 +14,6 @@ export const Welcome = () => (
<p> <p>
<strong>Please report bugs!</strong> I can't fix issues if I am unaware of them. <strong>Please report bugs!</strong> I can't fix issues if I am unaware of them.
</p> </p>
<p>
Client version: {import.meta.env.VITE_VERSION}
</p>
</div> </div>
</section> </section>
); );
+18 -240
View File
@@ -2,280 +2,58 @@
/* override styles for when the div is collapsed */ /* override styles for when the div is collapsed */
div.reminderContent.is-collapsed .column.discord-frame { div.reminderContent.is-collapsed .column.discord-frame {
display: none; display: none;
} }
div.reminderContent.is-collapsed .column.settings { div.reminderContent.is-collapsed .column.settings {
display: none; display: none;
} }
div.reminderContent.is-collapsed .reminder-settings { div.reminderContent.is-collapsed .reminder-settings {
margin-bottom: 0; margin-bottom: 0;
} }
div.reminderContent.is-collapsed .button-row { div.reminderContent.is-collapsed .button-row {
display: none; display: none;
} }
div.reminderContent.is-collapsed .button-row-edit { div.reminderContent.is-collapsed .button-row-edit {
display: none; display: none;
} }
div.reminderContent.is-collapsed .reminder-topbar { div.reminderContent.is-collapsed .reminder-topbar {
padding-bottom: 0; padding-bottom: 0;
} }
div.reminderContent.is-collapsed .invert-collapses { div.reminderContent.is-collapsed .invert-collapses {
display: inline-flex; display: inline-flex;
} }
div.reminderContent .invert-collapses { div.reminderContent .invert-collapses {
display: none; display: none;
} }
div.reminderContent.is-collapsed input[name="name"] { div.reminderContent.is-collapsed input[name="name"] {
display: inline-flex; display: inline-flex;
flex-grow: 1; flex-grow: 1;
border: none; border: none;
background: none; background: none;
box-shadow: none; box-shadow: none;
opacity: 1; opacity: 1;
} }
div.reminderContent.is-collapsed .hide-box { div.reminderContent.is-collapsed .hide-box {
display: inline-flex; display: inline-flex;
} }
div.reminderContent.is-collapsed .hide-box i { div.reminderContent.is-collapsed .hide-box i {
transform: rotate(90deg); transform: rotate(90deg);
} }
.button.is-success:not(.is-outlined) { .button.is-success:not(.is-outlined) {
color: white; color: white;
} }
.button.is-outlined.is-success:not(.is-focused, :hover, :focus) { .button.is-outlined.is-success:not(.is-focused, :hover, :focus) {
background-color: white; background-color: white;
}
@import "vars";
.scheme-light {
background-color: $primary-background-light;
color: $primary-text-light;
.column.is-main-content {
background-color: $primary-background-light;
}
.reminderContent {
background-color: $secondary-background-light;
}
}
.scheme-dark {
background-color: $secondary-background-dark;
color: $primary-text-dark;
.column.is-main-content {
background-color: $secondary-background-dark;
}
.reminderContent {
background-color: $primary-background-dark;
color: $primary-text-dark;
}
.title,
.subtitle,
.label,
.help,
strong {
color: $primary-text-dark;
}
input,
textarea,
select,
.input,
.textarea {
background-color: $secondary-background-dark;
color: $primary-text-dark;
border-color: $contrast-background-dark;
&::placeholder {
color: #aaa;
}
}
// Buttons
.button.is-outlined.is-success:not(.is-focused, :hover, :focus) {
background-color: $primary-background-dark;
color: #48c78e;
border-color: #48c78e;
}
.button.is-outlined.is-danger:not(.is-focused, :hover, :focus) {
background-color: $primary-background-dark;
}
.button.is-outlined.is-warning:not(.is-focused, :hover, :focus) {
background-color: $primary-background-dark;
}
.button:not(.is-success, .is-danger, .is-warning, .is-light) {
background-color: $secondary-background-dark;
color: $primary-text-dark;
border-color: $contrast-background-dark;
}
.button.is-light {
background-color: $secondary-background-dark;
color: $primary-text-dark;
}
// Modal
.modal-card-head,
.modal-card-foot {
background-color: $contrast-background-dark;
border-color: $contrast-background-dark;
}
.modal-card-head .modal-card-title {
color: $primary-text-dark;
}
.modal-card-body {
background-color: $primary-background-dark;
color: $primary-text-dark;
}
// Navbar
.navbar {
background-color: $contrast-background-dark;
}
.navbar-item,
.navbar-burger {
color: $primary-text-dark;
}
// Select dropdown arrow
.select:not(.is-multiple):not(.is-loading)::after {
border-color: $primary-text-dark;
}
.select select {
background-color: $secondary-background-dark;
color: $primary-text-dark;
border-color: $contrast-background-dark;
}
// Interval selector inputs
.interval-group input {
background-color: $secondary-background-dark !important;
color: $primary-text-dark;
}
// Notification
.notification {
background-color: $secondary-background-dark;
color: $primary-text-dark;
}
// Links
a:not(.menu a):not(.button) {
color: #7eaef1;
}
// Content area
.content {
color: $primary-text-dark;
}
// Checkbox
.checkbox {
color: $primary-text-dark;
}
// Message input
.message-input {
background-color: $secondary-background-dark;
color: $primary-text-dark;
}
// Box
.box {
background-color: $secondary-background-dark;
color: $primary-text-dark;
}
// Panel
.panel {
background-color: $secondary-background-dark;
.panel-heading {
background-color: $contrast-background-dark;
color: $primary-text-dark;
}
.panel-block {
color: $primary-text-dark;
border-color: $contrast-background-dark;
}
}
// Horizontal rule
hr {
background-color: $contrast-background-dark;
}
// Tag
.tag:not(.is-success, .is-danger, .is-warning, .is-info) {
background-color: $contrast-background-dark;
color: $primary-text-dark;
}
// Todo
.todo {
color: $primary-text-dark;
}
// Page links
.page-links .button {
background-color: $secondary-background-dark;
color: $primary-text-dark;
border-color: $contrast-background-dark;
}
// Loading overlay
.load-screen {
background-color: rgba(36, 36, 36, 0.8);
color: $primary-text-dark;
}
// File/attachment input
.file-cta {
background-color: $secondary-background-dark;
color: $primary-text-dark;
border-color: $contrast-background-dark;
}
.file-name {
background-color: $secondary-background-dark;
color: $primary-text-dark;
border-color: $contrast-background-dark;
}
// Date/time inputs color scheme
input[type="date"],
input[type="time"],
input[type="datetime-local"] {
color-scheme: dark;
}
// Modal background overlay
.modal-background {
background-color: rgba(10, 10, 10, 0.86);
}
} }
+3 -2
View File
@@ -56,7 +56,7 @@ impl Recordable for Options {
}), }),
}; };
let channel_id = if let Some(channel) = ctx.cache().channel(ctx.channel_id()) { let channel_id = if let Some(channel) = ctx.channel_id().to_channel_cached(&ctx.cache()) {
if Some(channel.guild_id) == ctx.guild_id() { if Some(channel.guild_id) == ctx.guild_id() {
flags.channel_id.unwrap_or_else(|| ctx.channel_id()) flags.channel_id.unwrap_or_else(|| ctx.channel_id())
} else { } else {
@@ -66,7 +66,8 @@ impl Recordable for Options {
ctx.channel_id() ctx.channel_id()
}; };
let channel_name = ctx.cache().channel(channel_id).map(|channel| channel.name.clone()); let channel_name =
channel_id.to_channel_cached(&ctx.cache()).map(|channel| channel.name.clone());
let reminders = Reminder::from_channel(&ctx.data().database, channel_id, &flags).await; let reminders = Reminder::from_channel(&ctx.data().database, channel_id, &flags).await;
+2 -2
View File
@@ -63,7 +63,7 @@ impl ComponentDataModel {
let flags = pager.flags; let flags = pager.flags;
let channel_id = { let channel_id = {
let channel_opt = ctx.cache.channel(component.channel_id); let channel_opt = component.channel_id.to_channel_cached(&ctx.cache);
if let Some(channel) = channel_opt { if let Some(channel) = channel_opt {
if Some(channel.guild_id) == component.guild_id { if Some(channel.guild_id) == component.guild_id {
@@ -85,7 +85,7 @@ impl ComponentDataModel {
.div_ceil(EMBED_DESCRIPTION_MAX_LENGTH); .div_ceil(EMBED_DESCRIPTION_MAX_LENGTH);
let channel_name = let channel_name =
ctx.cache.channel(channel_id).map(|channel| channel.name.clone()); channel_id.to_channel_cached(&ctx.cache).map(|channel| channel.name.clone());
let next_page = pager.next_page(pages); let next_page = pager.next_page(pages);
+10 -3
View File
@@ -8,7 +8,9 @@ use crate::{consts::DEFAULT_AVATAR, Error};
pub struct ChannelData { pub struct ChannelData {
pub id: u32, pub id: u32,
pub channel: u64, pub channel: u64,
pub name: Option<String>,
pub nudge: i16, pub nudge: i16,
pub blacklisted: bool,
pub webhook_id: Option<u64>, pub webhook_id: Option<u64>,
pub webhook_token: Option<String>, pub webhook_token: Option<String>,
pub paused: bool, pub paused: bool,
@@ -25,7 +27,7 @@ impl ChannelData {
if let Ok(c) = sqlx::query_as_unchecked!( if let Ok(c) = sqlx::query_as_unchecked!(
Self, Self,
" "
SELECT id, channel, nudge, webhook_id, webhook_token, paused, SELECT id, channel, name, nudge, blacklisted, webhook_id, webhook_token, paused,
paused_until paused_until
FROM channels FROM channels
WHERE channel = ? WHERE channel = ?
@@ -43,8 +45,9 @@ impl ChannelData {
if let Some((a, b)) = props { (Some(a), Some(b)) } else { (None, None) }; if let Some((a, b)) = props { (Some(a), Some(b)) } else { (None, None) };
sqlx::query!( sqlx::query!(
"INSERT IGNORE INTO channels (channel, guild_id) VALUES (?, (SELECT id FROM guilds WHERE guild = ?))", "INSERT IGNORE INTO channels (channel, name, guild_id) VALUES (?, ?, (SELECT id FROM guilds WHERE guild = ?))",
channel_id, channel_id,
channel_name,
guild_id guild_id
) )
.execute(&pool.clone()) .execute(&pool.clone())
@@ -53,7 +56,7 @@ impl ChannelData {
Ok(sqlx::query_as_unchecked!( Ok(sqlx::query_as_unchecked!(
Self, Self,
" "
SELECT id, channel, nudge, webhook_id, webhook_token, paused, paused_until SELECT id, channel, name, nudge, blacklisted, webhook_id, webhook_token, paused, paused_until
FROM channels FROM channels
WHERE channel = ? WHERE channel = ?
", ",
@@ -69,14 +72,18 @@ impl ChannelData {
" "
UPDATE channels UPDATE channels
SET SET
name = ?,
nudge = ?, nudge = ?,
blacklisted = ?,
webhook_id = ?, webhook_id = ?,
webhook_token = ?, webhook_token = ?,
paused = ?, paused = ?,
paused_until = ? paused_until = ?
WHERE id = ? WHERE id = ?
", ",
self.name,
self.nudge, self.nudge,
self.blacklisted,
self.webhook_id, self.webhook_id,
self.webhook_token, self.webhook_token,
self.paused, self.paused,
+20 -20
View File
@@ -215,30 +215,30 @@ impl<'a> MultiReminderBuilder<'a> {
for scope in self.scopes { for scope in self.scopes {
let db_channel_id = match scope { let db_channel_id = match scope {
ReminderScope::User(user_id) => { ReminderScope::User(user_id) => {
let user_id = UserId::new(user_id); if let Ok(user) = UserId::new(user_id).to_user(&self.ctx).await {
match UserData::from_user( let user_data = UserData::from_user(
&user_id, &user,
&self.ctx.serenity_context(), &self.ctx.serenity_context(),
&self.ctx.data().database, &self.ctx.data().database,
) )
.await .await
{ .unwrap();
Ok(user_data) => {
if let Some(guild_id) = self.guild_id { if let Some(guild_id) = self.guild_id {
if guild_id.member(&self.ctx, user_id).await.is_err() { if guild_id.member(&self.ctx, user).await.is_err() {
Err(ReminderError::InvalidTag) Err(ReminderError::InvalidTag)
} else if self.set_by.map_or(true, |i| i != user_data.id) } else if self.set_by.map_or(true, |i| i != user_data.id)
&& !user_data.allowed_dm && !user_data.allowed_dm
{ {
Err(ReminderError::UserBlockedDm) Err(ReminderError::UserBlockedDm)
} else {
Ok((user_data.dm_channel, None))
}
} else { } else {
Ok((user_data.dm_channel, None)) Ok((user_data.dm_channel, None))
} }
} else {
Ok((user_data.dm_channel, None))
} }
Err(_) => Err(ReminderError::InvalidTag), } else {
Err(ReminderError::InvalidTag)
} }
} }
ReminderScope::Channel(channel_with_thread) => { ReminderScope::Channel(channel_with_thread) => {
+13 -92
View File
@@ -195,100 +195,21 @@ impl Embed {
} }
pub fn has_content(&self) -> bool { pub fn has_content(&self) -> bool {
// Discord reports "description required" for empty embeds, but descriptions are optional. if self.title.is_empty()
// An author icon alone does not count as content; a footer icon does. && self.description.is_empty()
!self.title.trim().is_empty() && self.image_url.is_none()
|| !self.description.trim().is_empty() && self.thumbnail_url.is_none()
|| self.image_url.as_ref().is_some_and(|url| !url.trim().is_empty()) && self.footer.is_empty()
|| self.thumbnail_url.as_ref().is_some_and(|url| !url.trim().is_empty()) && self.footer_url.is_none()
|| !self.footer.trim().is_empty() && self.author.is_empty()
|| self.footer_url.as_ref().is_some_and(|url| !url.trim().is_empty()) && self.author_url.is_none()
|| !self.author.trim().is_empty() && self.fields.0.is_empty()
|| !self.fields.0.is_empty() {
} false
} } else {
true
#[cfg(test)]
mod tests {
use super::*;
fn embed(description: &str) -> Embed {
Embed {
title: String::new(),
description: description.to_string(),
image_url: None,
thumbnail_url: None,
footer: String::new(),
footer_url: None,
author: String::new(),
author_url: None,
color: 0,
fields: Json(vec![]),
} }
} }
#[test]
fn embed_without_description_can_have_content() {
let url = Some("https://example.com/image.png".to_string());
let embeds = [
Embed { title: "Reminder".to_string(), ..embed("") },
Embed { image_url: url.clone(), ..embed("") },
Embed { thumbnail_url: url.clone(), ..embed("") },
Embed { footer: "Footer".to_string(), ..embed("") },
Embed { footer_url: url, ..embed("") },
Embed { author: "Author".to_string(), ..embed("") },
Embed {
fields: Json(vec![EmbedField {
title: String::new(),
value: String::new(),
inline: false,
}]),
..embed("")
},
];
for embed in embeds {
assert!(embed.has_content());
}
}
#[test]
fn empty_embed_after_substitution_has_no_content() {
let mut embed = embed("<<timenow:Invalid/Timezone:%H>>");
embed.author_url = Some("https://example.com/image.png".to_string());
embed.description = substitute(&embed.description);
assert!(embed.description.is_empty());
assert!(!embed.has_content());
}
#[test]
fn embed_with_only_description_has_content() {
assert!(embed("Remember to do the thing").has_content());
}
#[test]
fn empty_or_whitespace_embed_has_no_content() {
assert!(!embed("").has_content());
let embed = Embed {
title: " \n\t ".to_string(),
image_url: Some(String::new()),
thumbnail_url: Some(" ".to_string()),
footer: " \n\t ".to_string(),
footer_url: Some(String::new()),
author: " \n\t ".to_string(),
color: 12345,
..embed(" \n\t ")
};
assert!(!embed.has_content());
}
#[test]
fn author_icon_alone_has_no_content() {
let embed =
Embed { author_url: Some("https://example.com/image.png".to_string()), ..embed("") };
assert!(!embed.has_content());
}
} }
impl Into<CreateEmbed> for Embed { impl Into<CreateEmbed> for Embed {
+1 -1
View File
@@ -42,7 +42,7 @@ impl IpBlocking {
fn contains<I: Into<u32>>(&self, ip: I) -> bool { fn contains<I: Into<u32>>(&self, ip: I) -> bool {
let ip: u32 = ip.into(); let ip: u32 = ip.into();
let _prev_index = self.upper_ips.len() - 1; let mut prev_index = self.upper_ips.len() - 1;
let mut index = self.upper_ips.len() / 2; let mut index = self.upper_ips.len() / 2;
loop { loop {
if self.upper_ips[index] <= ip && self.lower_ips[index] >= ip { if self.upper_ips[index] <= ip && self.lower_ips[index] >= ip {
+6 -2
View File
@@ -63,6 +63,7 @@ use log::{error, info, warn};
use oauth2::{basic::BasicClient, AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl}; use oauth2::{basic::BasicClient, AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl};
use poise::serenity_prelude::{ use poise::serenity_prelude::{
client::Context, client::Context,
http::CacheHttp,
model::id::{GuildId, UserId}, model::id::{GuildId, UserId},
}; };
use rocket::{ use rocket::{
@@ -75,10 +76,13 @@ use rocket::{
}; };
use rocket_dyn_templates::Template; use rocket_dyn_templates::Template;
use sqlx::{MySql, Pool}; use sqlx::{MySql, Pool};
use std::net::Ipv4Addr;
use std::{env, path::Path}; use std::{env, path::Path};
use crate::web::consts::{DISCORD_OAUTH_AUTHORIZE, DISCORD_OAUTH_TOKEN}; use crate::web::{
use crate::web::fairings::metrics::MetricProducer; consts::{CNC_GUILD, DISCORD_OAUTH_AUTHORIZE, DISCORD_OAUTH_TOKEN, SUBSCRIPTION_ROLES},
fairings::metrics::MetricProducer,
};
type Database = MySql; type Database = MySql;
+6 -1
View File
@@ -5,6 +5,8 @@ mod roles;
mod templates; mod templates;
pub mod todos; pub mod todos;
use std::env;
use crate::utils::check_subscription; use crate::utils::check_subscription;
use crate::web::guards::transaction::Transaction; use crate::web::guards::transaction::Transaction;
use crate::web::{check_authorization, routes::JsonResult}; use crate::web::{check_authorization, routes::JsonResult};
@@ -14,7 +16,10 @@ pub use reminders::*;
use rocket::{get, http::CookieJar, serde::json::json, State}; use rocket::{get, http::CookieJar, serde::json::json, State};
pub use roles::get_guild_roles; pub use roles::get_guild_roles;
use serenity::all::UserId; use serenity::all::UserId;
use serenity::{client::Context, model::id::GuildId}; use serenity::{
client::Context,
model::id::{GuildId, RoleId},
};
pub use templates::*; pub use templates::*;
#[get("/api/guild/<id>")] #[get("/api/guild/<id>")]
@@ -267,8 +267,9 @@ pub async fn edit_reminder(
} }
if reminder.channel > 0 { if reminder.channel > 0 {
let channel_guild = let channel_guild = ChannelId::new(reminder.channel)
ctx.cache.channel(ChannelId::new(reminder.channel)).map(|channel| channel.guild_id); .to_channel_cached(&ctx.cache)
.map(|channel| channel.guild_id);
match channel_guild { match channel_guild {
Some(channel_guild) => { Some(channel_guild) => {
let channel_matches_guild = channel_guild.get() == id; let channel_matches_guild = channel_guild.get() == id;
@@ -3,10 +3,13 @@ use crate::web::{
check_authorization, check_authorization,
guards::transaction::Transaction, guards::transaction::Transaction,
routes::{ routes::{
dashboard::{ImportBody, ReminderTemplateCsv}, dashboard::{
create_reminder, CreateReminder, ImportBody, ReminderCsv, ReminderTemplateCsv, TodoCsv,
},
JsonResult, JsonResult,
}, },
}; };
use crate::Database;
use base64::{prelude::BASE64_STANDARD, Engine}; use base64::{prelude::BASE64_STANDARD, Engine};
use csv::{QuoteStyle, WriterBuilder}; use csv::{QuoteStyle, WriterBuilder};
use log::warn; use log::warn;
@@ -17,7 +20,10 @@ use rocket::{
serde::json::{json, Json}, serde::json::{json, Json},
State, State,
}; };
use serenity::{client::Context, model::id::GuildId}; use serenity::{
client::Context,
model::id::{ChannelId, GuildId, UserId},
};
use sqlx::{MySql, Pool}; use sqlx::{MySql, Pool};
#[get("/api/guild/<id>/export/reminder_templates")] #[get("/api/guild/<id>/export/reminder_templates")]
+4 -2
View File
@@ -2,7 +2,9 @@ use crate::web::{
check_authorization, check_authorization,
guards::transaction::Transaction, guards::transaction::Transaction,
routes::{ routes::{
dashboard::{create_reminder, CreateReminder, ImportBody, ReminderCsv}, dashboard::{
create_reminder, CreateReminder, ImportBody, ReminderCsv, ReminderTemplateCsv, TodoCsv,
},
JsonResult, JsonResult,
}, },
}; };
@@ -19,7 +21,7 @@ use rocket::{
}; };
use serenity::{ use serenity::{
client::Context, client::Context,
model::id::{GuildId, UserId}, model::id::{ChannelId, GuildId, UserId},
}; };
use sqlx::{MySql, Pool}; use sqlx::{MySql, Pool};
+6 -2
View File
@@ -1,10 +1,14 @@
use crate::web::{ use crate::web::{
check_authorization, check_authorization,
guards::transaction::Transaction,
routes::{ routes::{
dashboard::{ImportBody, TodoCsv}, dashboard::{
create_reminder, CreateReminder, ImportBody, ReminderCsv, ReminderTemplateCsv, TodoCsv,
},
JsonResult, JsonResult,
}, },
}; };
use crate::Database;
use base64::{prelude::BASE64_STANDARD, Engine}; use base64::{prelude::BASE64_STANDARD, Engine};
use csv::{QuoteStyle, WriterBuilder}; use csv::{QuoteStyle, WriterBuilder};
use log::warn; use log::warn;
@@ -17,7 +21,7 @@ use rocket::{
}; };
use serenity::{ use serenity::{
client::Context, client::Context,
model::id::{ChannelId, GuildId}, model::id::{ChannelId, GuildId, UserId},
}; };
use sqlx::{MySql, Pool}; use sqlx::{MySql, Pool};
+34
View File
@@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="EN">
<head>
<meta name="description" content="The most powerful Discord Reminders Bot">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="UTF-8">
<meta name="yandex-verification" content="bb77b8681eb64a90"/>
<meta name="google-site-verification" content="7h7UVTeEe0AOzHiH3cFtsqMULYGN-zCZdMT_YCkW1Ho"/>
<!-- <meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src *; font-src fonts.gstatic.com 'self'"> -->
<!-- favicon -->
<link rel="apple-touch-icon" sizes="180x180"
href="/static/favicon/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32"
href="/static/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16"
href="/static/favicon/favicon-16x16.png">
<link rel="manifest" href="/static/site.webmanifest">
<meta name="msapplication-TileColor" content="#da532c">
<meta name="theme-color" content="#ffffff">
<title>Reminder Bot | Dashboard</title>
<!-- styles -->
<link rel="stylesheet" href="/static/css/fa.css">
<link rel="stylesheet" href="/static/css/font.css">
<link rel="stylesheet" href="/static/css/style.css">
<script type="module" crossorigin src="/static/assets/index-B8f0viPI.js"></script>
<link rel="stylesheet" crossorigin href="/static/assets/index-BZ8NJuKt.css">
</head>
<body>
<div id="app"></div>
</body>
</html>