Compare commits
18 Commits
jude/restr
...
jude/impro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c98265657 | ||
|
|
b0c6eedba9 | ||
| 77690b57c6 | |||
|
|
b273d8035a | ||
|
|
a4ec39e4a2 | ||
|
|
901cf575c4 | ||
|
|
e98cb67f5f | ||
|
|
1d8fd39d13 | ||
|
|
91310d47d3 | ||
|
|
5ae4baa2a6 | ||
|
|
6884adc5b2 | ||
|
|
6ade91e11b | ||
| 20f0fb1c20 | |||
|
|
4d14365f2b | ||
|
|
0f4df703eb | ||
|
|
a9edcec43c | ||
|
|
cc5f6d9d55 | ||
|
|
761d545496 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -3,8 +3,8 @@ target
|
|||||||
/venv
|
/venv
|
||||||
.cargo
|
.cargo
|
||||||
.idea
|
.idea
|
||||||
web/static/index.html
|
static/index.html
|
||||||
web/static/assets
|
static/assets
|
||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
*.log
|
*.log
|
||||||
|
|||||||
12
.sqlx/query-0402e16b1ec89a96d893d43f6b40500ccbde3c619116a702c87954df49898e23.json
generated
Normal file
12
.sqlx/query-0402e16b1ec89a96d893d43f6b40500ccbde3c619116a702c87954df49898e23.json
generated
Normal file
@@ -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"
|
|
||||||
}
|
|
||||||
24
.sqlx/query-19bc60a2ff67ce6e169985a76405af51d7d16d4d7b84d1c239de5af79da93268.json
generated
Normal file
24
.sqlx/query-19bc60a2ff67ce6e169985a76405af51d7d16d4d7b84d1c239de5af79da93268.json
generated
Normal file
@@ -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"
|
||||||
|
}
|
||||||
12
.sqlx/query-2d780695fe98347ea4ab2cb745462f0a9c55cf913c71d4d822b91958f4f8a729.json
generated
Normal file
12
.sqlx/query-2d780695fe98347ea4ab2cb745462f0a9c55cf913c71d4d822b91958f4f8a729.json
generated
Normal file
@@ -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"
|
|
||||||
}
|
|
||||||
24
.sqlx/query-92cdd6af01e398b22112ffe88b9ff63d9cc61faaf0dee9eda974efbc8bf84173.json
generated
Normal file
24
.sqlx/query-92cdd6af01e398b22112ffe88b9ff63d9cc61faaf0dee9eda974efbc8bf84173.json
generated
Normal file
@@ -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"
|
||||||
|
}
|
||||||
12
.sqlx/query-93897198be27266cd9de90063ee67594cf65c1216c9b9787fc96cd8ffcc1cdef.json
generated
Normal file
12
.sqlx/query-93897198be27266cd9de90063ee67594cf65c1216c9b9787fc96cd8ffcc1cdef.json
generated
Normal file
@@ -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"
|
||||||
|
}
|
||||||
12
.sqlx/query-9b871f08d294555453696808185c6d29d4753619fbee6295a053cefaa9dcc0ae.json
generated
Normal file
12
.sqlx/query-9b871f08d294555453696808185c6d29d4753619fbee6295a053cefaa9dcc0ae.json
generated
Normal file
@@ -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"
|
||||||
|
}
|
||||||
12
.sqlx/query-a647934dc5485cfbf430c77b71f7b181f888b0961d5274621e5e1dd76417080e.json
generated
Normal file
12
.sqlx/query-a647934dc5485cfbf430c77b71f7b181f888b0961d5274621e5e1dd76417080e.json
generated
Normal file
@@ -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",
|
"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": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -130,5 +130,5 @@
|
|||||||
true
|
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
Caddyfile
Normal file
24
Caddyfile
Normal 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
|
||||||
|
}
|
||||||
14
Cargo.lock
generated
14
Cargo.lock
generated
@@ -1,6 +1,6 @@
|
|||||||
# This file is automatically @generated by Cargo.
|
# This file is automatically @generated by Cargo.
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
version = 4
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "addr2line"
|
name = "addr2line"
|
||||||
@@ -524,6 +524,15 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cron-parser"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "baa5650eabdaa360e2c240c2a5f544f10185b439cd76d748e44e3f28128a016b"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-channel"
|
name = "crossbeam-channel"
|
||||||
version = "0.5.13"
|
version = "0.5.13"
|
||||||
@@ -2614,11 +2623,12 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reminder-rs"
|
name = "reminder-rs"
|
||||||
version = "1.7.37"
|
version = "1.7.41"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"chrono",
|
"chrono",
|
||||||
"chrono-tz",
|
"chrono-tz",
|
||||||
|
"cron-parser",
|
||||||
"csv",
|
"csv",
|
||||||
"dotenv",
|
"dotenv",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "reminder-rs"
|
name = "reminder-rs"
|
||||||
version = "1.7.37"
|
version = "1.7.41"
|
||||||
authors = ["Jude Southworth <judesouthworth@pm.me>"]
|
authors = ["Jude Southworth <judesouthworth@pm.me>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "AGPL-3.0 only"
|
license = "AGPL-3.0 only"
|
||||||
@@ -35,6 +35,7 @@ serenity = { version = "0.12", default-features = false, features = ["builder",
|
|||||||
oauth2 = "4"
|
oauth2 = "4"
|
||||||
csv = "1.2"
|
csv = "1.2"
|
||||||
sd-notify = "0.4.1"
|
sd-notify = "0.4.1"
|
||||||
|
cron-parser = "0.10"
|
||||||
|
|
||||||
[dependencies.extract_derive]
|
[dependencies.extract_derive]
|
||||||
path = "extract_derive"
|
path = "extract_derive"
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
FROM ubuntu:24.04
|
FROM alpine:latest AS build
|
||||||
|
|
||||||
ENV RUSTUP_HOME=/usr/local/rustup \
|
ENV RUSTUP_HOME=/usr/local/rustup \
|
||||||
CARGO_HOME=/usr/local/cargo \
|
CARGO_HOME=/usr/local/cargo \
|
||||||
PATH=/usr/local/cargo/bin:$PATH
|
PATH=/usr/local/cargo/bin:$PATH
|
||||||
|
|
||||||
RUN apt update
|
RUN apk add gcc build-base pkgconfig openssl-dev openssl-libs-static curl mysql-client npm
|
||||||
RUN DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC apt install -y gcc gcc-multilib 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 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
|
WORKDIR /usr/src/reminder-rs
|
||||||
@@ -17,13 +16,7 @@ COPY ./conf ./conf
|
|||||||
COPY ./extract_derive ./extract_derive
|
COPY ./extract_derive ./extract_derive
|
||||||
COPY ./migrations ./migrations
|
COPY ./migrations ./migrations
|
||||||
COPY ./recordable_derive ./recordable_derive
|
COPY ./recordable_derive ./recordable_derive
|
||||||
COPY ./reminder-dashboard/public ./reminder-dashboard/public
|
COPY ./reminder-dashboard ./reminder-dashboard
|
||||||
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 ./src ./src
|
COPY ./src ./src
|
||||||
COPY ./static ./static
|
COPY ./static ./static
|
||||||
COPY ./templates ./templates
|
COPY ./templates ./templates
|
||||||
@@ -32,6 +25,22 @@ COPY ./Cargo.lock ./
|
|||||||
COPY ./Cargo.toml ./
|
COPY ./Cargo.toml ./
|
||||||
COPY ./dp.py ./
|
COPY ./dp.py ./
|
||||||
|
|
||||||
|
# Build dashboard assets explicitly to ensure dist exists
|
||||||
|
RUN npm ci --prefix reminder-dashboard && npm run build --prefix reminder-dashboard
|
||||||
|
|
||||||
|
# Build and install the Rust binary
|
||||||
RUN cargo install --path .
|
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"]
|
CMD ["reminder-rs"]
|
||||||
|
|||||||
58
README.md
58
README.md
@@ -1,52 +1,78 @@
|
|||||||
# reminder-rs
|
# reminder-rs
|
||||||
|
|
||||||
Reminder Bot for Discord.
|
Reminder Bot for Discord.
|
||||||
|
|
||||||
## 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
|
|
||||||
|
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.
|
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
|
### Build APT package
|
||||||
|
|
||||||
Recommended method.
|
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`.
|
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`
|
3. Install SQLx CLI: `cargo install sqlx-cli`
|
||||||
4. From the source code directory, execute `sqlx migrate run`
|
4. From the source code directory, execute `sqlx migrate run`
|
||||||
5. Build container image: `podman build -t reminder-rs .`
|
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
|
### Compiling for other target
|
||||||
|
|
||||||
1. Install requirements:
|
1. Install requirements:
|
||||||
`sudo apt install gcc gcc-multilib cmake libssl-dev build-essential python3-dateparser`
|
`sudo apt install gcc gcc-multilib cmake libssl-dev build-essential python3-dateparser`
|
||||||
2. Install rustup from https://rustup.rs
|
2. Install rustup from https://rustup.rs
|
||||||
3. Install the nightly toolchain: `rustup toolchain default nightly`
|
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`.
|
5. Install `sqlx-cli`: `cargo install sqlx-cli`.
|
||||||
6. Run migrations: `sqlx migrate run`.
|
6. Run migrations: `sqlx migrate run`.
|
||||||
7. Set environment variables:
|
7. Set environment variables:
|
||||||
* `DATABASE_URL` - the URL of your MySQL database (`mysql://user[:password]@domain/database`)
|
* `DATABASE_URL` - the URL of your MySQL database (`mysql://user[:password]@domain/database`)
|
||||||
8. Build: `cargo build --release`
|
8. Build: `cargo build --release`
|
||||||
|
|
||||||
|
|
||||||
### Configuring
|
### 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__
|
__Required Variables__
|
||||||
|
|
||||||
* `DATABASE_URL` - the URL of your MySQL database (`mysql://user[:password]@domain/database`)
|
* `DATABASE_URL` - the URL of your MySQL database (`mysql://user[:password]@domain/database`)
|
||||||
* `DISCORD_TOKEN` - your application's bot user's authorization token
|
* `DISCORD_TOKEN` - your application's bot user's authorization token
|
||||||
|
|
||||||
__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
|
||||||
* `LOCAL_TIMEZONE` - default `UTC`, necessary for calculations in the natural language processor
|
* `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
|
* `SUBSCRIPTION_ROLES` - default `None`, accepts a list of Discord role IDs that are given to
|
||||||
* `CNC_GUILD` - default `None`, accepts a single Discord guild ID for the server that the subscription roles belong to
|
subscribed users
|
||||||
* `PYTHON_LOCATION` - default `/usr/bin/python3`. Can be changed if your Python executable is located somewhere else
|
* `CNC_GUILD` - default `None`, accepts a single Discord guild ID for the server that the
|
||||||
* `THEME_COLOR` - default `8fb677`. Specifies the hex value of the color to use on info message embeds
|
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.
|
||||||
|
|||||||
13
build.rs
13
build.rs
@@ -10,4 +10,17 @@ fn main() {
|
|||||||
.current_dir(Path::new("reminder-dashboard"))
|
.current_dir(Path::new("reminder-dashboard"))
|
||||||
.spawn()
|
.spawn()
|
||||||
.expect("Failed to build NPM");
|
.expect("Failed to build NPM");
|
||||||
|
|
||||||
|
Command::new("cp")
|
||||||
|
.arg("reminder-dashboard/dist/index.html")
|
||||||
|
.arg("static/index.html")
|
||||||
|
.spawn()
|
||||||
|
.expect("Failed to copy index.html");
|
||||||
|
|
||||||
|
Command::new("cp")
|
||||||
|
.arg("-r")
|
||||||
|
.arg("reminder-dashboard/dist/static/assets")
|
||||||
|
.arg("static/")
|
||||||
|
.spawn()
|
||||||
|
.expect("Failed to copy assets");
|
||||||
}
|
}
|
||||||
|
|||||||
66
compose.yaml
66
compose.yaml
@@ -1,21 +1,59 @@
|
|||||||
version: '3.3'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
bot:
|
|
||||||
build: ./Containerfile.run
|
|
||||||
image: reminder-rs-run
|
|
||||||
restart: always
|
|
||||||
expose:
|
|
||||||
- '80'
|
|
||||||
database:
|
database:
|
||||||
image: mysql:8.0
|
image: mysql:8.0
|
||||||
restart: always
|
restart: always
|
||||||
|
command: --log-bin-trust-function-creators=1
|
||||||
environment:
|
environment:
|
||||||
MYSQL_DATABASE: 'reminders'
|
MYSQL_ROOT_PASSWORD: root
|
||||||
MYSQL_USER: 'reminder-bot'
|
MYSQL_DATABASE: reminders
|
||||||
ports:
|
MYSQL_USER: reminder-bot
|
||||||
- '3306:3306'
|
MYSQL_PASSWORD: password
|
||||||
expose:
|
|
||||||
- '3306'
|
|
||||||
volumes:
|
volumes:
|
||||||
- reminders:/var/lib/mysql
|
- 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:
|
||||||
|
|||||||
11
migrations/20250924203400_patreon_link.sql
Normal file
11
migrations/20250924203400_patreon_link.sql
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
CREATE TABLE patreon_link
|
||||||
|
(
|
||||||
|
user_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
guild_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
linked_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
PRIMARY KEY (user_id),
|
||||||
|
INDEX `idx_user_id` (user_id),
|
||||||
|
INDEX `idx_guild_id` (guild_id),
|
||||||
|
INDEX `idx_linked_at` (linked_at)
|
||||||
|
);
|
||||||
@@ -6,7 +6,7 @@ enum ColorScheme {
|
|||||||
Light = "light",
|
Light = "light",
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserInfo = {
|
export type UserInfo = {
|
||||||
name: string;
|
name: string;
|
||||||
patreon: boolean;
|
patreon: boolean;
|
||||||
preferences: {
|
preferences: {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useQuery, useQueryClient } from "react-query";
|
import { useQuery, useQueryClient } from "react-query";
|
||||||
import { fetchGuildChannels, fetchGuildReminders } from "../../api";
|
import { fetchGuildChannels, fetchGuildInfo, fetchGuildReminders, fetchUserInfo } from "../../api";
|
||||||
import { EditReminder } from "../Reminder/EditReminder";
|
import { EditReminder } from "../Reminder/EditReminder";
|
||||||
import { CreateReminder } from "../Reminder/CreateReminder";
|
import { CreateReminder } from "../Reminder/CreateReminder";
|
||||||
import { useCallback, useState } from "preact/hooks";
|
import { useCallback, useMemo, useState } from "preact/hooks";
|
||||||
import { Loader } from "../Loader";
|
import { Loader } from "../Loader";
|
||||||
import { useGuild } from "../App/useGuild";
|
import { useGuild } from "../App/useGuild";
|
||||||
|
|
||||||
@@ -22,6 +22,11 @@ export const GuildReminders = () => {
|
|||||||
data: guildReminders,
|
data: guildReminders,
|
||||||
} = useQuery(fetchGuildReminders(guild));
|
} = useQuery(fetchGuildReminders(guild));
|
||||||
const { data: channels } = useQuery(fetchGuildChannels(guild));
|
const { data: channels } = useQuery(fetchGuildChannels(guild));
|
||||||
|
const channelNames = useMemo(() => {
|
||||||
|
return new Map(channels.map((ch) => [ch.id, ch.name]));
|
||||||
|
}, [channels]);
|
||||||
|
const { isSuccess: guildFetched, data: guildInfo } = useQuery({ ...fetchGuildInfo(guild) });
|
||||||
|
const { isSuccess: userFetched, data: userInfo } = useQuery({ ...fetchUserInfo() });
|
||||||
|
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
const [sort, _setSort] = useState(Sort.Time);
|
const [sort, _setSort] = useState(Sort.Time);
|
||||||
@@ -34,9 +39,49 @@ export const GuildReminders = () => {
|
|||||||
_setSort(sort);
|
_setSort(sort);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const sorted = useMemo(
|
||||||
|
() =>
|
||||||
|
[...guildReminders]
|
||||||
|
.sort((r1, r2) => {
|
||||||
|
if (sort === Sort.Time) {
|
||||||
|
return r1.utc_time > r2.utc_time ? 1 : -1;
|
||||||
|
} else if (sort === Sort.Name) {
|
||||||
|
return r1.name > r2.name ? 1 : -1;
|
||||||
|
} else {
|
||||||
|
return r1.channel > r2.channel ? 1 : -1;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map((reminder) => {
|
||||||
|
let breaker = <></>;
|
||||||
|
if (sort === Sort.Channel && channels) {
|
||||||
|
if (prevReminder === null || prevReminder.channel !== reminder.channel) {
|
||||||
|
breaker = (
|
||||||
|
<div class={"channel-tag"}>#{channelNames[reminder.channel]}</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prevReminder = reminder;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{breaker}
|
||||||
|
<EditReminder
|
||||||
|
key={reminder.uid}
|
||||||
|
reminder={reminder}
|
||||||
|
guildInfo={guildInfo}
|
||||||
|
userInfo={userInfo}
|
||||||
|
globalCollapse={collapsed}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
[guildReminders, sort, channelNames, collapsed],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!isFetched && <Loader />}
|
{!(userFetched && guildFetched && isFetched) && <Loader />}
|
||||||
|
|
||||||
<strong>Create Reminder</strong>
|
<strong>Create Reminder</strong>
|
||||||
<div id={"reminderCreator"}>
|
<div id={"reminderCreator"}>
|
||||||
@@ -100,44 +145,7 @@ export const GuildReminders = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id={"guildReminders"} className={isFetching ? "loading" : ""}>
|
<div id={"guildReminders"} className={isFetching ? "loading" : ""}>
|
||||||
{isSuccess &&
|
{isSuccess && sorted}
|
||||||
guildReminders
|
|
||||||
.sort((r1, r2) => {
|
|
||||||
if (sort === Sort.Time) {
|
|
||||||
return r1.utc_time > r2.utc_time ? 1 : -1;
|
|
||||||
} else if (sort === Sort.Name) {
|
|
||||||
return r1.name > r2.name ? 1 : -1;
|
|
||||||
} else {
|
|
||||||
return r1.channel > r2.channel ? 1 : -1;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.map((reminder) => {
|
|
||||||
let breaker = <></>;
|
|
||||||
if (sort === Sort.Channel && channels) {
|
|
||||||
if (
|
|
||||||
prevReminder === null ||
|
|
||||||
prevReminder.channel !== reminder.channel
|
|
||||||
) {
|
|
||||||
const channel = channels.find(
|
|
||||||
(ch) => ch.id === reminder.channel,
|
|
||||||
);
|
|
||||||
breaker = <div class={"channel-tag"}>#{channel.name}</div>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
prevReminder = reminder;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{breaker}
|
|
||||||
<EditReminder
|
|
||||||
key={reminder.uid}
|
|
||||||
reminder={reminder}
|
|
||||||
globalCollapse={collapsed}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useRef, useState } from "preact/hooks";
|
import { useRef, useState } from "preact/hooks";
|
||||||
import { useMutation, useQueryClient } from "react-query";
|
import { useMutation } from "react-query";
|
||||||
import { patchGuildReminder, patchUserReminder } from "../../../api";
|
import { patchGuildReminder, patchUserReminder } from "../../../api";
|
||||||
import { ICON_FLASH_TIME } from "../../../consts";
|
import { ICON_FLASH_TIME } from "../../../consts";
|
||||||
import { DeleteButton } from "./DeleteButton";
|
import { DeleteButton } from "./DeleteButton";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Reminder } from "../../api";
|
import { Reminder, UserInfo, GuildInfo } from "../../api";
|
||||||
import { useEffect, useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
import { EditButtonRow } from "./ButtonRow/EditButtonRow";
|
import { EditButtonRow } from "./ButtonRow/EditButtonRow";
|
||||||
import { Message } from "./Message";
|
import { Message } from "./Message";
|
||||||
@@ -10,9 +10,16 @@ import "./styles.scss";
|
|||||||
type Props = {
|
type Props = {
|
||||||
reminder: Reminder;
|
reminder: Reminder;
|
||||||
globalCollapse: boolean;
|
globalCollapse: boolean;
|
||||||
|
userInfo: UserInfo;
|
||||||
|
guildInfo: GuildInfo;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EditReminder = ({ reminder: initialReminder, globalCollapse }: Props) => {
|
export const EditReminder = ({
|
||||||
|
reminder: initialReminder,
|
||||||
|
globalCollapse,
|
||||||
|
userInfo,
|
||||||
|
guildInfo,
|
||||||
|
}: Props) => {
|
||||||
const [propReminder, setPropReminder] = useState(initialReminder);
|
const [propReminder, setPropReminder] = useState(initialReminder);
|
||||||
const [reminder, setReminder] = useState(initialReminder);
|
const [reminder, setReminder] = useState(initialReminder);
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
@@ -39,11 +46,15 @@ export const EditReminder = ({ reminder: initialReminder, globalCollapse }: Prop
|
|||||||
setCollapsed(!collapsed);
|
setCollapsed(!collapsed);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div class="columns reminder-settings">
|
{!collapsed && (
|
||||||
<Message />
|
<>
|
||||||
<Settings />
|
<div class="columns reminder-settings">
|
||||||
</div>
|
<Message />
|
||||||
<EditButtonRow />
|
<Settings userInfo={userInfo} guildInfo={guildInfo} />
|
||||||
|
</div>
|
||||||
|
<EditButtonRow />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ReminderContext.Provider>
|
</ReminderContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,24 +1,21 @@
|
|||||||
import { ChannelSelector } from "./ChannelSelector";
|
import { ChannelSelector } from "./ChannelSelector";
|
||||||
import { IntervalSelector } from "./IntervalSelector";
|
import { IntervalSelector } from "./IntervalSelector";
|
||||||
import { useQuery } from "react-query";
|
|
||||||
import { fetchGuildInfo, fetchUserInfo } from "../../api";
|
|
||||||
import { useReminder } from "./ReminderContext";
|
import { useReminder } from "./ReminderContext";
|
||||||
import { Attachment } from "./Attachment";
|
import { Attachment } from "./Attachment";
|
||||||
import { TTS } from "./TTS";
|
import { TTS } from "./TTS";
|
||||||
import { TimeInput } from "./TimeInput";
|
import { TimeInput } from "./TimeInput";
|
||||||
import { useGuild } from "../App/useGuild";
|
import { useGuild } from "../App/useGuild";
|
||||||
|
import { GuildInfo, UserInfo } from "../../api";
|
||||||
|
|
||||||
export const Settings = () => {
|
type Props = {
|
||||||
|
userInfo: UserInfo;
|
||||||
|
guildInfo: GuildInfo;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Settings = ({ guildInfo, userInfo }: Props) => {
|
||||||
const guild = useGuild();
|
const guild = useGuild();
|
||||||
const { isSuccess: userFetched, data: userInfo } = useQuery({ ...fetchUserInfo() });
|
|
||||||
const { isSuccess: guildFetched, data: guildInfo } = useQuery({ ...fetchGuildInfo(guild) });
|
|
||||||
|
|
||||||
const [reminder, setReminder] = useReminder();
|
const [reminder, setReminder] = useReminder();
|
||||||
|
|
||||||
if (!userFetched || !guildFetched) {
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="column settings">
|
<div class="column settings">
|
||||||
{guild && (
|
{guild && (
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use std::time::{SystemTime, UNIX_EPOCH};
|
|||||||
use chrono_tz::TZ_VARIANTS;
|
use chrono_tz::TZ_VARIANTS;
|
||||||
use poise::serenity_prelude::AutocompleteChoice;
|
use poise::serenity_prelude::AutocompleteChoice;
|
||||||
|
|
||||||
|
use crate::time_parser::cron_next_timestamp;
|
||||||
use crate::{models::CtxData, time_parser::natural_parser, Context};
|
use crate::{models::CtxData, time_parser::natural_parser, Context};
|
||||||
|
|
||||||
pub async fn timezone_autocomplete(ctx: Context<'_>, partial: &str) -> Vec<String> {
|
pub async fn timezone_autocomplete(ctx: Context<'_>, partial: &str) -> Vec<String> {
|
||||||
@@ -42,7 +43,13 @@ pub async fn time_hint_autocomplete(ctx: Context<'_>, partial: &str) -> Vec<Auto
|
|||||||
if partial.is_empty() {
|
if partial.is_empty() {
|
||||||
vec![AutocompleteChoice::new("Start typing a time...".to_string(), "now".to_string())]
|
vec![AutocompleteChoice::new("Start typing a time...".to_string(), "now".to_string())]
|
||||||
} else {
|
} else {
|
||||||
match natural_parser(partial, &ctx.timezone().await.to_string()).await {
|
let timezone = ctx.timezone().await;
|
||||||
|
let timestamp = match cron_next_timestamp(partial, timezone) {
|
||||||
|
Some(ts) => Some(ts),
|
||||||
|
None => natural_parser(partial, &timezone.to_string()).await,
|
||||||
|
};
|
||||||
|
|
||||||
|
match timestamp {
|
||||||
Some(timestamp) => match SystemTime::now().duration_since(UNIX_EPOCH) {
|
Some(timestamp) => match SystemTime::now().duration_since(UNIX_EPOCH) {
|
||||||
Ok(now) => {
|
Ok(now) => {
|
||||||
let diff = timestamp - now.as_secs() as i64;
|
let diff = timestamp - now.as_secs() as i64;
|
||||||
|
|||||||
@@ -75,8 +75,8 @@ Please select a unique name for your macro.",
|
|||||||
CreateEmbed::new()
|
CreateEmbed::new()
|
||||||
.title("Macro Recording Started")
|
.title("Macro Recording Started")
|
||||||
.description(
|
.description(
|
||||||
"Run up to 5 commands, or type `/macro finish` to stop at any point.
|
"Run up to 5 commands to record in this macro. Use `/macro finish` to stop recording at any point.
|
||||||
Any commands ran as part of recording will be inconsequential",
|
Any commands performed during recording won't take any actual action- they are only captured for the macro.",
|
||||||
)
|
)
|
||||||
.color(*THEME_COLOR),
|
.color(*THEME_COLOR),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ pub mod dashboard;
|
|||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
pub mod delete;
|
pub mod delete;
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
pub mod donate;
|
|
||||||
#[cfg(not(test))]
|
|
||||||
pub mod help;
|
pub mod help;
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
pub mod info;
|
pub mod info;
|
||||||
@@ -26,6 +24,8 @@ pub mod nudge;
|
|||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
pub mod offset;
|
pub mod offset;
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
|
pub mod patreon;
|
||||||
|
#[cfg(not(test))]
|
||||||
pub mod pause;
|
pub mod pause;
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
pub mod remind;
|
pub mod remind;
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ impl Recordable for Options {
|
|||||||
let footer = footer(ctx);
|
let footer = footer(ctx);
|
||||||
|
|
||||||
ctx.send(CreateReply::default().embed(CreateEmbed::new().title("Donate")
|
ctx.send(CreateReply::default().embed(CreateEmbed::new().title("Donate")
|
||||||
.description("Thinking of adding a monthly contribution?
|
.description("Thinking of subscribing?
|
||||||
Click below for my Patreon and official bot server :)
|
Click below for my Patreon and official bot server
|
||||||
|
|
||||||
**https://www.patreon.com/jellywx/**
|
**https://www.patreon.com/jellywx/**
|
||||||
**https://discord.jellywx.com/**
|
**https://discord.jellywx.com/**
|
||||||
@@ -26,7 +26,7 @@ With your new rank, you'll be able to:
|
|||||||
• Set repeating reminders with `/remind` or the dashboard
|
• Set repeating reminders with `/remind` or the dashboard
|
||||||
• Use unlimited uploads on SoundFX
|
• Use unlimited uploads on SoundFX
|
||||||
|
|
||||||
(Also, members of servers you __own__ will be able to set repeating reminders via commands)
|
Members of servers you __own__ will be able to set repeating reminders via commands. You can also choose to share your membership with one other server.
|
||||||
|
|
||||||
Just $2 USD/month!
|
Just $2 USD/month!
|
||||||
|
|
||||||
@@ -41,8 +41,8 @@ Just $2 USD/month!
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Details on supporting the bot and Patreon benefits
|
/// Show Patreon information
|
||||||
#[poise::command(slash_command, rename = "patreon", identifying_name = "patreon")]
|
#[poise::command(slash_command, rename = "info", identifying_name = "patreon_info")]
|
||||||
pub async fn command(ctx: Context<'_>) -> Result<(), Error> {
|
pub async fn info(ctx: Context<'_>) -> Result<(), Error> {
|
||||||
(Options {}).run(ctx).await
|
(Options {}).run(ctx).await
|
||||||
}
|
}
|
||||||
57
src/commands/patreon/link.rs
Normal file
57
src/commands/patreon/link.rs
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
use poise::CreateReply;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
utils::{check_user_subscription, Extract, Recordable},
|
||||||
|
Context, Error,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Extract)]
|
||||||
|
pub struct Options;
|
||||||
|
|
||||||
|
impl Recordable for Options {
|
||||||
|
async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
|
||||||
|
let guild_id = ctx.guild_id().ok_or("This command must be used in a server")?;
|
||||||
|
let user_id = ctx.author().id;
|
||||||
|
|
||||||
|
// Check if user has Patreon subscription
|
||||||
|
if !check_user_subscription(ctx, user_id).await {
|
||||||
|
ctx.send(CreateReply::default()
|
||||||
|
.content("❌ You must be a Patreon subscriber to use this command. Use `/patreon info` for more information.")
|
||||||
|
.ephemeral(true)
|
||||||
|
).await?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert or update the patreon_link entry
|
||||||
|
sqlx::query!(
|
||||||
|
"INSERT INTO patreon_link (user_id, guild_id, linked_at) VALUES (?, ?, NOW())
|
||||||
|
ON DUPLICATE KEY UPDATE guild_id = ?",
|
||||||
|
user_id.get(),
|
||||||
|
guild_id.get(),
|
||||||
|
guild_id.get()
|
||||||
|
)
|
||||||
|
.execute(&ctx.data().database)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
ctx.send(
|
||||||
|
CreateReply::default()
|
||||||
|
.content("✅ Successfully linked your Patreon subscription to this server!")
|
||||||
|
.ephemeral(true),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Link your Patreon subscription to this server to allow other users Patreon access.
|
||||||
|
#[poise::command(
|
||||||
|
slash_command,
|
||||||
|
rename = "link",
|
||||||
|
identifying_name = "patreon_link",
|
||||||
|
guild_only = true
|
||||||
|
)]
|
||||||
|
pub async fn link(ctx: Context<'_>) -> Result<(), Error> {
|
||||||
|
(Options {}).run(ctx).await
|
||||||
|
}
|
||||||
11
src/commands/patreon/mod.rs
Normal file
11
src/commands/patreon/mod.rs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
pub mod info;
|
||||||
|
pub mod link;
|
||||||
|
pub mod unlink;
|
||||||
|
|
||||||
|
use crate::{Context, Error};
|
||||||
|
|
||||||
|
/// Manage Patreon subscription features
|
||||||
|
#[poise::command(slash_command, rename = "patreon", identifying_name = "patreon")]
|
||||||
|
pub async fn command(_ctx: Context<'_>) -> Result<(), Error> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
50
src/commands/patreon/unlink.rs
Normal file
50
src/commands/patreon/unlink.rs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
use poise::CreateReply;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
utils::{Extract, Recordable},
|
||||||
|
Context, Error,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Extract)]
|
||||||
|
pub struct Options;
|
||||||
|
|
||||||
|
impl Recordable for Options {
|
||||||
|
async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
|
||||||
|
let user_id = ctx.author().id;
|
||||||
|
|
||||||
|
// Remove the patreon_link entry
|
||||||
|
let result = sqlx::query!("DELETE FROM patreon_link WHERE user_id = ?", user_id.get())
|
||||||
|
.execute(&ctx.data().database)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if result.rows_affected() > 0 {
|
||||||
|
ctx.send(
|
||||||
|
CreateReply::default()
|
||||||
|
.content("✅ Successfully unlinked your Patreon subscription!")
|
||||||
|
.ephemeral(true),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
|
ctx.send(
|
||||||
|
CreateReply::default()
|
||||||
|
.content("❌ No existing Patreon link found.")
|
||||||
|
.ephemeral(true),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unlink your Patreon subscription
|
||||||
|
#[poise::command(
|
||||||
|
slash_command,
|
||||||
|
rename = "unlink",
|
||||||
|
identifying_name = "patreon_unlink",
|
||||||
|
guild_only = false
|
||||||
|
)]
|
||||||
|
pub async fn unlink(ctx: Context<'_>) -> Result<(), Error> {
|
||||||
|
(Options {}).run(ctx).await
|
||||||
|
}
|
||||||
57
src/hooks.rs
57
src/hooks.rs
@@ -1,4 +1,6 @@
|
|||||||
use poise::{CommandInteractionType, CreateReply};
|
use crate::consts::THEME_COLOR;
|
||||||
|
use poise::{serenity_prelude::CreateEmbed, CommandInteractionType, CreateReply};
|
||||||
|
use serenity::builder::CreateEmbedFooter;
|
||||||
|
|
||||||
use crate::{consts::MACRO_MAX_COMMANDS, models::command_macro::RecordedCommand, Context, Error};
|
use crate::{consts::MACRO_MAX_COMMANDS, models::command_macro::RecordedCommand, Context, Error};
|
||||||
|
|
||||||
@@ -18,7 +20,18 @@ async fn macro_check(ctx: Context<'_>) -> bool {
|
|||||||
.send(
|
.send(
|
||||||
CreateReply::default()
|
CreateReply::default()
|
||||||
.ephemeral(true)
|
.ephemeral(true)
|
||||||
.content(format!("{} commands already recorded. Please use `/macro finish` to end recording.", MACRO_MAX_COMMANDS))
|
.embed(CreateEmbed::new()
|
||||||
|
.title("💾 Currently recording macro")
|
||||||
|
.description(
|
||||||
|
format!("{} commands already recorded. Please use `/macro finish` to end recording.", MACRO_MAX_COMMANDS),
|
||||||
|
)
|
||||||
|
.footer(
|
||||||
|
CreateEmbedFooter::new(
|
||||||
|
"Any commands performed during recording won't take any actual action- they are only captured for the macro"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.color(*THEME_COLOR),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
} else {
|
} else {
|
||||||
@@ -28,9 +41,19 @@ async fn macro_check(ctx: Context<'_>) -> bool {
|
|||||||
|
|
||||||
let _ = ctx
|
let _ = ctx
|
||||||
.send(
|
.send(
|
||||||
CreateReply::default()
|
CreateReply::default().ephemeral(true).embed(
|
||||||
.ephemeral(true)
|
CreateEmbed::new()
|
||||||
.content("Command recorded to macro"),
|
.title("💾 Currently recording macro")
|
||||||
|
.description(
|
||||||
|
"Command recorded. Use `/macro finish` to end recording.",
|
||||||
|
)
|
||||||
|
.footer(
|
||||||
|
CreateEmbedFooter::new(
|
||||||
|
"Any commands performed during recording won't take any actual action- they are only captured for the macro"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.color(*THEME_COLOR),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
@@ -38,8 +61,18 @@ async fn macro_check(ctx: Context<'_>) -> bool {
|
|||||||
None => {
|
None => {
|
||||||
let _ = ctx
|
let _ = ctx
|
||||||
.send(
|
.send(
|
||||||
CreateReply::default().ephemeral(true).content(
|
CreateReply::default().ephemeral(true).embed(
|
||||||
"This command is not supported in macros yet.",
|
CreateEmbed::new()
|
||||||
|
.title("💾 Currently recording macro")
|
||||||
|
.description(
|
||||||
|
"This command is not supported in macros, so it hasn't been recorded. Use `/macro finish` to end recording.",
|
||||||
|
)
|
||||||
|
.footer(
|
||||||
|
CreateEmbedFooter::new(
|
||||||
|
"Any commands performed during recording won't take any actual action- they are only captured for the macro"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.color(*THEME_COLOR),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
@@ -74,6 +107,7 @@ async fn check_self_permissions(ctx: Context<'_>) -> bool {
|
|||||||
return if permissions.send_messages()
|
return if permissions.send_messages()
|
||||||
&& permissions.embed_links()
|
&& permissions.embed_links()
|
||||||
&& manage_webhooks
|
&& manage_webhooks
|
||||||
|
&& permissions.view_channel()
|
||||||
{
|
{
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
@@ -81,12 +115,13 @@ async fn check_self_permissions(ctx: Context<'_>) -> bool {
|
|||||||
.send(CreateReply::default().content(format!(
|
.send(CreateReply::default().content(format!(
|
||||||
"The bot appears to be missing some permissions:
|
"The bot appears to be missing some permissions:
|
||||||
|
|
||||||
|
{} **View Channels**
|
||||||
{} **Send Message**
|
{} **Send Message**
|
||||||
{} **Embed Links**
|
{} **Embed Links**
|
||||||
{} **Manage Webhooks**
|
{} **Manage Webhooks**
|
||||||
|
|
||||||
Please check the bot's roles, and any channel overrides. Alternatively, giving the bot
|
Please check the bot's roles, and any channel overrides. Alternatively, giving the bot \"Administrator\" will bypass permission checks",
|
||||||
\"Administrator\" will bypass permission checks",
|
if permissions.view_channel() { "✅" } else { "❌" },
|
||||||
if permissions.send_messages() { "✅" } else { "❌" },
|
if permissions.send_messages() { "✅" } else { "❌" },
|
||||||
if permissions.embed_links() { "✅" } else { "❌" },
|
if permissions.embed_links() { "✅" } else { "❌" },
|
||||||
if manage_webhooks { "✅" } else { "❌" },
|
if manage_webhooks { "✅" } else { "❌" },
|
||||||
@@ -100,9 +135,7 @@ Please check the bot's roles, and any channel overrides. Alternatively, giving t
|
|||||||
manage_webhooks
|
manage_webhooks
|
||||||
}
|
}
|
||||||
|
|
||||||
None => {
|
None => true,
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
13
src/main.rs
13
src/main.rs
@@ -48,8 +48,8 @@ use crate::test::TestContext;
|
|||||||
use crate::{
|
use crate::{
|
||||||
commands::{
|
commands::{
|
||||||
allowed_dm, clock, clock_context_menu::clock_context_menu, command_macro, dashboard,
|
allowed_dm, clock, clock_context_menu::clock_context_menu, command_macro, dashboard,
|
||||||
delete, donate, help, info, look, multiline, nudge, offset, pause, remind, settings, timer,
|
delete, help, info, look, multiline, nudge, offset, patreon, pause, remind, settings,
|
||||||
timezone, todo, webhook,
|
timer, timezone, todo, webhook,
|
||||||
},
|
},
|
||||||
consts::THEME_COLOR,
|
consts::THEME_COLOR,
|
||||||
event_handlers::listener,
|
event_handlers::listener,
|
||||||
@@ -165,7 +165,14 @@ async fn _main(tx: Sender<()>) -> Result<(), Box<dyn StdError + Send + Sync>> {
|
|||||||
help::command(),
|
help::command(),
|
||||||
info::command(),
|
info::command(),
|
||||||
clock::command(),
|
clock::command(),
|
||||||
donate::command(),
|
poise::Command {
|
||||||
|
subcommands: vec![
|
||||||
|
patreon::link::link(),
|
||||||
|
patreon::unlink::unlink(),
|
||||||
|
patreon::info::info(),
|
||||||
|
],
|
||||||
|
..patreon::command()
|
||||||
|
},
|
||||||
clock_context_menu(),
|
clock_context_menu(),
|
||||||
dashboard::command(),
|
dashboard::command(),
|
||||||
timezone::command(),
|
timezone::command(),
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ pub enum RecordedCommand {
|
|||||||
#[serde(rename = "delete")]
|
#[serde(rename = "delete")]
|
||||||
Delete(crate::commands::delete::Options),
|
Delete(crate::commands::delete::Options),
|
||||||
#[serde(rename = "donate")]
|
#[serde(rename = "donate")]
|
||||||
Donate(crate::commands::donate::Options),
|
Donate(crate::commands::patreon::info::Options),
|
||||||
#[serde(rename = "help")]
|
#[serde(rename = "help")]
|
||||||
Help(crate::commands::help::Options),
|
Help(crate::commands::help::Options),
|
||||||
#[serde(rename = "info")]
|
#[serde(rename = "info")]
|
||||||
@@ -111,7 +111,7 @@ impl RecordedCommand {
|
|||||||
"clock" => Some(Self::Clock(crate::commands::clock::Options::extract(ctx))),
|
"clock" => Some(Self::Clock(crate::commands::clock::Options::extract(ctx))),
|
||||||
"dashboard" => Some(Self::Dashboard(crate::commands::dashboard::Options::extract(ctx))),
|
"dashboard" => Some(Self::Dashboard(crate::commands::dashboard::Options::extract(ctx))),
|
||||||
"delete" => Some(Self::Delete(crate::commands::delete::Options::extract(ctx))),
|
"delete" => Some(Self::Delete(crate::commands::delete::Options::extract(ctx))),
|
||||||
"donate" => Some(Self::Donate(crate::commands::donate::Options::extract(ctx))),
|
"donate" => Some(Self::Donate(crate::commands::patreon::info::Options::extract(ctx))),
|
||||||
"help" => Some(Self::Help(crate::commands::help::Options::extract(ctx))),
|
"help" => Some(Self::Help(crate::commands::help::Options::extract(ctx))),
|
||||||
"info" => Some(Self::Info(crate::commands::info::Options::extract(ctx))),
|
"info" => Some(Self::Info(crate::commands::info::Options::extract(ctx))),
|
||||||
"look" => Some(Self::Look(crate::commands::look::Options::extract(ctx))),
|
"look" => Some(Self::Look(crate::commands::look::Options::extract(ctx))),
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ use crate::{
|
|||||||
},
|
},
|
||||||
CtxData,
|
CtxData,
|
||||||
},
|
},
|
||||||
time_parser::natural_parser,
|
time_parser::{cron_next_timestamp, natural_parser},
|
||||||
utils::{check_guild_subscription, check_subscription},
|
utils::check_subscription,
|
||||||
Context, Database, Error,
|
Context, Database, Error,
|
||||||
};
|
};
|
||||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||||
@@ -486,7 +486,10 @@ pub async fn create_reminder(
|
|||||||
let user_data = ctx.author_data().await.unwrap();
|
let user_data = ctx.author_data().await.unwrap();
|
||||||
let timezone = timezone.unwrap_or(ctx.timezone().await);
|
let timezone = timezone.unwrap_or(ctx.timezone().await);
|
||||||
|
|
||||||
let time = natural_parser(&time, &timezone.to_string()).await;
|
let time = match cron_next_timestamp(&time, timezone) {
|
||||||
|
Some(ts) => Some(ts),
|
||||||
|
None => natural_parser(&time, &timezone.to_string()).await,
|
||||||
|
};
|
||||||
|
|
||||||
match time {
|
match time {
|
||||||
Some(time) => {
|
Some(time) => {
|
||||||
@@ -529,9 +532,8 @@ pub async fn create_reminder(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let (processed_interval, processed_expires) = if let Some(repeat) = &interval {
|
let (processed_interval, processed_expires) = if let Some(repeat) = &interval {
|
||||||
if check_subscription(&ctx, ctx.author().id).await
|
if check_subscription(&ctx, &ctx.data().database, ctx.author().id, ctx.guild_id())
|
||||||
|| (ctx.guild_id().is_some()
|
.await
|
||||||
&& check_guild_subscription(&ctx, ctx.guild_id().unwrap()).await)
|
|
||||||
{
|
{
|
||||||
(
|
(
|
||||||
parse_duration(repeat)
|
parse_duration(repeat)
|
||||||
@@ -547,7 +549,7 @@ pub async fn create_reminder(
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
ctx.send(CreateReply::default().content(
|
ctx.send(CreateReply::default().content(
|
||||||
"`repeat` is only available to Patreon subscribers or self-hosted users",
|
"`interval` is only available to Patreon subscribers or self-hosted users",
|
||||||
))
|
))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|||||||
@@ -34,8 +34,10 @@ use crate::{
|
|||||||
lazy_static! {
|
lazy_static! {
|
||||||
pub static ref TIMEFROM_REGEX: Regex =
|
pub static ref TIMEFROM_REGEX: Regex =
|
||||||
Regex::new(r#"<<timefrom:(?P<time>\d+):(?P<format>.+)?>>"#).unwrap();
|
Regex::new(r#"<<timefrom:(?P<time>\d+):(?P<format>.+)?>>"#).unwrap();
|
||||||
pub static ref TIMENOW_REGEX: Regex =
|
pub static ref TIMENOW_REGEX: Regex = Regex::new(
|
||||||
Regex::new(r#"<<timenow:(?P<timezone>(?:\w|/|_)+):(?P<format>.+)?>>"#).unwrap();
|
r#"<<timenow(?:(?P<sign>[+-])(?P<offset>\d+))?:(?P<timezone>(?:\w|/|_)+?):(?P<format>.+?)?>>"#
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
pub static ref LOG_TO_DATABASE: bool = env::var("LOG_TO_DATABASE").map_or(true, |v| v == "1");
|
pub static ref LOG_TO_DATABASE: bool = env::var("LOG_TO_DATABASE").map_or(true, |v| v == "1");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +66,7 @@ fn fmt_displacement(format: &str, seconds: u64) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn substitute(string: &str) -> String {
|
pub fn substitute(string: &str) -> String {
|
||||||
let new = TIMEFROM_REGEX.replace(string, |caps: &Captures| {
|
let new = TIMEFROM_REGEX.replace_all(string, |caps: &Captures| {
|
||||||
let final_time = caps.name("time").map(|m| m.as_str().parse::<i64>().ok()).flatten();
|
let final_time = caps.name("time").map(|m| m.as_str().parse::<i64>().ok()).flatten();
|
||||||
let format = caps.name("format").map(|m| m.as_str());
|
let format = caps.name("format").map(|m| m.as_str());
|
||||||
|
|
||||||
@@ -92,12 +94,26 @@ pub fn substitute(string: &str) -> String {
|
|||||||
});
|
});
|
||||||
|
|
||||||
TIMENOW_REGEX
|
TIMENOW_REGEX
|
||||||
.replace(&new, |caps: &Captures| {
|
.replace_all(&new, |caps: &Captures| {
|
||||||
let timezone = caps.name("timezone").map(|m| m.as_str().parse::<Tz>().ok()).flatten();
|
let timezone = caps.name("timezone").map(|m| m.as_str().parse::<Tz>().ok()).flatten();
|
||||||
let format = caps.name("format").map(|m| m.as_str());
|
let format = caps.name("format").map(|m| m.as_str());
|
||||||
|
let sign = caps.name("sign").map(|m| m.as_str());
|
||||||
|
let offset = caps.name("offset").map(|m| m.as_str().parse::<i64>().ok()).flatten();
|
||||||
|
|
||||||
if let (Some(timezone), Some(format)) = (timezone, format) {
|
if let (Some(timezone), Some(format)) = (timezone, format) {
|
||||||
let now = Utc::now().with_timezone(&timezone);
|
let mut now = Utc::now().with_timezone(&timezone);
|
||||||
|
if let (Some(sign), Some(offset)) = (sign, offset) {
|
||||||
|
now = now
|
||||||
|
.checked_add_signed(TimeDelta::seconds(
|
||||||
|
offset * {
|
||||||
|
match sign {
|
||||||
|
"-" => -1,
|
||||||
|
_ => 1,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
))
|
||||||
|
.unwrap_or(now)
|
||||||
|
}
|
||||||
|
|
||||||
now.format(format).to_string()
|
now.format(format).to_string()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ use poise::{
|
|||||||
CreateReply,
|
CreateReply,
|
||||||
};
|
};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
use serenity::all::Http;
|
||||||
|
use serenity::http::CacheHttp;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
use crate::{Data, Error};
|
use crate::{Data, Error};
|
||||||
@@ -19,6 +21,12 @@ pub(crate) struct TestContext<'a> {
|
|||||||
pub(crate) shard_id: usize,
|
pub(crate) shard_id: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl CacheHttp for TestContext<'_> {
|
||||||
|
fn http(&self) -> &Http {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) struct MockUser {
|
pub(crate) struct MockUser {
|
||||||
pub(crate) id: UserId,
|
pub(crate) id: UserId,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use std::{
|
|||||||
|
|
||||||
use chrono::{DateTime, Datelike, Timelike, Utc};
|
use chrono::{DateTime, Datelike, Timelike, Utc};
|
||||||
use chrono_tz::Tz;
|
use chrono_tz::Tz;
|
||||||
|
use cron_parser::parse;
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
|
|
||||||
use crate::consts::{LOCAL_TIMEZONE, PYTHON_LOCATION};
|
use crate::consts::{LOCAL_TIMEZONE, PYTHON_LOCATION};
|
||||||
@@ -219,3 +220,7 @@ pub async fn natural_parser(time: &str, timezone: &str) -> Option<i64> {
|
|||||||
})
|
})
|
||||||
.and_then(|inner| if inner < 0 { None } else { Some(inner) })
|
.and_then(|inner| if inner < 0 { None } else { Some(inner) })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn cron_next_timestamp(expr: &str, timezone: Tz) -> Option<i64> {
|
||||||
|
parse(expr, &Utc::now().with_timezone(&timezone)).ok().map(|next| next.timestamp() as i64)
|
||||||
|
}
|
||||||
|
|||||||
68
src/utils.rs
68
src/utils.rs
@@ -9,10 +9,62 @@ use poise::{
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
consts::{CNC_GUILD, SUBSCRIPTION_ROLES},
|
consts::{CNC_GUILD, SUBSCRIPTION_ROLES},
|
||||||
ApplicationContext, Context, Error,
|
ApplicationContext, Context, Database, Error,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub async fn check_subscription(cache_http: impl CacheHttp, user_id: impl Into<UserId>) -> bool {
|
/// Check if this user/guild combination should be considered subscribed.
|
||||||
|
/// If the guild has a patreon linked, check the user involved in the link.
|
||||||
|
/// Otherwise, check the user and the guild's owner
|
||||||
|
pub async fn check_subscription(
|
||||||
|
ctx: impl CacheHttp,
|
||||||
|
database: impl Executor<'_, Database = Database>,
|
||||||
|
user_id: UserId,
|
||||||
|
guild_id: Option<GuildId>,
|
||||||
|
) -> bool {
|
||||||
|
if let Some(subscription_guild) = *CNC_GUILD {
|
||||||
|
let user_subscribed = check_user_subscription(&ctx, user_id).await;
|
||||||
|
|
||||||
|
let owner_subscribed = match guild_id {
|
||||||
|
Some(guild_id) => {
|
||||||
|
if let Some(owner) = ctx.cache().unwrap().guild(guild_id).map(|g| g.owner_id) {
|
||||||
|
check_user_subscription(&ctx, owner).await
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let link_subscribed = match guild_id {
|
||||||
|
Some(guild_id) => {
|
||||||
|
if let Ok(row) = sqlx::query!(
|
||||||
|
"SELECT user_id FROM patreon_link WHERE guild_id = ?",
|
||||||
|
guild_id.get()
|
||||||
|
)
|
||||||
|
.fetch_one(database)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
check_user_subscription(&ctx, row.user_id).await
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
user_subscribed || owner_subscribed || link_subscribed
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check a user's subscription status, ignoring Patreon linkage
|
||||||
|
pub async fn check_user_subscription(
|
||||||
|
cache_http: impl CacheHttp,
|
||||||
|
user_id: impl Into<UserId>,
|
||||||
|
) -> bool {
|
||||||
if let Some(subscription_guild) = *CNC_GUILD {
|
if let Some(subscription_guild) = *CNC_GUILD {
|
||||||
let guild_member = GuildId::new(subscription_guild).member(cache_http, user_id).await;
|
let guild_member = GuildId::new(subscription_guild).member(cache_http, user_id).await;
|
||||||
|
|
||||||
@@ -30,17 +82,6 @@ pub async fn check_subscription(cache_http: impl CacheHttp, user_id: impl Into<U
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn check_guild_subscription(
|
|
||||||
cache_http: impl CacheHttp,
|
|
||||||
guild_id: impl Into<GuildId>,
|
|
||||||
) -> bool {
|
|
||||||
if let Some(owner) = cache_http.cache().unwrap().guild(guild_id).map(|g| g.owner_id) {
|
|
||||||
check_subscription(&cache_http, owner).await
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn reply_to_interaction_response_message(
|
pub fn reply_to_interaction_response_message(
|
||||||
reply: CreateReply,
|
reply: CreateReply,
|
||||||
) -> CreateInteractionResponseMessage {
|
) -> CreateInteractionResponseMessage {
|
||||||
@@ -76,6 +117,7 @@ pub trait Extract {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub use extract_derive::Extract;
|
pub use extract_derive::Extract;
|
||||||
|
use sqlx::Executor;
|
||||||
|
|
||||||
macro_rules! extract_arg {
|
macro_rules! extract_arg {
|
||||||
($ctx:ident, $name:ident, String) => {
|
($ctx:ident, $name:ident, String) => {
|
||||||
|
|||||||
@@ -92,6 +92,8 @@ enum Error {
|
|||||||
SQLx(sqlx::Error),
|
SQLx(sqlx::Error),
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
Serenity(serenity::Error),
|
Serenity(serenity::Error),
|
||||||
|
#[allow(unused)]
|
||||||
|
MissingDiscordPermission(&'static str),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn initialize(
|
pub async fn initialize(
|
||||||
@@ -124,6 +126,7 @@ pub async fn initialize(
|
|||||||
|
|
||||||
let static_path =
|
let static_path =
|
||||||
if Path::new("static").exists() { "static" } else { "/lib/reminder-rs/static" };
|
if Path::new("static").exists() { "static" } else { "/lib/reminder-rs/static" };
|
||||||
|
info!("Using static path: {}", static_path);
|
||||||
|
|
||||||
rocket::build()
|
rocket::build()
|
||||||
.attach(MetricProducer)
|
.attach(MetricProducer)
|
||||||
@@ -231,39 +234,6 @@ pub async fn initialize(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn check_subscription(cache_http: impl CacheHttp, user_id: impl Into<UserId>) -> bool {
|
|
||||||
offline!(true);
|
|
||||||
|
|
||||||
if let Some(subscription_guild) = *CNC_GUILD {
|
|
||||||
let guild_member = GuildId::new(subscription_guild).member(cache_http, user_id).await;
|
|
||||||
|
|
||||||
if let Ok(member) = guild_member {
|
|
||||||
for role in member.roles {
|
|
||||||
if SUBSCRIPTION_ROLES.contains(&role.get()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn check_guild_subscription(
|
|
||||||
cache_http: impl CacheHttp,
|
|
||||||
guild_id: impl Into<GuildId>,
|
|
||||||
) -> bool {
|
|
||||||
offline!(true);
|
|
||||||
|
|
||||||
if let Some(owner) = cache_http.cache().unwrap().guild(guild_id).map(|guild| guild.owner_id) {
|
|
||||||
check_subscription(&cache_http, owner).await
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn check_authorization(
|
pub async fn check_authorization(
|
||||||
cookies: &CookieJar<'_>,
|
cookies: &CookieJar<'_>,
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
|
|||||||
@@ -7,38 +7,43 @@ pub mod todos;
|
|||||||
|
|
||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
|
use crate::utils::check_subscription;
|
||||||
|
use crate::web::guards::transaction::Transaction;
|
||||||
|
use crate::web::{check_authorization, routes::JsonResult};
|
||||||
pub use channels::get_guild_channels;
|
pub use channels::get_guild_channels;
|
||||||
pub use emojis::get_guild_emojis;
|
pub use emojis::get_guild_emojis;
|
||||||
pub use reminders::*;
|
pub use reminders::*;
|
||||||
use rocket::{get, http::CookieJar, serde::json::json, State};
|
use rocket::{get, http::CookieJar, serde::json::json, State};
|
||||||
pub use roles::get_guild_roles;
|
pub use roles::get_guild_roles;
|
||||||
|
use serenity::all::UserId;
|
||||||
use serenity::{
|
use serenity::{
|
||||||
client::Context,
|
client::Context,
|
||||||
model::id::{GuildId, RoleId},
|
model::id::{GuildId, RoleId},
|
||||||
};
|
};
|
||||||
pub use templates::*;
|
pub use templates::*;
|
||||||
|
|
||||||
use crate::web::{check_authorization, routes::JsonResult};
|
|
||||||
|
|
||||||
#[get("/api/guild/<id>")]
|
#[get("/api/guild/<id>")]
|
||||||
pub async fn get_guild_info(id: u64, cookies: &CookieJar<'_>, ctx: &State<Context>) -> JsonResult {
|
pub async fn get_guild_info(
|
||||||
|
id: u64,
|
||||||
|
cookies: &CookieJar<'_>,
|
||||||
|
ctx: &State<Context>,
|
||||||
|
mut transaction: Transaction<'_>,
|
||||||
|
) -> JsonResult {
|
||||||
offline!(Ok(json!({ "patreon": true, "name": "Guild" })));
|
offline!(Ok(json!({ "patreon": true, "name": "Guild" })));
|
||||||
check_authorization(cookies, ctx.inner(), id).await?;
|
check_authorization(cookies, ctx.inner(), id).await?;
|
||||||
|
|
||||||
match GuildId::new(id)
|
let user_id =
|
||||||
.to_guild_cached(ctx.inner())
|
cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap();
|
||||||
.map(|guild| (guild.owner_id, guild.name.clone()))
|
|
||||||
{
|
|
||||||
Some((owner_id, name)) => {
|
|
||||||
let member_res = GuildId::new(env::var("PATREON_GUILD_ID").unwrap().parse().unwrap())
|
|
||||||
.member(&ctx.inner(), owner_id)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let patreon = member_res.map_or(false, |member| {
|
match GuildId::new(id).to_guild_cached(ctx.inner()).map(|guild| guild.name.clone()) {
|
||||||
member
|
Some(name) => {
|
||||||
.roles
|
let patreon = check_subscription(
|
||||||
.contains(&RoleId::new(env::var("PATREON_ROLE_ID").unwrap().parse().unwrap()))
|
ctx.inner(),
|
||||||
});
|
transaction.executor(),
|
||||||
|
UserId::from(user_id),
|
||||||
|
Some(GuildId::from(id)),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
Ok(json!({ "patreon": patreon, "name": name }))
|
Ok(json!({ "patreon": patreon, "name": name }))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,9 @@ use serenity::{
|
|||||||
};
|
};
|
||||||
use sqlx::{MySql, Pool};
|
use sqlx::{MySql, Pool};
|
||||||
|
|
||||||
|
use crate::utils::check_subscription;
|
||||||
use crate::web::{
|
use crate::web::{
|
||||||
check_authorization, check_guild_subscription, check_subscription,
|
check_authorization,
|
||||||
consts::MIN_INTERVAL,
|
consts::MIN_INTERVAL,
|
||||||
guards::transaction::Transaction,
|
guards::transaction::Transaction,
|
||||||
routes::{
|
routes::{
|
||||||
@@ -186,8 +187,13 @@ pub async fn edit_reminder(
|
|||||||
|| reminder.interval_months.flatten().is_some()
|
|| reminder.interval_months.flatten().is_some()
|
||||||
|| reminder.interval_seconds.flatten().is_some()
|
|| reminder.interval_seconds.flatten().is_some()
|
||||||
{
|
{
|
||||||
if check_guild_subscription(&ctx.inner(), id).await
|
if check_subscription(
|
||||||
|| check_subscription(&ctx.inner(), user_id).await
|
ctx.inner(),
|
||||||
|
transaction.executor(),
|
||||||
|
UserId::from(user_id),
|
||||||
|
Some(GuildId::from(id)),
|
||||||
|
)
|
||||||
|
.await
|
||||||
{
|
{
|
||||||
let new_interval_length = match reminder.interval_days {
|
let new_interval_length = match reminder.interval_days {
|
||||||
Some(interval) => interval.unwrap_or(0),
|
Some(interval) => interval.unwrap_or(0),
|
||||||
@@ -305,7 +311,15 @@ pub async fn edit_reminder(
|
|||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("`create_database_channel` returned an error code: {:?}", e);
|
warn!("`create_database_channel` returned an error code: {:?}", e);
|
||||||
|
|
||||||
error.push("Failed to configure channel for reminders. Please check the bot permissions".to_string());
|
// Provide more specific error messages based on the error type
|
||||||
|
match e {
|
||||||
|
crate::web::Error::MissingDiscordPermission(permission) => {
|
||||||
|
error.push(format!("Please ensure the bot has the \"{}\" permission in the channel", permission));
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
error.push("Failed to configure channel for reminders. Please check the bot permissions".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ use serde::{Deserialize, Serialize};
|
|||||||
use serenity::{client::Context, model::id::UserId};
|
use serenity::{client::Context, model::id::UserId};
|
||||||
use sqlx::types::Json;
|
use sqlx::types::Json;
|
||||||
|
|
||||||
|
use crate::utils::check_subscription;
|
||||||
use crate::web::{
|
use crate::web::{
|
||||||
check_subscription,
|
|
||||||
consts::{
|
consts::{
|
||||||
DAY, MAX_CONTENT_LENGTH, MAX_EMBED_AUTHOR_LENGTH, MAX_EMBED_DESCRIPTION_LENGTH,
|
DAY, MAX_CONTENT_LENGTH, MAX_EMBED_AUTHOR_LENGTH, MAX_EMBED_DESCRIPTION_LENGTH,
|
||||||
MAX_EMBED_FIELDS, MAX_EMBED_FIELD_TITLE_LENGTH, MAX_EMBED_FIELD_VALUE_LENGTH,
|
MAX_EMBED_FIELDS, MAX_EMBED_FIELD_TITLE_LENGTH, MAX_EMBED_FIELD_VALUE_LENGTH,
|
||||||
@@ -65,7 +65,16 @@ pub async fn create_reminder(
|
|||||||
if let Err(e) = channel {
|
if let Err(e) = channel {
|
||||||
warn!("`create_database_channel` returned an error code: {:?}", e);
|
warn!("`create_database_channel` returned an error code: {:?}", e);
|
||||||
|
|
||||||
return Err(json!({"error": "Failed to configure channel for reminders."}));
|
// Provide more specific error messages based on the error type
|
||||||
|
let error_msg = match e {
|
||||||
|
Error::MissingDiscordPermission(permission) => format!(
|
||||||
|
"Please ensure the bot has the \"{}\" permission in the channel",
|
||||||
|
permission
|
||||||
|
),
|
||||||
|
_ => "Failed to configure channel for reminders.".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
return Err(json!({"error": error_msg}));
|
||||||
}
|
}
|
||||||
|
|
||||||
let channel = channel.unwrap();
|
let channel = channel.unwrap();
|
||||||
@@ -122,7 +131,7 @@ pub async fn create_reminder(
|
|||||||
|| reminder.interval_days.is_some()
|
|| reminder.interval_days.is_some()
|
||||||
|| reminder.interval_months.is_some()
|
|| reminder.interval_months.is_some()
|
||||||
{
|
{
|
||||||
if !check_subscription(&ctx, user_id).await {
|
if !check_subscription(&ctx, transaction.executor(), user_id, None).await {
|
||||||
return Err(json!({"error": "Patreon is required to set intervals"}));
|
return Err(json!({"error": "Patreon is required to set intervals"}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ use rocket::{
|
|||||||
use serenity::{client::Context, model::id::UserId};
|
use serenity::{client::Context, model::id::UserId};
|
||||||
use sqlx::{MySql, Pool};
|
use sqlx::{MySql, Pool};
|
||||||
|
|
||||||
|
use crate::utils::check_subscription;
|
||||||
use crate::web::{
|
use crate::web::{
|
||||||
check_subscription,
|
|
||||||
guards::transaction::Transaction,
|
guards::transaction::Transaction,
|
||||||
routes::{
|
routes::{
|
||||||
dashboard::{
|
dashboard::{
|
||||||
@@ -162,7 +162,9 @@ pub async fn edit_reminder(
|
|||||||
|| reminder.interval_months.flatten().is_some()
|
|| reminder.interval_months.flatten().is_some()
|
||||||
|| reminder.interval_seconds.flatten().is_some()
|
|| reminder.interval_seconds.flatten().is_some()
|
||||||
{
|
{
|
||||||
if check_subscription(&ctx.inner(), user_id).await {
|
if check_subscription(&ctx.inner(), transaction.executor(), UserId::from(user_id), None)
|
||||||
|
.await
|
||||||
|
{
|
||||||
let new_interval_length = match reminder.interval_days {
|
let new_interval_length = match reminder.interval_days {
|
||||||
Some(interval) => interval.unwrap_or(0),
|
Some(interval) => interval.unwrap_or(0),
|
||||||
None => sqlx::query!(
|
None => sqlx::query!(
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use rocket::{
|
|||||||
use rocket_dyn_templates::Template;
|
use rocket_dyn_templates::Template;
|
||||||
use secrecy::ExposeSecret;
|
use secrecy::ExposeSecret;
|
||||||
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
||||||
|
use serenity::http::HttpError;
|
||||||
use serenity::{
|
use serenity::{
|
||||||
all::CacheHttp,
|
all::CacheHttp,
|
||||||
builder::CreateWebhook,
|
builder::CreateWebhook,
|
||||||
@@ -19,9 +20,9 @@ use serenity::{
|
|||||||
use sqlx::types::Json;
|
use sqlx::types::Json;
|
||||||
use sqlx::FromRow;
|
use sqlx::FromRow;
|
||||||
|
|
||||||
|
use crate::utils::check_subscription;
|
||||||
use crate::web::{
|
use crate::web::{
|
||||||
catchers::internal_server_error,
|
catchers::internal_server_error,
|
||||||
check_guild_subscription, check_subscription,
|
|
||||||
consts::{
|
consts::{
|
||||||
CHARACTERS, DAY, DEFAULT_AVATAR, MAX_CONTENT_LENGTH, MAX_EMBED_AUTHOR_LENGTH,
|
CHARACTERS, DAY, DEFAULT_AVATAR, MAX_CONTENT_LENGTH, MAX_EMBED_AUTHOR_LENGTH,
|
||||||
MAX_EMBED_DESCRIPTION_LENGTH, MAX_EMBED_FIELDS, MAX_EMBED_FIELD_TITLE_LENGTH,
|
MAX_EMBED_DESCRIPTION_LENGTH, MAX_EMBED_FIELDS, MAX_EMBED_FIELD_TITLE_LENGTH,
|
||||||
@@ -404,9 +405,19 @@ pub(crate) async fn create_reminder(
|
|||||||
if let Err(e) = channel {
|
if let Err(e) = channel {
|
||||||
warn!("`create_database_channel` returned an error code: {:?}", e);
|
warn!("`create_database_channel` returned an error code: {:?}", e);
|
||||||
|
|
||||||
return Err(
|
// Provide more specific error messages based on the error type
|
||||||
json!({"error": "Failed to configure channel for reminders. Please check the bot permissions"}),
|
let error_msg = match e {
|
||||||
);
|
Error::MissingDiscordPermission(permission) => {
|
||||||
|
format!(
|
||||||
|
"Please ensure the bot has the \"{}\" permission in the channel",
|
||||||
|
permission
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_ => "Failed to configure channel for reminders. Please check the bot permissions"
|
||||||
|
.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
return Err(json!({"error": error_msg}));
|
||||||
}
|
}
|
||||||
|
|
||||||
let channel = channel.unwrap();
|
let channel = channel.unwrap();
|
||||||
@@ -466,9 +477,7 @@ pub(crate) async fn create_reminder(
|
|||||||
|| reminder.interval_days.is_some()
|
|| reminder.interval_days.is_some()
|
||||||
|| reminder.interval_months.is_some()
|
|| reminder.interval_months.is_some()
|
||||||
{
|
{
|
||||||
if !check_guild_subscription(&ctx, guild_id).await
|
if !check_subscription(&ctx, transaction.executor(), user_id, Some(guild_id)).await {
|
||||||
&& !check_subscription(&ctx, user_id).await
|
|
||||||
{
|
|
||||||
return Err(json!({"error": "Patreon is required to set intervals"}));
|
return Err(json!({"error": "Patreon is required to set intervals"}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -716,13 +725,36 @@ async fn create_database_channel(
|
|||||||
|
|
||||||
match row {
|
match row {
|
||||||
Ok(row) => {
|
Ok(row) => {
|
||||||
let is_dm =
|
let is_dm = channel
|
||||||
channel.to_channel(&ctx).await.map_err(|e| Error::Serenity(e))?.private().is_some();
|
.to_channel(&ctx)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
if let serenity::Error::Http(http_error) = &e {
|
||||||
|
if let HttpError::UnsuccessfulRequest(response) = http_error {
|
||||||
|
if response.error.code == 50001 {
|
||||||
|
return Error::MissingDiscordPermission("View Channel");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Error::Serenity(e)
|
||||||
|
})?
|
||||||
|
.private()
|
||||||
|
.is_some();
|
||||||
if !is_dm && (row.webhook_token.is_none() || row.webhook_id.is_none()) {
|
if !is_dm && (row.webhook_token.is_none() || row.webhook_id.is_none()) {
|
||||||
let webhook = channel
|
let webhook = channel
|
||||||
.create_webhook(&ctx, CreateWebhook::new("Reminder").avatar(&*DEFAULT_AVATAR))
|
.create_webhook(&ctx, CreateWebhook::new("Reminder").avatar(&*DEFAULT_AVATAR))
|
||||||
.await
|
.await
|
||||||
.map_err(|e| Error::Serenity(e))?;
|
.map_err(|e| match &e {
|
||||||
|
serenity::Error::Http(HttpError::UnsuccessfulRequest(response)) => {
|
||||||
|
match response.error.code {
|
||||||
|
50001 => Error::MissingDiscordPermission("View Channel"),
|
||||||
|
50013 => Error::MissingDiscordPermission("Manage Webhooks"),
|
||||||
|
_ => Error::Serenity(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Error::Serenity(e),
|
||||||
|
})?;
|
||||||
|
|
||||||
let token = webhook.token.unwrap();
|
let token = webhook.token.unwrap();
|
||||||
|
|
||||||
@@ -747,7 +779,16 @@ async fn create_database_channel(
|
|||||||
let webhook = channel
|
let webhook = channel
|
||||||
.create_webhook(&ctx, CreateWebhook::new("Reminder").avatar(&*DEFAULT_AVATAR))
|
.create_webhook(&ctx, CreateWebhook::new("Reminder").avatar(&*DEFAULT_AVATAR))
|
||||||
.await
|
.await
|
||||||
.map_err(|e| Error::Serenity(e))?;
|
.map_err(|e| match &e {
|
||||||
|
serenity::Error::Http(HttpError::UnsuccessfulRequest(response)) => {
|
||||||
|
match response.error.code {
|
||||||
|
50001 => Error::MissingDiscordPermission("View Channel"),
|
||||||
|
50013 => Error::MissingDiscordPermission("Manage Webhooks"),
|
||||||
|
_ => Error::Serenity(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Error::Serenity(e),
|
||||||
|
})?;
|
||||||
|
|
||||||
let token = webhook.token.unwrap();
|
let token = webhook.token.unwrap();
|
||||||
|
|
||||||
@@ -806,22 +847,15 @@ pub async fn todos_redirect(id: &str) -> Redirect {
|
|||||||
|
|
||||||
#[get("/")]
|
#[get("/")]
|
||||||
pub async fn dashboard_home(cookies: &CookieJar<'_>) -> DashboardPage {
|
pub async fn dashboard_home(cookies: &CookieJar<'_>) -> DashboardPage {
|
||||||
if cookies.get_private("userid").is_some() {
|
render_dashboard(cookies).await
|
||||||
match NamedFile::open(Path::new(path!("static/index.html"))).await {
|
|
||||||
Ok(f) => DashboardPage::Ok(f),
|
|
||||||
Err(e) => {
|
|
||||||
warn!("Couldn't render dashboard: {:?}", e);
|
|
||||||
|
|
||||||
DashboardPage::NotConfigured(internal_server_error().await)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
DashboardPage::Unauthorised(Redirect::to("/login/discord"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/<_..>")]
|
#[get("/<_..>")]
|
||||||
pub async fn dashboard(cookies: &CookieJar<'_>) -> DashboardPage {
|
pub async fn dashboard(cookies: &CookieJar<'_>) -> DashboardPage {
|
||||||
|
render_dashboard(cookies).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn render_dashboard(cookies: &CookieJar<'_>) -> DashboardPage {
|
||||||
if cookies.get_private("userid").is_some() {
|
if cookies.get_private("userid").is_some() {
|
||||||
match NamedFile::open(Path::new(path!("static/index.html"))).await {
|
match NamedFile::open(Path::new(path!("static/index.html"))).await {
|
||||||
Ok(f) => DashboardPage::Ok(f),
|
Ok(f) => DashboardPage::Ok(f),
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
/home/jude/reminder-bot/reminder-dashboard/dist/static/assets
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
/home/jude/reminder-bot/reminder-dashboard/dist/index.html
|
|
||||||
34
static/index.html
Normal file
34
static/index.html
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="EN">
|
||||||
|
<head>
|
||||||
|
<meta name="description" content="The most powerful Discord Reminders Bot">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="yandex-verification" content="bb77b8681eb64a90"/>
|
||||||
|
<meta name="google-site-verification" content="7h7UVTeEe0AOzHiH3cFtsqMULYGN-zCZdMT_YCkW1Ho"/>
|
||||||
|
<!-- <meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src *; font-src fonts.gstatic.com 'self'"> -->
|
||||||
|
|
||||||
|
<!-- favicon -->
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180"
|
||||||
|
href="/static/favicon/apple-touch-icon.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32"
|
||||||
|
href="/static/favicon/favicon-32x32.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16"
|
||||||
|
href="/static/favicon/favicon-16x16.png">
|
||||||
|
<link rel="manifest" href="/static/site.webmanifest">
|
||||||
|
<meta name="msapplication-TileColor" content="#da532c">
|
||||||
|
<meta name="theme-color" content="#ffffff">
|
||||||
|
|
||||||
|
<title>Reminder Bot | Dashboard</title>
|
||||||
|
|
||||||
|
<!-- styles -->
|
||||||
|
<link rel="stylesheet" href="/static/css/fa.css">
|
||||||
|
<link rel="stylesheet" href="/static/css/font.css">
|
||||||
|
<link rel="stylesheet" href="/static/css/style.css">
|
||||||
|
<script type="module" crossorigin src="/static/assets/index-B8f0viPI.js"></script>
|
||||||
|
<link rel="stylesheet" crossorigin href="/static/assets/index-BZ8NJuKt.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -19,6 +19,24 @@
|
|||||||
Fill out the "time" and "content" fields. If you wish, press on "Optional" to view other options
|
Fill out the "time" and "content" fields. If you wish, press on "Optional" to view other options
|
||||||
for the reminder.
|
for the reminder.
|
||||||
</p>
|
</p>
|
||||||
|
<p class="subtitle">Time</p>
|
||||||
|
<p class="content">
|
||||||
|
The bot will take a "best-guess" at what time you entered. It will favour UK date formats
|
||||||
|
over US date formats (MM/DD/YY) where possible.
|
||||||
|
<br>
|
||||||
|
You can also use <code>cron</code>-like syntax to specify the time. For example, using
|
||||||
|
<code>0 0 1 * *</code> will send the reminder at midnight on the first of the next month.
|
||||||
|
For more information on cron syntax, see <a href="https://crontab.guru/">crontab.guru</a>.
|
||||||
|
<br>
|
||||||
|
<strong>Cron syntax is not repeating</strong>. Please use the optional "interval" field to specify a repetition interval.
|
||||||
|
</p>
|
||||||
|
<p class="subtitle">Pings</p>
|
||||||
|
<p class="content">
|
||||||
|
Roles and users can be pinged by including their @ mention in the "content" field.
|
||||||
|
To ping a role, the role must be set as mentionable, and the bot must have permissions to mention the role.
|
||||||
|
<br>
|
||||||
|
Please note that when using the dashboard, roles can only be pinged in the "Content..." field and not the embed fields.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -37,4 +55,40 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="hero is-small">
|
||||||
|
<div class="hero-body">
|
||||||
|
<div class="container">
|
||||||
|
<p class="title">Custom formatting rules</p>
|
||||||
|
<p class="content">
|
||||||
|
Reminder content can be customized using formatting rules.
|
||||||
|
</p>
|
||||||
|
<p class="subtitle">timefrom</p>
|
||||||
|
<p class="content">
|
||||||
|
The <code>timefrom</code> formatting rule will display a formatted difference
|
||||||
|
between the time the reminder sends and a specified time.
|
||||||
|
<br>
|
||||||
|
For example, if the current time is 1755800000 (UNIX time), the format string
|
||||||
|
<code><<timefrom:1755803600>></code> would display "1 hour"
|
||||||
|
</p>
|
||||||
|
<p class="subtitle">timenow</p>
|
||||||
|
<p class="content">
|
||||||
|
The <code>timenow</code> formatting rule displays the current time or an offset
|
||||||
|
from the current time in a given timezone in a custom format.
|
||||||
|
<br>
|
||||||
|
For example, if the current time is 1755800000 (UNIX time), the format string
|
||||||
|
<code><<timenow:UTC:%H:%M:%S>></code> would display "18:13:20"
|
||||||
|
<br>
|
||||||
|
Optionally, an offset can be provided to display a time from your current time.
|
||||||
|
For example, if the current time is 1755800000 (UNIX time), the format string
|
||||||
|
<code><<timenow+120:UTC:%H:%M:%S>></code> would display "18:15:20",
|
||||||
|
or <code><<timenow-120:UTC:%H:%M:%S>></code> would display "18:11:20"
|
||||||
|
<br>
|
||||||
|
You can use this feature alongside Discord's timestamp formatting. The following
|
||||||
|
will show the text "in 2 minutes" for all users as a Discord timestamp:
|
||||||
|
<code><t:<<timenow+120:UTC:%s>>:R></code>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user