Compare commits
1 Commits
1.7.4-2
...
discord-ti
Author | SHA1 | Date | |
---|---|---|---|
51d2ac2b92 |
31
.gitignore
vendored
@ -1,30 +1,7 @@
|
|||||||
target
|
/target
|
||||||
.env
|
.env
|
||||||
/venv
|
/venv
|
||||||
.cargo
|
.cargo
|
||||||
.idea
|
assets
|
||||||
web/static/index.html
|
out.json
|
||||||
web/static/assets
|
/.idea
|
||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
pnpm-debug.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
node_modules
|
|
||||||
dist
|
|
||||||
dist-ssr
|
|
||||||
*.local
|
|
||||||
|
|
||||||
# Editor directories and files
|
|
||||||
.vscode/*
|
|
||||||
!.vscode/extensions.json
|
|
||||||
.DS_Store
|
|
||||||
*.suo
|
|
||||||
*.ntvs*
|
|
||||||
*.njsproj
|
|
||||||
*.sln
|
|
||||||
*.sw?
|
|
||||||
|
@ -1,2 +0,0 @@
|
|||||||
printWidth = 90
|
|
||||||
tabWidth = 4
|
|
3774
Cargo.lock
generated
75
Cargo.toml
@ -1,71 +1,30 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "reminder-rs"
|
name = "reminder_rs"
|
||||||
version = "1.7.4-2"
|
version = "1.5.0-2"
|
||||||
authors = ["Jude Southworth <judesouthworth@pm.me>"]
|
authors = ["jellywx <judesouthworth@pm.me>"]
|
||||||
edition = "2021"
|
edition = "2018"
|
||||||
license = "AGPL-3.0 only"
|
|
||||||
description = "Reminder Bot for Discord, now in Rust"
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
poise = "0.6.1"
|
dashmap = "4.0"
|
||||||
dotenv = "0.15"
|
dotenv = "0.15"
|
||||||
|
humantime = "2.1"
|
||||||
tokio = { version = "1", features = ["process", "full"] }
|
tokio = { version = "1", features = ["process", "full"] }
|
||||||
reqwest = { version = "0.12", features = ["json"] }
|
reqwest = "0.11"
|
||||||
regex = "1.10"
|
regex = "1.4"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
env_logger = "0.11"
|
env_logger = "0.8"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
chrono-tz = { version = "0.8", features = ["serde"] }
|
chrono-tz = "0.5"
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
num-integer = "0.1"
|
num-integer = "0.1"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde_repr = "0.1"
|
rand = "0.7"
|
||||||
rmp-serde = "1.1"
|
Inflector = "0.11"
|
||||||
rand = "0.8"
|
|
||||||
levenshtein = "1.0"
|
levenshtein = "1.0"
|
||||||
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono", "migrate"] }
|
# serenity = { version = "0.10", features = ["collector"] }
|
||||||
base64 = "0.21"
|
serenity = { path = "/home/jude/serenity", features = ["collector", "unstable_discord_api"] }
|
||||||
secrecy = "0.8.0"
|
sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono"]}
|
||||||
futures = "0.3.30"
|
|
||||||
prometheus = "0.13.3"
|
|
||||||
rocket = { version = "0.5.0", features = ["tls", "secrets", "json"] }
|
|
||||||
rocket_dyn_templates = { version = "0.1.0", features = ["tera"] }
|
|
||||||
serenity = { version = "0.12", default-features = false, features = ["builder", "cache", "client", "gateway", "http", "model", "utils", "rustls_backend"] }
|
|
||||||
oauth2 = "4"
|
|
||||||
csv = "1.2"
|
|
||||||
axum = "0.7"
|
|
||||||
|
|
||||||
[dependencies.extract_derive]
|
[dependencies.regex_command_attr]
|
||||||
path = "extract_derive"
|
path = "./regex_command_attr"
|
||||||
|
|
||||||
[dependencies.recordable_derive]
|
|
||||||
path = "recordable_derive"
|
|
||||||
|
|
||||||
[package.metadata.deb]
|
|
||||||
depends = "$auto, python3-dateparser (>= 1.0.0)"
|
|
||||||
suggests = "mysql-server-8.0, nginx"
|
|
||||||
maintainer-scripts = "debian"
|
|
||||||
assets = [
|
|
||||||
["target/release/reminder-rs", "usr/bin/reminder-rs", "755"],
|
|
||||||
["static/css/*", "lib/reminder-rs/static/css", "644"],
|
|
||||||
["static/favicon/*", "lib/reminder-rs/static/favicon", "644"],
|
|
||||||
["static/img/*", "lib/reminder-rs/static/img", "644"],
|
|
||||||
["static/js/*", "lib/reminder-rs/static/js", "644"],
|
|
||||||
["static/webfonts/*", "lib/reminder-rs/static/webfonts", "644"],
|
|
||||||
["static/site.webmanifest", "lib/reminder-rs/static/site.webmanifest", "644"],
|
|
||||||
["templates/**/*", "lib/reminder-rs/templates", "644"],
|
|
||||||
["reminder-dashboard/dist/static/assets/*", "lib/reminder-rs/static/assets", "644"],
|
|
||||||
["reminder-dashboard/dist/index.html", "lib/reminder-rs/static/index.html", "644"],
|
|
||||||
["conf/default.env", "etc/reminder-rs/config.env", "600"],
|
|
||||||
["conf/Rocket.toml", "etc/reminder-rs/Rocket.toml", "600"],
|
|
||||||
# ["nginx/reminder-rs", "etc/nginx/sites-available/reminder-rs", "755"]
|
|
||||||
]
|
|
||||||
conf-files = [
|
|
||||||
"/etc/reminder-rs/config.env",
|
|
||||||
"/etc/reminder-rs/Rocket.toml",
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.metadata.deb.systemd-units]
|
|
||||||
unit-scripts = "systemd"
|
|
||||||
start = false
|
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
FROM ubuntu:20.04
|
|
||||||
|
|
||||||
ENV RUSTUP_HOME=/usr/local/rustup \
|
|
||||||
CARGO_HOME=/usr/local/cargo \
|
|
||||||
PATH=/usr/local/cargo/bin:$PATH
|
|
||||||
|
|
||||||
RUN apt update && DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC apt install -y gcc gcc-multilib cmake pkg-config libssl-dev curl mysql-client-8.0 npm
|
|
||||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path --profile minimal --default-toolchain nightly
|
|
||||||
RUN cargo install cargo-deb
|
|
52
README.md
@ -1,42 +1,26 @@
|
|||||||
# reminder-rs
|
# reminder-rs
|
||||||
Reminder Bot for Discord.
|
Reminder Bot for Discord, now in Rust.
|
||||||
|
Old Python version: https://github.com/reminder-bot/bot
|
||||||
|
|
||||||
## How do I use it?
|
## How do I use it?
|
||||||
I offer a hosted version of the bot. You can invite it with: **https://invite.reminder-bot.com**. The catch is that repeating
|
We offer a hosted version of the bot. You can invite it with: **https://invite.reminder-bot.com**. The catch is that repeating
|
||||||
reminders are paid on the hosted version of the bot. Keep reading if you want to host it yourself.
|
reminders are paid on the hosted version of the bot. Keep reading if you want to host it yourself.
|
||||||
|
|
||||||
You'll need rustc and cargo for compilation. To run, you'll need Python 3 still (due to no suitable replacement for dateparser in Rust)
|
You'll need rustc and cargo for compilation. To run, you'll need Python 3 still (due to no suitable replacement for dateparser in Rust)
|
||||||
|
|
||||||
### Build APT package
|
### Compiling
|
||||||
|
Reminder Bot can be built by running `cargo build --release` in the top level directory. It is necessary to create a folder called 'assets' containing an image file with its name specified in the environment as `WEBHOOK_AVATAR`, of dimensions 128x128px to be used as the webhook avatar.
|
||||||
|
|
||||||
Recommended method.
|
#### Compilation environment variables
|
||||||
|
These environment variables must be provided when compiling the bot
|
||||||
|
* `DATABASE_URL` - the URL of your MySQL database (`mysql://user[:password]@domain/database`)
|
||||||
|
* `WEBHOOK_AVATAR` - accepts the name of an image file located in `$CARGO_MANIFEST_DIR/assets/` to be used as the avatar when creating webhooks. **IMPORTANT: image file must be 128x128 or smaller in size**
|
||||||
|
* `STRINGS_FILE` - accepts the name of a compiled strings file located in `$CARGO_MANIFEST_DIR/assets/` to be used for creating messages. Compiled string files can be generated with `compile.py` at https://github.com/reminder-bot/languages
|
||||||
|
|
||||||
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.
|
### Setting up Python
|
||||||
|
Reminder Bot by default looks for a venv within it's working directory to run Python out of. To set up a venv, install `python3-venv` and run `python3 -m venv venv`. Then, run `source venv/bin/activate` to activate the venv, and do `pip install dateparser` to install the required library
|
||||||
1. Install container software: `sudo apt install podman`.
|
|
||||||
2. Install database server: `sudo apt install mysql-server-8.0`. Create a database called `reminders`
|
|
||||||
3. Install SQLx CLI: `cargo install sqlx-cli`
|
|
||||||
4. From the source code directory, execute `sqlx migrate run`
|
|
||||||
5. Build container image: `podman build -t reminder-rs .`
|
|
||||||
6. Build with podman: `podman run --rm --network=host -v "$PWD":/mnt -w /mnt -e "DATABASE_URL=mysql://user@localhost/reminders" reminder-rs cargo deb`
|
|
||||||
|
|
||||||
|
|
||||||
### Compiling for other target
|
|
||||||
|
|
||||||
1. Install requirements:
|
|
||||||
`sudo apt install gcc gcc-multilib cmake libssl-dev build-essential python3-dateparser`
|
|
||||||
2. Install rustup from https://rustup.rs
|
|
||||||
3. Install the nightly toolchain: `rustup toolchain default nightly`
|
|
||||||
4. Install database server: `sudo apt install mysql-server-8.0`. Create a database called `reminders`.
|
|
||||||
5. Install `sqlx-cli`: `cargo install sqlx-cli`.
|
|
||||||
6. Run migrations: `sqlx migrate run`.
|
|
||||||
7. Set environment variables:
|
|
||||||
* `DATABASE_URL` - the URL of your MySQL database (`mysql://user[:password]@domain/database`)
|
|
||||||
8. Build: `cargo build --release`
|
|
||||||
|
|
||||||
|
|
||||||
### Configuring
|
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
Reminder Bot reads a number of environment variables. Some are essential, and others have hardcoded fallbacks. Environment variables can be loaded from a .env file in the working directory.
|
Reminder Bot reads a number of environment variables. Some are essential, and others have hardcoded fallbacks. Environment variables can be loaded from a .env file in the working directory.
|
||||||
|
|
||||||
__Required Variables__
|
__Required Variables__
|
||||||
@ -45,8 +29,16 @@ __Required Variables__
|
|||||||
|
|
||||||
__Other Variables__
|
__Other Variables__
|
||||||
* `MIN_INTERVAL` - default `600`, defines the shortest interval the bot should accept
|
* `MIN_INTERVAL` - default `600`, defines the shortest interval the bot should accept
|
||||||
|
* `MAX_TIME` - default `1576800000`, defines the maximum time ahead that reminders can be set for
|
||||||
* `LOCAL_TIMEZONE` - default `UTC`, necessary for calculations in the natural language processor
|
* `LOCAL_TIMEZONE` - default `UTC`, necessary for calculations in the natural language processor
|
||||||
|
* `DEFAULT_PREFIX` - default `$`, used for the default prefix on new guilds
|
||||||
* `SUBSCRIPTION_ROLES` - default `None`, accepts a list of Discord role IDs that are given to subscribed users
|
* `SUBSCRIPTION_ROLES` - default `None`, accepts a list of Discord role IDs that are given to subscribed users
|
||||||
* `CNC_GUILD` - default `None`, accepts a single Discord guild ID for the server that the subscription roles belong to
|
* `CNC_GUILD` - default `None`, accepts a single Discord guild ID for the server that the subscription roles belong to
|
||||||
* `PYTHON_LOCATION` - default `/usr/bin/python3`. Can be changed if your Python executable is located somewhere else
|
* `IGNORE_BOTS` - default `1`, if `1`, Reminder Bot will ignore all other bots
|
||||||
|
* `PYTHON_LOCATION` - default `venv/bin/python3`. Can be changed if your Python executable is located somewhere else
|
||||||
|
* `LOCAL_LANGUAGE` - default `EN`. Specifies the string set to fall back to if a string cannot be found (and to be used with new users)
|
||||||
* `THEME_COLOR` - default `8fb677`. Specifies the hex value of the color to use on info message embeds
|
* `THEME_COLOR` - default `8fb677`. Specifies the hex value of the color to use on info message embeds
|
||||||
|
* `CASE_INSENSITIVE` - default `1`, if `1`, commands will be treated with case insensitivity (so both `$help` and `$HELP` will work)
|
||||||
|
* `SHARD_COUNT` - default `None`, accepts the number of shards that are being ran
|
||||||
|
* `SHARD_RANGE` - default `None`, if `SHARD_COUNT` is specified, specifies what range of shards to start on this process
|
||||||
|
* `DM_ENABLED` - default `1`, if `1`, Reminder Bot will respond to direct messages
|
||||||
|
28
Rocket.toml
@ -1,28 +0,0 @@
|
|||||||
[default]
|
|
||||||
address = "0.0.0.0"
|
|
||||||
port = 18920
|
|
||||||
template_dir = "templates"
|
|
||||||
limits = { json = "10MiB" }
|
|
||||||
|
|
||||||
[debug]
|
|
||||||
secret_key = "tR8krio5FXTnnyIZNiJDXPondz0kI1v6X6BXZcBGIRY="
|
|
||||||
|
|
||||||
[debug.tls]
|
|
||||||
certs = "private/rsa_sha256_cert.pem"
|
|
||||||
key = "private/rsa_sha256_key.pem"
|
|
||||||
|
|
||||||
[debug.rsa_sha256.tls]
|
|
||||||
certs = "private/rsa_sha256_cert.pem"
|
|
||||||
key = "private/rsa_sha256_key.pem"
|
|
||||||
|
|
||||||
[debug.ecdsa_nistp256_sha256.tls]
|
|
||||||
certs = "private/ecdsa_nistp256_sha256_cert.pem"
|
|
||||||
key = "private/ecdsa_nistp256_sha256_key_pkcs8.pem"
|
|
||||||
|
|
||||||
[debug.ecdsa_nistp384_sha384.tls]
|
|
||||||
certs = "private/ecdsa_nistp384_sha384_cert.pem"
|
|
||||||
key = "private/ecdsa_nistp384_sha384_key_pkcs8.pem"
|
|
||||||
|
|
||||||
[debug.ed25519.tls]
|
|
||||||
certs = "private/ed25519_cert.pem"
|
|
||||||
key = "private/ed25519_key.pem"
|
|
Before Width: | Height: | Size: 21 KiB |
13
build.rs
@ -1,13 +0,0 @@
|
|||||||
use std::{path::Path, process::Command};
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
println!("cargo:rerun-if-changed=migrations");
|
|
||||||
println!("cargo:rerun-if-changed=reminder-dashboard");
|
|
||||||
|
|
||||||
Command::new("npm")
|
|
||||||
.arg("run")
|
|
||||||
.arg("build")
|
|
||||||
.current_dir(Path::new("reminder-dashboard"))
|
|
||||||
.spawn()
|
|
||||||
.expect("Failed to build NPM");
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
[default]
|
|
||||||
address = "127.0.0.1"
|
|
||||||
port = 18920
|
|
||||||
template_dir = "/lib/reminder-rs/templates"
|
|
||||||
limits = { json = "10MiB" }
|
|
||||||
|
|
||||||
[release]
|
|
||||||
# secret_key = ""
|
|
@ -1,19 +0,0 @@
|
|||||||
DATABASE_URL=
|
|
||||||
|
|
||||||
DISCORD_TOKEN=
|
|
||||||
PATREON_GUILD_ID=
|
|
||||||
PATREON_ROLE_ID=
|
|
||||||
|
|
||||||
LOCAL_TIMEZONE=
|
|
||||||
MIN_INTERVAL=
|
|
||||||
PYTHON_LOCATION=/usr/bin/python3
|
|
||||||
DONTRUN=
|
|
||||||
SECRET_KEY=
|
|
||||||
|
|
||||||
REMIND_INTERVAL=
|
|
||||||
OAUTH2_DISCORD_CALLBACK=
|
|
||||||
OAUTH2_CLIENT_ID=
|
|
||||||
OAUTH2_CLIENT_SECRET=
|
|
||||||
|
|
||||||
REPORT_EMAIL=
|
|
||||||
LOG_TO_DATABASE=1
|
|
@ -1,6 +1,10 @@
|
|||||||
|
CREATE DATABASE IF NOT EXISTS reminders;
|
||||||
|
|
||||||
SET FOREIGN_KEY_CHECKS=0;
|
SET FOREIGN_KEY_CHECKS=0;
|
||||||
|
|
||||||
CREATE TABLE guilds (
|
USE reminders;
|
||||||
|
|
||||||
|
CREATE TABLE reminders.guilds (
|
||||||
id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT,
|
id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT,
|
||||||
guild BIGINT UNSIGNED UNIQUE NOT NULL,
|
guild BIGINT UNSIGNED UNIQUE NOT NULL,
|
||||||
|
|
||||||
@ -14,10 +18,10 @@ CREATE TABLE guilds (
|
|||||||
default_avatar VARCHAR(512) DEFAULT 'https://raw.githubusercontent.com/reminder-bot/logos/master/Remind_Me_Bot_Logo_PPic.jpg' NOT NULL,
|
default_avatar VARCHAR(512) DEFAULT 'https://raw.githubusercontent.com/reminder-bot/logos/master/Remind_Me_Bot_Logo_PPic.jpg' NOT NULL,
|
||||||
|
|
||||||
PRIMARY KEY (id),
|
PRIMARY KEY (id),
|
||||||
FOREIGN KEY (default_channel_id) REFERENCES channels(id) ON DELETE SET NULL
|
FOREIGN KEY (default_channel_id) REFERENCES reminders.channels(id) ON DELETE SET NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE channels (
|
CREATE TABLE reminders.channels (
|
||||||
id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT,
|
id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT,
|
||||||
channel BIGINT UNSIGNED UNIQUE NOT NULL,
|
channel BIGINT UNSIGNED UNIQUE NOT NULL,
|
||||||
|
|
||||||
@ -35,10 +39,10 @@ CREATE TABLE channels (
|
|||||||
guild_id INT UNSIGNED,
|
guild_id INT UNSIGNED,
|
||||||
|
|
||||||
PRIMARY KEY (id),
|
PRIMARY KEY (id),
|
||||||
FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE
|
FOREIGN KEY (guild_id) REFERENCES reminders.guilds(id) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE users (
|
CREATE TABLE reminders.users (
|
||||||
id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
|
id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
|
||||||
user BIGINT UNSIGNED UNIQUE NOT NULL,
|
user BIGINT UNSIGNED UNIQUE NOT NULL,
|
||||||
|
|
||||||
@ -55,10 +59,10 @@ CREATE TABLE users (
|
|||||||
patreon BOOLEAN NOT NULL DEFAULT 0,
|
patreon BOOLEAN NOT NULL DEFAULT 0,
|
||||||
|
|
||||||
PRIMARY KEY (id),
|
PRIMARY KEY (id),
|
||||||
FOREIGN KEY (dm_channel) REFERENCES channels(id) ON DELETE RESTRICT
|
FOREIGN KEY (dm_channel) REFERENCES reminders.channels(id) ON DELETE RESTRICT
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE roles (
|
CREATE TABLE reminders.roles (
|
||||||
id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT,
|
id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT,
|
||||||
role BIGINT UNSIGNED UNIQUE NOT NULL,
|
role BIGINT UNSIGNED UNIQUE NOT NULL,
|
||||||
|
|
||||||
@ -67,10 +71,10 @@ CREATE TABLE roles (
|
|||||||
guild_id INT UNSIGNED NOT NULL,
|
guild_id INT UNSIGNED NOT NULL,
|
||||||
|
|
||||||
PRIMARY KEY (id),
|
PRIMARY KEY (id),
|
||||||
FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE
|
FOREIGN KEY (guild_id) REFERENCES reminders.guilds(id) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE embeds (
|
CREATE TABLE reminders.embeds (
|
||||||
id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
|
id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
|
||||||
|
|
||||||
title VARCHAR(256) NOT NULL DEFAULT '',
|
title VARCHAR(256) NOT NULL DEFAULT '',
|
||||||
@ -87,7 +91,7 @@ CREATE TABLE embeds (
|
|||||||
PRIMARY KEY (id)
|
PRIMARY KEY (id)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE embed_fields (
|
CREATE TABLE reminders.embed_fields (
|
||||||
id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
|
id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
|
||||||
|
|
||||||
title VARCHAR(256) NOT NULL DEFAULT '',
|
title VARCHAR(256) NOT NULL DEFAULT '',
|
||||||
@ -96,10 +100,10 @@ CREATE TABLE embed_fields (
|
|||||||
embed_id INT UNSIGNED NOT NULL,
|
embed_id INT UNSIGNED NOT NULL,
|
||||||
|
|
||||||
PRIMARY KEY (id),
|
PRIMARY KEY (id),
|
||||||
FOREIGN KEY (embed_id) REFERENCES embeds(id) ON DELETE CASCADE
|
FOREIGN KEY (embed_id) REFERENCES reminders.embeds(id) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE messages (
|
CREATE TABLE reminders.messages (
|
||||||
id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
|
id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
|
||||||
|
|
||||||
content VARCHAR(2048) NOT NULL DEFAULT '',
|
content VARCHAR(2048) NOT NULL DEFAULT '',
|
||||||
@ -110,10 +114,10 @@ CREATE TABLE messages (
|
|||||||
attachment_name VARCHAR(260),
|
attachment_name VARCHAR(260),
|
||||||
|
|
||||||
PRIMARY KEY (id),
|
PRIMARY KEY (id),
|
||||||
FOREIGN KEY (embed_id) REFERENCES embeds(id) ON DELETE SET NULL
|
FOREIGN KEY (embed_id) REFERENCES reminders.embeds(id) ON DELETE SET NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE reminders (
|
CREATE TABLE reminders.reminders (
|
||||||
id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
|
id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
|
||||||
uid VARCHAR(64) UNIQUE NOT NULL,
|
uid VARCHAR(64) UNIQUE NOT NULL,
|
||||||
|
|
||||||
@ -136,20 +140,20 @@ CREATE TABLE reminders (
|
|||||||
set_by INT UNSIGNED,
|
set_by INT UNSIGNED,
|
||||||
|
|
||||||
PRIMARY KEY (id),
|
PRIMARY KEY (id),
|
||||||
FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE RESTRICT,
|
FOREIGN KEY (message_id) REFERENCES reminders.messages(id) ON DELETE RESTRICT,
|
||||||
FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE,
|
FOREIGN KEY (channel_id) REFERENCES reminders.channels(id) ON DELETE CASCADE,
|
||||||
FOREIGN KEY (set_by) REFERENCES users(id) ON DELETE SET NULL
|
FOREIGN KEY (set_by) REFERENCES reminders.users(id) ON DELETE SET NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TRIGGER message_cleanup AFTER DELETE ON reminders
|
CREATE TRIGGER message_cleanup AFTER DELETE ON reminders.reminders
|
||||||
FOR EACH ROW
|
FOR EACH ROW
|
||||||
DELETE FROM messages WHERE id = OLD.message_id;
|
DELETE FROM reminders.messages WHERE id = OLD.message_id;
|
||||||
|
|
||||||
CREATE TRIGGER embed_cleanup AFTER DELETE ON messages
|
CREATE TRIGGER embed_cleanup AFTER DELETE ON reminders.messages
|
||||||
FOR EACH ROW
|
FOR EACH ROW
|
||||||
DELETE FROM embeds WHERE id = OLD.embed_id;
|
DELETE FROM reminders.embeds WHERE id = OLD.embed_id;
|
||||||
|
|
||||||
CREATE TABLE todos (
|
CREATE TABLE reminders.todos (
|
||||||
id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
|
id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
|
||||||
user_id INT UNSIGNED,
|
user_id INT UNSIGNED,
|
||||||
guild_id INT UNSIGNED,
|
guild_id INT UNSIGNED,
|
||||||
@ -157,23 +161,23 @@ CREATE TABLE todos (
|
|||||||
value VARCHAR(2000) NOT NULL,
|
value VARCHAR(2000) NOT NULL,
|
||||||
|
|
||||||
PRIMARY KEY (id),
|
PRIMARY KEY (id),
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
|
FOREIGN KEY (user_id) REFERENCES reminders.users(id) ON DELETE SET NULL,
|
||||||
FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE,
|
FOREIGN KEY (guild_id) REFERENCES reminders.guilds(id) ON DELETE CASCADE,
|
||||||
FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE SET NULL
|
FOREIGN KEY (channel_id) REFERENCES reminders.channels(id) ON DELETE SET NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE command_restrictions (
|
CREATE TABLE reminders.command_restrictions (
|
||||||
id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
|
id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
|
||||||
|
|
||||||
role_id INT UNSIGNED NOT NULL,
|
role_id INT UNSIGNED NOT NULL,
|
||||||
command ENUM('todos', 'natural', 'remind', 'interval', 'timer', 'del', 'look', 'alias', 'countdown') NOT NULL,
|
command ENUM('todos', 'natural', 'remind', 'interval', 'timer', 'del', 'look', 'alias', 'countdown') NOT NULL,
|
||||||
|
|
||||||
PRIMARY KEY (id),
|
PRIMARY KEY (id),
|
||||||
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
|
FOREIGN KEY (role_id) REFERENCES reminders.roles(id) ON DELETE CASCADE,
|
||||||
UNIQUE KEY (`role_id`, `command`)
|
UNIQUE KEY (`role_id`, `command`)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE timers (
|
CREATE TABLE reminders.timers (
|
||||||
id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
|
id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
|
||||||
start_time TIMESTAMP NOT NULL DEFAULT NOW(),
|
start_time TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
name VARCHAR(32) NOT NULL,
|
name VARCHAR(32) NOT NULL,
|
||||||
@ -182,7 +186,7 @@ CREATE TABLE timers (
|
|||||||
PRIMARY KEY (id)
|
PRIMARY KEY (id)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE events (
|
CREATE TABLE reminders.events (
|
||||||
id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
|
id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
|
||||||
`time` TIMESTAMP NOT NULL DEFAULT NOW(),
|
`time` TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
|
||||||
@ -194,12 +198,12 @@ CREATE TABLE events (
|
|||||||
reminder_id INT UNSIGNED,
|
reminder_id INT UNSIGNED,
|
||||||
|
|
||||||
PRIMARY KEY (id),
|
PRIMARY KEY (id),
|
||||||
FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE,
|
FOREIGN KEY (guild_id) REFERENCES reminders.guilds(id) ON DELETE CASCADE,
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
|
FOREIGN KEY (user_id) REFERENCES reminders.users(id) ON DELETE SET NULL,
|
||||||
FOREIGN KEY (reminder_id) REFERENCES reminders(id) ON DELETE SET NULL
|
FOREIGN KEY (reminder_id) REFERENCES reminders.reminders(id) ON DELETE SET NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE command_aliases (
|
CREATE TABLE reminders.command_aliases (
|
||||||
id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
|
id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
|
||||||
|
|
||||||
guild_id INT UNSIGNED NOT NULL,
|
guild_id INT UNSIGNED NOT NULL,
|
||||||
@ -208,22 +212,22 @@ CREATE TABLE command_aliases (
|
|||||||
command VARCHAR(2048) NOT NULL,
|
command VARCHAR(2048) NOT NULL,
|
||||||
|
|
||||||
PRIMARY KEY (id),
|
PRIMARY KEY (id),
|
||||||
FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE,
|
FOREIGN KEY (guild_id) REFERENCES reminders.guilds(id) ON DELETE CASCADE,
|
||||||
UNIQUE KEY (`guild_id`, `name`)
|
UNIQUE KEY (`guild_id`, `name`)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE guild_users (
|
CREATE TABLE reminders.guild_users (
|
||||||
guild INT UNSIGNED NOT NULL,
|
guild INT UNSIGNED NOT NULL,
|
||||||
user INT UNSIGNED NOT NULL,
|
user INT UNSIGNED NOT NULL,
|
||||||
|
|
||||||
can_access BOOL NOT NULL DEFAULT 0,
|
can_access BOOL NOT NULL DEFAULT 0,
|
||||||
|
|
||||||
FOREIGN KEY (guild) REFERENCES guilds(id) ON DELETE CASCADE,
|
FOREIGN KEY (guild) REFERENCES reminders.guilds(id) ON DELETE CASCADE,
|
||||||
FOREIGN KEY (user) REFERENCES users(id) ON DELETE CASCADE,
|
FOREIGN KEY (user) REFERENCES reminders.users(id) ON DELETE CASCADE,
|
||||||
UNIQUE KEY (guild, user)
|
UNIQUE KEY (guild, user)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE EVENT event_cleanup
|
CREATE EVENT reminders.event_cleanup
|
||||||
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 DAY
|
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 DAY
|
||||||
ON COMPLETION PRESERVE
|
ON COMPLETION PRESERVE
|
||||||
DO DELETE FROM events WHERE `time` < DATE_SUB(NOW(), INTERVAL 5 DAY);
|
DO DELETE FROM reminders.events WHERE `time` < DATE_SUB(NOW(), INTERVAL 5 DAY);
|
@ -1 +0,0 @@
|
|||||||
*/10 * * * * reminder /lib/reminder-rs/healthcheck
|
|
9
debian/postinst
vendored
@ -1,9 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
id -u reminder &>/dev/null || useradd -r -M reminder
|
|
||||||
|
|
||||||
chown -R reminder /etc/reminder-rs
|
|
||||||
|
|
||||||
#DEBHELPER#
|
|
7
debian/postrm
vendored
@ -1,7 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
id -u reminder &>/dev/null || userdel reminder
|
|
||||||
|
|
||||||
#DEBHELPER#
|
|
46
extract_derive/Cargo.lock
generated
@ -1,46 +0,0 @@
|
|||||||
# This file is automatically @generated by Cargo.
|
|
||||||
# It is not intended for manual editing.
|
|
||||||
version = 3
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "extract_macro"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "proc-macro2"
|
|
||||||
version = "1.0.78"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
|
|
||||||
dependencies = [
|
|
||||||
"unicode-ident",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "quote"
|
|
||||||
version = "1.0.35"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "syn"
|
|
||||||
version = "2.0.49"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "915aea9e586f80826ee59f8453c1101f9d1c4b3964cd2460185ee8e299ada496"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"unicode-ident",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicode-ident"
|
|
||||||
version = "1.0.12"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
|
@ -1,11 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "extract_derive"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
proc-macro = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
quote = "1.0.35"
|
|
||||||
syn = { version = "2.0.49", features = ["full"] }
|
|
@ -1,53 +0,0 @@
|
|||||||
use proc_macro::TokenStream;
|
|
||||||
use syn::{spanned::Spanned, Data, Fields};
|
|
||||||
|
|
||||||
#[proc_macro_derive(Extract)]
|
|
||||||
pub fn extract(input: TokenStream) -> TokenStream {
|
|
||||||
let ast = syn::parse_macro_input!(input);
|
|
||||||
|
|
||||||
impl_extract(&ast)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn impl_extract(ast: &syn::DeriveInput) -> TokenStream {
|
|
||||||
let name = &ast.ident;
|
|
||||||
|
|
||||||
match &ast.data {
|
|
||||||
// Dispatch over struct: extract args directly from context
|
|
||||||
Data::Struct(st) => match &st.fields {
|
|
||||||
Fields::Named(fields) => {
|
|
||||||
let extracted = fields.named.iter().map(|field| {
|
|
||||||
let ident = &field.ident;
|
|
||||||
let ty = &field.ty;
|
|
||||||
|
|
||||||
quote::quote_spanned! {field.span()=>
|
|
||||||
#ident : crate::utils::extract_arg!(ctx, #ident, #ty)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
TokenStream::from(quote::quote! {
|
|
||||||
impl Extract for #name {
|
|
||||||
fn extract(ctx: crate::ApplicationContext) -> Self {
|
|
||||||
Self {
|
|
||||||
#(#extracted,)*
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
Fields::Unit => TokenStream::from(quote::quote! {
|
|
||||||
impl Extract for #name {
|
|
||||||
fn extract(ctx: crate::ApplicationContext) -> Self {
|
|
||||||
Self {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
_ => {
|
|
||||||
panic!("Only named/unit structs can derive Extract");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_ => {
|
|
||||||
panic!("Only structs can derive Extract");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,3 +1,5 @@
|
|||||||
|
USE reminders;
|
||||||
|
|
||||||
SET FOREIGN_KEY_CHECKS = 0;
|
SET FOREIGN_KEY_CHECKS = 0;
|
||||||
|
|
||||||
DROP TABLE IF EXISTS reminders_new;
|
DROP TABLE IF EXISTS reminders_new;
|
||||||
@ -54,7 +56,8 @@ CREATE TABLE reminders_new (
|
|||||||
-- , CONSTRAINT interval_enabled_mutin CHECK (`enabled` = 1 OR `interval` IS NULL)
|
-- , CONSTRAINT interval_enabled_mutin CHECK (`enabled` = 1 OR `interval` IS NULL)
|
||||||
# disallow an expiry time if interval is unspecified
|
# disallow an expiry time if interval is unspecified
|
||||||
-- , CONSTRAINT interval_expires_mutin CHECK (`expires` IS NULL OR `interval` IS NOT NULL)
|
-- , CONSTRAINT interval_expires_mutin CHECK (`expires` IS NULL OR `interval` IS NOT NULL)
|
||||||
);
|
)
|
||||||
|
COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
# import data from other tables
|
# import data from other tables
|
||||||
INSERT INTO reminders_new (
|
INSERT INTO reminders_new (
|
||||||
@ -155,9 +158,4 @@ CREATE TABLE events (
|
|||||||
FOREIGN KEY (reminder_id) REFERENCES reminders_new(id) ON DELETE SET NULL
|
FOREIGN KEY (reminder_id) REFERENCES reminders_new(id) ON DELETE SET NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
DROP TABLE reminders;
|
|
||||||
DROP TABLE embed_fields;
|
|
||||||
RENAME TABLE reminders_new TO reminders;
|
|
||||||
RENAME TABLE embed_fields_new TO embed_fields;
|
|
||||||
|
|
||||||
SET FOREIGN_KEY_CHECKS = 1;
|
SET FOREIGN_KEY_CHECKS = 1;
|
@ -1,11 +0,0 @@
|
|||||||
CREATE TABLE macro (
|
|
||||||
id INT UNSIGNED AUTO_INCREMENT,
|
|
||||||
guild_id INT UNSIGNED NOT NULL,
|
|
||||||
|
|
||||||
name VARCHAR(100) NOT NULL,
|
|
||||||
description VARCHAR(100),
|
|
||||||
commands TEXT NOT NULL,
|
|
||||||
|
|
||||||
FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE,
|
|
||||||
PRIMARY KEY (id)
|
|
||||||
);
|
|
@ -1,2 +0,0 @@
|
|||||||
ALTER TABLE reminders RENAME COLUMN `interval` TO `interval_seconds`;
|
|
||||||
ALTER TABLE reminders ADD COLUMN `interval_months` INT UNSIGNED DEFAULT NULL;
|
|
@ -1,49 +0,0 @@
|
|||||||
CREATE TABLE reminder_template (
|
|
||||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
||||||
|
|
||||||
`name` VARCHAR(24) NOT NULL DEFAULT 'Reminder',
|
|
||||||
|
|
||||||
`guild_id` INT UNSIGNED NOT NULL,
|
|
||||||
|
|
||||||
`username` VARCHAR(32) DEFAULT NULL,
|
|
||||||
`avatar` VARCHAR(512) DEFAULT NULL,
|
|
||||||
|
|
||||||
`content` VARCHAR(2048) NOT NULL DEFAULT '',
|
|
||||||
`tts` BOOL NOT NULL DEFAULT 0,
|
|
||||||
`attachment` MEDIUMBLOB,
|
|
||||||
`attachment_name` VARCHAR(260),
|
|
||||||
|
|
||||||
`embed_title` VARCHAR(256) NOT NULL DEFAULT '',
|
|
||||||
`embed_description` VARCHAR(2048) NOT NULL DEFAULT '',
|
|
||||||
`embed_image_url` VARCHAR(512),
|
|
||||||
`embed_thumbnail_url` VARCHAR(512),
|
|
||||||
`embed_footer` VARCHAR(2048) NOT NULL DEFAULT '',
|
|
||||||
`embed_footer_url` VARCHAR(512),
|
|
||||||
`embed_author` VARCHAR(256) NOT NULL DEFAULT '',
|
|
||||||
`embed_author_url` VARCHAR(512),
|
|
||||||
`embed_color` INT UNSIGNED NOT NULL DEFAULT 0x0,
|
|
||||||
`embed_fields` JSON,
|
|
||||||
|
|
||||||
PRIMARY KEY (id),
|
|
||||||
|
|
||||||
FOREIGN KEY (`guild_id`) REFERENCES guilds (`id`) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
|
|
||||||
ALTER TABLE reminders ADD COLUMN embed_fields JSON;
|
|
||||||
|
|
||||||
update reminders
|
|
||||||
inner join embed_fields as E
|
|
||||||
on E.reminder_id = reminders.id
|
|
||||||
set embed_fields = (
|
|
||||||
select JSON_ARRAYAGG(
|
|
||||||
JSON_OBJECT(
|
|
||||||
'title', E.title,
|
|
||||||
'value', E.value,
|
|
||||||
'inline',
|
|
||||||
if(inline = 1, cast(TRUE as json), cast(FALSE as json))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
from embed_fields
|
|
||||||
group by reminder_id
|
|
||||||
having reminder_id = reminders.id
|
|
||||||
);
|
|
@ -1 +0,0 @@
|
|||||||
ALTER TABLE reminders ADD COLUMN `interval_days` INT UNSIGNED DEFAULT NULL;
|
|
@ -1 +0,0 @@
|
|||||||
ALTER TABLE reminders ADD COLUMN `thread_id` BIGINT DEFAULT NULL;
|
|
@ -1 +0,0 @@
|
|||||||
ALTER TABLE guilds ADD COLUMN ephemeral_confirmations BOOL NOT NULL DEFAULT 0;
|
|
@ -1,2 +0,0 @@
|
|||||||
ALTER TABLE reminders MODIFY `name` VARCHAR(100) NOT NULL DEFAULT 'Reminder';
|
|
||||||
ALTER TABLE reminder_template MODIFY `name` VARCHAR(100) NOT NULL DEFAULT 'Reminder';
|
|
@ -1,9 +0,0 @@
|
|||||||
CREATE TABLE stat (
|
|
||||||
`id` BIGINT NOT NULL AUTO_INCREMENT,
|
|
||||||
`utc_time` DATETIME NOT NULL DEFAULT NOW(),
|
|
||||||
`type` ENUM('reminder_sent', 'reminder_failed'),
|
|
||||||
`reminder_id` INT UNSIGNED,
|
|
||||||
`message` TEXT,
|
|
||||||
|
|
||||||
PRIMARY KEY (`id`)
|
|
||||||
);
|
|
@ -1,2 +0,0 @@
|
|||||||
ALTER TABLE reminders ADD COLUMN `status` ENUM ('pending', 'sent', 'failed', 'deleted') NOT NULL DEFAULT 'pending';
|
|
||||||
ALTER TABLE reminders ADD COLUMN `status_message` TEXT;
|
|
@ -1,3 +0,0 @@
|
|||||||
ALTER TABLE `reminder_template` ADD COLUMN `interval_seconds` INT UNSIGNED;
|
|
||||||
ALTER TABLE `reminder_template` ADD COLUMN `interval_days` INT UNSIGNED;
|
|
||||||
ALTER TABLE `reminder_template` ADD COLUMN `interval_months` INT UNSIGNED;
|
|
@ -1,50 +0,0 @@
|
|||||||
CREATE TABLE command_macro (
|
|
||||||
id INT UNSIGNED AUTO_INCREMENT,
|
|
||||||
guild_id INT UNSIGNED NOT NULL,
|
|
||||||
|
|
||||||
name VARCHAR(100) NOT NULL,
|
|
||||||
description VARCHAR(100),
|
|
||||||
commands JSON NOT NULL,
|
|
||||||
|
|
||||||
FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE,
|
|
||||||
PRIMARY KEY (id)
|
|
||||||
);
|
|
||||||
|
|
||||||
# New JSON structure is {command_name: "Remind", "<option name>": "<option value>", ...}
|
|
||||||
INSERT INTO command_macro (guild_id, description, name, commands)
|
|
||||||
SELECT
|
|
||||||
guild_id,
|
|
||||||
description,
|
|
||||||
name,
|
|
||||||
(
|
|
||||||
SELECT JSON_ARRAYAGG(
|
|
||||||
(
|
|
||||||
SELECT JSON_OBJECTAGG(t2.name, t2.value)
|
|
||||||
FROM JSON_TABLE(
|
|
||||||
JSON_ARRAY_APPEND(t1.options, '$', JSON_OBJECT('name', 'command_name', 'value', t1.command_name)),
|
|
||||||
'$[*]' COLUMNS (
|
|
||||||
name VARCHAR(64) PATH '$.name' ERROR ON ERROR,
|
|
||||||
value TEXT PATH '$.value' ERROR ON ERROR
|
|
||||||
)) AS t2
|
|
||||||
)
|
|
||||||
)
|
|
||||||
FROM macro m2
|
|
||||||
JOIN JSON_TABLE(
|
|
||||||
commands,
|
|
||||||
'$[*]' COLUMNS (
|
|
||||||
command_name VARCHAR(64) PATH '$.command_name' ERROR ON ERROR,
|
|
||||||
options JSON PATH '$.options' ERROR ON ERROR
|
|
||||||
)) AS t1
|
|
||||||
WHERE m1.id = m2.id
|
|
||||||
)
|
|
||||||
FROM macro m1;
|
|
||||||
|
|
||||||
# # Check which commands are used in macros
|
|
||||||
# SELECT DISTINCT command_name
|
|
||||||
# FROM macro m2
|
|
||||||
# JOIN JSON_TABLE(
|
|
||||||
# commands,
|
|
||||||
# '$[*]' COLUMNS (
|
|
||||||
# command_name VARCHAR(64) PATH '$.command_name' ERROR ON ERROR,
|
|
||||||
# options JSON PATH '$.options' ERROR ON ERROR
|
|
||||||
# )) AS t1
|
|
@ -1,5 +0,0 @@
|
|||||||
-- Add migration script here
|
|
||||||
ALTER TABLE reminders
|
|
||||||
ADD INDEX `utc_time_index` (`utc_time`);
|
|
||||||
ALTER TABLE reminders
|
|
||||||
ADD INDEX `status_index` (`status`);
|
|
@ -1,41 +0,0 @@
|
|||||||
server {
|
|
||||||
server_name www.reminder-bot.com;
|
|
||||||
|
|
||||||
return 301 $scheme://reminder-bot.com$request_uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name reminder-bot.com;
|
|
||||||
|
|
||||||
return 301 https://reminder-bot.com$request_uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 443 ssl;
|
|
||||||
server_name reminder-bot.com;
|
|
||||||
|
|
||||||
ssl_certificate /etc/letsencrypt/live/reminder-bot.com/fullchain.pem;
|
|
||||||
ssl_certificate_key /etc/letsencrypt/live/reminder-bot.com/privkey.pem;
|
|
||||||
|
|
||||||
access_log /var/log/nginx/access.log;
|
|
||||||
error_log /var/log/nginx/error.log;
|
|
||||||
|
|
||||||
proxy_buffer_size 128k;
|
|
||||||
proxy_buffers 4 256k;
|
|
||||||
proxy_busy_buffers_size 256k;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
proxy_pass http://localhost:18920;
|
|
||||||
proxy_redirect off;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /static {
|
|
||||||
alias /var/www/reminder-rs/static;
|
|
||||||
expires 30d;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,32 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
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
|
|
||||||
I4/u
|
|
||||||
-----END CERTIFICATE-----
|
|
@ -1,51 +0,0 @@
|
|||||||
-----BEGIN RSA PRIVATE KEY-----
|
|
||||||
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
|
|
||||||
-----END RSA PRIVATE KEY-----
|
|
@ -1,21 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIDYTCCAUmgAwIBAgIUA/EaCcXKgOxBiuaMNTackeF9DgcwDQYJKoZIhvcNAQEL
|
|
||||||
BQAwRzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQKDAlSb2NrZXQg
|
|
||||||
Q0ExFzAVBgNVBAMMDlJvY2tldCBSb290IENBMB4XDTIxMDUxNzE5MDIyNFoXDTMx
|
|
||||||
MDUxNTE5MDIyNFowPzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQ8wDQYDVQQK
|
|
||||||
DAZSb2NrZXQxEjAQBgNVBAMMCWxvY2FsaG9zdDBZMBMGByqGSM49AgEGCCqGSM49
|
|
||||||
AwEHA0IABOEVdmVd218y3Yy+ocjtt5siaFsNR7tknvS21pzKzxsFc05UrF74YqRx
|
|
||||||
Ic6/AQq56C48x6gDhUCdf+nMzD0NWTijGDAWMBQGA1UdEQQNMAuCCWxvY2FsaG9z
|
|
||||||
dDANBgkqhkiG9w0BAQsFAAOCAgEAW32kadHWEIsN5WXacjtgE9jbFii/rWJjrAO/
|
|
||||||
GWuARwLvjWeEFM5xSTny1cTDEz/7Bmfd9GRRpfgxKCFBGaU7zTmOV/AokrjUay+s
|
|
||||||
KPj0EV9ZE/84KVfr+MOuhwXhSSv+NvxduFw1sdhTUHs+Wopwjte+bnOUBXmnQH97
|
|
||||||
ITSQuUvEamPUS4tJD/kQ0qSGJKNv3CShhbe3XkjUd0UKK7lZzn6hY2fy3CrkkJDT
|
|
||||||
GyzFCRf3ZDfjW5/fGXN/TNoEK65pPQVr5taK/bovDesEO7rR4Y4YgtnGlJ7YToWh
|
|
||||||
E6I77CbsJCJdmgpjPNwRlwQ8upoWAfxOHqwg32s6uWq20v4TXZTOnEKOPEJG74bh
|
|
||||||
JHtwqsy2fIdGz4kZksKGerzJffyQPhX4f3qxxrijT9Hj2EoFIQ4u/jTRpK31IW3R
|
|
||||||
gEeNB8O4iyg3crx0oHRwc6zX63O6wBJCZ+0uxLoyjwtjKCxVYbJpccjoQh6zO3UO
|
|
||||||
pqAO+NKxQ469QDBV3uRyyQ7DRPu6p67/8gn1E/o8kyU1P7eLFIhBp4T4wCaMQBG6
|
|
||||||
IpL5W7eCyuOcqfdm471N07Z4KAqiV8eBJcKaAE+WzNIvrFvCZ/zq7Gj8p07nkjK8
|
|
||||||
+ZGDUieiY0mQEpiXLQZt1xNtT/MmlAzFYrK7t6gSygykQKjO1o4GG4g+eXu3h1YK
|
|
||||||
avsOwtc=
|
|
||||||
-----END CERTIFICATE-----
|
|
@ -1,5 +0,0 @@
|
|||||||
-----BEGIN PRIVATE KEY-----
|
|
||||||
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgeo/7wBIYVax9bj4m
|
|
||||||
1UEe2H+H4YLsbApDCZMslZbubRuhRANCAAThFXZlXdtfMt2MvqHI7bebImhbDUe7
|
|
||||||
ZJ70ttacys8bBXNOVKxe+GKkcSHOvwEKueguPMeoA4VAnX/pzMw9DVk4
|
|
||||||
-----END PRIVATE KEY-----
|
|
@ -1,21 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIDfjCCAWagAwIBAgIUfmkM99JpQUsQsh8TVILPQDBGOhowDQYJKoZIhvcNAQEM
|
|
||||||
BQAwRzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQKDAlSb2NrZXQg
|
|
||||||
Q0ExFzAVBgNVBAMMDlJvY2tldCBSb290IENBMB4XDTIxMDUxNzE5MDIyNFoXDTMx
|
|
||||||
MDUxNTE5MDIyNFowPzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQ8wDQYDVQQK
|
|
||||||
DAZSb2NrZXQxEjAQBgNVBAMMCWxvY2FsaG9zdDB2MBAGByqGSM49AgEGBSuBBAAi
|
|
||||||
A2IABM5rQQNZEGWeDyrg2dVQS15c+JsX/ginN9/ArHpTgGO6xaLfkbekIj7gewUR
|
|
||||||
VQcQrYulwu02HTFDzGVvf1DdCZzIJLJUpl+jagdI05yMPFg2s5lThzGB6HENyw1I
|
|
||||||
hU1dMaMYMBYwFAYDVR0RBA0wC4IJbG9jYWxob3N0MA0GCSqGSIb3DQEBDAUAA4IC
|
|
||||||
AQAzpXFEeCMvTBG+kzmHN/+esjCK8MO/Grrw3Qoa1stX0yjNWzfhlGHfWk9Mgwnp
|
|
||||||
DnIEFT9lB+nT9uTkKL81Opu2bkWXuL0MyjyYmcCfEgJeDblUbD4HYzPO/s2P7QZu
|
|
||||||
Oa1pNKBiSWe1xq+F/gkn0+nDfICq3QQOOQMzaAmlFmszN3v7pryz+x21MqTFsfuW
|
|
||||||
ymycRcwZ3VyYeceZuBxjOhR2l8I5yZbR+KPj8VS7A3vgqvkS8ixTK0LrxcLwrTIz
|
|
||||||
W9Z1skzVpux4K7iwn3qlWIJ7EbERi9Ydz0MzpjD+CHxGlgHTzT552DGZhPO8D7BE
|
|
||||||
+4IoS0YeXEGLeC2a9Hmiy3FXbM4nt3aIWMiWyhjslXIbvWOJL1Vi5GNpdQCG+rB7
|
|
||||||
lvA0IISp/tAi9duufdONjgRceRDsqf7o6TOBQdB3QxHRt9gCB2QquRh5llMUY8YH
|
|
||||||
PxFJAEzF4X5b0q0k4b50HRVNfDY0SC/XIR7S2xnLDGuoUqrOpUligBzfXV4JHrPv
|
|
||||||
YKHsWSOiVxU9eY1SYKuKqnOsGvXSSQlYqSK1ol94eOKDjNy8wekaVYAQNsdNL7o5
|
|
||||||
QSWZUcbZLkaNMFdD9twWGduAs0eTichVgyvRgPdevjTGDPBHKNSUURZRTYeW4aIJ
|
|
||||||
QzSQUQXwOXsQOmfCkaaTISsDwZAJ0wFqqST1AOhlCWD1OQ==
|
|
||||||
-----END CERTIFICATE-----
|
|
@ -1,6 +0,0 @@
|
|||||||
-----BEGIN PRIVATE KEY-----
|
|
||||||
MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDCRBt7MeJXyJRq7grGQ
|
|
||||||
jEdBHE31hG5LuKQ5iL8yTVGk75Kc8ISll2LewbvQ3NhR6E+hZANiAATOa0EDWRBl
|
|
||||||
ng8q4NnVUEteXPibF/4IpzffwKx6U4BjusWi35G3pCI+4HsFEVUHEK2LpcLtNh0x
|
|
||||||
Q8xlb39Q3QmcyCSyVKZfo2oHSNOcjDxYNrOZU4cxgehxDcsNSIVNXTE=
|
|
||||||
-----END PRIVATE KEY-----
|
|
@ -1,20 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIDMjCCARqgAwIBAgIUSu1CGfaWlNPjjLG7SaEEgxI+AsAwDQYJKoZIhvcNAQEL
|
|
||||||
BQAwRzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQKDAlSb2NrZXQg
|
|
||||||
Q0ExFzAVBgNVBAMMDlJvY2tldCBSb290IENBMB4XDTIxMDUxNzE5MDIyNFoXDTMx
|
|
||||||
MDUxNTE5MDIyNFowPzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQ8wDQYDVQQK
|
|
||||||
DAZSb2NrZXQxEjAQBgNVBAMMCWxvY2FsaG9zdDAqMAUGAytlcAMhAKNv1fkv5rxY
|
|
||||||
xGT84C98g2xPpain+jsliHvYrqNkS1zhoxgwFjAUBgNVHREEDTALgglsb2NhbGhv
|
|
||||||
c3QwDQYJKoZIhvcNAQELBQADggIBAMgWZhr3whYbFPNYm5RZxjgEonMY7cH/Rza1
|
|
||||||
UrBkyoMRL9v00YKJTEvjl5Xl4ku7jqxY2qjValacDroD8hYTLhhkkbHxH6u+kJSC
|
|
||||||
cKRQ8QVBZMmoor8qD2Y2BuXZYGWEtgeoXh+DLHWOim7gXDD6GyFPnYFGHnRhNmkE
|
|
||||||
6fPcfw1FZmBrMM9rk31EeK9nLVRabL+vuLDEddX1LH4LwK4OuV51wQMZGYiC3M2b
|
|
||||||
JpmuPAUAUyiyN53Utuw/ubeih3cEglOwCyDPFt6XISYHO0UaQdw25uhpjxs8KTZB
|
|
||||||
qOPJAI4cvGaN3GARXvz2P/QHUiTzsf2XOXXtrO0g+F9P7t6x2xL35c0A8yxKkfsa
|
|
||||||
RKdCveRuVlsLXVLVBikqLE/OgPC6SIhIFvTYzXTaZyNfge6dRy2Wg6qWPcGwseeA
|
|
||||||
QpFKgWiY3cuy7nJm6uybR6zOEMj5GgNWIbICGguQ5161MLnv0bEmKh2d9/jQ/XN5
|
|
||||||
M+nixtGla/G4hL3FQIy9rhHVZzPWwSxl+/eE4qgnInEhmlQ2KrcpgneCt38t0vjJ
|
|
||||||
dwHZSzVv8yX7fE0OSq/DWQXJraWUcT7Ds0g7T7jWkWfS0qStVd9VJ+rYQ8dBbE9Y
|
|
||||||
gcmkm1DMUd+y9xDRDjxby7gMDWFiN2Au9FQgaIpIctTNCj0+agFBPnfXPVSIH6gX
|
|
||||||
10kA2ZVX
|
|
||||||
-----END CERTIFICATE-----
|
|
@ -1,3 +0,0 @@
|
|||||||
-----BEGIN PRIVATE KEY-----
|
|
||||||
MC4CAQAwBQYDK2VwBCIEIGtLkaYE4D+P5HLkoc3a/tNhPP+gnhPLF3jEtdYcAtpd
|
|
||||||
-----END PRIVATE KEY-----
|
|
@ -1,114 +0,0 @@
|
|||||||
#! /bin/bash
|
|
||||||
|
|
||||||
# Usage:
|
|
||||||
# ./gen_certs.sh [cert-kind]
|
|
||||||
#
|
|
||||||
# [cert-kind]:
|
|
||||||
# ed25519
|
|
||||||
# rsa_sha256
|
|
||||||
# ecdsa_nistp256_sha256
|
|
||||||
# ecdsa_nistp384_sha384
|
|
||||||
#
|
|
||||||
# Generate a certificate of the [cert-kind] key type, or if no cert-kind is
|
|
||||||
# specified, all of the certificates.
|
|
||||||
#
|
|
||||||
# Examples:
|
|
||||||
# ./gen_certs.sh ed25519
|
|
||||||
# ./gen_certs.sh rsa_sha256
|
|
||||||
|
|
||||||
# TODO: `rustls` (really, `webpki`) doesn't currently use the CN in the subject
|
|
||||||
# to check if a certificate is valid for a server name sent via SNI. It's not
|
|
||||||
# clear if this is intended, since certificates _should_ have a `subjectAltName`
|
|
||||||
# with a DNS name, or if it simply hasn't been implemented yet. See
|
|
||||||
# https://bugzilla.mozilla.org/show_bug.cgi?id=552346 for a bit more info.
|
|
||||||
|
|
||||||
CA_SUBJECT="/C=US/ST=CA/O=Rocket CA/CN=Rocket Root CA"
|
|
||||||
SUBJECT="/C=US/ST=CA/O=Rocket/CN=localhost"
|
|
||||||
ALT="DNS:localhost"
|
|
||||||
|
|
||||||
function gen_ca() {
|
|
||||||
openssl genrsa -out ca_key.pem 4096
|
|
||||||
openssl req -new -x509 -days 3650 -key ca_key.pem \
|
|
||||||
-subj "${CA_SUBJECT}" -out ca_cert.pem
|
|
||||||
}
|
|
||||||
|
|
||||||
function gen_ca_if_non_existent() {
|
|
||||||
if ! [ -f ./ca_cert.pem ]; then gen_ca; fi
|
|
||||||
}
|
|
||||||
|
|
||||||
function gen_rsa_sha256() {
|
|
||||||
gen_ca_if_non_existent
|
|
||||||
|
|
||||||
openssl req -newkey rsa:4096 -nodes -sha256 -keyout rsa_sha256_key.pem \
|
|
||||||
-subj "${SUBJECT}" -out server.csr
|
|
||||||
|
|
||||||
openssl x509 -req -sha256 -extfile <(printf "subjectAltName=${ALT}") -days 3650 \
|
|
||||||
-CA ca_cert.pem -CAkey ca_key.pem -CAcreateserial \
|
|
||||||
-in server.csr -out rsa_sha256_cert.pem
|
|
||||||
|
|
||||||
rm ca_cert.srl server.csr
|
|
||||||
}
|
|
||||||
|
|
||||||
function gen_ed25519() {
|
|
||||||
gen_ca_if_non_existent
|
|
||||||
|
|
||||||
openssl genpkey -algorithm ED25519 > ed25519_key.pem
|
|
||||||
|
|
||||||
openssl req -new -key ed25519_key.pem -subj "${SUBJECT}" -out server.csr
|
|
||||||
openssl x509 -req -extfile <(printf "subjectAltName=${ALT}") -days 3650 \
|
|
||||||
-CA ca_cert.pem -CAkey ca_key.pem -CAcreateserial \
|
|
||||||
-in server.csr -out ed25519_cert.pem
|
|
||||||
|
|
||||||
rm ca_cert.srl server.csr
|
|
||||||
}
|
|
||||||
|
|
||||||
function gen_ecdsa_nistp256_sha256() {
|
|
||||||
gen_ca_if_non_existent
|
|
||||||
|
|
||||||
openssl ecparam -out ecdsa_nistp256_sha256_key.pem -name prime256v1 -genkey
|
|
||||||
|
|
||||||
# Convert to pkcs8 format supported by rustls
|
|
||||||
openssl pkcs8 -topk8 -nocrypt -in ecdsa_nistp256_sha256_key.pem \
|
|
||||||
-out ecdsa_nistp256_sha256_key_pkcs8.pem
|
|
||||||
|
|
||||||
openssl req -new -nodes -sha256 -key ecdsa_nistp256_sha256_key_pkcs8.pem \
|
|
||||||
-subj "${SUBJECT}" -out server.csr
|
|
||||||
|
|
||||||
openssl x509 -req -sha256 -extfile <(printf "subjectAltName=${ALT}") -days 3650 \
|
|
||||||
-CA ca_cert.pem -CAkey ca_key.pem -CAcreateserial \
|
|
||||||
-in server.csr -out ecdsa_nistp256_sha256_cert.pem
|
|
||||||
|
|
||||||
rm ca_cert.srl server.csr ecdsa_nistp256_sha256_key.pem
|
|
||||||
}
|
|
||||||
|
|
||||||
function gen_ecdsa_nistp384_sha384() {
|
|
||||||
gen_ca_if_non_existent
|
|
||||||
|
|
||||||
openssl ecparam -out ecdsa_nistp384_sha384_key.pem -name secp384r1 -genkey
|
|
||||||
|
|
||||||
# Convert to pkcs8 format supported by rustls
|
|
||||||
openssl pkcs8 -topk8 -nocrypt -in ecdsa_nistp384_sha384_key.pem \
|
|
||||||
-out ecdsa_nistp384_sha384_key_pkcs8.pem
|
|
||||||
|
|
||||||
openssl req -new -nodes -sha384 -key ecdsa_nistp384_sha384_key_pkcs8.pem \
|
|
||||||
-subj "${SUBJECT}" -out server.csr
|
|
||||||
|
|
||||||
openssl x509 -req -sha384 -extfile <(printf "subjectAltName=${ALT}") -days 3650 \
|
|
||||||
-CA ca_cert.pem -CAkey ca_key.pem -CAcreateserial \
|
|
||||||
-in server.csr -out ecdsa_nistp384_sha384_cert.pem
|
|
||||||
|
|
||||||
rm ca_cert.srl server.csr ecdsa_nistp384_sha384_key.pem
|
|
||||||
}
|
|
||||||
|
|
||||||
case $1 in
|
|
||||||
ed25519) gen_ed25519 ;;
|
|
||||||
rsa_sha256) gen_rsa_sha256 ;;
|
|
||||||
ecdsa_nistp256_sha256) gen_ecdsa_nistp256_sha256 ;;
|
|
||||||
ecdsa_nistp384_sha384) gen_ecdsa_nistp384_sha384 ;;
|
|
||||||
*)
|
|
||||||
gen_ed25519
|
|
||||||
gen_rsa_sha256
|
|
||||||
gen_ecdsa_nistp256_sha256
|
|
||||||
gen_ecdsa_nistp384_sha384
|
|
||||||
;;
|
|
||||||
esac
|
|
@ -1,30 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
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
|
|
||||||
-----END CERTIFICATE-----
|
|
@ -1,52 +0,0 @@
|
|||||||
-----BEGIN PRIVATE KEY-----
|
|
||||||
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
|
|
||||||
ce4G+zZOOYXwvWGJLwNhgsve8C3oqg==
|
|
||||||
-----END PRIVATE KEY-----
|
|
@ -1,11 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "recordable_derive"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
proc-macro = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
quote = "1.0.35"
|
|
||||||
syn = { version = "2.0.49", features = ["full"] }
|
|
@ -1,42 +0,0 @@
|
|||||||
use proc_macro::TokenStream;
|
|
||||||
use syn::{spanned::Spanned, Data};
|
|
||||||
|
|
||||||
/// Macro to allow Recordable to be implemented on an enum by dispatching over each variant.
|
|
||||||
#[proc_macro_derive(Recordable)]
|
|
||||||
pub fn extract(input: TokenStream) -> TokenStream {
|
|
||||||
let ast = syn::parse_macro_input!(input);
|
|
||||||
|
|
||||||
impl_recordable(&ast)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn impl_recordable(ast: &syn::DeriveInput) -> TokenStream {
|
|
||||||
let name = &ast.ident;
|
|
||||||
|
|
||||||
match &ast.data {
|
|
||||||
Data::Enum(en) => {
|
|
||||||
let extracted = en.variants.iter().map(|var| {
|
|
||||||
let ident = &var.ident;
|
|
||||||
|
|
||||||
quote::quote_spanned! {var.span()=>
|
|
||||||
Self::#ident (opt) => opt.run(ctx).await?
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
TokenStream::from(quote::quote! {
|
|
||||||
impl Recordable for #name {
|
|
||||||
async fn run(self, ctx: crate::Context<'_>) -> Result<(), crate::Error> {
|
|
||||||
match self {
|
|
||||||
#(#extracted,)*
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
_ => {
|
|
||||||
panic!("Only enums can derive Recordable");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
14
regex_command_attr/Cargo.toml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "regex_command_attr"
|
||||||
|
version = "0.2.0"
|
||||||
|
authors = ["acdenisSK <acdenissk69@gmail.com>", "jellywx <judesouthworth@pm.me>"]
|
||||||
|
edition = "2018"
|
||||||
|
description = "Procedural macros for command creation for the RegexFramework for serenity."
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
proc-macro = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
quote = "^1.0"
|
||||||
|
syn = { version = "^1.0", features = ["full", "derive", "extra-traits"] }
|
||||||
|
proc-macro2 = "1.0"
|
293
regex_command_attr/src/attributes.rs
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
use proc_macro2::Span;
|
||||||
|
use syn::parse::{Error, Result};
|
||||||
|
use syn::spanned::Spanned;
|
||||||
|
use syn::{Attribute, Ident, Lit, LitStr, Meta, NestedMeta, Path};
|
||||||
|
|
||||||
|
use crate::structures::PermissionLevel;
|
||||||
|
use crate::util::{AsOption, LitExt};
|
||||||
|
|
||||||
|
use std::fmt::{self, Write};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub enum ValueKind {
|
||||||
|
// #[<name>]
|
||||||
|
Name,
|
||||||
|
|
||||||
|
// #[<name> = <value>]
|
||||||
|
Equals,
|
||||||
|
|
||||||
|
// #[<name>([<value>, <value>, <value>, ...])]
|
||||||
|
List,
|
||||||
|
|
||||||
|
// #[<name>(<value>)]
|
||||||
|
SingleList,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ValueKind {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
ValueKind::Name => f.pad("`#[<name>]`"),
|
||||||
|
ValueKind::Equals => f.pad("`#[<name> = <value>]`"),
|
||||||
|
ValueKind::List => f.pad("`#[<name>([<value>, <value>, <value>, ...])]`"),
|
||||||
|
ValueKind::SingleList => f.pad("`#[<name>(<value>)]`"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_ident(p: Path) -> Result<Ident> {
|
||||||
|
if p.segments.is_empty() {
|
||||||
|
return Err(Error::new(
|
||||||
|
p.span(),
|
||||||
|
"cannot convert an empty path to an identifier",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.segments.len() > 1 {
|
||||||
|
return Err(Error::new(
|
||||||
|
p.span(),
|
||||||
|
"the path must not have more than one segment",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !p.segments[0].arguments.is_empty() {
|
||||||
|
return Err(Error::new(
|
||||||
|
p.span(),
|
||||||
|
"the singular path segment must not have any arguments",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(p.segments[0].ident.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Values {
|
||||||
|
pub name: Ident,
|
||||||
|
pub literals: Vec<Lit>,
|
||||||
|
pub kind: ValueKind,
|
||||||
|
pub span: Span,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Values {
|
||||||
|
#[inline]
|
||||||
|
pub fn new(name: Ident, kind: ValueKind, literals: Vec<Lit>, span: Span) -> Self {
|
||||||
|
Values {
|
||||||
|
name,
|
||||||
|
literals,
|
||||||
|
kind,
|
||||||
|
span,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_values(attr: &Attribute) -> Result<Values> {
|
||||||
|
let meta = attr.parse_meta()?;
|
||||||
|
|
||||||
|
match meta {
|
||||||
|
Meta::Path(path) => {
|
||||||
|
let name = to_ident(path)?;
|
||||||
|
|
||||||
|
Ok(Values::new(name, ValueKind::Name, Vec::new(), attr.span()))
|
||||||
|
}
|
||||||
|
Meta::List(meta) => {
|
||||||
|
let name = to_ident(meta.path)?;
|
||||||
|
let nested = meta.nested;
|
||||||
|
|
||||||
|
if nested.is_empty() {
|
||||||
|
return Err(Error::new(attr.span(), "list cannot be empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut lits = Vec::with_capacity(nested.len());
|
||||||
|
|
||||||
|
for meta in nested {
|
||||||
|
match meta {
|
||||||
|
NestedMeta::Lit(l) => lits.push(l),
|
||||||
|
NestedMeta::Meta(m) => match m {
|
||||||
|
Meta::Path(path) => {
|
||||||
|
let i = to_ident(path)?;
|
||||||
|
lits.push(Lit::Str(LitStr::new(&i.to_string(), i.span())))
|
||||||
|
}
|
||||||
|
Meta::List(_) | Meta::NameValue(_) => {
|
||||||
|
return Err(Error::new(attr.span(), "cannot nest a list; only accept literals and identifiers at this level"))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let kind = if lits.len() == 1 {
|
||||||
|
ValueKind::SingleList
|
||||||
|
} else {
|
||||||
|
ValueKind::List
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Values::new(name, kind, lits, attr.span()))
|
||||||
|
}
|
||||||
|
Meta::NameValue(meta) => {
|
||||||
|
let name = to_ident(meta.path)?;
|
||||||
|
let lit = meta.lit;
|
||||||
|
|
||||||
|
Ok(Values::new(name, ValueKind::Equals, vec![lit], attr.span()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct DisplaySlice<'a, T>(&'a [T]);
|
||||||
|
|
||||||
|
impl<'a, T: fmt::Display> fmt::Display for DisplaySlice<'a, T> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let mut iter = self.0.iter().enumerate();
|
||||||
|
|
||||||
|
match iter.next() {
|
||||||
|
None => f.write_str("nothing")?,
|
||||||
|
Some((idx, elem)) => {
|
||||||
|
write!(f, "{}: {}", idx, elem)?;
|
||||||
|
|
||||||
|
for (idx, elem) in iter {
|
||||||
|
f.write_char('\n')?;
|
||||||
|
write!(f, "{}: {}", idx, elem)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn is_form_acceptable(expect: &[ValueKind], kind: ValueKind) -> bool {
|
||||||
|
if expect.contains(&ValueKind::List) && kind == ValueKind::SingleList {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
expect.contains(&kind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn validate(values: &Values, forms: &[ValueKind]) -> Result<()> {
|
||||||
|
if !is_form_acceptable(forms, values.kind) {
|
||||||
|
return Err(Error::new(
|
||||||
|
values.span,
|
||||||
|
// Using the `_args` version here to avoid an allocation.
|
||||||
|
format_args!(
|
||||||
|
"the attribute must be in of these forms:\n{}",
|
||||||
|
DisplaySlice(forms)
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn parse<T: AttributeOption>(values: Values) -> Result<T> {
|
||||||
|
T::parse(values)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait AttributeOption: Sized {
|
||||||
|
fn parse(values: Values) -> Result<Self>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AttributeOption for Vec<String> {
|
||||||
|
fn parse(values: Values) -> Result<Self> {
|
||||||
|
validate(&values, &[ValueKind::List])?;
|
||||||
|
|
||||||
|
Ok(values
|
||||||
|
.literals
|
||||||
|
.into_iter()
|
||||||
|
.map(|lit| lit.to_str())
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AttributeOption for String {
|
||||||
|
#[inline]
|
||||||
|
fn parse(values: Values) -> Result<Self> {
|
||||||
|
validate(&values, &[ValueKind::Equals, ValueKind::SingleList])?;
|
||||||
|
|
||||||
|
Ok(values.literals[0].to_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AttributeOption for bool {
|
||||||
|
#[inline]
|
||||||
|
fn parse(values: Values) -> Result<Self> {
|
||||||
|
validate(&values, &[ValueKind::Name, ValueKind::SingleList])?;
|
||||||
|
|
||||||
|
Ok(values.literals.get(0).map_or(true, |l| l.to_bool()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AttributeOption for Ident {
|
||||||
|
#[inline]
|
||||||
|
fn parse(values: Values) -> Result<Self> {
|
||||||
|
validate(&values, &[ValueKind::SingleList])?;
|
||||||
|
|
||||||
|
Ok(values.literals[0].to_ident())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AttributeOption for Vec<Ident> {
|
||||||
|
#[inline]
|
||||||
|
fn parse(values: Values) -> Result<Self> {
|
||||||
|
validate(&values, &[ValueKind::List])?;
|
||||||
|
|
||||||
|
Ok(values.literals.into_iter().map(|l| l.to_ident()).collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AttributeOption for Option<String> {
|
||||||
|
fn parse(values: Values) -> Result<Self> {
|
||||||
|
validate(&values, &[ValueKind::Name, ValueKind::Equals, ValueKind::SingleList])?;
|
||||||
|
|
||||||
|
Ok(values.literals.get(0).map(|l| l.to_str()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AttributeOption for PermissionLevel {
|
||||||
|
fn parse(values: Values) -> Result<Self> {
|
||||||
|
validate(&values, &[ValueKind::SingleList])?;
|
||||||
|
|
||||||
|
Ok(values.literals.get(0).map(|l| PermissionLevel::from_str(&*l.to_str()).unwrap()).unwrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: AttributeOption> AttributeOption for AsOption<T> {
|
||||||
|
#[inline]
|
||||||
|
fn parse(values: Values) -> Result<Self> {
|
||||||
|
Ok(AsOption(Some(T::parse(values)?)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! attr_option_num {
|
||||||
|
($($n:ty),*) => {
|
||||||
|
$(
|
||||||
|
impl AttributeOption for $n {
|
||||||
|
fn parse(values: Values) -> Result<Self> {
|
||||||
|
validate(&values, &[ValueKind::SingleList])?;
|
||||||
|
|
||||||
|
Ok(match &values.literals[0] {
|
||||||
|
Lit::Int(l) => l.base10_parse::<$n>()?,
|
||||||
|
l => {
|
||||||
|
let s = l.to_str();
|
||||||
|
// Use `as_str` to guide the compiler to use `&str`'s parse method.
|
||||||
|
// We don't want to use our `parse` method here (`impl AttributeOption for String`).
|
||||||
|
match s.as_str().parse::<$n>() {
|
||||||
|
Ok(n) => n,
|
||||||
|
Err(_) => return Err(Error::new(l.span(), "invalid integer")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AttributeOption for Option<$n> {
|
||||||
|
#[inline]
|
||||||
|
fn parse(values: Values) -> Result<Self> {
|
||||||
|
<$n as AttributeOption>::parse(values).map(Some)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attr_option_num!(u16, u32, usize);
|
5
regex_command_attr/src/consts.rs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
pub mod suffixes {
|
||||||
|
pub const COMMAND: &str = "COMMAND";
|
||||||
|
}
|
||||||
|
|
||||||
|
pub use self::suffixes::*;
|
102
regex_command_attr/src/lib.rs
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
#![deny(rust_2018_idioms)]
|
||||||
|
// FIXME: Remove this in a foreseeable future.
|
||||||
|
// Currently exists for backwards compatibility to previous Rust versions.
|
||||||
|
#![recursion_limit = "128"]
|
||||||
|
|
||||||
|
#[allow(unused_extern_crates)]
|
||||||
|
extern crate proc_macro;
|
||||||
|
|
||||||
|
use proc_macro::TokenStream;
|
||||||
|
use quote::quote;
|
||||||
|
use syn::{parse::Error, parse_macro_input, spanned::Spanned, Lit};
|
||||||
|
|
||||||
|
pub(crate) mod attributes;
|
||||||
|
pub(crate) mod consts;
|
||||||
|
pub(crate) mod structures;
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
pub(crate) mod util;
|
||||||
|
|
||||||
|
use attributes::*;
|
||||||
|
use consts::*;
|
||||||
|
use structures::*;
|
||||||
|
use util::*;
|
||||||
|
|
||||||
|
macro_rules! match_options {
|
||||||
|
($v:expr, $values:ident, $options:ident, $span:expr => [$($name:ident);*]) => {
|
||||||
|
match $v {
|
||||||
|
$(
|
||||||
|
stringify!($name) => $options.$name = propagate_err!($crate::attributes::parse($values)),
|
||||||
|
)*
|
||||||
|
_ => {
|
||||||
|
return Error::new($span, format_args!("invalid attribute: {:?}", $v))
|
||||||
|
.to_compile_error()
|
||||||
|
.into();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[proc_macro_attribute]
|
||||||
|
pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream {
|
||||||
|
let mut fun = parse_macro_input!(input as CommandFun);
|
||||||
|
|
||||||
|
let lit_name = if !attr.is_empty() {
|
||||||
|
parse_macro_input!(attr as Lit).to_str()
|
||||||
|
} else {
|
||||||
|
fun.name.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut options = Options::new();
|
||||||
|
|
||||||
|
for attribute in &fun.attributes {
|
||||||
|
let span = attribute.span();
|
||||||
|
let values = propagate_err!(parse_values(attribute));
|
||||||
|
|
||||||
|
let name = values.name.to_string();
|
||||||
|
let name = &name[..];
|
||||||
|
|
||||||
|
match_options!(name, values, options, span => [
|
||||||
|
permission_level;
|
||||||
|
supports_dm;
|
||||||
|
can_blacklist
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let Options {
|
||||||
|
permission_level,
|
||||||
|
supports_dm,
|
||||||
|
can_blacklist,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
let visibility = fun.visibility;
|
||||||
|
let name = fun.name.clone();
|
||||||
|
let body = fun.body;
|
||||||
|
|
||||||
|
let n = name.with_suffix(COMMAND);
|
||||||
|
|
||||||
|
let cooked = fun.cooked.clone();
|
||||||
|
|
||||||
|
let command_path = quote!(crate::framework::Command);
|
||||||
|
|
||||||
|
populate_fut_lifetimes_on_refs(&mut fun.args);
|
||||||
|
let args = fun.args;
|
||||||
|
|
||||||
|
(quote! {
|
||||||
|
#(#cooked)*
|
||||||
|
pub static #n: #command_path = #command_path {
|
||||||
|
func: #name,
|
||||||
|
name: #lit_name,
|
||||||
|
required_perms: #permission_level,
|
||||||
|
supports_dm: #supports_dm,
|
||||||
|
can_blacklist: #can_blacklist,
|
||||||
|
};
|
||||||
|
|
||||||
|
#visibility fn #name<'fut> (#(#args),*) -> ::serenity::futures::future::BoxFuture<'fut, ()> {
|
||||||
|
use ::serenity::futures::future::FutureExt;
|
||||||
|
|
||||||
|
async move { #(#body)* }.boxed()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.into()
|
||||||
|
}
|
231
regex_command_attr/src/structures.rs
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
use crate::util::{Argument, Parenthesised};
|
||||||
|
use proc_macro2::Span;
|
||||||
|
use proc_macro2::TokenStream as TokenStream2;
|
||||||
|
use quote::{quote, ToTokens};
|
||||||
|
use syn::{
|
||||||
|
braced,
|
||||||
|
parse::{Error, Parse, ParseStream, Result},
|
||||||
|
spanned::Spanned,
|
||||||
|
Attribute, Block, FnArg, Ident, Pat, Path, PathSegment, Stmt, Token, Visibility,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn parse_argument(arg: FnArg) -> Result<Argument> {
|
||||||
|
match arg {
|
||||||
|
FnArg::Typed(typed) => {
|
||||||
|
let pat = typed.pat;
|
||||||
|
let kind = typed.ty;
|
||||||
|
|
||||||
|
match *pat {
|
||||||
|
Pat::Ident(id) => {
|
||||||
|
let name = id.ident;
|
||||||
|
let mutable = id.mutability;
|
||||||
|
|
||||||
|
Ok(Argument {
|
||||||
|
mutable,
|
||||||
|
name,
|
||||||
|
kind: *kind,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Pat::Wild(wild) => {
|
||||||
|
let token = wild.underscore_token;
|
||||||
|
|
||||||
|
let name = Ident::new("_", token.spans[0]);
|
||||||
|
|
||||||
|
Ok(Argument {
|
||||||
|
mutable: None,
|
||||||
|
name,
|
||||||
|
kind: *kind,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => Err(Error::new(
|
||||||
|
pat.span(),
|
||||||
|
format_args!("unsupported pattern: {:?}", pat),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FnArg::Receiver(_) => Err(Error::new(
|
||||||
|
arg.span(),
|
||||||
|
format_args!("`self` arguments are prohibited: {:?}", arg),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test if the attribute is cooked.
|
||||||
|
fn is_cooked(attr: &Attribute) -> bool {
|
||||||
|
const COOKED_ATTRIBUTE_NAMES: &[&str] = &[
|
||||||
|
"cfg", "cfg_attr", "doc", "derive", "inline", "allow", "warn", "deny", "forbid",
|
||||||
|
];
|
||||||
|
|
||||||
|
COOKED_ATTRIBUTE_NAMES.iter().any(|n| attr.path.is_ident(n))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes cooked attributes from a vector of attributes. Uncooked attributes are left in the vector.
|
||||||
|
///
|
||||||
|
/// # Return
|
||||||
|
///
|
||||||
|
/// Returns a vector of cooked attributes that have been removed from the input vector.
|
||||||
|
fn remove_cooked(attrs: &mut Vec<Attribute>) -> Vec<Attribute> {
|
||||||
|
let mut cooked = Vec::new();
|
||||||
|
|
||||||
|
// FIXME: Replace with `Vec::drain_filter` once it is stable.
|
||||||
|
let mut i = 0;
|
||||||
|
while i < attrs.len() {
|
||||||
|
if !is_cooked(&attrs[i]) {
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
cooked.push(attrs.remove(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
cooked
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct CommandFun {
|
||||||
|
/// `#[...]`-style attributes.
|
||||||
|
pub attributes: Vec<Attribute>,
|
||||||
|
/// Populated cooked attributes. These are attributes outside of the realm of this crate's procedural macros
|
||||||
|
/// and will appear in generated output.
|
||||||
|
pub cooked: Vec<Attribute>,
|
||||||
|
pub visibility: Visibility,
|
||||||
|
pub name: Ident,
|
||||||
|
pub args: Vec<Argument>,
|
||||||
|
pub body: Vec<Stmt>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parse for CommandFun {
|
||||||
|
fn parse(input: ParseStream<'_>) -> Result<Self> {
|
||||||
|
let mut attributes = input.call(Attribute::parse_outer)?;
|
||||||
|
|
||||||
|
// `#[doc = "..."]` is a cooked attribute but it is special-cased for commands.
|
||||||
|
for attr in &mut attributes {
|
||||||
|
// Rename documentation comment attributes (`#[doc = "..."]`) to `#[description = "..."]`.
|
||||||
|
if attr.path.is_ident("doc") {
|
||||||
|
attr.path = Path::from(PathSegment::from(Ident::new(
|
||||||
|
"description",
|
||||||
|
Span::call_site(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let cooked = remove_cooked(&mut attributes);
|
||||||
|
|
||||||
|
let visibility = input.parse::<Visibility>()?;
|
||||||
|
|
||||||
|
input.parse::<Token![async]>()?;
|
||||||
|
|
||||||
|
input.parse::<Token![fn]>()?;
|
||||||
|
let name = input.parse()?;
|
||||||
|
|
||||||
|
// (...)
|
||||||
|
let Parenthesised(args) = input.parse::<Parenthesised<FnArg>>()?;
|
||||||
|
|
||||||
|
// { ... }
|
||||||
|
let bcont;
|
||||||
|
braced!(bcont in input);
|
||||||
|
let body = bcont.call(Block::parse_within)?;
|
||||||
|
|
||||||
|
let args = args
|
||||||
|
.into_iter()
|
||||||
|
.map(parse_argument)
|
||||||
|
.collect::<Result<Vec<_>>>()?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
attributes,
|
||||||
|
cooked,
|
||||||
|
visibility,
|
||||||
|
name,
|
||||||
|
args,
|
||||||
|
body,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToTokens for CommandFun {
|
||||||
|
fn to_tokens(&self, stream: &mut TokenStream2) {
|
||||||
|
let Self {
|
||||||
|
attributes: _,
|
||||||
|
cooked,
|
||||||
|
visibility,
|
||||||
|
name,
|
||||||
|
args,
|
||||||
|
body,
|
||||||
|
} = self;
|
||||||
|
|
||||||
|
stream.extend(quote! {
|
||||||
|
#(#cooked)*
|
||||||
|
#visibility async fn #name (#(#args),*) -> () {
|
||||||
|
#(#body)*
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum PermissionLevel {
|
||||||
|
Unrestricted,
|
||||||
|
Managed,
|
||||||
|
Restricted,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PermissionLevel {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Unrestricted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PermissionLevel {
|
||||||
|
pub fn from_str(s: &str) -> Option<Self> {
|
||||||
|
Some(match s.to_uppercase().as_str() {
|
||||||
|
"UNRESTRICTED" => Self::Unrestricted,
|
||||||
|
"MANAGED" => Self::Managed,
|
||||||
|
"RESTRICTED" => Self::Restricted,
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToTokens for PermissionLevel {
|
||||||
|
fn to_tokens(&self, stream: &mut TokenStream2) {
|
||||||
|
let path = quote!(crate::framework::PermissionLevel);
|
||||||
|
let variant;
|
||||||
|
|
||||||
|
match self {
|
||||||
|
Self::Unrestricted => {
|
||||||
|
variant = quote!(Unrestricted);
|
||||||
|
}
|
||||||
|
|
||||||
|
Self::Managed => {
|
||||||
|
variant = quote!(Managed);
|
||||||
|
}
|
||||||
|
|
||||||
|
Self::Restricted => {
|
||||||
|
variant = quote!(Restricted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.extend(quote! {
|
||||||
|
#path::#variant
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct Options {
|
||||||
|
pub permission_level: PermissionLevel,
|
||||||
|
pub supports_dm: bool,
|
||||||
|
pub can_blacklist: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Options {
|
||||||
|
#[inline]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let mut options = Self::default();
|
||||||
|
|
||||||
|
options.can_blacklist = true;
|
||||||
|
options.supports_dm = true;
|
||||||
|
|
||||||
|
options
|
||||||
|
}
|
||||||
|
}
|
160
regex_command_attr/src/util.rs
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
use proc_macro::TokenStream;
|
||||||
|
use proc_macro2::Span;
|
||||||
|
use proc_macro2::TokenStream as TokenStream2;
|
||||||
|
use quote::{format_ident, quote, ToTokens};
|
||||||
|
use syn::{
|
||||||
|
braced, bracketed, parenthesized,
|
||||||
|
parse::{Error, Parse, ParseStream, Result as SynResult},
|
||||||
|
punctuated::Punctuated,
|
||||||
|
token::{Comma, Mut},
|
||||||
|
Ident, Lifetime, Lit, Type,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub trait LitExt {
|
||||||
|
fn to_str(&self) -> String;
|
||||||
|
fn to_bool(&self) -> bool;
|
||||||
|
fn to_ident(&self) -> Ident;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LitExt for Lit {
|
||||||
|
fn to_str(&self) -> String {
|
||||||
|
match self {
|
||||||
|
Lit::Str(s) => s.value(),
|
||||||
|
Lit::ByteStr(s) => unsafe { String::from_utf8_unchecked(s.value()) },
|
||||||
|
Lit::Char(c) => c.value().to_string(),
|
||||||
|
Lit::Byte(b) => (b.value() as char).to_string(),
|
||||||
|
_ => panic!("values must be a (byte)string or a char"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_bool(&self) -> bool {
|
||||||
|
if let Lit::Bool(b) = self {
|
||||||
|
b.value
|
||||||
|
} else {
|
||||||
|
self.to_str()
|
||||||
|
.parse()
|
||||||
|
.unwrap_or_else(|_| panic!("expected bool from {:?}", self))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn to_ident(&self) -> Ident {
|
||||||
|
Ident::new(&self.to_str(), self.span())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait IdentExt2: Sized {
|
||||||
|
fn to_uppercase(&self) -> Self;
|
||||||
|
fn with_suffix(&self, suf: &str) -> Ident;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IdentExt2 for Ident {
|
||||||
|
#[inline]
|
||||||
|
fn to_uppercase(&self) -> Self {
|
||||||
|
format_ident!("{}", self.to_string().to_uppercase())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn with_suffix(&self, suffix: &str) -> Ident {
|
||||||
|
format_ident!("{}_{}", self.to_string().to_uppercase(), suffix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn into_stream(e: Error) -> TokenStream {
|
||||||
|
e.to_compile_error().into()
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! propagate_err {
|
||||||
|
($res:expr) => {{
|
||||||
|
match $res {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => return $crate::util::into_stream(e),
|
||||||
|
}
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Bracketed<T>(pub Punctuated<T, Comma>);
|
||||||
|
|
||||||
|
impl<T: Parse> Parse for Bracketed<T> {
|
||||||
|
fn parse(input: ParseStream<'_>) -> SynResult<Self> {
|
||||||
|
let content;
|
||||||
|
bracketed!(content in input);
|
||||||
|
|
||||||
|
Ok(Bracketed(content.parse_terminated(T::parse)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Braced<T>(pub Punctuated<T, Comma>);
|
||||||
|
|
||||||
|
impl<T: Parse> Parse for Braced<T> {
|
||||||
|
fn parse(input: ParseStream<'_>) -> SynResult<Self> {
|
||||||
|
let content;
|
||||||
|
braced!(content in input);
|
||||||
|
|
||||||
|
Ok(Braced(content.parse_terminated(T::parse)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Parenthesised<T>(pub Punctuated<T, Comma>);
|
||||||
|
|
||||||
|
impl<T: Parse> Parse for Parenthesised<T> {
|
||||||
|
fn parse(input: ParseStream<'_>) -> SynResult<Self> {
|
||||||
|
let content;
|
||||||
|
parenthesized!(content in input);
|
||||||
|
|
||||||
|
Ok(Parenthesised(content.parse_terminated(T::parse)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct AsOption<T>(pub Option<T>);
|
||||||
|
|
||||||
|
impl<T: ToTokens> ToTokens for AsOption<T> {
|
||||||
|
fn to_tokens(&self, stream: &mut TokenStream2) {
|
||||||
|
match &self.0 {
|
||||||
|
Some(o) => stream.extend(quote!(Some(#o))),
|
||||||
|
None => stream.extend(quote!(None)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Default for AsOption<T> {
|
||||||
|
#[inline]
|
||||||
|
fn default() -> Self {
|
||||||
|
AsOption(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Argument {
|
||||||
|
pub mutable: Option<Mut>,
|
||||||
|
pub name: Ident,
|
||||||
|
pub kind: Type,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToTokens for Argument {
|
||||||
|
fn to_tokens(&self, stream: &mut TokenStream2) {
|
||||||
|
let Argument {
|
||||||
|
mutable,
|
||||||
|
name,
|
||||||
|
kind,
|
||||||
|
} = self;
|
||||||
|
|
||||||
|
stream.extend(quote! {
|
||||||
|
#mutable #name: #kind
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn populate_fut_lifetimes_on_refs(args: &mut Vec<Argument>) {
|
||||||
|
for arg in args {
|
||||||
|
if let Type::Reference(reference) = &mut arg.kind {
|
||||||
|
reference.lifetime = Some(Lifetime::new("'fut", Span::call_site()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,2 +0,0 @@
|
|||||||
printWidth = 100
|
|
||||||
tabWidth = 4
|
|
@ -1,19 +0,0 @@
|
|||||||
# reminder-dashboard
|
|
||||||
|
|
||||||
The re-re-rewrite of the dashboard.
|
|
||||||
|
|
||||||
## Why
|
|
||||||
|
|
||||||
The existing beta variant of the dashboard is written using vanilla JavaScript. This is fine,
|
|
||||||
but annoying to update. This would've been okay if I was more dedicated to updating the vanilla
|
|
||||||
JavaScript too, but I want to experiment with "new" things.
|
|
||||||
|
|
||||||
This also allows me to expand my frontend skills, which is relevant to part of my job.
|
|
||||||
|
|
||||||
## Developing
|
|
||||||
|
|
||||||
1. Run both `npm run dev` and `cargo run`
|
|
||||||
2. Symlink assets: assuming cloned
|
|
||||||
into `$HOME`, `ln -s $HOME/reminder-bot/reminder-dashboard/dist/index.html $HOME/reminder-bot/web/static/index.html`
|
|
||||||
and
|
|
||||||
`ln -s $HOME/reminder-bot/reminder-dashboard/dist/static/assets $HOME/reminder-bot/web/static/assets`
|
|
@ -1,35 +0,0 @@
|
|||||||
<!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/bulma.min.css">
|
|
||||||
<link rel="stylesheet" href="/static/css/fa.css">
|
|
||||||
<link rel="stylesheet" href="/static/css/font.css">
|
|
||||||
<link rel="stylesheet" href="/static/css/style.css">
|
|
||||||
<link rel="stylesheet" href="/static/css/dtsel.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app"></div>
|
|
||||||
<script type="module" src="/src/index.tsx"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
5467
reminder-dashboard/package-lock.json
generated
@ -1,32 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "example",
|
|
||||||
"private": true,
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite build --watch --mode development",
|
|
||||||
"build": "vite build",
|
|
||||||
"preview": "vite preview"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"axios": "^1.5.1",
|
|
||||||
"bulma": "^0.9.4",
|
|
||||||
"luxon": "^3.4.3",
|
|
||||||
"preact": "^10.13.1",
|
|
||||||
"react-colorful": "^5.6.1",
|
|
||||||
"react-query": "^3.39.3",
|
|
||||||
"tributejs": "^5.1.3",
|
|
||||||
"use-debounce": "^10.0.0",
|
|
||||||
"wouter": "^3.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@preact/preset-vite": "^2.5.0",
|
|
||||||
"@types/luxon": "^3.3.2",
|
|
||||||
"eslint": "^8.50.0",
|
|
||||||
"eslint-config-preact": "^1.3.0",
|
|
||||||
"prettier": "^3.0.3",
|
|
||||||
"react-datepicker": "^4.21.0",
|
|
||||||
"sass": "^1.71.1",
|
|
||||||
"typescript": "^5.2.2",
|
|
||||||
"vite": "^5.1"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,91 +0,0 @@
|
|||||||
.date-selector-wrapper {
|
|
||||||
width: 200px;
|
|
||||||
padding: 3px;
|
|
||||||
background-color: #fff;
|
|
||||||
box-shadow: 1px 1px 10px 1px #5c5c5c;
|
|
||||||
position: absolute;
|
|
||||||
font-size: 12px;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
-khtml-user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
-ms-user-select: none;
|
|
||||||
-o-user-select: none;
|
|
||||||
/* user-select: none; */
|
|
||||||
}
|
|
||||||
.cal-header, .cal-row {
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
height: 30px;
|
|
||||||
line-height: 30px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.cal-cell, .cal-nav {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.cal-day-names {
|
|
||||||
height: 25px;
|
|
||||||
line-height: 25px;
|
|
||||||
}
|
|
||||||
.cal-day-names .cal-cell {
|
|
||||||
cursor: default;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.cal-cell-prev, .cal-cell-next {
|
|
||||||
color: #777;
|
|
||||||
}
|
|
||||||
.cal-months .cal-row, .cal-years .cal-row {
|
|
||||||
height: 60px;
|
|
||||||
line-height: 60px;
|
|
||||||
}
|
|
||||||
.cal-nav-prev, .cal-nav-next {
|
|
||||||
flex: 0.15;
|
|
||||||
}
|
|
||||||
.cal-nav-current {
|
|
||||||
flex: 0.75;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.cal-months .cal-cell, .cal-years .cal-cell {
|
|
||||||
flex: 0.25;
|
|
||||||
}
|
|
||||||
.cal-days .cal-cell {
|
|
||||||
flex: 0.143;
|
|
||||||
}
|
|
||||||
.cal-value {
|
|
||||||
color: #fff;
|
|
||||||
background-color: #286090;
|
|
||||||
}
|
|
||||||
.cal-cell:hover, .cal-nav:hover {
|
|
||||||
background-color: #eee;
|
|
||||||
}
|
|
||||||
.cal-value:hover {
|
|
||||||
background-color: #204d74;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* time footer */
|
|
||||||
.cal-time {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-start;
|
|
||||||
height: 27px;
|
|
||||||
line-height: 27px;
|
|
||||||
}
|
|
||||||
.cal-time-label, .cal-time-value {
|
|
||||||
flex: 0.12;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.cal-time-slider {
|
|
||||||
flex: 0.77;
|
|
||||||
background-image: linear-gradient(to right, #d1d8dd, #d1d8dd);
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: 100% 1px;
|
|
||||||
background-position: left 50%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
.cal-time-slider input {
|
|
||||||
width: 100%;
|
|
||||||
-webkit-appearance: none;
|
|
||||||
background: 0 0;
|
|
||||||
cursor: pointer;
|
|
||||||
height: 100%;
|
|
||||||
outline: 0;
|
|
||||||
user-select: auto;
|
|
||||||
}
|
|
@ -1,63 +0,0 @@
|
|||||||
@font-face {
|
|
||||||
font-family: 'Source Sans Pro';
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 300;
|
|
||||||
src: local('Source Sans Pro Light Italic'), local('SourceSansPro-LightItalic'), url(https://fonts.gstatic.com/s/sourcesanspro/v13/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZZMkids18E.ttf) format('truetype');
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Source Sans Pro';
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 400;
|
|
||||||
src: local('Source Sans Pro Italic'), local('SourceSansPro-Italic'), url(https://fonts.gstatic.com/s/sourcesanspro/v13/6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7nsDc.ttf) format('truetype');
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Source Sans Pro';
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 600;
|
|
||||||
src: local('Source Sans Pro SemiBold Italic'), local('SourceSansPro-SemiBoldItalic'), url(https://fonts.gstatic.com/s/sourcesanspro/v13/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZY4lCds18E.ttf) format('truetype');
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Source Sans Pro';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 300;
|
|
||||||
src: local('Source Sans Pro Light'), local('SourceSansPro-Light'), url(https://fonts.gstatic.com/s/sourcesanspro/v13/6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwlxdr.ttf) format('truetype');
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Source Sans Pro';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url(https://fonts.gstatic.com/s/sourcesanspro/v13/6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7g.ttf) format('truetype');
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Source Sans Pro';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 600;
|
|
||||||
src: local('Source Sans Pro SemiBold'), local('SourceSansPro-SemiBold'), url(https://fonts.gstatic.com/s/sourcesanspro/v13/6xKydSBYKcSV-LCoeQqfX1RYOo3i54rwlxdr.ttf) format('truetype');
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Source Sans Pro';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 700;
|
|
||||||
src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url(https://fonts.gstatic.com/s/sourcesanspro/v13/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlxdr.ttf) format('truetype');
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Ubuntu';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
src: url(https://fonts.gstatic.com/s/ubuntu/v15/4iCs6KVjbNBYlgo6eA.ttf) format('truetype');
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Ubuntu';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 700;
|
|
||||||
src: url(https://fonts.gstatic.com/s/ubuntu/v15/4iCv6KVjbNBYlgoCxCvTtw.ttf) format('truetype');
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
@ -1,888 +0,0 @@
|
|||||||
* {
|
|
||||||
font-family: "Ubuntu Bold", "Ubuntu", sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* override styles for when the div is collapsed */
|
|
||||||
div.reminderContent.is-collapsed .column.discord-frame {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.reminderContent.is-collapsed .column.settings {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.reminderContent.is-collapsed .reminder-settings {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.reminderContent.is-collapsed .button-row {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.reminderContent.is-collapsed .button-row-edit {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.reminderContent.is-collapsed .reminder-topbar {
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.reminderContent.is-collapsed .invert-collapses {
|
|
||||||
display: inline-flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.reminderContent .invert-collapses {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.reminderContent.is-collapsed input[name="name"] {
|
|
||||||
display: inline-flex;
|
|
||||||
flex-grow: 1;
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
box-shadow: none;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.reminderContent.is-collapsed .hide-box {
|
|
||||||
display: inline-flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.reminderContent.is-collapsed .hide-box i {
|
|
||||||
transform: rotate(90deg);
|
|
||||||
}
|
|
||||||
/* END */
|
|
||||||
|
|
||||||
/* dashboard styles */
|
|
||||||
.hide-box {
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hide-box:focus {
|
|
||||||
outline: none;
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.channel-bar {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
flex-direction: column;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.inline-btn {
|
|
||||||
height: 100%;
|
|
||||||
padding: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.change-color {
|
|
||||||
position: absolute;
|
|
||||||
left: calc(-1rem - 40px);
|
|
||||||
}
|
|
||||||
|
|
||||||
button.disable-enable[data-action="enable"]:after {
|
|
||||||
content: "Enable";
|
|
||||||
}
|
|
||||||
|
|
||||||
button.disable-enable[data-action="disable"]:after {
|
|
||||||
content: "Disable";
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-content {
|
|
||||||
overflow-x: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.discord-embed {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.split-controls {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-between;
|
|
||||||
flex-grow: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reminder-topbar > div {
|
|
||||||
padding-left: 6px;
|
|
||||||
padding-right: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name-bar {
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-shrink: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hide-button-bar {
|
|
||||||
flex-grow: 0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.patreon-only {
|
|
||||||
padding-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tts-row {
|
|
||||||
padding-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reminder-topbar {
|
|
||||||
display: flex;
|
|
||||||
margin-bottom: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reminder-settings {
|
|
||||||
margin-top: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reminder-settings > .column {
|
|
||||||
flex-grow: 0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
flex-basis: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.reminderContent {
|
|
||||||
margin-top: 10px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
padding: 14px;
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Interval inputs */
|
|
||||||
div.interval-group {
|
|
||||||
height: unset !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.interval-group .clear:focus {
|
|
||||||
outline: none;
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.interval-group .no-break {
|
|
||||||
text-wrap: avoid;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.interval-group .clear {
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
padding: 1px;
|
|
||||||
margin-right: -3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.interval-group > .interval-group-left input {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
border-style: none;
|
|
||||||
background-color: #eee;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.interval-group > .interval-group-left input.w2 {
|
|
||||||
width: 3ch;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.interval-group > .interval-group-left input.w3 {
|
|
||||||
width: 3ch;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.interval-group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
/* !Interval inputs */
|
|
||||||
|
|
||||||
.left-pad {
|
|
||||||
padding-left: 1rem;
|
|
||||||
padding-right: 0.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification {
|
|
||||||
padding-right: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.inset-content {
|
|
||||||
margin-left: 10%;
|
|
||||||
margin-right: 10%;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.flash-message {
|
|
||||||
position: fixed;
|
|
||||||
width: calc(100% - 32px);
|
|
||||||
margin: 16px !important;
|
|
||||||
z-index: 99;
|
|
||||||
bottom: 0;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.flash-message.is-active {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
span.spacer {
|
|
||||||
width: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav .dashboard-button {
|
|
||||||
background: white ;
|
|
||||||
}
|
|
||||||
|
|
||||||
span.patreon-color {
|
|
||||||
color: #f96854;
|
|
||||||
}
|
|
||||||
|
|
||||||
p.pageTitle {
|
|
||||||
margin-left: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#welcome > div {
|
|
||||||
height: 100%;
|
|
||||||
padding-top: 30vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
div#pageNavbar {
|
|
||||||
background-color: #363636;
|
|
||||||
}
|
|
||||||
|
|
||||||
div#pageNavbar a {
|
|
||||||
color: #fff;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-burger {
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-item.pageTitle {
|
|
||||||
flex-shrink: 1;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard-burger, .dashboard-burger:active, .dashboard-burger.is-active {
|
|
||||||
background-color: #adc99c !important;
|
|
||||||
border-radius: 14px;
|
|
||||||
padding: 6px;
|
|
||||||
background-clip: content-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
div#pageNavbar a:hover {
|
|
||||||
background-color: #4a4a4a;
|
|
||||||
}
|
|
||||||
|
|
||||||
img.rounded-corners {
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.brand {
|
|
||||||
text-align: center;
|
|
||||||
height: 52px;
|
|
||||||
background-color: #8fb677;
|
|
||||||
}
|
|
||||||
|
|
||||||
img.dashboard-brand {
|
|
||||||
text-align: center;
|
|
||||||
height: 100%;
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.dashboard-sidebar {
|
|
||||||
background-color: #363636;
|
|
||||||
width: 230px !important;
|
|
||||||
padding-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul.guildList {
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-shrink: 1;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.dashboard-sidebar:not(.mobile-sidebar) .aside-footer {
|
|
||||||
flex-shrink: 0;
|
|
||||||
flex-grow: 0;
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
|
||||||
width: 226px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.dashboard-sidebar svg {
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.mobile-sidebar {
|
|
||||||
z-index: 100;
|
|
||||||
min-height: 100vh;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
display: none;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
#expandAll {
|
|
||||||
width: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.mobile-sidebar .aside-footer {
|
|
||||||
margin-top: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.mobile-sidebar.is-active {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
aside.menu {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.dashboard-frame {
|
|
||||||
min-height: 100vh;
|
|
||||||
margin-bottom: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.embed-field-box[data-inlined="0"] .inline-btn > i {
|
|
||||||
transform: rotate(90deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.embed-field-box[data-inlined="0"] {
|
|
||||||
min-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.embed-field-box[data-inlined="1"] {
|
|
||||||
min-width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu a {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu .menu-label {
|
|
||||||
color: #bbb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu {
|
|
||||||
padding-left: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard-navbar {
|
|
||||||
background-color: #8fb677 !important;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea.autoresize {
|
|
||||||
resize: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea, input {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
input.default-width {
|
|
||||||
width: initial;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-input:placeholder-shown {
|
|
||||||
font-style: italic;
|
|
||||||
background-color: #40444b;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-input {
|
|
||||||
border: none;
|
|
||||||
background-color: rgba(0, 0, 0, 0);
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.time-input {
|
|
||||||
border-top: none;
|
|
||||||
border-left: none;
|
|
||||||
border-right: none;
|
|
||||||
border-bottom-style: solid;
|
|
||||||
background-color: #40444b;
|
|
||||||
color: #fff;
|
|
||||||
width: 120px;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.message-input::placeholder {
|
|
||||||
color: #72767b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.discord-title {
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 1rem;
|
|
||||||
margin: 4px 0 4px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.discord-description {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.discord-username {
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
width: initial;
|
|
||||||
}
|
|
||||||
|
|
||||||
.discord-message-header {
|
|
||||||
white-space: nowrap;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.discord-content {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customizable img {
|
|
||||||
background-color: #72767b;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customizable.is-20x20 img {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customizable.is-24x24 img {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customizable.is-400x300 img {
|
|
||||||
margin-top: 10px;
|
|
||||||
width: 100%;
|
|
||||||
height: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customizable.is-32x32 img {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customizable.thumbnail img {
|
|
||||||
width: 100px;
|
|
||||||
height: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customizable input.imageInput {
|
|
||||||
display: none;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 36px;
|
|
||||||
width: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customizable.thumbnail input.imageInput {
|
|
||||||
display: none;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: -400px;
|
|
||||||
width: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customizable input.is-active {
|
|
||||||
display: block !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.discord-frame {
|
|
||||||
color: #fff;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 8px;
|
|
||||||
background-color: #36393f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.discord-embed {
|
|
||||||
padding: 8px 16px 16px 12px;
|
|
||||||
margin: 0 20px 4px 0;
|
|
||||||
border-radius: 4px;
|
|
||||||
border-left: 4px solid #fff;
|
|
||||||
background-color: #2f3136;
|
|
||||||
}
|
|
||||||
|
|
||||||
.embed-author-box {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.embed-author-box > .a {
|
|
||||||
flex: initial;
|
|
||||||
}
|
|
||||||
|
|
||||||
.embed-author-box > .b {
|
|
||||||
flex: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.embed-footer-box {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.embed-author-box .image {
|
|
||||||
margin: 0 8px 0 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.embed-footer-box .image {
|
|
||||||
margin: 0 8px 0 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.discord-embed-author {
|
|
||||||
display: inline-block;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.discord-embed-footer {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.embed-body {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.embed-body > .a {
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-shrink: 1;
|
|
||||||
flex-basis: auto;
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.embed-body input, .embed-body textarea {
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.embed-body > .b {
|
|
||||||
flex-grow: 0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
flex-basis: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.discord-field-title, .discord-field-value {
|
|
||||||
max-width: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.discord-field-title {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.embed-field-box {
|
|
||||||
margin: 12px 8px 0 0;
|
|
||||||
max-width: 120px;
|
|
||||||
flex: initial;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-input {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
width: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.embed-multifield-box {
|
|
||||||
display: flex;
|
|
||||||
max-width: 100%;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.channel-select {
|
|
||||||
font-size: 1.125rem;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
margin-left: 48px;
|
|
||||||
display: inline-flex;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #6e89da;
|
|
||||||
width: auto;
|
|
||||||
border-radius: 2px;
|
|
||||||
border-bottom: 1px solid #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.channel-selector {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
li.highlight {
|
|
||||||
margin-bottom: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-row {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-row .button-row-reminder {
|
|
||||||
flex-grow: 0;
|
|
||||||
padding: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-row-template {
|
|
||||||
display: flex;
|
|
||||||
flex-grow: 1;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-row .button-row-template > div {
|
|
||||||
padding: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 1023px) {
|
|
||||||
p.title.pageTitle {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard-frame {
|
|
||||||
margin-top: 4rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customizable.thumbnail img {
|
|
||||||
width: 60px;
|
|
||||||
height: 60px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 768px) {
|
|
||||||
.button-row {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-row .button-row-reminder {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-row .button-row-template > div {
|
|
||||||
flex-basis: 0;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-row button {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reminder-settings {
|
|
||||||
margin-bottom: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tts-row {
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* loader */
|
|
||||||
#loader {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
background-color: rgba(255, 255, 255, 0.8);
|
|
||||||
width: 100vw;
|
|
||||||
z-index: 999;
|
|
||||||
}
|
|
||||||
|
|
||||||
#loader .title {
|
|
||||||
font-size: 6rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* END */
|
|
||||||
|
|
||||||
div.reminderError {
|
|
||||||
margin: 10px;
|
|
||||||
padding: 14px;
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.reminderError .errorHead {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.reminderError .errorIcon {
|
|
||||||
padding: 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin-right: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.reminderError .errorIcon .fas {
|
|
||||||
display: none
|
|
||||||
}
|
|
||||||
|
|
||||||
div.reminderError[data-case="deleted"] .errorIcon {
|
|
||||||
background-color: #e7e5e4;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.reminderError[data-case="failed"] .errorIcon {
|
|
||||||
background-color: #fecaca;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.reminderError[data-case="sent"] .errorIcon {
|
|
||||||
background-color: #d9f99d;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.reminderError[data-case="deleted"] .errorIcon .fas.fa-trash {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.reminderError[data-case="failed"] .errorIcon .fas.fa-exclamation-triangle {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.reminderError[data-case="sent"] .errorIcon .fas.fa-check {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.reminderError .errorHead .reminderName {
|
|
||||||
font-size: 1rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
color: rgb(54, 54, 54);
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.reminderError .errorHead .reminderTime {
|
|
||||||
font-size: 1rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex-shrink: 1;
|
|
||||||
justify-content: center;
|
|
||||||
color: rgb(54, 54, 54);
|
|
||||||
background-color: #ffffff;
|
|
||||||
padding: 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
border-color: #e5e5e5;
|
|
||||||
border-width: 1px;
|
|
||||||
border-style: solid;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.reminderError .reminderMessage {
|
|
||||||
font-size: 1rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
color: rgb(54, 54, 54);
|
|
||||||
flex-grow: 1;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* other stuff */
|
|
||||||
|
|
||||||
.half-rem {
|
|
||||||
width: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-left {
|
|
||||||
width: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#dead {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.colorpicker-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.create-reminder {
|
|
||||||
margin: 0 12px 12px 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button.is-success:not(.is-outlined) {
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button.is-outlined.is-success {
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.switch-pane {
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.guild-submenu {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.guild-submenu li {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.switch-pane.is-active ~ .guild-submenu {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feedback {
|
|
||||||
background-color: #5865F2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.is-locked {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.is-locked > :not(.patreon-invert) {
|
|
||||||
opacity: 0.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.is-locked .patreon-invert {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.patreon-invert {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.is-locked .foreground {
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.is-locked .field:last-of-type {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-row {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-box {
|
|
||||||
flex-grow: 1;
|
|
||||||
border-radius: 6px;
|
|
||||||
background-color: #fcfcfc;
|
|
||||||
border-color: #efefef;
|
|
||||||
border-style: solid;
|
|
||||||
border-width: 1px;
|
|
||||||
margin: 4px;
|
|
||||||
padding: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.figure {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.figure-num {
|
|
||||||
font-size: 2rem;
|
|
||||||
}
|
|
Before Width: | Height: | Size: 7.2 KiB |
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 6.8 KiB |
@ -1,9 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<browserconfig>
|
|
||||||
<msapplication>
|
|
||||||
<tile>
|
|
||||||
<square150x150logo src="/mstile-150x150.png"/>
|
|
||||||
<TileColor>#da532c</TileColor>
|
|
||||||
</tile>
|
|
||||||
</msapplication>
|
|
||||||
</browserconfig>
|
|
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 7.2 KiB |
Before Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 762 B |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 323 KiB |
Before Width: | Height: | Size: 61 KiB |
Before Width: | Height: | Size: 81 KiB |
Before Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 65 KiB |
@ -1,131 +0,0 @@
|
|||||||
document.addEventListener("DOMContentLoaded", () => {
|
|
||||||
fetch("/admin/data")
|
|
||||||
.then((resp) => resp.json())
|
|
||||||
.then((data) => {
|
|
||||||
document.querySelector("#backlog").textContent = data.backlog;
|
|
||||||
document.querySelector("#reminders").textContent = data.count.reminders;
|
|
||||||
document.querySelector("#intervals").textContent = data.count.intervals;
|
|
||||||
|
|
||||||
let historySent = data.historyLong.sent.reduce(
|
|
||||||
(iv, frame) => iv + frame.count,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
let historyFailed = data.historyLong.failed.reduce(
|
|
||||||
(iv, frame) => iv + frame.count,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
let rate = historyFailed / (historySent + historyFailed);
|
|
||||||
let formatted = Math.round(rate * 10000) / 100;
|
|
||||||
|
|
||||||
document.querySelector("#historySent").textContent = historySent;
|
|
||||||
document.querySelector("#historyFailed").textContent = historyFailed;
|
|
||||||
document.querySelector("#failRate").textContent = `${formatted}%`;
|
|
||||||
|
|
||||||
new Chart(document.getElementById("schedule"), {
|
|
||||||
type: "bar",
|
|
||||||
data: {
|
|
||||||
labels: [
|
|
||||||
...data.scheduleShort.once,
|
|
||||||
...data.scheduleShort.interval,
|
|
||||||
].map((row) => luxon.DateTime.fromISO(row.time_key)),
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
label: "Reminders",
|
|
||||||
data: data.scheduleShort.once.map((row) => row.count),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Intervals",
|
|
||||||
data: data.scheduleShort.interval.map((row) => row.count),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
scales: {
|
|
||||||
x: {
|
|
||||||
stacked: true,
|
|
||||||
type: "time",
|
|
||||||
time: {
|
|
||||||
unit: "minute",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
y: {
|
|
||||||
stacked: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
new Chart(document.getElementById("scheduleLong"), {
|
|
||||||
type: "bar",
|
|
||||||
data: {
|
|
||||||
labels: [
|
|
||||||
...data.scheduleLong.once,
|
|
||||||
...data.scheduleLong.interval,
|
|
||||||
].map((row) => luxon.DateTime.fromISO(row.time_key)),
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
label: "Reminders",
|
|
||||||
data: data.scheduleLong.once.map((row) => row.count),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Intervals",
|
|
||||||
data: data.scheduleLong.interval.map((row) => row.count),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
scales: {
|
|
||||||
x: {
|
|
||||||
stacked: true,
|
|
||||||
type: "time",
|
|
||||||
time: {
|
|
||||||
unit: "day",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
y: {
|
|
||||||
stacked: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
new Chart(document.getElementById("historyLong"), {
|
|
||||||
type: "bar",
|
|
||||||
data: {
|
|
||||||
labels: [...data.historyLong.sent, ...data.historyLong.failed].map(
|
|
||||||
(row) => luxon.DateTime.fromISO(row.time_key)
|
|
||||||
),
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
label: "Success",
|
|
||||||
data: data.historyLong.sent.map((row) => row.count),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Fail",
|
|
||||||
data: data.historyLong.failed.map((row) => row.count),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
scales: {
|
|
||||||
x: {
|
|
||||||
stacked: true,
|
|
||||||
type: "time",
|
|
||||||
time: {
|
|
||||||
unit: "day",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
y: {
|
|
||||||
stacked: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,7 +0,0 @@
|
|||||||
/*!
|
|
||||||
* chartjs-adapter-luxon v1.3.1
|
|
||||||
* https://www.chartjs.org
|
|
||||||
* (c) 2023 chartjs-adapter-luxon Contributors
|
|
||||||
* Released under the MIT license
|
|
||||||
*/
|
|
||||||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(require("chart.js"),require("luxon")):"function"==typeof define&&define.amd?define(["chart.js","luxon"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).Chart,e.luxon)}(this,(function(e,t){"use strict";const n={datetime:t.DateTime.DATETIME_MED_WITH_SECONDS,millisecond:"h:mm:ss.SSS a",second:t.DateTime.TIME_WITH_SECONDS,minute:t.DateTime.TIME_SIMPLE,hour:{hour:"numeric"},day:{day:"numeric",month:"short"},week:"DD",month:{month:"short",year:"numeric"},quarter:"'Q'q - yyyy",year:{year:"numeric"}};e._adapters._date.override({_id:"luxon",_create:function(e){return t.DateTime.fromMillis(e,this.options)},init(e){this.options.locale||(this.options.locale=e.locale)},formats:function(){return n},parse:function(e,n){const i=this.options,r=typeof e;return null===e||"undefined"===r?null:("number"===r?e=this._create(e):"string"===r?e="string"==typeof n?t.DateTime.fromFormat(e,n,i):t.DateTime.fromISO(e,i):e instanceof Date?e=t.DateTime.fromJSDate(e,i):"object"!==r||e instanceof t.DateTime||(e=t.DateTime.fromObject(e,i)),e.isValid?e.valueOf():null)},format:function(e,t){const n=this._create(e);return"string"==typeof t?n.toFormat(t):n.toLocaleString(t)},add:function(e,t,n){const i={};return i[n]=t,this._create(e).plus(i).valueOf()},diff:function(e,t,n){return this._create(e).diff(this._create(t)).as(n).valueOf()},startOf:function(e,t,n){if("isoWeek"===t){n=Math.trunc(Math.min(Math.max(0,n),6));const t=this._create(e);return t.minus({days:(t.weekday-n+7)%7}).startOf("day").valueOf()}return t?this._create(e).startOf(t).valueOf():e},endOf:function(e,t){return this._create(e).endOf(t).valueOf()}})}));
|
|
@ -1,931 +0,0 @@
|
|||||||
(function () {
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
var BODYTYPES = ["DAYS", "MONTHS", "YEARS"];
|
|
||||||
var MONTHS = [
|
|
||||||
"January", "February", "March", "April", "May", "June",
|
|
||||||
"July", "August", "September", "October", "November", "December"
|
|
||||||
];
|
|
||||||
var WEEKDAYS = [
|
|
||||||
"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
|
|
||||||
];
|
|
||||||
|
|
||||||
/** @typedef {Object.<string, Function[]>} Handlers */
|
|
||||||
/** @typedef {function(String, Function): null} AddHandler */
|
|
||||||
/** @typedef {("DAYS"|"MONTHS"|"YEARS")} BodyType */
|
|
||||||
/** @typedef {string|number} StringNum */
|
|
||||||
/** @typedef {Object.<string, StringNum>} StringNumObj */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The local state
|
|
||||||
* @typedef {Object} InstanceState
|
|
||||||
* @property {Date} value
|
|
||||||
* @property {Number} year
|
|
||||||
* @property {Number} month
|
|
||||||
* @property {Number} day
|
|
||||||
* @property {Number} time
|
|
||||||
* @property {Number} hours
|
|
||||||
* @property {Number} minutes
|
|
||||||
* @property {Number} seconds
|
|
||||||
* @property {BodyType} bodyType
|
|
||||||
* @property {Boolean} visible
|
|
||||||
* @property {Number} cancelBlur
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {Object} Config
|
|
||||||
* @property {String} dateFormat
|
|
||||||
* @property {String} timeFormat
|
|
||||||
* @property {Boolean} showDate
|
|
||||||
* @property {Boolean} showTime
|
|
||||||
* @property {Number} paddingX
|
|
||||||
* @property {Number} paddingY
|
|
||||||
* @property {BodyType} defaultView
|
|
||||||
* @property {"TOP"|"BOTTOM"} direction
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @class
|
|
||||||
* @param {HTMLElement} elem
|
|
||||||
* @param {Config} config
|
|
||||||
*/
|
|
||||||
function DTS(elem, config) {
|
|
||||||
var config = config || {};
|
|
||||||
|
|
||||||
/** @type {Config} */
|
|
||||||
var defaultConfig = {
|
|
||||||
defaultView: BODYTYPES[0],
|
|
||||||
dateFormat: "yyyy-mm-dd",
|
|
||||||
timeFormat: "HH:MM:SS",
|
|
||||||
showDate: true,
|
|
||||||
showTime: false,
|
|
||||||
paddingX: 5,
|
|
||||||
paddingY: 5,
|
|
||||||
direction: 'TOP'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!elem) {
|
|
||||||
throw TypeError("input element or selector required for contructor");
|
|
||||||
}
|
|
||||||
if (Object.getPrototypeOf(elem) === String.prototype) {
|
|
||||||
var _elem = document.querySelectorAll(elem);
|
|
||||||
if (!_elem[0]){
|
|
||||||
throw Error('"' + elem + '" not found.');
|
|
||||||
}
|
|
||||||
elem = _elem[0];
|
|
||||||
}
|
|
||||||
this.config = setDefaults(config, defaultConfig);
|
|
||||||
this.dateFormat = this.config.dateFormat;
|
|
||||||
this.timeFormat = this.config.timeFormat;
|
|
||||||
this.dateFormatRegEx = new RegExp("yyyy|yy|mm|dd", "gi");
|
|
||||||
this.timeFormatRegEx = new RegExp("hh|mm|ss|a", "gi");
|
|
||||||
this.inputElem = elem;
|
|
||||||
this.dtbox = null;
|
|
||||||
this.setup();
|
|
||||||
}
|
|
||||||
DTS.prototype.setup = function () {
|
|
||||||
var handler = this.inputElemHandler.bind(this);
|
|
||||||
this.inputElem.addEventListener("focus", handler, false)
|
|
||||||
this.inputElem.addEventListener("blur", handler, false);
|
|
||||||
}
|
|
||||||
DTS.prototype.inputElemHandler = function (e) {
|
|
||||||
if (e.type == "focus") {
|
|
||||||
if (!this.dtbox) {
|
|
||||||
this.dtbox = new DTBox(e.target, this);
|
|
||||||
}
|
|
||||||
this.dtbox.visible = true;
|
|
||||||
} else if (e.type == "blur" && this.dtbox && this.dtbox.visible) {
|
|
||||||
var self = this;
|
|
||||||
setTimeout(function () {
|
|
||||||
if (self.dtbox.cancelBlur > 0) {
|
|
||||||
self.dtbox.cancelBlur -= 1;
|
|
||||||
} else {
|
|
||||||
self.dtbox.visible = false;
|
|
||||||
self.inputElem.blur();
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @class
|
|
||||||
* @param {HTMLElement} elem
|
|
||||||
* @param {DTS} settings
|
|
||||||
*/
|
|
||||||
function DTBox(elem, settings) {
|
|
||||||
/** @type {DTBox} */
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
/** @type {Handlers} */
|
|
||||||
var handlers = {};
|
|
||||||
|
|
||||||
/** @type {InstanceState} */
|
|
||||||
var localState = {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {String} key
|
|
||||||
* @param {*} default_val
|
|
||||||
*/
|
|
||||||
function getterSetter(key, default_val) {
|
|
||||||
return {
|
|
||||||
get: function () {
|
|
||||||
var val = localState[key];
|
|
||||||
return val === undefined ? default_val : val;
|
|
||||||
},
|
|
||||||
set: function (val) {
|
|
||||||
var prevState = self.state;
|
|
||||||
var _handlers = handlers[key] || [];
|
|
||||||
localState[key] = val;
|
|
||||||
for (var i = 0; i < _handlers.length; i++) {
|
|
||||||
_handlers[i].bind(self)(localState, prevState);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/** @type {AddHandler} */
|
|
||||||
function addHandler(key, handlerFn) {
|
|
||||||
if (!key || !handlerFn) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!handlers[key]) {
|
|
||||||
handlers[key] = [];
|
|
||||||
}
|
|
||||||
handlers[key].push(handlerFn);
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.defineProperties(this, {
|
|
||||||
visible: getterSetter("visible", false),
|
|
||||||
bodyType: getterSetter("bodyType", settings.config.defaultView),
|
|
||||||
value: getterSetter("value"),
|
|
||||||
year: getterSetter("year", 0),
|
|
||||||
month: getterSetter("month", 0),
|
|
||||||
day: getterSetter("day", 0),
|
|
||||||
hours: getterSetter("hours", 0),
|
|
||||||
minutes: getterSetter("minutes", 0),
|
|
||||||
seconds: getterSetter("seconds", 0),
|
|
||||||
cancelBlur: getterSetter("cancelBlur", 0),
|
|
||||||
addHandler: {value: addHandler},
|
|
||||||
month_long: {
|
|
||||||
get: function () {
|
|
||||||
return MONTHS[self.month];
|
|
||||||
},
|
|
||||||
},
|
|
||||||
month_short: {
|
|
||||||
get: function () {
|
|
||||||
return self.month_long.slice(0, 3);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
state: {
|
|
||||||
get: function () {
|
|
||||||
return Object.assign({}, localState);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
time: {
|
|
||||||
get: function() {
|
|
||||||
var hours = self.hours * 60 * 60 * 1000;
|
|
||||||
var minutes = self.minutes * 60 * 1000;
|
|
||||||
var seconds = self.seconds * 1000;
|
|
||||||
return hours + minutes + seconds;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.el = {};
|
|
||||||
this.settings = settings;
|
|
||||||
this.elem = elem;
|
|
||||||
this.setup();
|
|
||||||
}
|
|
||||||
DTBox.prototype.setup = function () {
|
|
||||||
Object.defineProperties(this.el, {
|
|
||||||
wrapper: { value: null, configurable: true },
|
|
||||||
header: { value: null, configurable: true },
|
|
||||||
body: { value: null, configurable: true },
|
|
||||||
footer: { value: null, configurable: true }
|
|
||||||
});
|
|
||||||
this.setupWrapper();
|
|
||||||
if (this.settings.config.showDate) {
|
|
||||||
this.setupHeader();
|
|
||||||
this.setupBody();
|
|
||||||
}
|
|
||||||
if (this.settings.config.showTime) {
|
|
||||||
this.setupFooter();
|
|
||||||
}
|
|
||||||
|
|
||||||
var self = this;
|
|
||||||
this.addHandler("visible", function (state, prevState) {
|
|
||||||
if (state.visible && !prevState.visible){
|
|
||||||
document.body.appendChild(this.el.wrapper);
|
|
||||||
|
|
||||||
var parts = self.elem.value.split(/\s*,\s*/);
|
|
||||||
var startDate = undefined;
|
|
||||||
var startTime = 0;
|
|
||||||
if (self.settings.config.showDate) {
|
|
||||||
startDate = parseDate(parts[0], self.settings);
|
|
||||||
}
|
|
||||||
if (self.settings.config.showTime) {
|
|
||||||
startTime = parseTime(parts[parts.length-1], self.settings);
|
|
||||||
startTime = startTime || 0;
|
|
||||||
}
|
|
||||||
if (!(startDate && startDate.getTime())) {
|
|
||||||
startDate = new Date();
|
|
||||||
startDate = new Date(
|
|
||||||
startDate.getFullYear(),
|
|
||||||
startDate.getMonth(),
|
|
||||||
startDate.getDate()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
var value = new Date(startDate.getTime() + startTime);
|
|
||||||
self.value = value;
|
|
||||||
self.year = value.getFullYear();
|
|
||||||
self.month = value.getMonth();
|
|
||||||
self.day = value.getDate();
|
|
||||||
self.hours = value.getHours();
|
|
||||||
self.minutes = value.getMinutes();
|
|
||||||
self.seconds = value.getSeconds();
|
|
||||||
|
|
||||||
if (self.settings.config.showDate) {
|
|
||||||
self.setHeaderContent();
|
|
||||||
self.setBodyContent();
|
|
||||||
}
|
|
||||||
if (self.settings.config.showTime) {
|
|
||||||
self.setFooterContent();
|
|
||||||
}
|
|
||||||
} else if (!state.visible && prevState.visible) {
|
|
||||||
document.body.removeChild(this.el.wrapper);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
DTBox.prototype.setupWrapper = function () {
|
|
||||||
if (!this.el.wrapper) {
|
|
||||||
var el = document.createElement("div");
|
|
||||||
el.classList.add("date-selector-wrapper");
|
|
||||||
Object.defineProperty(this.el, "wrapper", { value: el });
|
|
||||||
}
|
|
||||||
var self = this;
|
|
||||||
var htmlRoot = document.getElementsByTagName('html')[0];
|
|
||||||
function setPosition(e){
|
|
||||||
var minTopSpace = 300;
|
|
||||||
var box = getOffset(self.elem);
|
|
||||||
var config = self.settings.config;
|
|
||||||
var paddingY = config.paddingY || 5;
|
|
||||||
var paddingX = config.paddingX || 5;
|
|
||||||
var top = box.top + self.elem.offsetHeight + paddingY;
|
|
||||||
var left = box.left + paddingX;
|
|
||||||
var bottom = htmlRoot.clientHeight - box.top + paddingY;
|
|
||||||
|
|
||||||
self.el.wrapper.style.left = `${left}px`;
|
|
||||||
if (box.top > minTopSpace && config.direction != 'BOTTOM') {
|
|
||||||
self.el.wrapper.style.bottom = `${bottom}px`;
|
|
||||||
self.el.wrapper.style.top = '';
|
|
||||||
} else {
|
|
||||||
self.el.wrapper.style.top = `${top}px`;
|
|
||||||
self.el.wrapper.style.bottom = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handler(e) {
|
|
||||||
self.cancelBlur += 1;
|
|
||||||
setTimeout(function(){
|
|
||||||
self.elem.focus();
|
|
||||||
}, 50);
|
|
||||||
}
|
|
||||||
setPosition();
|
|
||||||
this.setPosition = setPosition;
|
|
||||||
this.el.wrapper.addEventListener("mousedown", handler, false);
|
|
||||||
this.el.wrapper.addEventListener("touchstart", handler, false);
|
|
||||||
window.addEventListener('resize', this.setPosition);
|
|
||||||
}
|
|
||||||
DTBox.prototype.setupHeader = function () {
|
|
||||||
if (!this.el.header) {
|
|
||||||
var row = document.createElement("div");
|
|
||||||
var classes = ["cal-nav-prev", "cal-nav-current", "cal-nav-next"];
|
|
||||||
row.classList.add("cal-header");
|
|
||||||
for (var i = 0; i < 3; i++) {
|
|
||||||
var cell = document.createElement("div");
|
|
||||||
cell.classList.add("cal-nav", classes[i]);
|
|
||||||
cell.onclick = this.onHeaderChange.bind(this);
|
|
||||||
row.appendChild(cell);
|
|
||||||
}
|
|
||||||
row.children[0].innerHTML = "<";
|
|
||||||
row.children[2].innerHTML = ">";
|
|
||||||
Object.defineProperty(this.el, "header", { value: row });
|
|
||||||
tryAppendChild(row, this.el.wrapper);
|
|
||||||
}
|
|
||||||
this.setHeaderContent();
|
|
||||||
}
|
|
||||||
DTBox.prototype.setHeaderContent = function () {
|
|
||||||
var content = this.year;
|
|
||||||
if ("DAYS" == this.bodyType) {
|
|
||||||
content = this.month_long + " " + content;
|
|
||||||
} else if ("YEARS" == this.bodyType) {
|
|
||||||
var start = this.year + 10 - (this.year % 10);
|
|
||||||
content = start - 10 + "-" + (start - 1);
|
|
||||||
}
|
|
||||||
this.el.header.children[1].innerText = content;
|
|
||||||
}
|
|
||||||
DTBox.prototype.setupBody = function () {
|
|
||||||
if (!this.el.body) {
|
|
||||||
var el = document.createElement("div");
|
|
||||||
el.classList.add("cal-body");
|
|
||||||
Object.defineProperty(this.el, "body", { value: el });
|
|
||||||
tryAppendChild(el, this.el.wrapper);
|
|
||||||
}
|
|
||||||
var toAppend = null;
|
|
||||||
function makeGrid(rows, cols, className, firstRowClass, clickHandler) {
|
|
||||||
var grid = document.createElement("div");
|
|
||||||
grid.classList.add(className);
|
|
||||||
for (var i = 1; i < rows + 1; i++) {
|
|
||||||
var row = document.createElement("div");
|
|
||||||
row.classList.add("cal-row", "cal-row-" + i);
|
|
||||||
if (i == 1 && firstRowClass) {
|
|
||||||
row.classList.add(firstRowClass);
|
|
||||||
}
|
|
||||||
for (var j = 1; j < cols + 1; j++) {
|
|
||||||
var col = document.createElement("div");
|
|
||||||
col.classList.add("cal-cell", "cal-col-" + j);
|
|
||||||
col.onclick = clickHandler;
|
|
||||||
row.appendChild(col);
|
|
||||||
}
|
|
||||||
grid.appendChild(row);
|
|
||||||
}
|
|
||||||
return grid;
|
|
||||||
}
|
|
||||||
if ("DAYS" == this.bodyType) {
|
|
||||||
toAppend = this.el.body.calDays;
|
|
||||||
if (!toAppend) {
|
|
||||||
toAppend = makeGrid(7, 7, "cal-days", "cal-day-names", this.onDateSelected.bind(this));
|
|
||||||
for (var i = 0; i < 7; i++) {
|
|
||||||
var cell = toAppend.children[0].children[i];
|
|
||||||
cell.innerText = WEEKDAYS[i].slice(0, 2);
|
|
||||||
cell.onclick = null;
|
|
||||||
}
|
|
||||||
this.el.body.calDays = toAppend;
|
|
||||||
}
|
|
||||||
} else if ("MONTHS" == this.bodyType) {
|
|
||||||
toAppend = this.el.body.calMonths;
|
|
||||||
if (!toAppend) {
|
|
||||||
toAppend = makeGrid(3, 4, "cal-months", null, this.onMonthSelected.bind(this));
|
|
||||||
for (var i = 0; i < 3; i++) {
|
|
||||||
for (var j = 0; j < 4; j++) {
|
|
||||||
var monthShort = MONTHS[4 * i + j].slice(0, 3);
|
|
||||||
toAppend.children[i].children[j].innerText = monthShort;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.el.body.calMonths = toAppend;
|
|
||||||
}
|
|
||||||
} else if ("YEARS" == this.bodyType) {
|
|
||||||
toAppend = this.el.body.calYears;
|
|
||||||
if (!toAppend) {
|
|
||||||
toAppend = makeGrid(3, 4, "cal-years", null, this.onYearSelected.bind(this));
|
|
||||||
this.el.body.calYears = toAppend;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
empty(this.el.body);
|
|
||||||
tryAppendChild(toAppend, this.el.body);
|
|
||||||
this.setBodyContent();
|
|
||||||
}
|
|
||||||
DTBox.prototype.setBodyContent = function () {
|
|
||||||
var grid = this.el.body.children[0];
|
|
||||||
var classes = ["cal-cell-prev", "cal-cell-next", "cal-value"];
|
|
||||||
if ("DAYS" == this.bodyType) {
|
|
||||||
var oneDayMilliSecs = 24 * 60 * 60 * 1000;
|
|
||||||
var start = new Date(this.year, this.month, 1);
|
|
||||||
var adjusted = new Date(start.getTime() - oneDayMilliSecs * start.getDay());
|
|
||||||
|
|
||||||
grid.children[6].style.display = "";
|
|
||||||
for (var i = 1; i < 7; i++) {
|
|
||||||
for (var j = 0; j < 7; j++) {
|
|
||||||
var cell = grid.children[i].children[j];
|
|
||||||
var month = adjusted.getMonth();
|
|
||||||
var date = adjusted.getDate();
|
|
||||||
|
|
||||||
cell.innerText = date;
|
|
||||||
cell.classList.remove(classes[0], classes[1], classes[2]);
|
|
||||||
if (month != this.month) {
|
|
||||||
if (i == 6 && j == 0) {
|
|
||||||
grid.children[6].style.display = "none";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
cell.classList.add(month < this.month ? classes[0] : classes[1]);
|
|
||||||
} else if (isEqualDate(adjusted, this.value)){
|
|
||||||
cell.classList.add(classes[2]);
|
|
||||||
}
|
|
||||||
adjusted = new Date(adjusted.getTime() + oneDayMilliSecs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if ("YEARS" == this.bodyType) {
|
|
||||||
var year = this.year - (this.year % 10) - 1;
|
|
||||||
for (i = 0; i < 3; i++) {
|
|
||||||
for (j = 0; j < 4; j++) {
|
|
||||||
grid.children[i].children[j].innerText = year;
|
|
||||||
year += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
grid.children[0].children[0].classList.add(classes[0]);
|
|
||||||
grid.children[2].children[3].classList.add(classes[1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {Event} e */
|
|
||||||
DTBox.prototype.onTimeChange = function(e) {
|
|
||||||
e.stopPropagation();
|
|
||||||
if (e.type == 'mousedown') {
|
|
||||||
this.cancelBlur += 1;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var el = e.target;
|
|
||||||
this[el.name] = parseInt(el.value) || 0;
|
|
||||||
this.setupFooter();
|
|
||||||
if (e.type == 'change') {
|
|
||||||
var self = this;
|
|
||||||
setTimeout(function(){
|
|
||||||
self.elem.focus();
|
|
||||||
}, 50);
|
|
||||||
}
|
|
||||||
this.setInputValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
DTBox.prototype.setupFooter = function() {
|
|
||||||
if (!this.el.footer) {
|
|
||||||
var footer = document.createElement("div");
|
|
||||||
var handler = this.onTimeChange.bind(this);
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
function makeRow(label, name, range, changeHandler) {
|
|
||||||
var row = document.createElement("div");
|
|
||||||
row.classList.add('cal-time');
|
|
||||||
|
|
||||||
var labelCol = row.appendChild(document.createElement("div"));
|
|
||||||
labelCol.classList.add('cal-time-label');
|
|
||||||
labelCol.innerText = label;
|
|
||||||
|
|
||||||
var valueCol = row.appendChild(document.createElement("div"));
|
|
||||||
valueCol.classList.add('cal-time-value');
|
|
||||||
valueCol.innerText = '00';
|
|
||||||
|
|
||||||
var inputCol = row.appendChild(document.createElement("div"));
|
|
||||||
var slider = inputCol.appendChild(document.createElement("input"));
|
|
||||||
Object.assign(slider, {step:1, min:0, max:range, name:name, type:'range'});
|
|
||||||
Object.defineProperty(footer, name, {value: slider});
|
|
||||||
inputCol.classList.add('cal-time-slider');
|
|
||||||
slider.onchange = changeHandler;
|
|
||||||
slider.oninput = changeHandler;
|
|
||||||
slider.onmousedown = changeHandler;
|
|
||||||
self[name] = self[name] || parseInt(slider.value) || 0;
|
|
||||||
footer.appendChild(row)
|
|
||||||
}
|
|
||||||
makeRow('HH:', 'hours', 23, handler);
|
|
||||||
makeRow('MM:', 'minutes', 59, handler);
|
|
||||||
makeRow('SS:', 'seconds', 59, handler);
|
|
||||||
|
|
||||||
footer.classList.add("cal-footer");
|
|
||||||
Object.defineProperty(this.el, "footer", { value: footer });
|
|
||||||
tryAppendChild(footer, this.el.wrapper);
|
|
||||||
}
|
|
||||||
this.setFooterContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
DTBox.prototype.setFooterContent = function() {
|
|
||||||
if (this.el.footer) {
|
|
||||||
var footer = this.el.footer;
|
|
||||||
footer.hours.value = this.hours;
|
|
||||||
footer.children[0].children[1].innerText = padded(this.hours, 2);
|
|
||||||
footer.minutes.value = this.minutes;
|
|
||||||
footer.children[1].children[1].innerText = padded(this.minutes, 2);
|
|
||||||
footer.seconds.value = this.seconds;
|
|
||||||
footer.children[2].children[1].innerText = padded(this.seconds, 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DTBox.prototype.setInputValue = function() {
|
|
||||||
var date = new Date(this.year, this.month, this.day);
|
|
||||||
var strings = [];
|
|
||||||
if (this.settings.config.showDate) {
|
|
||||||
strings.push(renderDate(date, this.settings));
|
|
||||||
}
|
|
||||||
if (this.settings.config.showTime) {
|
|
||||||
var joined = new Date(date.getTime() + this.time);
|
|
||||||
strings.push(renderTime(joined, this.settings));
|
|
||||||
}
|
|
||||||
this.elem.value = strings.join(', ');
|
|
||||||
}
|
|
||||||
|
|
||||||
DTBox.prototype.onDateSelected = function (e) {
|
|
||||||
var row = e.target.parentNode;
|
|
||||||
var date = parseInt(e.target.innerText);
|
|
||||||
if (!(row.nextSibling && row.nextSibling.nextSibling) && date < 8) {
|
|
||||||
this.month += 1;
|
|
||||||
} else if (!(row.previousSibling && row.previousSibling.previousSibling) && date > 7) {
|
|
||||||
this.month -= 1;
|
|
||||||
}
|
|
||||||
this.day = parseInt(e.target.innerText);
|
|
||||||
this.value = new Date(this.year, this.month, this.day);
|
|
||||||
this.setInputValue();
|
|
||||||
this.setHeaderContent();
|
|
||||||
this.setBodyContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {Event} e */
|
|
||||||
DTBox.prototype.onMonthSelected = function (e) {
|
|
||||||
var col = 0;
|
|
||||||
var row = 2;
|
|
||||||
var cell = e.target;
|
|
||||||
if (cell.parentNode.nextSibling){
|
|
||||||
row = cell.parentNode.previousSibling ? 1: 0;
|
|
||||||
}
|
|
||||||
if (cell.previousSibling) {
|
|
||||||
col = 3;
|
|
||||||
if (cell.nextSibling) {
|
|
||||||
col = cell.previousSibling.previousSibling ? 2 : 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.month = 4 * row + col;
|
|
||||||
this.bodyType = "DAYS";
|
|
||||||
this.setHeaderContent();
|
|
||||||
this.setupBody();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {Event} e */
|
|
||||||
DTBox.prototype.onYearSelected = function (e) {
|
|
||||||
this.year = parseInt(e.target.innerText);
|
|
||||||
this.bodyType = "MONTHS";
|
|
||||||
this.setHeaderContent();
|
|
||||||
this.setupBody();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {Event} e */
|
|
||||||
DTBox.prototype.onHeaderChange = function (e) {
|
|
||||||
var cell = e.target;
|
|
||||||
if (cell.previousSibling && cell.nextSibling) {
|
|
||||||
var idx = BODYTYPES.indexOf(this.bodyType);
|
|
||||||
if (idx < 0 || !BODYTYPES[idx + 1]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.bodyType = BODYTYPES[idx + 1];
|
|
||||||
this.setupBody();
|
|
||||||
} else {
|
|
||||||
var sign = cell.previousSibling ? 1 : -1;
|
|
||||||
switch (this.bodyType) {
|
|
||||||
case "DAYS":
|
|
||||||
this.month += sign * 1;
|
|
||||||
break;
|
|
||||||
case "MONTHS":
|
|
||||||
this.year += sign * 1;
|
|
||||||
break;
|
|
||||||
case "YEARS":
|
|
||||||
this.year += sign * 10;
|
|
||||||
}
|
|
||||||
if (this.month > 11 || this.month < 0) {
|
|
||||||
this.year += Math.floor(this.month / 11);
|
|
||||||
this.month = this.month > 11 ? 0 : 11;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.setHeaderContent();
|
|
||||||
this.setBodyContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {HTMLElement} elem
|
|
||||||
* @returns {{left:number, top:number}}
|
|
||||||
*/
|
|
||||||
function getOffset(elem) {
|
|
||||||
var box = elem.getBoundingClientRect();
|
|
||||||
var left = window.pageXOffset !== undefined ? window.pageXOffset :
|
|
||||||
(document.documentElement || document.body.parentNode || document.body).scrollLeft;
|
|
||||||
var top = window.pageYOffset !== undefined ? window.pageYOffset :
|
|
||||||
(document.documentElement || document.body.parentNode || document.body).scrollTop;
|
|
||||||
return { left: box.left + left, top: box.top + top };
|
|
||||||
}
|
|
||||||
function empty(e) {
|
|
||||||
for (; e.children.length; ) e.removeChild(e.children[0]);
|
|
||||||
}
|
|
||||||
function tryAppendChild(newChild, refNode) {
|
|
||||||
try {
|
|
||||||
refNode.appendChild(newChild);
|
|
||||||
return newChild;
|
|
||||||
} catch (e) {
|
|
||||||
console.trace(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @class */
|
|
||||||
function hookFuncs() {
|
|
||||||
/** @type {Handlers} */
|
|
||||||
this._funcs = {};
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @param {string} key
|
|
||||||
* @param {Function} func
|
|
||||||
*/
|
|
||||||
hookFuncs.prototype.add = function(key, func){
|
|
||||||
if (!this._funcs[key]){
|
|
||||||
this._funcs[key] = [];
|
|
||||||
}
|
|
||||||
this._funcs[key].push(func)
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @param {String} key
|
|
||||||
* @returns {Function[]} handlers
|
|
||||||
*/
|
|
||||||
hookFuncs.prototype.get = function(key){
|
|
||||||
return this._funcs[key] ? this._funcs[key] : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Array.<string>} arr
|
|
||||||
* @param {String} string
|
|
||||||
* @returns {Array.<string>} sorted string
|
|
||||||
*/
|
|
||||||
function sortByStringIndex(arr, string) {
|
|
||||||
return arr.sort(function(a, b){
|
|
||||||
var h = string.indexOf(a);
|
|
||||||
var l = string.indexOf(b);
|
|
||||||
var rank = 0;
|
|
||||||
if (h < l) {
|
|
||||||
rank = -1;
|
|
||||||
} else if (l < h) {
|
|
||||||
rank = 1;
|
|
||||||
} else if (a.length > b.length) {
|
|
||||||
rank = -1;
|
|
||||||
} else if (b.length > a.length) {
|
|
||||||
rank = 1;
|
|
||||||
}
|
|
||||||
return rank;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove keys from array that are not in format
|
|
||||||
* @param {string[]} keys
|
|
||||||
* @param {string} format
|
|
||||||
* @returns {string[]} new filtered array
|
|
||||||
*/
|
|
||||||
function filterFormatKeys(keys, format) {
|
|
||||||
var out = [];
|
|
||||||
var formatIdx = 0;
|
|
||||||
for (var i = 0; i<keys.length; i++) {
|
|
||||||
var key = keys[i];
|
|
||||||
if (format.slice(formatIdx).indexOf(key) > -1) {
|
|
||||||
formatIdx += key.length;
|
|
||||||
out.push(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @template {StringNumObj} FormatObj
|
|
||||||
* @param {string} value
|
|
||||||
* @param {string} format
|
|
||||||
* @param {FormatObj} formatObj
|
|
||||||
* @param {function(Object.<string, hookFuncs>): null} setHooks
|
|
||||||
* @returns {FormatObj} formatObj
|
|
||||||
*/
|
|
||||||
function parseData(value, format, formatObj, setHooks) {
|
|
||||||
var hooks = {
|
|
||||||
canSkip: new hookFuncs(),
|
|
||||||
updateValue: new hookFuncs(),
|
|
||||||
}
|
|
||||||
var keys = sortByStringIndex(Object.keys(formatObj), format);
|
|
||||||
var filterdKeys = filterFormatKeys(keys, format);
|
|
||||||
var vstart = 0; // value start
|
|
||||||
if (setHooks) {
|
|
||||||
setHooks(hooks);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = 0; i < keys.length; i++) {
|
|
||||||
var key = keys[i];
|
|
||||||
var fstart = format.indexOf(key);
|
|
||||||
var _vstart = vstart; // next value start
|
|
||||||
var val = null;
|
|
||||||
var canSkip = false;
|
|
||||||
var funcs = hooks.canSkip.get(key);
|
|
||||||
|
|
||||||
vstart = vstart || fstart;
|
|
||||||
|
|
||||||
for (var j = 0; j < funcs.length; j++) {
|
|
||||||
if (funcs[j](formatObj)){
|
|
||||||
canSkip = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (fstart > -1 && !canSkip) {
|
|
||||||
var sep = null;
|
|
||||||
var stop = vstart + key.length;
|
|
||||||
var fnext = -1;
|
|
||||||
var nextKeyIdx = i + 1;
|
|
||||||
_vstart += key.length; // set next value start if current key is found
|
|
||||||
|
|
||||||
// get next format token used to determine separator
|
|
||||||
while (fnext == -1 && nextKeyIdx < keys.length){
|
|
||||||
var nextKey = keys[nextKeyIdx];
|
|
||||||
nextKeyIdx += 1;
|
|
||||||
if (filterdKeys.indexOf(nextKey) === -1) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
fnext = nextKey ? format.indexOf(nextKey) : -1; // next format start
|
|
||||||
}
|
|
||||||
if (fnext > -1){
|
|
||||||
sep = format.slice(stop, fnext);
|
|
||||||
if (sep) {
|
|
||||||
var _stop = value.slice(vstart).indexOf(sep);
|
|
||||||
if (_stop && _stop > -1){
|
|
||||||
stop = _stop + vstart;
|
|
||||||
_vstart = stop + sep.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val = parseInt(value.slice(vstart, stop));
|
|
||||||
|
|
||||||
var funcs = hooks.updateValue.get(key);
|
|
||||||
for (var k = 0; k < funcs.length; k++) {
|
|
||||||
val = funcs[k](val, formatObj, vstart, stop);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
formatObj[key] = { index: vstart, value: val };
|
|
||||||
vstart = _vstart; // set next value start
|
|
||||||
}
|
|
||||||
return formatObj;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {String} value
|
|
||||||
* @param {DTS} settings
|
|
||||||
* @returns {Date} date object
|
|
||||||
*/
|
|
||||||
function parseDate(value, settings) {
|
|
||||||
/** @type {{yyyy:number=, yy:number=, mm:number=, dd:number=}} */
|
|
||||||
var formatObj = {yyyy:null, yy:null, mm:null, dd:null};
|
|
||||||
var format = ((settings.dateFormat) || '').toLowerCase();
|
|
||||||
if (!format) {
|
|
||||||
throw new TypeError('dateFormat not found (' + settings.dateFormat + ')');
|
|
||||||
}
|
|
||||||
var formatObj = parseData(value, format, formatObj, function(hooks){
|
|
||||||
hooks.canSkip.add("yy", function(data){
|
|
||||||
return data["yyyy"].value;
|
|
||||||
});
|
|
||||||
hooks.updateValue.add("yy", function(val){
|
|
||||||
return 100 * Math.floor(new Date().getFullYear() / 100) + val;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
var year = formatObj["yyyy"].value || formatObj["yy"].value;
|
|
||||||
var month = formatObj["mm"].value - 1;
|
|
||||||
var date = formatObj["dd"].value;
|
|
||||||
var result = new Date(year, month, date);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {String} value
|
|
||||||
* @param {DTS} settings
|
|
||||||
* @returns {Number} time in milliseconds <= (24 * 60 * 60 * 1000) - 1
|
|
||||||
*/
|
|
||||||
function parseTime(value, settings) {
|
|
||||||
var format = ((settings.timeFormat) || '').toLowerCase();
|
|
||||||
if (!format) {
|
|
||||||
throw new TypeError('timeFormat not found (' + settings.timeFormat + ')');
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @type {{hh:number=, mm:number=, ss:number=, a:string=}} */
|
|
||||||
var formatObj = {hh:null, mm:null, ss:null, a:null};
|
|
||||||
var formatObj = parseData(value, format, formatObj, function(hooks){
|
|
||||||
hooks.updateValue.add("a", function(val, data, start, stop){
|
|
||||||
return value.slice(start, start + 2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
var hours = formatObj["hh"].value;
|
|
||||||
var minutes = formatObj["mm"].value;
|
|
||||||
var seconds = formatObj["ss"].value;
|
|
||||||
var am_pm = formatObj["a"].value;
|
|
||||||
var am_pm_lower = am_pm ? am_pm.toLowerCase() : am_pm;
|
|
||||||
if (am_pm && ["am", "pm"].indexOf(am_pm_lower) > -1){
|
|
||||||
if (am_pm_lower == 'am' && hours == 12){
|
|
||||||
hours = 0;
|
|
||||||
} else if (am_pm_lower == 'pm') {
|
|
||||||
hours += 12;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var time = hours * 60 * 60 * 1000 + minutes * 60 * 1000 + seconds * 1000;
|
|
||||||
return time;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Date} value
|
|
||||||
* @param {DTS} settings
|
|
||||||
* @returns {String} date string
|
|
||||||
*/
|
|
||||||
function renderDate(value, settings) {
|
|
||||||
var format = settings.dateFormat.toLowerCase();
|
|
||||||
var date = value.getDate();
|
|
||||||
var month = value.getMonth() + 1;
|
|
||||||
var year = value.getFullYear();
|
|
||||||
var yearShort = year % 100;
|
|
||||||
var formatObj = {
|
|
||||||
dd: date < 10 ? "0" + date : date,
|
|
||||||
mm: month < 10 ? "0" + month : month,
|
|
||||||
yyyy: year,
|
|
||||||
yy: yearShort < 10 ? "0" + yearShort : yearShort
|
|
||||||
};
|
|
||||||
var str = format.replace(settings.dateFormatRegEx, function (found) {
|
|
||||||
return formatObj[found];
|
|
||||||
});
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Date} value
|
|
||||||
* @param {DTS} settings
|
|
||||||
* @returns {String} date string
|
|
||||||
*/
|
|
||||||
function renderTime(value, settings) {
|
|
||||||
var Format = settings.timeFormat;
|
|
||||||
var format = Format.toLowerCase();
|
|
||||||
var hours = value.getHours();
|
|
||||||
var minutes = value.getMinutes();
|
|
||||||
var seconds = value.getSeconds();
|
|
||||||
var am_pm = null;
|
|
||||||
var hh_am_pm = null;
|
|
||||||
if (format.indexOf('a') > -1) {
|
|
||||||
am_pm = hours >= 12 ? 'pm' : 'am';
|
|
||||||
am_pm = Format.indexOf('A') > -1 ? am_pm.toUpperCase() : am_pm;
|
|
||||||
hh_am_pm = hours == 0 ? '12' : (hours > 12 ? hours%12 : hours);
|
|
||||||
}
|
|
||||||
var formatObj = {
|
|
||||||
hh: am_pm ? hh_am_pm : (hours < 10 ? "0" + hours : hours),
|
|
||||||
mm: minutes < 10 ? "0" + minutes : minutes,
|
|
||||||
ss: seconds < 10 ? "0" + seconds : seconds,
|
|
||||||
a: am_pm,
|
|
||||||
};
|
|
||||||
var str = format.replace(settings.timeFormatRegEx, function (found) {
|
|
||||||
return formatObj[found];
|
|
||||||
});
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* checks if two dates are equal
|
|
||||||
* @param {Date} date1
|
|
||||||
* @param {Date} date2
|
|
||||||
* @returns {Boolean} true or false
|
|
||||||
*/
|
|
||||||
function isEqualDate(date1, date2) {
|
|
||||||
if (!(date1 && date2)) return false;
|
|
||||||
return (date1.getFullYear() == date2.getFullYear() &&
|
|
||||||
date1.getMonth() == date2.getMonth() &&
|
|
||||||
date1.getDate() == date2.getDate());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Number} val
|
|
||||||
* @param {Number} pad
|
|
||||||
* @param {*} default_val
|
|
||||||
* @returns {String} padded string
|
|
||||||
*/
|
|
||||||
function padded(val, pad, default_val) {
|
|
||||||
var default_val = default_val || 0;
|
|
||||||
var valStr = '' + (parseInt(val) || default_val);
|
|
||||||
var diff = Math.max(pad, valStr.length) - valStr.length;
|
|
||||||
return ('' + default_val).repeat(diff) + valStr;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @template X
|
|
||||||
* @template Y
|
|
||||||
* @param {X} obj
|
|
||||||
* @param {Y} objDefaults
|
|
||||||
* @returns {X|Y} merged object
|
|
||||||
*/
|
|
||||||
function setDefaults(obj, objDefaults) {
|
|
||||||
var keys = Object.keys(objDefaults);
|
|
||||||
for (var i=0; i<keys.length; i++) {
|
|
||||||
var key = keys[i];
|
|
||||||
if (!Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
||||||
obj[key] = objDefaults[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
window.dtsel = Object.create({},{
|
|
||||||
DTS: { value: DTS },
|
|
||||||
DTObj: { value: DTBox },
|
|
||||||
fn: {
|
|
||||||
value: Object.defineProperties({}, {
|
|
||||||
empty: { value: empty },
|
|
||||||
appendAfter: {
|
|
||||||
value: function (newElem, refNode) {
|
|
||||||
refNode.parentNode.insertBefore(newElem, refNode.nextSibling);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
getOffset: { value: getOffset },
|
|
||||||
parseDate: { value: parseDate },
|
|
||||||
renderDate: { value: renderDate },
|
|
||||||
parseTime: {value: parseTime},
|
|
||||||
renderTime: {value: renderTime},
|
|
||||||
setDefaults: {value: setDefaults},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
})();
|
|
@ -1,23 +0,0 @@
|
|||||||
function collapse_all() {
|
|
||||||
document.querySelectorAll("div.reminderContent:not(.creator)").forEach((el) => {
|
|
||||||
el.classList.add("is-collapsed");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function expand_all() {
|
|
||||||
document.querySelectorAll("div.reminderContent:not(.creator)").forEach((el) => {
|
|
||||||
el.classList.remove("is-collapsed");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const expandAll = document.querySelector("#expandAll");
|
|
||||||
|
|
||||||
expandAll.addEventListener("change", (ev) => {
|
|
||||||
if (ev.target.value === "expand") {
|
|
||||||
expand_all();
|
|
||||||
} else if (ev.target.value === "collapse") {
|
|
||||||
collapse_all();
|
|
||||||
}
|
|
||||||
|
|
||||||
ev.target.value = "";
|
|
||||||
});
|
|
@ -1,94 +0,0 @@
|
|||||||
function get_interval(element) {
|
|
||||||
let months = element.querySelector('input[name="interval_months"]').value;
|
|
||||||
let days = element.querySelector('input[name="interval_days"]').value;
|
|
||||||
let hours = element.querySelector('input[name="interval_hours"]').value;
|
|
||||||
let minutes = element.querySelector('input[name="interval_minutes"]').value;
|
|
||||||
let seconds = element.querySelector('input[name="interval_seconds"]').value;
|
|
||||||
|
|
||||||
return {
|
|
||||||
months: parseInt(months) || null,
|
|
||||||
days: parseInt(days) || null,
|
|
||||||
seconds:
|
|
||||||
(parseInt(hours) || 0) * 3600 +
|
|
||||||
(parseInt(minutes) || 0) * 60 +
|
|
||||||
(parseInt(seconds) || 0) || null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function update_interval(element) {
|
|
||||||
let months = element.querySelector('input[name="interval_months"]');
|
|
||||||
let days = element.querySelector('input[name="interval_days"]');
|
|
||||||
let hours = element.querySelector('input[name="interval_hours"]');
|
|
||||||
let minutes = element.querySelector('input[name="interval_minutes"]');
|
|
||||||
let seconds = element.querySelector('input[name="interval_seconds"]');
|
|
||||||
|
|
||||||
let interval = get_interval(element);
|
|
||||||
|
|
||||||
if (interval.months === null && interval.days === null && interval.seconds === null) {
|
|
||||||
months.value = "";
|
|
||||||
days.value = "";
|
|
||||||
hours.value = "";
|
|
||||||
minutes.value = "";
|
|
||||||
seconds.value = "";
|
|
||||||
} else {
|
|
||||||
months.value = months.value.padStart(1, "0");
|
|
||||||
days.value = days.value.padStart(1, "0");
|
|
||||||
hours.value = hours.value.padStart(2, "0");
|
|
||||||
minutes.value = minutes.value.padStart(2, "0");
|
|
||||||
seconds.value = seconds.value.padStart(2, "0");
|
|
||||||
|
|
||||||
if (seconds.value >= 60) {
|
|
||||||
let quotient = Math.floor(seconds.value / 60);
|
|
||||||
let remainder = seconds.value % 60;
|
|
||||||
|
|
||||||
seconds.value = String(remainder).padStart(2, "0");
|
|
||||||
minutes.value = String(Number(minutes.value) + Number(quotient)).padStart(
|
|
||||||
2,
|
|
||||||
"0"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (minutes.value >= 60) {
|
|
||||||
let quotient = Math.floor(minutes.value / 60);
|
|
||||||
let remainder = minutes.value % 60;
|
|
||||||
|
|
||||||
minutes.value = String(remainder).padStart(2, "0");
|
|
||||||
hours.value = String(Number(hours.value) + Number(quotient)).padStart(2, "0");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const $intervalGroup = document.querySelector(".interval-group");
|
|
||||||
|
|
||||||
document.querySelector(".interval-group").addEventListener(
|
|
||||||
"blur",
|
|
||||||
(ev) => {
|
|
||||||
if (ev.target.nodeName !== "BUTTON") update_interval($intervalGroup);
|
|
||||||
},
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
$intervalGroup.querySelector("button.clear").addEventListener("click", () => {
|
|
||||||
$intervalGroup.querySelectorAll("input").forEach((el) => {
|
|
||||||
el.value = "";
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener("remindersLoaded", (event) => {
|
|
||||||
for (reminder of event.detail) {
|
|
||||||
let $intervalGroup = reminder.node.querySelector(".interval-group");
|
|
||||||
|
|
||||||
$intervalGroup.addEventListener(
|
|
||||||
"blur",
|
|
||||||
(ev) => {
|
|
||||||
if (ev.target.nodeName !== "BUTTON") update_interval($intervalGroup);
|
|
||||||
},
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
$intervalGroup.querySelector("button.clear").addEventListener("click", () => {
|
|
||||||
$intervalGroup.querySelectorAll("input").forEach((el) => {
|
|
||||||
el.value = "";
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,2 +0,0 @@
|
|||||||
/*! js-cookie v3.0.0-rc.0 | MIT */
|
|
||||||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self,function(){var r=e.Cookies,n=e.Cookies=t();n.noConflict=function(){return e.Cookies=r,n}}())}(this,function(){"use strict";function e(e){for(var t=1;t<arguments.length;t++){var r=arguments[t];for(var n in r)e[n]=r[n]}return e}var t={read:function(e){return e.replace(/%3B/g,";")},write:function(e){return e.replace(/;/g,"%3B")}};return function r(n,i){function o(r,o,u){if("undefined"!=typeof document){"number"==typeof(u=e({},i,u)).expires&&(u.expires=new Date(Date.now()+864e5*u.expires)),u.expires&&(u.expires=u.expires.toUTCString()),r=t.write(r).replace(/=/g,"%3D"),o=n.write(String(o),r);var c="";for(var f in u)u[f]&&(c+="; "+f,!0!==u[f]&&(c+="="+u[f].split(";")[0]));return document.cookie=r+"="+o+c}}return Object.create({set:o,get:function(e){if("undefined"!=typeof document&&(!arguments.length||e)){for(var r=document.cookie?document.cookie.split("; "):[],i={},o=0;o<r.length;o++){var u=r[o].split("="),c=u.slice(1).join("="),f=t.read(u[0]).replace(/%3D/g,"=");if(i[f]=n.read(c,f),e===f)break}return e?i[e]:i}},remove:function(t,r){o(t,"",e({},r,{expires:-1}))},withAttributes:function(t){return r(this.converter,e({},this.attributes,t))},withConverter:function(t){return r(e({},this.converter,t),this.attributes)}},{attributes:{value:Object.freeze(i)},converter:{value:Object.freeze(n)}})}(t,{path:"/"})});
|
|
@ -1,16 +0,0 @@
|
|||||||
const REPORTER_ID = crypto.randomUUID();
|
|
||||||
|
|
||||||
window.addEventListener("error", async (ev) => {
|
|
||||||
await fetch("/report", {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({
|
|
||||||
reporterId: REPORTER_ID,
|
|
||||||
url: window.location.href,
|
|
||||||
relativeTimestamp: ev.timeStamp,
|
|
||||||
errorMessage: ev.message,
|
|
||||||
errorLine: ev.lineno,
|
|
||||||
errorFile: ev.filename,
|
|
||||||
errorType: ev.type,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,70 +0,0 @@
|
|||||||
let guildReminders = document.querySelector("#guildReminders");
|
|
||||||
|
|
||||||
function sort_by(cond) {
|
|
||||||
if (cond === "channel") {
|
|
||||||
[...guildReminders.children]
|
|
||||||
.sort((a, b) => {
|
|
||||||
let channel1 = a.querySelector("select.channel-selector").value;
|
|
||||||
let channel2 = b.querySelector("select.channel-selector").value;
|
|
||||||
|
|
||||||
return channel1 > channel2 ? 1 : -1;
|
|
||||||
})
|
|
||||||
.forEach((node) => guildReminders.appendChild(node));
|
|
||||||
|
|
||||||
// go through and add channel categories
|
|
||||||
let currentChannelGroup = null;
|
|
||||||
for (let child of guildReminders.querySelectorAll("div.reminderContent")) {
|
|
||||||
let thisChannelGroup = child.querySelector("select.channel-selector").value;
|
|
||||||
|
|
||||||
if (currentChannelGroup !== thisChannelGroup) {
|
|
||||||
let newNode = document.createElement("div");
|
|
||||||
newNode.textContent =
|
|
||||||
"#" + channels.find((a) => a.id === thisChannelGroup).name;
|
|
||||||
newNode.classList.add("channel-tag");
|
|
||||||
|
|
||||||
guildReminders.insertBefore(newNode, child);
|
|
||||||
|
|
||||||
currentChannelGroup = thisChannelGroup;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// remove any channel tags if previous ordering was by channel
|
|
||||||
guildReminders.querySelectorAll("div.channel-tag").forEach((el) => {
|
|
||||||
el.remove();
|
|
||||||
});
|
|
||||||
|
|
||||||
if (cond === "time") {
|
|
||||||
[...guildReminders.children]
|
|
||||||
.sort((a, b) => {
|
|
||||||
let time1 = luxon.DateTime.fromISO(
|
|
||||||
a.querySelector('input[name="time"]').value
|
|
||||||
);
|
|
||||||
let time2 = luxon.DateTime.fromISO(
|
|
||||||
b.querySelector('input[name="time"]').value
|
|
||||||
);
|
|
||||||
|
|
||||||
return time1 > time2 ? 1 : -1;
|
|
||||||
})
|
|
||||||
.forEach((node) => guildReminders.appendChild(node));
|
|
||||||
} else {
|
|
||||||
[...guildReminders.children]
|
|
||||||
.sort((a, b) => {
|
|
||||||
let name1 = a.querySelector('input[name="name"]').value;
|
|
||||||
let name2 = b.querySelector('input[name="name"]').value;
|
|
||||||
|
|
||||||
return name1 > name2 ? 1 : -1;
|
|
||||||
})
|
|
||||||
.forEach((node) => guildReminders.appendChild(node));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const selector = document.querySelector("#orderBy");
|
|
||||||
|
|
||||||
selector.addEventListener("change", () => {
|
|
||||||
sort_by(selector.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener("remindersLoaded", () => {
|
|
||||||
sort_by(selector.value);
|
|
||||||
});
|
|