Compare commits

..

12 Commits

Author SHA1 Message Date
jude d77ff6b216 2nd attempt 2026-05-25 20:29:14 +01:00
jude b1fda50d4c Fix re-rendering issue 2026-05-23 18:15:41 +01:00
jude 05c7b9151c Remove column drop 2026-05-21 20:15:43 +01:00
jude f1e0a7cdf8 Disable cache lookup for users 2026-05-21 20:09:06 +01:00
jude 050277ac8b Add dark mode support 2026-05-16 19:17:29 +01:00
jude 9c6c324b02 Remove npm build as its done by build.rs 2025-12-22 16:54:21 +00:00
jude d7065cf0d6 Remove ignored file 2025-12-17 18:32:16 +00:00
jude b0c6eedba9 Update run containerfile for /remind support 2025-11-26 18:35:20 +00:00
jude 77690b57c6 Merge pull request 'jude/docker' (#5) from jude/docker into current
Reviewed-on: #5
2025-11-23 10:34:03 +00:00
jude b273d8035a Use Caddy to serve localhost HTTPS 2025-11-23 10:32:37 +00:00
jude a4ec39e4a2 Fix dashboard rendering 2025-11-09 16:07:14 +00:00
jude 901cf575c4 Docker compose setup 2025-11-05 18:30:49 +00:00
49 changed files with 717 additions and 930 deletions
+3 -2
View File
@@ -3,8 +3,8 @@ target
/venv
.cargo
.idea
web/static/index.html
web/static/assets
static/index.html
static/assets
# Logs
logs
*.log
@@ -28,3 +28,4 @@ dist-ssr
*.njsproj
*.sln
*.sw?
.junie
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "DELETE FROM patreon_link WHERE user_id = ?",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "0402e16b1ec89a96d893d43f6b40500ccbde3c619116a702c87954df49898e23"
}
@@ -1,12 +0,0 @@
{
"db_name": "MySQL",
"query": "\n UPDATE reminders SET `status` = 'deleted' WHERE FIND_IN_SET(id, ?)\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "09f6269e5df3acc01e8e2660532b5fcab21c0cd4fd126b580176d24578932d7e"
}
@@ -0,0 +1,24 @@
{
"db_name": "MySQL",
"query": "\n SELECT IFNULL(timezone, 'UTC') AS timezone\n FROM users\n WHERE timezone IS NOT NULL\n GROUP BY timezone\n ORDER BY COUNT(timezone) DESC\n LIMIT 21\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "timezone",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL",
"max_size": 128
}
}
],
"parameters": {
"Right": 0
},
"nullable": [
false
]
},
"hash": "19bc60a2ff67ce6e169985a76405af51d7d16d4d7b84d1c239de5af79da93268"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\n DELETE FROM reminders\n WHERE `utc_time` < NOW() - INTERVAL ? DAY\n AND status != 'pending'\n ORDER BY `utc_time`\n LIMIT 1000\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "2d780695fe98347ea4ab2cb745462f0a9c55cf913c71d4d822b91958f4f8a729"
}
@@ -1,12 +0,0 @@
{
"db_name": "MySQL",
"query": "\n UPDATE reminders\n INNER JOIN `channels`\n ON `channels`.id = reminders.channel_id\n SET reminders.`utc_time` = reminders.`utc_time` + ?\n WHERE channels.`channel` = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "2db489e076c93a5a2baf2dd48eb3278d68296aea93097a642e2bbb5112d51fe8"
}
@@ -1,134 +0,0 @@
{
"db_name": "MySQL",
"query": "\n SELECT\n reminders.id,\n reminders.uid,\n channels.channel,\n reminders.utc_time,\n reminders.interval_seconds,\n reminders.interval_days,\n reminders.interval_months,\n reminders.expires,\n reminders.enabled,\n reminders.content,\n reminders.embed_description,\n reminders.set_by\n FROM\n reminders\n LEFT JOIN\n channels\n ON\n channels.id = reminders.channel_id\n WHERE\n `status` = 'pending' AND\n FIND_IN_SET(channels.channel, ?)\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT",
"max_size": 10
}
},
{
"ordinal": 1,
"name": "uid",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE",
"max_size": 256
}
},
{
"ordinal": 2,
"name": "channel",
"type_info": {
"type": "LongLong",
"flags": "UNIQUE_KEY | UNSIGNED | NO_DEFAULT_VALUE",
"max_size": 20
}
},
{
"ordinal": 3,
"name": "utc_time",
"type_info": {
"type": "Datetime",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"max_size": 19
}
},
{
"ordinal": 4,
"name": "interval_seconds",
"type_info": {
"type": "Long",
"flags": "UNSIGNED",
"max_size": 10
}
},
{
"ordinal": 5,
"name": "interval_days",
"type_info": {
"type": "Long",
"flags": "UNSIGNED",
"max_size": 10
}
},
{
"ordinal": 6,
"name": "interval_months",
"type_info": {
"type": "Long",
"flags": "UNSIGNED",
"max_size": 10
}
},
{
"ordinal": 7,
"name": "expires",
"type_info": {
"type": "Datetime",
"flags": "BINARY",
"max_size": 19
}
},
{
"ordinal": 8,
"name": "enabled",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"max_size": 1
}
},
{
"ordinal": 9,
"name": "content",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL",
"max_size": 8192
}
},
{
"ordinal": 10,
"name": "embed_description",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL",
"max_size": 8192
}
},
{
"ordinal": 11,
"name": "set_by",
"type_info": {
"type": "LongLong",
"flags": "MULTIPLE_KEY | UNSIGNED",
"max_size": 20
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
true,
false,
true,
true,
true,
true,
false,
false,
false,
true
]
},
"hash": "3695f95cea95c075b2b3becdf1b5d75bf1ccace3b9a176086faa4ad76c0a0fbd"
}
@@ -1,12 +0,0 @@
{
"db_name": "MySQL",
"query": "\n UPDATE reminders\n INNER JOIN `channels`\n ON `channels`.id = reminders.channel_id\n SET reminders.`utc_time` = DATE_ADD(reminders.`utc_time`, INTERVAL ? SECOND)\n WHERE FIND_IN_SET(channels.`channel`, ?)\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "65496ff85dd92b5aaf12519628afdc16ca7d70131744c9c53880dc56b92991d9"
}
@@ -1,12 +0,0 @@
{
"db_name": "MySQL",
"query": "\n DELETE FROM todos WHERE FIND_IN_SET(id, ?)\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "6e00a27fa770d1aa8cac48cd8878e712ef536c67eeb4fb9a4a801459ada35715"
}
@@ -0,0 +1,24 @@
{
"db_name": "MySQL",
"query": "SELECT user_id FROM patreon_link WHERE guild_id = ?",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "user_id",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | PRIMARY_KEY | MULTIPLE_KEY | UNSIGNED | NO_DEFAULT_VALUE",
"max_size": 20
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false
]
},
"hash": "92cdd6af01e398b22112ffe88b9ff63d9cc61faaf0dee9eda974efbc8bf84173"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\n DELETE FROM reminders\n WHERE `utc_time` < NOW() - INTERVAL ? DAY\n ORDER BY `utc_time`\n LIMIT 1000\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "93897198be27266cd9de90063ee67594cf65c1216c9b9787fc96cd8ffcc1cdef"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\n UPDATE reminders\n INNER JOIN `channels`\n ON `channels`.id = reminders.channel_id\n SET reminders.`utc_time` = reminders.`utc_time` + ?\n WHERE channels.`channel` = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "9b871f08d294555453696808185c6d29d4753619fbee6295a053cefaa9dcc0ae"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "INSERT INTO patreon_link (user_id, guild_id, linked_at) VALUES (?, ?, NOW())\n ON DUPLICATE KEY UPDATE guild_id = ?",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "a647934dc5485cfbf430c77b71f7b181f888b0961d5274621e5e1dd76417080e"
}
@@ -1,264 +0,0 @@
{
"db_name": "MySQL",
"query": "\n SELECT\n reminders.attachment_name,\n reminders.avatar,\n channels.channel,\n reminders.content,\n reminders.embed_author,\n reminders.embed_author_url,\n reminders.embed_color,\n reminders.embed_description,\n reminders.embed_footer,\n reminders.embed_footer_url,\n reminders.embed_image_url,\n reminders.embed_thumbnail_url,\n reminders.embed_title,\n IFNULL(reminders.embed_fields, '[]') AS embed_fields,\n reminders.enabled,\n reminders.expires,\n reminders.interval_seconds,\n reminders.interval_days,\n reminders.interval_months,\n reminders.name,\n reminders.restartable,\n reminders.tts,\n reminders.uid,\n reminders.username,\n reminders.utc_time\n FROM reminders\n INNER JOIN channels ON channels.id = reminders.channel_id\n WHERE `status` = 'pending' AND FIND_IN_SET(channels.channel, ?)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "attachment_name",
"type_info": {
"type": "VarString",
"flags": "",
"max_size": 1040
}
},
{
"ordinal": 1,
"name": "avatar",
"type_info": {
"type": "VarString",
"flags": "",
"max_size": 2048
}
},
{
"ordinal": 2,
"name": "channel",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNIQUE_KEY | UNSIGNED | NO_DEFAULT_VALUE",
"max_size": 20
}
},
{
"ordinal": 3,
"name": "content",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL",
"max_size": 8192
}
},
{
"ordinal": 4,
"name": "embed_author",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL",
"max_size": 1024
}
},
{
"ordinal": 5,
"name": "embed_author_url",
"type_info": {
"type": "VarString",
"flags": "",
"max_size": 2048
}
},
{
"ordinal": 6,
"name": "embed_color",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | UNSIGNED",
"max_size": 10
}
},
{
"ordinal": 7,
"name": "embed_description",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL",
"max_size": 8192
}
},
{
"ordinal": 8,
"name": "embed_footer",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL",
"max_size": 8192
}
},
{
"ordinal": 9,
"name": "embed_footer_url",
"type_info": {
"type": "VarString",
"flags": "",
"max_size": 2048
}
},
{
"ordinal": 10,
"name": "embed_image_url",
"type_info": {
"type": "VarString",
"flags": "",
"max_size": 2048
}
},
{
"ordinal": 11,
"name": "embed_thumbnail_url",
"type_info": {
"type": "VarString",
"flags": "",
"max_size": 2048
}
},
{
"ordinal": 12,
"name": "embed_title",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL",
"max_size": 1024
}
},
{
"ordinal": 13,
"name": "embed_fields",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY",
"max_size": 4294967292
}
},
{
"ordinal": 14,
"name": "enabled",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"max_size": 1
}
},
{
"ordinal": 15,
"name": "expires",
"type_info": {
"type": "Datetime",
"flags": "BINARY",
"max_size": 19
}
},
{
"ordinal": 16,
"name": "interval_seconds",
"type_info": {
"type": "Long",
"flags": "UNSIGNED",
"max_size": 10
}
},
{
"ordinal": 17,
"name": "interval_days",
"type_info": {
"type": "Long",
"flags": "UNSIGNED",
"max_size": 10
}
},
{
"ordinal": 18,
"name": "interval_months",
"type_info": {
"type": "Long",
"flags": "UNSIGNED",
"max_size": 10
}
},
{
"ordinal": 19,
"name": "name",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL",
"max_size": 400
}
},
{
"ordinal": 20,
"name": "restartable",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"max_size": 1
}
},
{
"ordinal": 21,
"name": "tts",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"max_size": 1
}
},
{
"ordinal": 22,
"name": "uid",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE",
"max_size": 256
}
},
{
"ordinal": 23,
"name": "username",
"type_info": {
"type": "VarString",
"flags": "",
"max_size": 128
}
},
{
"ordinal": 24,
"name": "utc_time",
"type_info": {
"type": "Datetime",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"max_size": 19
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
true,
true,
false,
false,
false,
true,
false,
false,
false,
true,
true,
true,
false,
false,
false,
true,
true,
true,
true,
false,
false,
false,
false,
true,
false
]
},
"hash": "af5bf4c6b30ffd316ecebc2dd53554e41f0d4f40cad63736930d20cb18159b38"
}
@@ -1,24 +0,0 @@
{
"db_name": "MySQL",
"query": "\n SELECT IFNULL(timezone, 'UTC') AS timezone\n FROM users\n WHERE timezone IS NOT NULL\n GROUP BY timezone\n ORDER BY COUNT(timezone) DESC\n LIMIT 21\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "timezone",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL",
"max_size": 128
}
}
],
"parameters": {
"Right": 0
},
"nullable": [
false
]
},
"hash": "bbdd4bd7ebffb97efab8ba7e829159e104615260929341ec0e961b4d5cd6ca0c"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\n SELECT\n reminders.id,\n reminders.uid,\n channels.channel,\n reminders.utc_time,\n reminders.interval_seconds,\n reminders.interval_days,\n reminders.interval_months,\n reminders.expires,\n reminders.enabled,\n reminders.content,\n reminders.embed_description,\n reminders.set_by\n FROM\n reminders\n INNER JOIN\n channels\n ON\n reminders.channel_id = channels.id\n WHERE\n `status` = 'pending' AND\n channels.channel = ? AND\n FIND_IN_SET(reminders.enabled, ?)\n ORDER BY\n reminders.utc_time\n ",
"query": "\n SELECT\n reminders.id,\n reminders.uid,\n channels.channel,\n reminders.utc_time,\n reminders.interval_seconds,\n reminders.interval_days,\n reminders.interval_months,\n reminders.expires,\n reminders.enabled,\n reminders.content,\n reminders.embed_description,\n reminders.set_by\n FROM\n reminders\n INNER JOIN\n channels\n ON\n reminders.channel_id = channels.id\n WHERE\n `status` = 'pending' AND\n channels.channel = ? AND\n reminders.enabled >= ?\n ORDER BY\n reminders.utc_time\n ",
"describe": {
"columns": [
{
@@ -130,5 +130,5 @@
true
]
},
"hash": "d60ea641070dbd882cd53878fa109d08bd3f65e0da8c263e78fe0b200228bc2b"
"hash": "d2921961627fef0e12892dbbcd4b891e58c0e52c20897aab3d95365774c01bda"
}
@@ -1,264 +0,0 @@
{
"db_name": "MySQL",
"query": "SELECT\n reminders.attachment,\n reminders.attachment_name,\n reminders.avatar,\n CONCAT('#', channels.channel) AS channel,\n reminders.content,\n reminders.embed_author,\n reminders.embed_author_url,\n reminders.embed_color,\n reminders.embed_description,\n reminders.embed_footer,\n reminders.embed_footer_url,\n reminders.embed_image_url,\n reminders.embed_thumbnail_url,\n reminders.embed_title,\n reminders.embed_fields,\n reminders.enabled,\n reminders.expires,\n reminders.interval_seconds,\n reminders.interval_days,\n reminders.interval_months,\n reminders.name,\n reminders.restartable,\n reminders.tts,\n reminders.username,\n reminders.utc_time\n FROM reminders\n LEFT JOIN channels ON channels.id = reminders.channel_id\n WHERE FIND_IN_SET(channels.channel, ?) AND status = 'pending'",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "attachment",
"type_info": {
"type": "Blob",
"flags": "BLOB | BINARY",
"max_size": 16777215
}
},
{
"ordinal": 1,
"name": "attachment_name",
"type_info": {
"type": "VarString",
"flags": "",
"max_size": 1040
}
},
{
"ordinal": 2,
"name": "avatar",
"type_info": {
"type": "VarString",
"flags": "",
"max_size": 2048
}
},
{
"ordinal": 3,
"name": "channel",
"type_info": {
"type": "VarString",
"flags": "",
"max_size": 84
}
},
{
"ordinal": 4,
"name": "content",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL",
"max_size": 8192
}
},
{
"ordinal": 5,
"name": "embed_author",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL",
"max_size": 1024
}
},
{
"ordinal": 6,
"name": "embed_author_url",
"type_info": {
"type": "VarString",
"flags": "",
"max_size": 2048
}
},
{
"ordinal": 7,
"name": "embed_color",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | UNSIGNED",
"max_size": 10
}
},
{
"ordinal": 8,
"name": "embed_description",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL",
"max_size": 8192
}
},
{
"ordinal": 9,
"name": "embed_footer",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL",
"max_size": 8192
}
},
{
"ordinal": 10,
"name": "embed_footer_url",
"type_info": {
"type": "VarString",
"flags": "",
"max_size": 2048
}
},
{
"ordinal": 11,
"name": "embed_image_url",
"type_info": {
"type": "VarString",
"flags": "",
"max_size": 2048
}
},
{
"ordinal": 12,
"name": "embed_thumbnail_url",
"type_info": {
"type": "VarString",
"flags": "",
"max_size": 2048
}
},
{
"ordinal": 13,
"name": "embed_title",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL",
"max_size": 1024
}
},
{
"ordinal": 14,
"name": "embed_fields",
"type_info": {
"type": "Json",
"flags": "BLOB | BINARY",
"max_size": 4294967295
}
},
{
"ordinal": 15,
"name": "enabled",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"max_size": 1
}
},
{
"ordinal": 16,
"name": "expires",
"type_info": {
"type": "Datetime",
"flags": "BINARY",
"max_size": 19
}
},
{
"ordinal": 17,
"name": "interval_seconds",
"type_info": {
"type": "Long",
"flags": "UNSIGNED",
"max_size": 10
}
},
{
"ordinal": 18,
"name": "interval_days",
"type_info": {
"type": "Long",
"flags": "UNSIGNED",
"max_size": 10
}
},
{
"ordinal": 19,
"name": "interval_months",
"type_info": {
"type": "Long",
"flags": "UNSIGNED",
"max_size": 10
}
},
{
"ordinal": 20,
"name": "name",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL",
"max_size": 400
}
},
{
"ordinal": 21,
"name": "restartable",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"max_size": 1
}
},
{
"ordinal": 22,
"name": "tts",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"max_size": 1
}
},
{
"ordinal": 23,
"name": "username",
"type_info": {
"type": "VarString",
"flags": "",
"max_size": 128
}
},
{
"ordinal": 24,
"name": "utc_time",
"type_info": {
"type": "Datetime",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"max_size": 19
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
true,
true,
true,
true,
false,
false,
true,
false,
false,
false,
true,
true,
true,
false,
true,
false,
true,
true,
true,
true,
false,
false,
false,
true,
false
]
},
"hash": "db69def9391283efb9bf915223d7d6b2d169203b7dc45481a509137f7590d9a6"
}
+24
View File
@@ -0,0 +1,24 @@
# Caddy v2 configuration for local development (HTTPS on localhost)
# Reverse-proxy to the bot service and serve static assets.
# Uses Caddy's internal CA to generate a self-signed certificate for localhost.
# HTTP -> HTTPS redirect for local development
:80 {
redir https://localhost{uri}
}
# Local HTTPS site with self-signed cert
localhost {
encode zstd gzip
# Issue a locally-trusted certificate via Caddy's internal CA
tls internal
# Serve static files under /static from the mounted volume
handle_path /static* {
root * /var/www/reminder-rs/static
file_server
}
# Proxy everything else to the bot service inside the Docker network
reverse_proxy bot:18920
}
Generated
+1 -1
View File
@@ -2623,7 +2623,7 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "reminder-rs"
version = "1.7.41"
version = "1.7.44"
dependencies = [
"base64 0.22.1",
"chrono",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "reminder-rs"
version = "1.7.41"
version = "1.7.44"
authors = ["Jude Southworth <judesouthworth@pm.me>"]
edition = "2021"
license = "AGPL-3.0 only"
+16 -10
View File
@@ -1,11 +1,10 @@
FROM ubuntu:24.04
FROM alpine:latest AS build
ENV RUSTUP_HOME=/usr/local/rustup \
CARGO_HOME=/usr/local/cargo \
PATH=/usr/local/cargo/bin:$PATH
RUN apt update
RUN DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC apt install -y gcc gcc-multilib pkg-config libssl-dev curl mysql-client-8.0 npm
RUN apk add gcc build-base pkgconfig openssl-dev openssl-libs-static curl mysql-client npm
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path --profile minimal --default-toolchain nightly
WORKDIR /usr/src/reminder-rs
@@ -17,13 +16,7 @@ COPY ./conf ./conf
COPY ./extract_derive ./extract_derive
COPY ./migrations ./migrations
COPY ./recordable_derive ./recordable_derive
COPY ./reminder-dashboard/public ./reminder-dashboard/public
COPY ./reminder-dashboard/src ./reminder-dashboard/src
COPY ./reminder-dashboard/index.html ./reminder-dashboard/
COPY ./reminder-dashboard/package.json ./reminder-dashboard/
COPY ./reminder-dashboard/package-lock.json ./reminder-dashboard/
COPY ./reminder-dashboard/tsconfig.json ./reminder-dashboard/
COPY ./reminder-dashboard/vite.config.ts ./reminder-dashboard/
COPY ./reminder-dashboard ./reminder-dashboard
COPY ./src ./src
COPY ./static ./static
COPY ./templates ./templates
@@ -32,6 +25,19 @@ COPY ./Cargo.lock ./
COPY ./Cargo.toml ./
COPY ./dp.py ./
# Build and install the Rust binary
RUN cargo install --path .
FROM alpine:latest AS runtime
WORKDIR /usr/src/reminder-rs
COPY --from=build /usr/local/cargo/bin/reminder-rs /usr/local/bin/reminder-rs
COPY --from=build /usr/src/reminder-rs/static /usr/src/reminder-rs/static
COPY --from=build /usr/src/reminder-rs/templates /usr/src/reminder-rs/templates
RUN apk add python3 py3-pip
RUN pip3 install --no-cache --break-system-packages dateparser
EXPOSE 18920
CMD ["reminder-rs"]
+39 -13
View File
@@ -1,25 +1,31 @@
# reminder-rs
Reminder Bot for Discord.
## 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
I 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.
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
Recommended method.
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.
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.
1. Install container software: `sudo apt install podman`.
2. Install database server: `sudo apt install mysql-server-8.0`. Create a database called `reminders`
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`
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
@@ -27,26 +33,46 @@ By default, this builds targeting Ubuntu 20.04. Modify the Containerfile if you
`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`.
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
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__
* `DATABASE_URL` - the URL of your MySQL database (`mysql://user[:password]@domain/database`)
* `DISCORD_TOKEN` - your application's bot user's authorization token
__Other Variables__
* `MIN_INTERVAL` - default `600`, defines the shortest interval the bot should accept
* `LOCAL_TIMEZONE` - default `UTC`, necessary for calculations in the natural language processor
* `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
* `PYTHON_LOCATION` - default `/usr/bin/python3`. Can be changed if your Python executable is located somewhere else
* `THEME_COLOR` - default `8fb677`. Specifies the hex value of the color to use on info message embeds
* `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
* `PYTHON_LOCATION` - default `/usr/bin/python3`. Can be changed if your Python executable is
located somewhere else
* `THEME_COLOR` - default `8fb677`. Specifies the hex value of the color to use on info message
embeds
## Running with Docker
A `compose.yml` file is provided to aid in running the bot agnostically using docker.
* Populate a `.env` file as in `conf/default.env`
* Add the additional variable `ROCKET_SECRET_KEY` with a key generated from
`head -c64 /dev/urandom | base64`
* Run `docker compose up`
Please note that this is _not_ production-ready when run via compose. We do not offer a way for
backing up of your data, or a way to run the dashboard securely via HTTPS, which is required for
OAuth.
+21 -1
View File
@@ -8,6 +8,26 @@ fn main() {
.arg("run")
.arg("build")
.current_dir(Path::new("reminder-dashboard"))
.env("VITE_VERSION", env!("CARGO_PKG_VERSION"))
.spawn()
.expect("Failed to build NPM");
.expect("Failed to build NPM")
.wait()
.expect("Failed to wait for NPM build");
Command::new("cp")
.arg("reminder-dashboard/dist/index.html")
.arg("static/index.html")
.spawn()
.expect("Failed to copy index.html")
.wait()
.expect("Failed to wait for index.html copy");
Command::new("cp")
.arg("-r")
.arg("reminder-dashboard/dist/static/assets")
.arg("static/")
.spawn()
.expect("Failed to copy assets")
.wait()
.expect("Failed to wait for assets copy");
}
+52 -14
View File
@@ -1,21 +1,59 @@
version: '3.3'
services:
bot:
build: ./Containerfile.run
image: reminder-rs-run
restart: always
expose:
- '80'
database:
image: mysql:8.0
restart: always
command: --log-bin-trust-function-creators=1
environment:
MYSQL_DATABASE: 'reminders'
MYSQL_USER: 'reminder-bot'
ports:
- '3306:3306'
expose:
- '3306'
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: reminders
MYSQL_USER: reminder-bot
MYSQL_PASSWORD: password
volumes:
- reminders:/var/lib/mysql
bot:
build:
context: .
dockerfile: Containerfile.run
image: reminder-rs-run
restart: always
depends_on:
- database
env_file:
- .env
environment:
DATABASE_URL: "mysql://reminder-bot:password@database/reminders"
DISCORD_TOKEN:
PATREON_GUILD_ID:
PATREON_ROLE_ID:
LOCAL_TIMEZONE:
MIN_INTERVAL:
ROCKET_SECRET_KEY:
ROCKET_ADDRESS: "0.0.0.0"
ROCKET_PORT: "18920"
REMIND_INTERVAL:
OAUTH2_DISCORD_CALLBACK:
OAUTH2_CLIENT_ID:
OAUTH2_CLIENT_SECRET:
ports:
- "18920:18920"
proxy:
image: caddy:2.4.6-alpine
restart: always
depends_on:
- bot
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./static:/var/www/reminder-rs/static:ro
- caddy_data:/data
- caddy_config:/config
volumes:
reminders:
caddy_data:
caddy_config:
@@ -0,0 +1,13 @@
SET foreign_key_checks = 0;
-- Drop all old tables
DROP TABLE IF EXISTS users_old;
DROP TABLE IF EXISTS messages;
DROP TABLE IF EXISTS embeds;
DROP TABLE IF EXISTS embed_fields;
DROP TABLE IF EXISTS command_aliases;
DROP TABLE IF EXISTS macro;
DROP TABLE IF EXISTS roles;
DROP TABLE IF EXISTS command_restrictions;
SET foreign_key_checks = 1;
+2 -1
View File
@@ -5,7 +5,8 @@
"scripts": {
"dev": "vite build --watch --mode development",
"build": "vite build",
"preview": "vite preview"
"preview": "vite preview",
"prettier": "prettier -w src/"
},
"dependencies": {
"axios": "^1.5.1",
+1 -1
View File
@@ -116,7 +116,7 @@ type Template = {
embed_fields: EmbedField[] | null;
};
const USER_INFO_STALE_TIME = 120_000;
const USER_INFO_STALE_TIME = 86_400_000;
const GUILD_INFO_STALE_TIME = 300_000;
const OTHER_STALE_TIME = 120_000;
@@ -0,0 +1,43 @@
import { createContext } from "preact";
import { useContext } from "preact/compat";
import { useEffect, useState } from "preact/hooks";
import { fetchUserInfo } from "../../api";
import { useQuery } from "react-query";
type TColorScheme = "light" | "dark";
type TColorSchemeContext = {
colorScheme: TColorScheme;
};
const ColorSchemeContext = createContext({ colorScheme: "light" } as TColorSchemeContext);
export const ColorSchemeProvider = ({ children }) => {
const { data } = useQuery({ ...fetchUserInfo() });
const [activeScheme, setActiveScheme] = useState<TColorScheme>("light");
useEffect(() => {
const preference = data?.preferences?.dashboard_color_scheme || "system";
if (preference === "system") {
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
const handleChange = () => {
setActiveScheme(mediaQuery.matches ? "dark" : "light");
};
handleChange();
mediaQuery.addEventListener("change", handleChange);
return () => mediaQuery.removeEventListener("change", handleChange);
} else {
setActiveScheme(preference as TColorScheme);
}
}, [data]);
return (
<ColorSchemeContext.Provider value={{ colorScheme: activeScheme }}>
{children}
</ColorSchemeContext.Provider>
);
};
export const useColorScheme = () => useContext(ColorSchemeContext);
+27 -20
View File
@@ -8,42 +8,36 @@ import { TimezoneProvider } from "./TimezoneProvider";
import {User} from "../User";
import {GuildReminders} from "../Guild/GuildReminders";
import {GuildTodos} from "../Guild/GuildTodos";
import {ColorSchemeProvider, useColorScheme} from "./ColorSchemeProvider";
import {useEffect} from "preact/hooks";
export function App() {
const queryClient = new QueryClient();
const InnerApp = () => {
const {colorScheme} = useColorScheme();
let scheme = "light";
// if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
// scheme = "dark";
// }
useEffect(() => {
const body = document.querySelector("body");
body.className = body.className.replace(/scheme-\w+/g, "");
body.classList.add(`scheme-${colorScheme}`);
}, [colorScheme]);
return (
<QueryClientProvider client={queryClient}>
<TimezoneProvider>
<FlashProvider>
<Router base={"/dashboard"}>
<div class={`columns is-gapless dashboard-frame scheme-${scheme}`}>
<div class={`columns is-gapless dashboard-frame scheme-${colorScheme}`}>
<Sidebar/>
<div class="column is-main-content">
<div style={{margin: "0 12px 12px 12px"}}>
<Switch>
<Route path={"/@me/reminders"} component={User}></Route>
<Route
path={"/:guild/reminders"}
component={() => (
<Route path={"/:guild/reminders"}>
<Guild>
<GuildReminders/>
</Guild>
)}
></Route>
<Route
path={"/:guild/todos"}
component={() => (
</Route>
<Route path={"/:guild/todos"}>
<Guild>
<GuildTodos/>
</Guild>
)}
></Route>
</Route>
<Route>
<Welcome/>
</Route>
@@ -52,8 +46,21 @@ export function App() {
</div>
</div>
</Router>
);
};
const queryClient = new QueryClient();
export function App() {
return (
<QueryClientProvider client={queryClient}>
<ColorSchemeProvider>
<TimezoneProvider>
<FlashProvider>
<InnerApp/>
</FlashProvider>
</TimezoneProvider>
</ColorSchemeProvider>
</QueryClientProvider>
);
}
@@ -5,6 +5,12 @@ import { useRef, useState } from "preact/hooks";
import { ICON_FLASH_TIME } from "../../consts";
import { useFlash } from "../App/FlashContext";
enum ColorScheme {
System = "system",
Dark = "dark",
Light = "light",
}
export const UserPreferences = () => {
const [modalOpen, setModalOpen] = useState(false);
@@ -96,6 +102,40 @@ const PreferencesModal = ({ setModalOpen }) => {
</label>
</div>
<br></br>
<div style={{ display: "flex", flexDirection: "row", alignContent: "center" }}>
<label>
<div class={"is-inline-block"} style={{ marginRight: "6px" }}>
Dashboard Color Scheme:
</div>
<div class={"control"}>
<div class={"is-inline-block select"}>
{isLoading && <i class={"fa fa-spinner"} />}
{isSuccess && (
<select
class={"channel-selector"}
value={
updatedSettings.dashboard_color_scheme === undefined
? data.preferences.dashboard_color_scheme
: updatedSettings.dashboard_color_scheme
}
onChange={(e) =>
setUpdatedSettings((s) => ({
...s,
dashboard_color_scheme: (e.target as HTMLSelectElement)
.value as ColorScheme,
}))
}
>
<option value={ColorScheme.System}>System default</option>
<option value={ColorScheme.Light}>Light</option>
<option value={ColorScheme.Dark}>Dark</option>
</select>
)}
</div>
</div>
</label>
</div>
<br></br>
<div class="has-text-centered">
<button
class="button is-success is-outlined"
@@ -14,6 +14,9 @@ export const Welcome = () => (
<p>
<strong>Please report bugs!</strong> I can't fix issues if I am unaware of them.
</p>
<p>
Client version: {import.meta.env.VITE_VERSION}
</p>
</div>
</section>
);
+222
View File
@@ -57,3 +57,225 @@ div.reminderContent.is-collapsed .hide-box i {
.button.is-outlined.is-success:not(.is-focused, :hover, :focus) {
background-color: white;
}
@import "vars";
.scheme-light {
background-color: $primary-background-light;
color: $primary-text-light;
.column.is-main-content {
background-color: $primary-background-light;
}
.reminderContent {
background-color: $secondary-background-light;
}
}
.scheme-dark {
background-color: $secondary-background-dark;
color: $primary-text-dark;
.column.is-main-content {
background-color: $secondary-background-dark;
}
.reminderContent {
background-color: $primary-background-dark;
color: $primary-text-dark;
}
.title,
.subtitle,
.label,
.help,
strong {
color: $primary-text-dark;
}
input,
textarea,
select,
.input,
.textarea {
background-color: $secondary-background-dark;
color: $primary-text-dark;
border-color: $contrast-background-dark;
&::placeholder {
color: #aaa;
}
}
// Buttons
.button.is-outlined.is-success:not(.is-focused, :hover, :focus) {
background-color: $primary-background-dark;
color: #48c78e;
border-color: #48c78e;
}
.button.is-outlined.is-danger:not(.is-focused, :hover, :focus) {
background-color: $primary-background-dark;
}
.button.is-outlined.is-warning:not(.is-focused, :hover, :focus) {
background-color: $primary-background-dark;
}
.button:not(.is-success, .is-danger, .is-warning, .is-light) {
background-color: $secondary-background-dark;
color: $primary-text-dark;
border-color: $contrast-background-dark;
}
.button.is-light {
background-color: $secondary-background-dark;
color: $primary-text-dark;
}
// Modal
.modal-card-head,
.modal-card-foot {
background-color: $contrast-background-dark;
border-color: $contrast-background-dark;
}
.modal-card-head .modal-card-title {
color: $primary-text-dark;
}
.modal-card-body {
background-color: $primary-background-dark;
color: $primary-text-dark;
}
// Navbar
.navbar {
background-color: $contrast-background-dark;
}
.navbar-item,
.navbar-burger {
color: $primary-text-dark;
}
// Select dropdown arrow
.select:not(.is-multiple):not(.is-loading)::after {
border-color: $primary-text-dark;
}
.select select {
background-color: $secondary-background-dark;
color: $primary-text-dark;
border-color: $contrast-background-dark;
}
// Interval selector inputs
.interval-group input {
background-color: $secondary-background-dark !important;
color: $primary-text-dark;
}
// Notification
.notification {
background-color: $secondary-background-dark;
color: $primary-text-dark;
}
// Links
a:not(.menu a):not(.button) {
color: #7eaef1;
}
// Content area
.content {
color: $primary-text-dark;
}
// Checkbox
.checkbox {
color: $primary-text-dark;
}
// Message input
.message-input {
background-color: $secondary-background-dark;
color: $primary-text-dark;
}
// Box
.box {
background-color: $secondary-background-dark;
color: $primary-text-dark;
}
// Panel
.panel {
background-color: $secondary-background-dark;
.panel-heading {
background-color: $contrast-background-dark;
color: $primary-text-dark;
}
.panel-block {
color: $primary-text-dark;
border-color: $contrast-background-dark;
}
}
// Horizontal rule
hr {
background-color: $contrast-background-dark;
}
// Tag
.tag:not(.is-success, .is-danger, .is-warning, .is-info) {
background-color: $contrast-background-dark;
color: $primary-text-dark;
}
// Todo
.todo {
color: $primary-text-dark;
}
// Page links
.page-links .button {
background-color: $secondary-background-dark;
color: $primary-text-dark;
border-color: $contrast-background-dark;
}
// Loading overlay
.load-screen {
background-color: rgba(36, 36, 36, 0.8);
color: $primary-text-dark;
}
// File/attachment input
.file-cta {
background-color: $secondary-background-dark;
color: $primary-text-dark;
border-color: $contrast-background-dark;
}
.file-name {
background-color: $secondary-background-dark;
color: $primary-text-dark;
border-color: $contrast-background-dark;
}
// Date/time inputs color scheme
input[type="date"],
input[type="time"],
input[type="datetime-local"] {
color-scheme: dark;
}
// Modal background overlay
.modal-background {
background-color: rgba(10, 10, 10, 0.86);
}
}
+2 -3
View File
@@ -56,7 +56,7 @@ impl Recordable for Options {
}),
};
let channel_id = if let Some(channel) = ctx.channel_id().to_channel_cached(&ctx.cache()) {
let channel_id = if let Some(channel) = ctx.cache().channel(ctx.channel_id()) {
if Some(channel.guild_id) == ctx.guild_id() {
flags.channel_id.unwrap_or_else(|| ctx.channel_id())
} else {
@@ -66,8 +66,7 @@ impl Recordable for Options {
ctx.channel_id()
};
let channel_name =
channel_id.to_channel_cached(&ctx.cache()).map(|channel| channel.name.clone());
let channel_name = ctx.cache().channel(channel_id).map(|channel| channel.name.clone());
let reminders = Reminder::from_channel(&ctx.data().database, channel_id, &flags).await;
+2 -2
View File
@@ -63,7 +63,7 @@ impl ComponentDataModel {
let flags = pager.flags;
let channel_id = {
let channel_opt = component.channel_id.to_channel_cached(&ctx.cache);
let channel_opt = ctx.cache.channel(component.channel_id);
if let Some(channel) = channel_opt {
if Some(channel.guild_id) == component.guild_id {
@@ -85,7 +85,7 @@ impl ComponentDataModel {
.div_ceil(EMBED_DESCRIPTION_MAX_LENGTH);
let channel_name =
channel_id.to_channel_cached(&ctx.cache).map(|channel| channel.name.clone());
ctx.cache.channel(channel_id).map(|channel| channel.name.clone());
let next_page = pager.next_page(pages);
+3 -10
View File
@@ -8,9 +8,7 @@ use crate::{consts::DEFAULT_AVATAR, Error};
pub struct ChannelData {
pub id: u32,
pub channel: u64,
pub name: Option<String>,
pub nudge: i16,
pub blacklisted: bool,
pub webhook_id: Option<u64>,
pub webhook_token: Option<String>,
pub paused: bool,
@@ -27,7 +25,7 @@ impl ChannelData {
if let Ok(c) = sqlx::query_as_unchecked!(
Self,
"
SELECT id, channel, name, nudge, blacklisted, webhook_id, webhook_token, paused,
SELECT id, channel, nudge, webhook_id, webhook_token, paused,
paused_until
FROM channels
WHERE channel = ?
@@ -45,9 +43,8 @@ impl ChannelData {
if let Some((a, b)) = props { (Some(a), Some(b)) } else { (None, None) };
sqlx::query!(
"INSERT IGNORE INTO channels (channel, name, guild_id) VALUES (?, ?, (SELECT id FROM guilds WHERE guild = ?))",
"INSERT IGNORE INTO channels (channel, guild_id) VALUES (?, (SELECT id FROM guilds WHERE guild = ?))",
channel_id,
channel_name,
guild_id
)
.execute(&pool.clone())
@@ -56,7 +53,7 @@ impl ChannelData {
Ok(sqlx::query_as_unchecked!(
Self,
"
SELECT id, channel, name, nudge, blacklisted, webhook_id, webhook_token, paused, paused_until
SELECT id, channel, nudge, webhook_id, webhook_token, paused, paused_until
FROM channels
WHERE channel = ?
",
@@ -72,18 +69,14 @@ impl ChannelData {
"
UPDATE channels
SET
name = ?,
nudge = ?,
blacklisted = ?,
webhook_id = ?,
webhook_token = ?,
paused = ?,
paused_until = ?
WHERE id = ?
",
self.name,
self.nudge,
self.blacklisted,
self.webhook_id,
self.webhook_token,
self.paused,
+8 -8
View File
@@ -215,17 +215,17 @@ impl<'a> MultiReminderBuilder<'a> {
for scope in self.scopes {
let db_channel_id = match scope {
ReminderScope::User(user_id) => {
if let Ok(user) = UserId::new(user_id).to_user(&self.ctx).await {
let user_data = UserData::from_user(
&user,
let user_id = UserId::new(user_id);
match UserData::from_user(
&user_id,
&self.ctx.serenity_context(),
&self.ctx.data().database,
)
.await
.unwrap();
{
Ok(user_data) => {
if let Some(guild_id) = self.guild_id {
if guild_id.member(&self.ctx, user).await.is_err() {
if guild_id.member(&self.ctx, user_id).await.is_err() {
Err(ReminderError::InvalidTag)
} else if self.set_by.map_or(true, |i| i != user_data.id)
&& !user_data.allowed_dm
@@ -237,8 +237,8 @@ impl<'a> MultiReminderBuilder<'a> {
} else {
Ok((user_data.dm_channel, None))
}
} else {
Err(ReminderError::InvalidTag)
}
Err(_) => Err(ReminderError::InvalidTag),
}
}
ReminderScope::Channel(channel_with_thread) => {
+1 -1
View File
@@ -42,7 +42,7 @@ impl IpBlocking {
fn contains<I: Into<u32>>(&self, ip: I) -> bool {
let ip: u32 = ip.into();
let mut prev_index = self.upper_ips.len() - 1;
let _prev_index = self.upper_ips.len() - 1;
let mut index = self.upper_ips.len() / 2;
loop {
if self.upper_ips[index] <= ip && self.lower_ips[index] >= ip {
+3 -6
View File
@@ -63,7 +63,6 @@ use log::{error, info, warn};
use oauth2::{basic::BasicClient, AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl};
use poise::serenity_prelude::{
client::Context,
http::CacheHttp,
model::id::{GuildId, UserId},
};
use rocket::{
@@ -76,13 +75,10 @@ use rocket::{
};
use rocket_dyn_templates::Template;
use sqlx::{MySql, Pool};
use std::net::Ipv4Addr;
use std::{env, path::Path};
use crate::web::{
consts::{CNC_GUILD, DISCORD_OAUTH_AUTHORIZE, DISCORD_OAUTH_TOKEN, SUBSCRIPTION_ROLES},
fairings::metrics::MetricProducer,
};
use crate::web::consts::{DISCORD_OAUTH_AUTHORIZE, DISCORD_OAUTH_TOKEN};
use crate::web::fairings::metrics::MetricProducer;
type Database = MySql;
@@ -126,6 +122,7 @@ pub async fn initialize(
let static_path =
if Path::new("static").exists() { "static" } else { "/lib/reminder-rs/static" };
info!("Using static path: {}", static_path);
rocket::build()
.attach(MetricProducer)
+1 -6
View File
@@ -5,8 +5,6 @@ mod roles;
mod templates;
pub mod todos;
use std::env;
use crate::utils::check_subscription;
use crate::web::guards::transaction::Transaction;
use crate::web::{check_authorization, routes::JsonResult};
@@ -16,10 +14,7 @@ pub use reminders::*;
use rocket::{get, http::CookieJar, serde::json::json, State};
pub use roles::get_guild_roles;
use serenity::all::UserId;
use serenity::{
client::Context,
model::id::{GuildId, RoleId},
};
use serenity::{client::Context, model::id::GuildId};
pub use templates::*;
#[get("/api/guild/<id>")]
@@ -267,9 +267,8 @@ pub async fn edit_reminder(
}
if reminder.channel > 0 {
let channel_guild = ChannelId::new(reminder.channel)
.to_channel_cached(&ctx.cache)
.map(|channel| channel.guild_id);
let channel_guild =
ctx.cache.channel(ChannelId::new(reminder.channel)).map(|channel| channel.guild_id);
match channel_guild {
Some(channel_guild) => {
let channel_matches_guild = channel_guild.get() == id;
@@ -3,13 +3,10 @@ use crate::web::{
check_authorization,
guards::transaction::Transaction,
routes::{
dashboard::{
create_reminder, CreateReminder, ImportBody, ReminderCsv, ReminderTemplateCsv, TodoCsv,
},
dashboard::{ImportBody, ReminderTemplateCsv},
JsonResult,
},
};
use crate::Database;
use base64::{prelude::BASE64_STANDARD, Engine};
use csv::{QuoteStyle, WriterBuilder};
use log::warn;
@@ -20,10 +17,7 @@ use rocket::{
serde::json::{json, Json},
State,
};
use serenity::{
client::Context,
model::id::{ChannelId, GuildId, UserId},
};
use serenity::{client::Context, model::id::GuildId};
use sqlx::{MySql, Pool};
#[get("/api/guild/<id>/export/reminder_templates")]
+2 -4
View File
@@ -2,9 +2,7 @@ use crate::web::{
check_authorization,
guards::transaction::Transaction,
routes::{
dashboard::{
create_reminder, CreateReminder, ImportBody, ReminderCsv, ReminderTemplateCsv, TodoCsv,
},
dashboard::{create_reminder, CreateReminder, ImportBody, ReminderCsv},
JsonResult,
},
};
@@ -21,7 +19,7 @@ use rocket::{
};
use serenity::{
client::Context,
model::id::{ChannelId, GuildId, UserId},
model::id::{GuildId, UserId},
};
use sqlx::{MySql, Pool};
+2 -6
View File
@@ -1,14 +1,10 @@
use crate::web::{
check_authorization,
guards::transaction::Transaction,
routes::{
dashboard::{
create_reminder, CreateReminder, ImportBody, ReminderCsv, ReminderTemplateCsv, TodoCsv,
},
dashboard::{ImportBody, TodoCsv},
JsonResult,
},
};
use crate::Database;
use base64::{prelude::BASE64_STANDARD, Engine};
use csv::{QuoteStyle, WriterBuilder};
use log::warn;
@@ -21,7 +17,7 @@ use rocket::{
};
use serenity::{
client::Context,
model::id::{ChannelId, GuildId, UserId},
model::id::{ChannelId, GuildId},
};
use sqlx::{MySql, Pool};
-1
View File
@@ -1 +0,0 @@
/home/jude/reminder-bot/reminder-dashboard/dist/static/assets
-1
View File
@@ -1 +0,0 @@
/home/jude/reminder-bot/reminder-dashboard/dist/index.html