372 Commits

Author SHA1 Message Date
9a6b65f3a3 Don't delete guild data when guild becomes unavailable 2024-09-17 23:47:27 +01:00
b6ff149d51 Fix macro list/delete 2024-09-14 12:07:09 +01:00
748e33566b Fix patreon not sharing between guild members 2024-08-19 21:50:14 +01:00
d7e90614c8 Bump ver 2024-07-07 16:35:32 +01:00
b5dbfe336d Don't set activity in ready event 2024-07-07 16:31:23 +01:00
218be2f0b1 Bump ver 2024-06-18 19:32:47 +01:00
d7515f3611 Don't require View Channel permission 2024-06-18 19:28:53 +01:00
6ae1096d79 Bump ver 2024-06-12 17:44:55 +01:00
1f0d7adae3 Correct service file 2024-06-12 17:21:42 +01:00
fc96ae526f Default permission checks to true 2024-06-10 18:30:55 +01:00
8881ef0f85 Fix DM reminders trying to load guild data 2024-06-06 16:56:19 +01:00
5e82a687f9 Increase watchdog 2024-06-04 22:34:58 +01:00
de4ecf8dd6 QoL
* Made todo added responses ephemeral if /settings ephemeral is on
* Enabled systemd watchdog
* Move metrics to rocket
2024-06-04 18:40:49 +01:00
064efd4386 Bump version 2024-06-04 16:48:26 +01:00
65b8ba3b47 Redirect old dashboard routes to new routes 2024-06-04 16:42:42 +01:00
9d452ed8cb Fix role selector 2024-05-10 17:37:27 +01:00
441419b92b Bump ver 2024-05-04 13:00:30 +01:00
aecf2c15be Store times as local time not UTC 2024-05-04 10:24:20 +01:00
79da56c794 Bump ver 2024-05-03 16:26:52 +01:00
ef10902c1e Fix todo list deletion not working properly 2024-05-03 16:21:27 +01:00
c277f85c2a Bump dependencies 2024-05-03 16:07:34 +01:00
035653c7fa Bump ver 2024-04-29 08:57:47 +01:00
6358bc3deb Partially revert timezone change 2024-04-29 08:49:01 +01:00
9f5066f982 Bump ver 2024-04-29 08:46:36 +01:00
1d06999e41 Fix bugs with time picker
* Load UTC time correctly at page load
* Don't translate to/from timezone when using the browser date/time
  input
2024-04-16 12:44:19 +01:00
1cf707140c Bump version 2024-04-16 12:22:02 +01:00
e38c63f5ba Don't show empty channels 2024-04-16 11:42:19 +01:00
d52b8b26f2 Upgrade dependencies 2024-04-16 11:19:21 +01:00
bb2128a7ed Tweaks
* Don't show @everyone in the role picker
* Show some text on the image picker talking about Discord CDN
* Correct == in todos
2024-04-11 15:40:50 +01:00
5e99a6f9de Add create todo under each channel
Sort channels for consistency
2024-04-11 15:32:34 +01:00
5406e6b8ec Show all channels and filter todos accordingly 2024-04-11 15:26:24 +01:00
4ee0bc4e37 Bump ver 2024-04-11 12:43:22 +01:00
b99bb7dcbf Fix todo sorting 2024-04-11 12:39:02 +01:00
98f925dc84 Bump version 2024-04-10 18:54:30 +01:00
24e316b12f Add delete/patch todos 2024-04-10 18:42:29 +01:00
4063334953 More work on todo list 2024-04-09 21:21:46 +01:00
e128b9848f More work on todo list support 2024-04-07 20:20:16 +01:00
9989ab3b35 Start work on todo list support for dashboard 2024-04-06 14:27:58 +01:00
b951db3f55 Bump version 2024-03-31 13:30:30 +01:00
884a47bf36 Always show remaining time in top bar 2024-03-31 12:54:48 +01:00
b0f932445c Add server emoji picker 2024-03-31 12:49:52 +01:00
2861cdda0b Bump version 2024-03-29 16:28:09 +00:00
7ba8fcd6b7 Add note to the server data page that states the bot might be restarting 2024-03-29 16:24:24 +00:00
850f0fad57 Bump version 2024-03-29 16:22:13 +00:00
a770a17ee7 Don't invalidate reminders when saving 2024-03-29 16:15:01 +00:00
d15a66d9d9 Bump version 2024-03-28 19:36:23 +00:00
30f011fcd5 Don't send attachments over API 2024-03-28 19:34:30 +00:00
15dbed2f0f Bump version 2024-03-27 17:28:35 +00:00
18cac0345b Allow removing attachments
Show HTTP errors
2024-03-27 17:19:19 +00:00
334b1bc084 Bump version 2024-03-26 17:46:56 +00:00
ba3c76c25f Fix import/export showing "Malformed base64" 2024-03-26 17:43:35 +00:00
67b6f30c62 Add IDs to metrics 2024-03-25 16:41:49 +00:00
8ae311190f Fix panic????????? 2024-03-25 06:07:30 +00:00
016164affb Remove unused javascript/css 2024-03-24 21:00:27 +00:00
2c0aeef700 Fix build. Bump version 2024-03-24 20:55:07 +00:00
ecd75d6f55 Add metrics 2024-03-24 20:38:19 +00:00
4a80d42f86 Move postman and web inside src 2024-03-24 20:23:16 +00:00
075fde71df Bump version 2024-03-11 18:17:22 +00:00
55136aecdc Set default embed color correctly 2024-03-11 18:14:27 +00:00
63fc2cdcbc Block editing username and avatar on DMs 2024-03-10 19:43:57 +00:00
3190738fc5 Extend user reminder API endpoints 2024-03-09 16:17:55 +00:00
8f4810b532 Convert to/from timezone 2024-03-08 16:36:24 +00:00
a5e6c41fa5 Bump ver
Update build file
2024-03-05 20:55:20 +00:00
5f0aa0f834 Add routes for getting/posting user reminders 2024-03-05 20:36:38 +00:00
dbe8e8e358 Add mentioning for channels 2024-03-04 20:36:37 +00:00
85a114e55c Start adding stuff for user reminders 2024-03-03 21:58:48 +00:00
329492b244 Add mention support
Allow vertical resizing of inputs
2024-03-03 21:44:35 +00:00
66135ecd08 Show time until on collapsed reminders 2024-03-03 20:38:17 +00:00
382c2a5a1e Stick options 2024-03-03 19:43:02 +00:00
b91245a3f7 Build dashboard with bot 2024-03-03 13:21:06 +00:00
6f0bdf9852 Support sending reminders to threads 2024-03-03 13:04:50 +00:00
dcee9e0d2a Begin to work on thread support 2024-03-03 11:58:22 +00:00
8e6e1a18b7 Bump ver 2024-03-01 18:04:34 +00:00
72af0532fa Fix timezones 2024-03-01 17:54:05 +00:00
e83b643d86 Show error for files that are too large 2024-03-01 16:56:31 +00:00
0e0ab053f3 Fix time inputs 2024-03-01 16:54:56 +00:00
8c2296b9c8 Bump versions 2024-02-28 21:37:10 +00:00
1c6103142f Fix color picker not working 2024-02-28 21:30:53 +00:00
328127c55e Fix images not setting properly 2024-02-28 21:30:49 +00:00
b0e37b56c0 Bump version 2024-02-26 10:42:46 +00:00
45f5b6261a Convert times to/from UTC 2024-02-26 10:26:07 +00:00
5f6326179c Move styles into Vite
Make sidebar work better
2024-02-25 09:50:10 +00:00
6254f91841 Bump version 2024-02-25 09:18:04 +00:00
60b90a61d4 Adjust permission check
Correct response code for oauth redirect
2024-02-25 09:09:00 +00:00
90f05758d0 Bypass self permission check for DMs 2024-02-24 22:27:29 +00:00
74b7b5d711 Remove glob patterns from static file includes 2024-02-24 17:56:27 +00:00
90550dc2c7 Add loader 2024-02-24 17:47:00 +00:00
79e6498245 Add overlay when data fetching 2024-02-24 17:31:39 +00:00
a8ef3d03f9 Add dashboard to build 2024-02-24 16:12:34 +00:00
53e13844f9 Add unit tests 2024-02-24 15:02:34 +00:00
dd7e681285 Update rust 2024-02-22 18:35:37 +00:00
6c20bf2a0f Bump version 2024-02-22 17:47:40 +00:00
15aa9ccffd Update help text 2024-02-22 17:42:29 +00:00
525471bcad Correct help text 2024-02-22 17:35:50 +00:00
86d53b63b6 Bump deps 2024-02-20 17:09:50 +00:00
d8f266852a Add remaining commands 2024-02-18 14:32:58 +00:00
76a286076b Link all top-level commands with macro recording/replaying logic 2024-02-18 13:24:37 +00:00
5e39e16060 Add option types for top-level commands 2024-02-18 11:04:43 +00:00
c1305cfb36 Extract trait 2024-02-17 20:25:14 +00:00
4823754955 Move all commands to their own files 2024-02-17 18:55:16 +00:00
eb92eacb90 Rearranged some commands
Working on a macro to automatically add option wrappers
2024-02-17 14:09:01 +00:00
d0833b7bca Add macro for extracting arguments 2024-02-16 20:09:32 +00:00
b81c3c80c1 Record some parameters for /remind 2024-02-15 17:28:43 +00:00
2f6d035efe Rename table references 2024-02-14 19:44:53 +00:00
96012ce43c Add migration script 2024-02-14 19:35:23 +00:00
fa7ec8731b Fix hook 2024-02-09 17:03:04 +00:00
def43bfa78 Refactor macros 2024-02-06 20:08:59 +00:00
e4e9af2bb4 Wip commit 2024-01-07 17:10:22 +00:00
cce0de7c75 wip bump versions 2023-12-22 19:12:42 +00:00
e7803b98e8 Merge pull request 'jude/react-dashboard' (#3) from jude/react-dashboard into current
Reviewed-on: #3
2023-12-22 16:58:30 +00:00
7aae246388 Remove submodule 2023-12-22 16:58:30 +00:00
a2d442bc54 Reset intervals correctly 2023-12-22 16:58:30 +00:00
59982df827 Correct merge errors 2023-12-22 16:58:30 +00:00
7a6372ed02 Update styles for notification flash 2023-12-22 16:58:30 +00:00
14a54471f7 Build dashboard 2023-12-22 16:58:30 +00:00
5d3b77f1cd Add metrics
Change dashboards to load an index.html file compiled otherwise
2023-12-22 16:58:30 +00:00
1d64c8bb79 Remove stat table 2023-12-22 16:58:03 +00:00
8ba0f02b98 Bump version 2023-11-12 10:00:46 +00:00
d36438c6ce Bump package lock. Add attachment serializer 2023-11-12 09:39:45 +00:00
e0c60e2ce3 Decode attachments correctly when patching a reminder 2023-11-11 15:05:35 +00:00
e7160215b0 Defer offset response 2023-11-11 13:36:40 +00:00
6eaa6f0f28 Bump version 2023-10-19 20:32:01 +01:00
9db0fa2513 Fix attachment decoding 2023-10-19 20:10:40 +01:00
ca13fd4fa7 Restructure code 2023-10-08 18:24:04 +01:00
55acc8fd16 Bump ver 2023-10-08 12:39:31 +01:00
145711fa5d Add version strings to files 2023-10-08 12:21:38 +01:00
5524215786 Bump ver 2023-10-07 16:10:01 +01:00
e8bd05893f Transmit guild name with patreon information 2023-10-07 16:08:25 +01:00
e3d3418f99 Change routing. Remove a macro 2023-10-05 18:54:53 +01:00
2681280a39 Fix interval parsing for different cases 2023-10-01 09:42:58 +01:00
00579428a1 Bump version 2023-09-25 18:20:22 +01:00
b8ef999710 Reload reminders after import 2023-09-25 18:17:19 +01:00
e8f84e281a Bump version 2023-09-24 14:53:16 +01:00
8ddff698e5 Show messages when imports succeed. 2023-09-24 14:14:21 +01:00
541633270c Fix margin on bottom of collapsed reminders 2023-09-24 13:58:24 +01:00
25286da5e0 Use transactions for certain routes 2023-09-24 13:57:27 +01:00
4bad1324b9 Restructure
Move some code out to other files. Add transaction guard
2023-09-24 13:11:53 +01:00
bd1462a00c Reposition "options"
Fix import/export
2023-09-23 23:38:16 +01:00
56ffc43616 Store intervals in templates 2023-09-23 22:47:21 +01:00
52cf642455 Send edit button to beta dashboard 2023-09-23 20:32:57 +01:00
0bf578357a Bump version 2023-09-23 18:31:51 +01:00
6e9eccb62e Update dependencies 2023-09-23 18:29:25 +01:00
6ea28284ce Bump ver 2023-09-23 18:24:39 +01:00
a6525f3052 Move button row down. Correct image sizes on some browsers 2023-09-23 18:14:01 +01:00
348639270d Move button row down 2023-09-23 18:05:26 +01:00
37177c2431 Update styles for mobile 2023-09-23 18:04:41 +01:00
8587bed703 Bump ver 2023-08-19 17:10:45 +01:00
6c9af1ae8e Correct styles on mobile burger 2023-08-19 17:09:46 +01:00
7695b7a476 Fix delete command 2023-08-19 14:35:07 +01:00
651da7b28e Improve some styles. Add an offline mode 2023-08-19 14:20:48 +01:00
eb086146bf Bump version 2023-08-16 17:05:18 +01:00
4ebd705e5e Add clearer indication of interval patreon requirements 2023-08-16 17:03:38 +01:00
5a85f1d83a Extract error sections to templates 2023-08-13 18:29:30 +01:00
68ba25886a Correct javascript comparisons 2023-08-11 13:19:31 +01:00
e25bf6b828 bump 2023-08-10 18:41:47 +01:00
5a386daa9d Fix expirations 2023-08-10 18:25:41 +01:00
0d4a02fb1e Bump ver 2023-08-08 17:48:49 +01:00
e135a74a9b Fix avatars not loading correctly 2023-08-08 17:44:40 +01:00
77f17c8dc2 Partially fix reminder usernames resetting 2023-08-07 21:50:11 +01:00
6a94f990cf Bump ver 2023-08-03 20:08:14 +01:00
3aa5bd37aa Fix duplicating reminder fields 2023-08-03 19:57:28 +01:00
fa83fed1af Fix interval updating 2023-08-03 19:50:15 +01:00
666cb7fa2f Fix padding etc. 2023-08-03 19:28:12 +01:00
a5678e15dc Fix styling on buttons
Prevent template buttons from wrapping by consuming more vertical space on middle-sized screens
2023-08-03 18:07:03 +01:00
9405cfcee9 Fix "Reminder needs content".
Certain fields were not being checked correctly for content.
2023-08-03 17:32:17 +01:00
cb25d02cdf Bump ver 2023-08-01 21:12:15 +01:00
bfe651a125 Change autocomplete to use a past date in the past 2023-08-01 20:13:05 +01:00
dc5e52d9ce Default datetime inputs to current date/time 2023-08-01 17:51:29 +01:00
229ada83e1 Fix cron username 2023-07-31 20:14:45 +01:00
13171d6744 Bump ver 2023-07-31 20:09:00 +01:00
2ad941c94c Fix not sending followup reminders 2023-07-31 20:07:54 +01:00
924d31e978 Bump ver 2023-07-31 20:05:43 +01:00
f9a1b23212 Update privacy 2023-07-31 19:28:23 +01:00
ae5795a7ea Update opcode handling 2023-07-31 19:25:06 +01:00
ee36c38eda Update manifest 2023-07-31 19:18:53 +01:00
eca7df3d9f Update style 2023-07-31 18:39:39 +01:00
902b7e1b4a Change reminder sending behaviour to keep reminders but flag them as sent 2023-07-31 18:39:27 +01:00
db1a53a797 Bump ver 2023-07-31 18:04:16 +01:00
3605d71b73 Suppress errors. Restyle 2023-07-31 17:59:38 +01:00
ea2cea573e Bump ver. Round failure rate. 2023-07-30 19:17:44 +01:00
d5fa8036e8 Add data to admin page for success/fail history 2023-07-30 19:09:48 +01:00
b8707bbc9a Fix deleting template making a call on empty template list 2023-07-30 17:16:37 +01:00
99eea16f62 Bump ver 2023-07-30 17:11:37 +01:00
88737302f3 Log reminder send status 2023-07-30 17:00:55 +01:00
213e3a5100 Fix styles. Feedback button 2023-07-30 15:50:46 +01:00
8fa1402ecc Bump ver 2023-07-30 15:42:46 +01:00
e63996bb61 Fix create template not testing for errors 2023-07-30 15:36:58 +01:00
9ede879630 Stats table migration 2023-07-30 15:28:26 +01:00
88e9826a62 Update terms. Fix issue with role picker 2023-07-30 15:26:51 +01:00
5d655c7e6d Update privacy policy 2023-07-30 15:16:34 +01:00
51c9d8a7ae Fix client error on selecting server with no channels 2023-07-30 15:11:34 +01:00
90df265114 Add handler for 50001 Missing Access 2023-07-30 14:13:20 +01:00
e65429aa9c Fix interval input styles 2023-07-30 13:22:57 +01:00
8d2232f0da Bump ver. Use Discord's error codes where possible to improve logging 2023-07-30 12:44:01 +01:00
a58b9866ea Reduce log level 2023-07-30 12:14:47 +01:00
b1f25be5d7 Use transparent background with dashboard logo 2023-07-29 17:13:05 +01:00
f0f9787326 Bump ver 2023-07-23 17:00:09 +01:00
302f5835e6 Fix wrapping on long server names 2023-07-23 16:30:15 +01:00
58c778632e Fix wrapping on long server names 2023-07-23 16:28:27 +01:00
5671fd462b Update contrast on the burger button. fix error thrown by update_select 2023-07-23 16:15:24 +01:00
5ac9733f15 Bump ver 2023-07-23 14:44:35 +01:00
01dc0334fd Fix arbitrary access to reminder list. 2023-07-23 14:29:59 +01:00
4a17aac15c Bump ver 2023-07-23 12:36:25 +01:00
8ce4fc9c6d Fix enable/disable button. Hide demo button 2023-07-23 12:16:09 +01:00
b4f07cfc1c Fix some mobile styles. Fix race condition in client side 2023-07-23 12:06:03 +01:00
8799089b2d Increase the size of reminder names. Restyle. 2023-07-22 15:09:06 +01:00
88c4830209 Fix dashboard embed fields 2023-07-22 13:34:18 +01:00
4dd3df5cc2 bump ver 2023-07-22 13:13:46 +01:00
369a325a46 bump ver 2023-07-22 10:46:33 +01:00
1a1a0fdefb show total reminders and intervals on admin dash 2023-07-10 09:59:11 +01:00
dda8bd3e10 Fix dead link. Hopefully extract mysql details from environment 2023-06-23 11:56:53 +01:00
edbfc92cb9 Add health check email notifications 2023-06-23 09:44:42 +01:00
6de11f09db Change graph periods 2023-06-21 15:36:05 +01:00
284bfcd9ad Split intervals 2023-06-21 15:24:43 +01:00
3d627b5bf0 Add charts 2023-06-21 15:09:24 +01:00
c3c0dbbbae Fetch upcoming schedule and backlog count 2023-06-21 13:26:28 +01:00
64dd81e941 Admin only routes 2023-06-21 10:54:20 +01:00
799298ca34 Add fail cutoff for reminder updating 2023-06-20 15:41:28 +01:00
fa542bb24f Clear up warning from new Rust version 2023-06-20 15:33:25 +01:00
e025d945cf Fix serious issue with adding days. Origin chrono v4.23 2023-06-20 15:30:44 +01:00
bb1c61d0b9 Fallback for reminder days 2023-06-20 14:44:05 +01:00
1519474f93 Report errors to server 2023-06-20 13:13:26 +01:00
9d8622f418 Add logout button 2023-06-20 08:50:12 +01:00
a66db37b33 update poise 2023-06-18 10:47:31 +01:00
c8c1a171d4 Bump version 2023-06-18 10:04:55 +01:00
88cfb829e3 Use conffiles 2023-06-17 12:49:01 +01:00
16be7a328e Correct permissions 2023-06-16 14:00:44 +01:00
04babf7930 updated some dashboard text. fixed authentication. hidden broken stuff 2023-06-16 13:38:42 +01:00
96bc09e8b5 correct authentication 2023-06-16 10:20:42 +01:00
976fb91ecc set default logs 2023-06-15 10:53:13 +01:00
1305b6e64e Bump version 2023-06-14 17:50:56 +01:00
cdfe44d958 Configure permissions properly on Rocket.toml. Make static path behave better 2023-06-14 13:29:48 +01:00
c824a36832 Corrected a number of apt packaging issues 2023-06-13 10:40:48 +01:00
c4bd2c1d18 bump dateparser requirement 2023-06-12 22:47:23 +01:00
561555ab7e updated tos and privacy 2023-05-27 16:40:41 +01:00
115fbd44cb update some frontend 2023-05-27 16:12:09 +01:00
aa931328b0 Support ephemeral reminder confirmations 2023-05-11 19:40:33 +01:00
4b42966284 Moved stuff around since threads are ridiculous 2023-05-11 18:33:06 +01:00
523ab7f03a Partial thread support 2023-05-11 18:32:58 +01:00
6e831c8253 Add migration for threads. Add ability to load .env from wd 2023-05-11 18:32:50 +01:00
4416e5d175 Remove need to supply webhook avatar 2023-05-08 17:32:59 +01:00
734a39a001 Change default Python location. Update build instructions. Add container build instructions 2023-05-08 17:04:51 +01:00
98191d29ee deb-related stuff 2023-05-07 21:08:59 +01:00
1c4c4a8b31 Add deb stuff. Correct dependency on database name 2023-05-07 20:59:07 +01:00
d496c81003 Correct typo in path 2023-05-07 20:38:08 +01:00
094d210f64 Fix orphaned channels issue again 2023-03-24 19:52:41 +00:00
314c72e132 Changed data import to add alongside rather than removing. 2023-03-24 19:41:34 +00:00
4e0163f2cb Rename some environment variables. Add partial deb metadata 2023-03-24 17:44:43 +00:00
e5b8c418af Merge remote-tracking branch 'origin/next' into next 2023-03-24 11:11:59 +00:00
3ef8584189 Use SQLx migrations 2023-03-24 11:11:51 +00:00
df2ad09c86 Update README.md 2023-01-21 12:25:24 +00:00
d70fb24eb1 Fix todo viewing not working for large entries
Was not checking the length of the item when trying to add it to the
dropdown, causing failures.
2023-01-06 17:08:09 +00:00
3150c7267d Add validating to length-validated fields on edit
Can't just replace edit logic with overwrite logic because partial editing is used in enabling/disabling. So need to replicate logic in a sensible way.
2022-12-18 13:38:43 +00:00
6e65e4ff3d update some help pages 2022-12-18 13:09:02 +00:00
67a4db2e9a Ensure interval updating is performed properly
Validate patreon status. Validate interval length against minimum. Update the reminder pane to reflect changes that were made. Properly deserialize.
2022-12-11 10:09:26 +00:00
e9bcb1973f Update web for daily intervals 2022-12-10 16:21:43 +00:00
9b87fd4258 Ver bump 2022-12-10 15:38:21 +00:00
a49a849917 Support daily intervals
Add new database column for interval_days. Update humantime to return days as a separate field.
2022-12-10 15:32:49 +00:00
aa74a7f9a3 Use timezones wherever possible.
Replace uses of NaiveDateTime with DateTime<Utc>. Use timezones in postman to update days correctly. Use chrono::Months to update months rather than using MySQL query.
2022-11-22 20:41:07 +00:00
08e4c6cb57 ver bump 2022-11-20 12:20:52 +00:00
6e087bd2dd Fix character counting on /look. Initial support for jumping over DST boundaries 2022-11-20 12:20:10 +00:00
e9792e6322 ver bump 2022-09-26 16:59:57 +01:00
130504b964 Add notice to macro initial run 2022-09-26 16:44:30 +01:00
2a8117d0c1 Revert multiline changes 2022-09-20 17:00:33 +01:00
94bfd39085 Patch compilation against live schema 2022-09-17 13:05:50 +01:00
40cd5f8a36 Patch compilation against live schema 2022-09-17 13:03:52 +01:00
133b00a2ce Patch compilation against live schema 2022-09-17 12:52:03 +01:00
57336f5c81 Change macro list to use fields to prevent going over limit
Add length checks for name and description
2022-09-17 12:37:58 +01:00
b62d24c024 Add an autocomplete for time hints
Shows the approximate time until a reminder will send in the autocomplete area.
2022-09-12 17:49:10 +01:00
8f8235a86e Move macro commands to own module
Lots of code here
2022-09-12 16:45:00 +01:00
c8f646a8fa Override timezone per command
Timezone option that will override the timezone on a per-command basis
2022-09-11 18:59:46 +01:00
ecaa382a1e Add join message 2022-09-11 17:38:53 +01:00
8991198fd3 Use autocomplete to ensure content box is shown 2022-09-11 15:24:02 +01:00
f20b95a482 Upgrade poise. Combine remind/multiline into one command 2022-09-08 17:58:05 +01:00
8dd7dc6409 Added command for multiline reminders 2022-09-07 18:27:13 +01:00
c799d10727 Move extra processes to user data setup 2022-09-03 16:19:59 +01:00
ceb6fb7b12 bump version 2022-09-03 15:49:05 +01:00
6708abdb0f Merge pull request #10 from reminder-bot/jellywx/fix-dm-reminders
group by channel instead of guild
2022-09-03 15:44:00 +01:00
a38f6024c1 Migrate natural commands 2022-09-03 15:40:29 +01:00
7d8748e3ef group by channel instead of guild 2022-08-19 09:04:12 +01:00
bb3386c4e8 migration for $r commands 2022-08-14 16:22:00 +01:00
25b84880a5 Don't send non-interval disabled reminders
Skip the sending logic as some users use disabled one-time reminders as presets
2022-08-04 19:06:29 +01:00
7b6e967a5d Block/allow DM reminders
Only affects slash commands but this is sort of a non-issue post September
2022-07-29 19:22:15 +01:00
2781f2923e Restrict reminder selection to one-per-guild during fetch loop 2022-07-28 19:20:15 +01:00
03f08f0a18 Update deps. Drop limiter on reminder query 2022-07-27 21:42:09 +01:00
79c86d43f2 Changed return types to results 2022-07-24 20:06:37 +01:00
e19af54caf Import todo lists. Export other data. 2022-07-22 23:30:45 +01:00
f4213c6a83 Cache channel in todo list command
Channel was not being cached, placing channel todos into the server todo list.
2022-07-02 08:31:17 +01:00
f56db14720 Webhook command
Add a command to view the webhook, as some users wish to use the webhook to edit past reminders.
2022-06-17 17:15:48 +01:00
6f7d0f67b3 mentions 2022-05-15 12:14:07 +01:00
bfc2d71ca0 patreon 2022-05-14 12:02:46 +01:00
8eb46f1f23 delete reminders when the user cannot be direct messaged 2022-05-14 10:56:03 +01:00
c4087bf569 pos mysql 2022-05-14 08:12:50 +01:00
f25cfed8d7 edit button 2022-05-13 23:30:01 +01:00
d2a8bd1982 update readme with better build instructions 2022-05-13 23:13:39 +01:00
437ee6b446 ver bump 2022-05-13 23:10:29 +01:00
7d43aa5918 cleared up all unwraps from the reminder sender. cleared up clippy lints. added undo button 2022-05-13 23:08:52 +01:00
8bad95510d configure playing status 2022-05-13 12:43:27 +01:00
d7a0b727fb Merge pull request #7 from reminder-bot/poise-2
Poise 2
2022-05-13 09:00:26 +01:00
1c1f5662d3 removed guild only hook. permissions on commands. fix for macro command count. 2022-05-13 08:59:46 +01:00
ded750aa2d update dependencies 2022-04-19 15:23:27 +01:00
4c4f0927f1 fix attachments. remove webhook sending for speedup 2022-04-09 12:21:28 +01:00
0f05018cab removed remainder of old personal dashboard code. fixed big lighthouse issues. 2022-04-07 21:41:24 +01:00
85d27c5bba fields are now json and work. fix for intervals. moved some code together 2022-04-07 17:13:02 +01:00
d946ef1dca process intervals. inlining fields 2022-04-03 21:53:28 +01:00
f21d522435 mobile appearence 2022-03-27 19:17:30 +01:00
3add718cdf interval field client processing 2022-03-27 18:03:42 +01:00
f4ef7afea0 show newly created reminders. fix color rendering. 2022-03-27 14:15:01 +01:00
f8547bba70 upload attachments 2022-03-26 20:03:58 +00:00
08fd88ce54 styling interval inputs 2022-03-24 22:40:05 +00:00
abfe492192 put a plain background on images 2022-03-24 21:36:22 +00:00
afb2fbe4ff patch reminders 2022-03-22 22:21:47 +00:00
878ea11502 graceful shutdown 2022-03-21 23:11:52 +00:00
93da746bdc support articles 2022-03-20 21:41:38 +00:00
9e6a387f82 support articles 2022-03-20 21:04:24 +00:00
af9d8bea62 collapse/expand elements. moved the embed color picker 2022-03-20 18:29:27 +00:00
318be1fa5e prettier for javascript formatting. sorting 2022-03-20 15:46:22 +00:00
3b6e02e16e working on editing reminders 2022-03-20 00:10:19 +00:00
a56f84f659 timezone help. moved javascript to separate file 2022-03-19 23:47:40 +00:00
3e4dd0fa48 channel selection shows properly. loader 2022-03-19 21:28:11 +00:00
d0d2d50966 create reminders :) 2022-03-19 17:41:34 +00:00
e2e5b022a0 create reminder route. formatting on frontend 2022-03-05 19:43:02 +00:00
6ae2353c92 add distinct identifying names. log errors in run_macro 2022-02-20 12:19:39 +00:00
06c4deeaa9 component models 2022-02-19 22:11:21 +00:00
afc376c44f everything except component model actions 2022-02-19 18:21:11 +00:00
84ee7e77c5 2nd attempt at doing poise stuff 2022-02-19 14:32:03 +00:00
620f054703 extracted event handler. removed custom sharding code. extracted util functions 2022-02-19 13:28:24 +00:00
cb471c52f3 optionally dont run web/postman 2022-02-19 12:45:33 +00:00
37420b2b1f ..... 2022-02-11 20:03:53 +00:00
49974b7153 moved dashboard crate into here 2022-02-11 17:44:08 +00:00
a3844dde9e moved postman into separate crate 2022-02-06 15:47:59 +00:00
d62c8c95c2 support months in sender 2022-02-01 23:41:28 +00:00
05606dfec1 update lock 2022-02-01 23:05:14 +00:00
68ee42f244 Merge remote-tracking branch 'origin/next' into next
# Conflicts:
#	Cargo.lock
2022-02-01 23:04:44 +00:00
fad28faabb interval months/interval seconds 2022-02-01 23:04:31 +00:00
e5ab99f67b removed some log messages. rustfmt 2021-12-21 13:46:10 +00:00
e47715917e integrate reminder sender 2021-12-20 13:48:18 +00:00
4f9eb58c16 made the missing perms send a message (since the webhook responses bypass perms) 2021-11-18 21:05:49 +00:00
c953bc0cd3 various todo fixes 2021-11-15 08:09:48 +00:00
610779a293 added mention blocker to everything 2021-11-15 07:51:38 +00:00
ebd1efa990 added check for guild only commands 2021-11-13 22:30:18 +00:00
5230101a8d beta0 2021-11-13 14:12:37 +00:00
d8f42c1b25 fixed an issue with utc time. removed intents 2021-11-07 13:23:41 +00:00
23c6b3869e patreon gated repeat argument 2021-11-06 23:30:38 +00:00
a21f518b21 removed framework impl 2021-11-02 20:19:29 +00:00
f1bfc11160 removed all remaining restriction code 2021-11-02 20:10:10 +00:00
72228911f2 Readded some guild data code. fixed some weird cases with macro command. removed restrict command. changed db to be 'as it was'. removed execution limiters since commands are quite heavily ratelimited anyway 2021-10-30 21:02:11 +01:00
db7cca6296 added to the migration file somewhat. added some checks to components 2021-10-26 22:13:51 +01:00
e36e718f28 removed all guild data related code 2021-10-26 21:10:14 +01:00
44debf93c5 removed dead code 2021-10-26 20:54:22 +01:00
9b54fba5e5 Revert "turned pager into a single type"
This reverts commit 4490f19c
2021-10-26 20:11:19 +01:00
6cf660c7ee macro stuff 2021-10-16 19:18:16 +01:00
4490f19c04 turned pager into a single type 2021-10-13 17:23:50 +01:00
a362a24cfc changed a bunch of types so the macro run command works nicely 2021-10-13 16:37:15 +01:00
903daf65e6 ... 2021-10-12 21:52:43 +01:00
b310e99085 todo pager and selector 2021-10-11 21:19:08 +01:00
ebabe0e85a todo stuff 2021-10-02 22:54:34 +01:00
6b5d6ae288 fixed del pager. todo stuff 2021-09-27 17:34:13 +01:00
379e488f7a subcommand group syntax 2021-09-24 12:55:35 +01:00
d84d7ab62b added functionality for reusable hook functions that will execute on commands 2021-09-22 21:12:29 +01:00
a0974795e1 ... 2021-09-18 13:40:30 +01:00
a9c91bee93 pager improvements. deleting working 2021-09-16 18:30:16 +01:00
b2207e308a optimized packing slightly. restrict interactions 2021-09-16 15:42:50 +01:00
3c1eeed92f look command pager 2021-09-16 14:48:29 +01:00
395a8481f1 typing 2021-09-12 16:59:19 +01:00
bae0433bd9 framework now supports subcommands. timer cmd working 2021-09-12 16:09:57 +01:00
3e547861ea components 2021-09-11 20:40:58 +01:00
9b5333dc87 more commands. fixed an issue with text only commands 2021-09-11 00:14:23 +01:00
471948bed3 linked everything together 2021-09-10 18:09:25 +01:00
c148cdf556 removed language_manager.rs. framework reworked for slash commands. updated info commands for new framework 2021-09-06 13:46:16 +01:00
98aed91d21 revert some usages of discord builtin timestamp formatting 2021-09-02 23:59:30 +01:00
40630c0014 restructured all the reminder creation stuff into builders 2021-09-02 23:38:12 +01:00
382 changed files with 159578 additions and 6190 deletions

31
.gitignore vendored
View File

@ -1,7 +1,30 @@
/target target
.env .env
/venv /venv
.cargo .cargo
assets .idea
out.json web/static/index.html
/.idea web/static/assets
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

2
.prettierrc.toml Normal file
View File

@ -0,0 +1,2 @@
printWidth = 90
tabWidth = 4

3675
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,29 +1,71 @@
[package] [package]
name = "reminder_rs" name = "reminder-rs"
version = "1.5.2" version = "1.7.27"
authors = ["jellywx <judesouthworth@pm.me>"] authors = ["Jude Southworth <judesouthworth@pm.me>"]
edition = "2018" edition = "2021"
license = "AGPL-3.0 only"
description = "Reminder Bot for Discord, now in Rust"
[dependencies] [dependencies]
dashmap = "4.0" poise = "0.6.1"
dotenv = "0.15" dotenv = "0.15"
humantime = "2.1"
tokio = { version = "1", features = ["process", "full"] } tokio = { version = "1", features = ["process", "full"] }
reqwest = "0.11" reqwest = { version = "0.12", features = ["json"] }
regex = "1.4" regex = "1.10"
log = "0.4" log = "0.4"
env_logger = "0.8" env_logger = "0.11"
chrono = "0.4" chrono = "0.4"
chrono-tz = "0.5" chrono-tz = { version = "0.9", features = ["serde"] }
lazy_static = "1.4" lazy_static = "1.4"
num-integer = "0.1" num-integer = "0.1"
serde = "1.0" serde = "1.0"
serde_json = "1.0" serde_json = "1.0"
rand = "0.7" serde_repr = "0.1"
Inflector = "0.11" rmp-serde = "1.1"
rand = "0.8"
levenshtein = "1.0" levenshtein = "1.0"
serenity = { git = "https://github.com/jellywx/serenity", branch = "jellywx-attachment_option", features = ["collector", "unstable_discord_api"] } sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono", "migrate"] }
sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono"]} base64 = "0.22"
secrecy = "0.8.0"
futures = "0.3.30"
prometheus = "0.13.3"
rocket = { version = "0.5.0", features = ["tls", "secrets", "json"] }
rocket_dyn_templates = { version = "0.1.0", features = ["tera"] }
serenity = { version = "0.12", default-features = false, features = ["builder", "cache", "client", "gateway", "http", "model", "utils", "rustls_backend"] }
oauth2 = "4"
csv = "1.2"
sd-notify = "0.4.1"
[dependencies.regex_command_attr] [dependencies.extract_derive]
path = "./regex_command_attr" path = "extract_derive"
[dependencies.recordable_derive]
path = "recordable_derive"
[package.metadata.deb]
depends = "$auto, python3-dateparser (>= 1.0.0)"
suggests = "mysql-server-8.0, nginx"
maintainer-scripts = "debian"
assets = [
["target/release/reminder-rs", "usr/bin/reminder-rs", "755"],
["static/css/*", "lib/reminder-rs/static/css", "644"],
["static/favicon/*", "lib/reminder-rs/static/favicon", "644"],
["static/img/*", "lib/reminder-rs/static/img", "644"],
["static/js/*", "lib/reminder-rs/static/js", "644"],
["static/webfonts/*", "lib/reminder-rs/static/webfonts", "644"],
["static/site.webmanifest", "lib/reminder-rs/static/site.webmanifest", "644"],
["templates/**/*", "lib/reminder-rs/templates", "644"],
["reminder-dashboard/dist/static/assets/*", "lib/reminder-rs/static/assets", "644"],
["reminder-dashboard/dist/index.html", "lib/reminder-rs/static/index.html", "644"],
["conf/default.env", "etc/reminder-rs/config.env", "600"],
["conf/Rocket.toml", "etc/reminder-rs/Rocket.toml", "600"],
# ["nginx/reminder-rs", "etc/nginx/sites-available/reminder-rs", "755"]
]
conf-files = [
"/etc/reminder-rs/config.env",
"/etc/reminder-rs/Rocket.toml",
]
[package.metadata.deb.systemd-units]
unit-scripts = "systemd"
start = false

9
Containerfile Normal file
View File

@ -0,0 +1,9 @@
FROM ubuntu:20.04
ENV RUSTUP_HOME=/usr/local/rustup \
CARGO_HOME=/usr/local/cargo \
PATH=/usr/local/cargo/bin:$PATH
RUN apt update && DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC apt install -y gcc gcc-multilib cmake pkg-config libssl-dev curl mysql-client-8.0 npm
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path --profile minimal --default-toolchain nightly
RUN cargo install cargo-deb

View File

@ -1,26 +1,42 @@
# reminder-rs # reminder-rs
Reminder Bot for Discord, now in Rust. Reminder Bot for Discord.
Old Python version: https://github.com/reminder-bot/bot
## How do I use it? ## How do I use it?
We 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)
### Compiling ### Build APT package
Reminder Bot can be built by running `cargo build --release` in the top level directory. It is necessary to create a folder called 'assets' containing an image file with its name specified in the environment as `WEBHOOK_AVATAR`, of dimensions 128x128px to be used as the webhook avatar.
#### Compilation environment variables Recommended method.
These environment variables must be provided when compiling the bot
By default, this builds targeting Ubuntu 20.04. Modify the Containerfile if you wish to target a different platform. These instructions are written using `podman`, but `docker` should work too.
1. Install container software: `sudo apt install podman`.
2. Install database server: `sudo apt install mysql-server-8.0`. Create a database called `reminders`
3. Install SQLx CLI: `cargo install sqlx-cli`
4. From the source code directory, execute `sqlx migrate run`
5. Build container image: `podman build -t reminder-rs .`
6. Build with podman: `podman run --rm --network=host -v "$PWD":/mnt -w /mnt -e "DATABASE_URL=mysql://user@localhost/reminders" reminder-rs cargo deb`
### Compiling for other target
1. Install requirements:
`sudo apt install gcc gcc-multilib cmake libssl-dev build-essential python3-dateparser`
2. Install rustup from https://rustup.rs
3. Install the nightly toolchain: `rustup toolchain default nightly`
4. Install database server: `sudo apt install mysql-server-8.0`. Create a database called `reminders`.
5. Install `sqlx-cli`: `cargo install sqlx-cli`.
6. Run migrations: `sqlx migrate run`.
7. Set environment variables:
* `DATABASE_URL` - the URL of your MySQL database (`mysql://user[:password]@domain/database`) * `DATABASE_URL` - the URL of your MySQL database (`mysql://user[:password]@domain/database`)
* `WEBHOOK_AVATAR` - accepts the name of an image file located in `$CARGO_MANIFEST_DIR/assets/` to be used as the avatar when creating webhooks. **IMPORTANT: image file must be 128x128 or smaller in size** 8. Build: `cargo build --release`
* `STRINGS_FILE` - accepts the name of a compiled strings file located in `$CARGO_MANIFEST_DIR/assets/` to be used for creating messages. Compiled string files can be generated with `compile.py` at https://github.com/reminder-bot/languages
### Setting up Python
Reminder Bot by default looks for a venv within it's working directory to run Python out of. To set up a venv, install `python3-venv` and run `python3 -m venv venv`. Then, run `source venv/bin/activate` to activate the venv, and do `pip install dateparser` to install the required library
### Environment Variables ### 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__
@ -29,16 +45,8 @@ __Required Variables__
__Other Variables__ __Other Variables__
* `MIN_INTERVAL` - default `600`, defines the shortest interval the bot should accept * `MIN_INTERVAL` - default `600`, defines the shortest interval the bot should accept
* `MAX_TIME` - default `1576800000`, defines the maximum time ahead that reminders can be set for
* `LOCAL_TIMEZONE` - default `UTC`, necessary for calculations in the natural language processor * `LOCAL_TIMEZONE` - default `UTC`, necessary for calculations in the natural language processor
* `DEFAULT_PREFIX` - default `$`, used for the default prefix on new guilds
* `SUBSCRIPTION_ROLES` - default `None`, accepts a list of Discord role IDs that are given to subscribed users * `SUBSCRIPTION_ROLES` - default `None`, accepts a list of Discord role IDs that are given to subscribed users
* `CNC_GUILD` - default `None`, accepts a single Discord guild ID for the server that the subscription roles belong to * `CNC_GUILD` - default `None`, accepts a single Discord guild ID for the server that the subscription roles belong to
* `IGNORE_BOTS` - default `1`, if `1`, Reminder Bot will ignore all other bots * `PYTHON_LOCATION` - default `/usr/bin/python3`. Can be changed if your Python executable is located somewhere else
* `PYTHON_LOCATION` - default `venv/bin/python3`. Can be changed if your Python executable is located somewhere else
* `LOCAL_LANGUAGE` - default `EN`. Specifies the string set to fall back to if a string cannot be found (and to be used with new users)
* `THEME_COLOR` - default `8fb677`. Specifies the hex value of the color to use on info message embeds * `THEME_COLOR` - default `8fb677`. Specifies the hex value of the color to use on info message embeds
* `CASE_INSENSITIVE` - default `1`, if `1`, commands will be treated with case insensitivity (so both `$help` and `$HELP` will work)
* `SHARD_COUNT` - default `None`, accepts the number of shards that are being ran
* `SHARD_RANGE` - default `None`, if `SHARD_COUNT` is specified, specifies what range of shards to start on this process
* `DM_ENABLED` - default `1`, if `1`, Reminder Bot will respond to direct messages

28
Rocket.toml Normal file
View File

@ -0,0 +1,28 @@
[default]
address = "0.0.0.0"
port = 18920
template_dir = "templates"
limits = { json = "10MiB" }
[debug]
secret_key = "tR8krio5FXTnnyIZNiJDXPondz0kI1v6X6BXZcBGIRY="
[debug.tls]
certs = "private/rsa_sha256_cert.pem"
key = "private/rsa_sha256_key.pem"
[debug.rsa_sha256.tls]
certs = "private/rsa_sha256_cert.pem"
key = "private/rsa_sha256_key.pem"
[debug.ecdsa_nistp256_sha256.tls]
certs = "private/ecdsa_nistp256_sha256_cert.pem"
key = "private/ecdsa_nistp256_sha256_key_pkcs8.pem"
[debug.ecdsa_nistp384_sha384.tls]
certs = "private/ecdsa_nistp384_sha384_cert.pem"
key = "private/ecdsa_nistp384_sha384_key_pkcs8.pem"
[debug.ed25519.tls]
certs = "private/ed25519_cert.pem"
key = "private/ed25519_key.pem"

BIN
assets/webhook.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

13
build.rs Normal file
View File

@ -0,0 +1,13 @@
use std::{path::Path, process::Command};
fn main() {
println!("cargo:rerun-if-changed=migrations");
println!("cargo:rerun-if-changed=reminder-dashboard");
Command::new("npm")
.arg("run")
.arg("build")
.current_dir(Path::new("reminder-dashboard"))
.spawn()
.expect("Failed to build NPM");
}

8
conf/Rocket.toml Normal file
View File

@ -0,0 +1,8 @@
[default]
address = "127.0.0.1"
port = 18920
template_dir = "/lib/reminder-rs/templates"
limits = { json = "10MiB" }
[release]
# secret_key = ""

19
conf/default.env Normal file
View File

@ -0,0 +1,19 @@
DATABASE_URL=
DISCORD_TOKEN=
PATREON_GUILD_ID=
PATREON_ROLE_ID=
LOCAL_TIMEZONE=
MIN_INTERVAL=
PYTHON_LOCATION=/usr/bin/python3
DONTRUN=
SECRET_KEY=
REMIND_INTERVAL=
OAUTH2_DISCORD_CALLBACK=
OAUTH2_CLIENT_ID=
OAUTH2_CLIENT_SECRET=
REPORT_EMAIL=
LOG_TO_DATABASE=1

1
cron.d/reminder_health Normal file
View File

@ -0,0 +1 @@
*/10 * * * * reminder /lib/reminder-rs/healthcheck

9
debian/postinst vendored Normal file
View File

@ -0,0 +1,9 @@
#!/bin/bash
set -e
id -u reminder &>/dev/null || useradd -r -M reminder
chown -R reminder /etc/reminder-rs
#DEBHELPER#

7
debian/postrm vendored Normal file
View File

@ -0,0 +1,7 @@
#!/bin/bash
set -e
id -u reminder &>/dev/null || userdel reminder
#DEBHELPER#

46
extract_derive/Cargo.lock generated Normal file
View File

@ -0,0 +1,46 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "extract_macro"
version = "0.1.0"
dependencies = [
"quote",
"syn",
]
[[package]]
name = "proc-macro2"
version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
dependencies = [
"proc-macro2",
]
[[package]]
name = "syn"
version = "2.0.49"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "915aea9e586f80826ee59f8453c1101f9d1c4b3964cd2460185ee8e299ada496"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"

11
extract_derive/Cargo.toml Normal file
View File

@ -0,0 +1,11 @@
[package]
name = "extract_derive"
version = "0.1.0"
edition = "2021"
[lib]
proc-macro = true
[dependencies]
quote = "1.0.35"
syn = { version = "2.0.49", features = ["full"] }

53
extract_derive/src/lib.rs Normal file
View File

@ -0,0 +1,53 @@
use proc_macro::TokenStream;
use syn::{spanned::Spanned, Data, Fields};
#[proc_macro_derive(Extract)]
pub fn extract(input: TokenStream) -> TokenStream {
let ast = syn::parse_macro_input!(input);
impl_extract(&ast)
}
fn impl_extract(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
match &ast.data {
// Dispatch over struct: extract args directly from context
Data::Struct(st) => match &st.fields {
Fields::Named(fields) => {
let extracted = fields.named.iter().map(|field| {
let ident = &field.ident;
let ty = &field.ty;
quote::quote_spanned! {field.span()=>
#ident : crate::utils::extract_arg!(ctx, #ident, #ty)
}
});
TokenStream::from(quote::quote! {
impl Extract for #name {
fn extract(ctx: crate::ApplicationContext) -> Self {
Self {
#(#extracted,)*
}
}
}
})
}
Fields::Unit => TokenStream::from(quote::quote! {
impl Extract for #name {
fn extract(ctx: crate::ApplicationContext) -> Self {
Self {}
}
}
}),
_ => {
panic!("Only named/unit structs can derive Extract");
}
},
_ => {
panic!("Only structs can derive Extract");
}
}
}

View File

@ -1,10 +1,6 @@
CREATE DATABASE IF NOT EXISTS reminders;
SET FOREIGN_KEY_CHECKS=0; SET FOREIGN_KEY_CHECKS=0;
USE reminders; CREATE TABLE guilds (
CREATE TABLE reminders.guilds (
id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT, id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT,
guild BIGINT UNSIGNED UNIQUE NOT NULL, guild BIGINT UNSIGNED UNIQUE NOT NULL,
@ -18,10 +14,10 @@ CREATE TABLE reminders.guilds (
default_avatar VARCHAR(512) DEFAULT 'https://raw.githubusercontent.com/reminder-bot/logos/master/Remind_Me_Bot_Logo_PPic.jpg' NOT NULL, default_avatar VARCHAR(512) DEFAULT 'https://raw.githubusercontent.com/reminder-bot/logos/master/Remind_Me_Bot_Logo_PPic.jpg' NOT NULL,
PRIMARY KEY (id), PRIMARY KEY (id),
FOREIGN KEY (default_channel_id) REFERENCES reminders.channels(id) ON DELETE SET NULL FOREIGN KEY (default_channel_id) REFERENCES channels(id) ON DELETE SET NULL
); );
CREATE TABLE reminders.channels ( CREATE TABLE channels (
id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT, id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT,
channel BIGINT UNSIGNED UNIQUE NOT NULL, channel BIGINT UNSIGNED UNIQUE NOT NULL,
@ -39,10 +35,10 @@ CREATE TABLE reminders.channels (
guild_id INT UNSIGNED, guild_id INT UNSIGNED,
PRIMARY KEY (id), PRIMARY KEY (id),
FOREIGN KEY (guild_id) REFERENCES reminders.guilds(id) ON DELETE CASCADE FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE
); );
CREATE TABLE reminders.users ( CREATE TABLE users (
id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
user BIGINT UNSIGNED UNIQUE NOT NULL, user BIGINT UNSIGNED UNIQUE NOT NULL,
@ -59,10 +55,10 @@ CREATE TABLE reminders.users (
patreon BOOLEAN NOT NULL DEFAULT 0, patreon BOOLEAN NOT NULL DEFAULT 0,
PRIMARY KEY (id), PRIMARY KEY (id),
FOREIGN KEY (dm_channel) REFERENCES reminders.channels(id) ON DELETE RESTRICT FOREIGN KEY (dm_channel) REFERENCES channels(id) ON DELETE RESTRICT
); );
CREATE TABLE reminders.roles ( CREATE TABLE roles (
id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT, id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT,
role BIGINT UNSIGNED UNIQUE NOT NULL, role BIGINT UNSIGNED UNIQUE NOT NULL,
@ -71,10 +67,10 @@ CREATE TABLE reminders.roles (
guild_id INT UNSIGNED NOT NULL, guild_id INT UNSIGNED NOT NULL,
PRIMARY KEY (id), PRIMARY KEY (id),
FOREIGN KEY (guild_id) REFERENCES reminders.guilds(id) ON DELETE CASCADE FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE
); );
CREATE TABLE reminders.embeds ( CREATE TABLE embeds (
id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
title VARCHAR(256) NOT NULL DEFAULT '', title VARCHAR(256) NOT NULL DEFAULT '',
@ -91,7 +87,7 @@ CREATE TABLE reminders.embeds (
PRIMARY KEY (id) PRIMARY KEY (id)
); );
CREATE TABLE reminders.embed_fields ( CREATE TABLE embed_fields (
id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
title VARCHAR(256) NOT NULL DEFAULT '', title VARCHAR(256) NOT NULL DEFAULT '',
@ -100,10 +96,10 @@ CREATE TABLE reminders.embed_fields (
embed_id INT UNSIGNED NOT NULL, embed_id INT UNSIGNED NOT NULL,
PRIMARY KEY (id), PRIMARY KEY (id),
FOREIGN KEY (embed_id) REFERENCES reminders.embeds(id) ON DELETE CASCADE FOREIGN KEY (embed_id) REFERENCES embeds(id) ON DELETE CASCADE
); );
CREATE TABLE reminders.messages ( CREATE TABLE messages (
id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
content VARCHAR(2048) NOT NULL DEFAULT '', content VARCHAR(2048) NOT NULL DEFAULT '',
@ -114,10 +110,10 @@ CREATE TABLE reminders.messages (
attachment_name VARCHAR(260), attachment_name VARCHAR(260),
PRIMARY KEY (id), PRIMARY KEY (id),
FOREIGN KEY (embed_id) REFERENCES reminders.embeds(id) ON DELETE SET NULL FOREIGN KEY (embed_id) REFERENCES embeds(id) ON DELETE SET NULL
); );
CREATE TABLE reminders.reminders ( CREATE TABLE reminders (
id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
uid VARCHAR(64) UNIQUE NOT NULL, uid VARCHAR(64) UNIQUE NOT NULL,
@ -140,20 +136,20 @@ CREATE TABLE reminders.reminders (
set_by INT UNSIGNED, set_by INT UNSIGNED,
PRIMARY KEY (id), PRIMARY KEY (id),
FOREIGN KEY (message_id) REFERENCES reminders.messages(id) ON DELETE RESTRICT, FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE RESTRICT,
FOREIGN KEY (channel_id) REFERENCES reminders.channels(id) ON DELETE CASCADE, FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE,
FOREIGN KEY (set_by) REFERENCES reminders.users(id) ON DELETE SET NULL FOREIGN KEY (set_by) REFERENCES users(id) ON DELETE SET NULL
); );
CREATE TRIGGER message_cleanup AFTER DELETE ON reminders.reminders CREATE TRIGGER message_cleanup AFTER DELETE ON reminders
FOR EACH ROW FOR EACH ROW
DELETE FROM reminders.messages WHERE id = OLD.message_id; DELETE FROM messages WHERE id = OLD.message_id;
CREATE TRIGGER embed_cleanup AFTER DELETE ON reminders.messages CREATE TRIGGER embed_cleanup AFTER DELETE ON messages
FOR EACH ROW FOR EACH ROW
DELETE FROM reminders.embeds WHERE id = OLD.embed_id; DELETE FROM embeds WHERE id = OLD.embed_id;
CREATE TABLE reminders.todos ( CREATE TABLE todos (
id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
user_id INT UNSIGNED, user_id INT UNSIGNED,
guild_id INT UNSIGNED, guild_id INT UNSIGNED,
@ -161,23 +157,23 @@ CREATE TABLE reminders.todos (
value VARCHAR(2000) NOT NULL, value VARCHAR(2000) NOT NULL,
PRIMARY KEY (id), PRIMARY KEY (id),
FOREIGN KEY (user_id) REFERENCES reminders.users(id) ON DELETE SET NULL, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
FOREIGN KEY (guild_id) REFERENCES reminders.guilds(id) ON DELETE CASCADE, FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE,
FOREIGN KEY (channel_id) REFERENCES reminders.channels(id) ON DELETE SET NULL FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE SET NULL
); );
CREATE TABLE reminders.command_restrictions ( CREATE TABLE command_restrictions (
id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
role_id INT UNSIGNED NOT NULL, role_id INT UNSIGNED NOT NULL,
command ENUM('todos', 'natural', 'remind', 'interval', 'timer', 'del', 'look', 'alias', 'countdown') NOT NULL, command ENUM('todos', 'natural', 'remind', 'interval', 'timer', 'del', 'look', 'alias', 'countdown') NOT NULL,
PRIMARY KEY (id), PRIMARY KEY (id),
FOREIGN KEY (role_id) REFERENCES reminders.roles(id) ON DELETE CASCADE, FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
UNIQUE KEY (`role_id`, `command`) UNIQUE KEY (`role_id`, `command`)
); );
CREATE TABLE reminders.timers ( CREATE TABLE timers (
id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
start_time TIMESTAMP NOT NULL DEFAULT NOW(), start_time TIMESTAMP NOT NULL DEFAULT NOW(),
name VARCHAR(32) NOT NULL, name VARCHAR(32) NOT NULL,
@ -186,7 +182,7 @@ CREATE TABLE reminders.timers (
PRIMARY KEY (id) PRIMARY KEY (id)
); );
CREATE TABLE reminders.events ( CREATE TABLE events (
id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
`time` TIMESTAMP NOT NULL DEFAULT NOW(), `time` TIMESTAMP NOT NULL DEFAULT NOW(),
@ -198,12 +194,12 @@ CREATE TABLE reminders.events (
reminder_id INT UNSIGNED, reminder_id INT UNSIGNED,
PRIMARY KEY (id), PRIMARY KEY (id),
FOREIGN KEY (guild_id) REFERENCES reminders.guilds(id) ON DELETE CASCADE, FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES reminders.users(id) ON DELETE SET NULL, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
FOREIGN KEY (reminder_id) REFERENCES reminders.reminders(id) ON DELETE SET NULL FOREIGN KEY (reminder_id) REFERENCES reminders(id) ON DELETE SET NULL
); );
CREATE TABLE reminders.command_aliases ( CREATE TABLE command_aliases (
id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
guild_id INT UNSIGNED NOT NULL, guild_id INT UNSIGNED NOT NULL,
@ -212,22 +208,22 @@ CREATE TABLE reminders.command_aliases (
command VARCHAR(2048) NOT NULL, command VARCHAR(2048) NOT NULL,
PRIMARY KEY (id), PRIMARY KEY (id),
FOREIGN KEY (guild_id) REFERENCES reminders.guilds(id) ON DELETE CASCADE, FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE,
UNIQUE KEY (`guild_id`, `name`) UNIQUE KEY (`guild_id`, `name`)
); );
CREATE TABLE reminders.guild_users ( CREATE TABLE guild_users (
guild INT UNSIGNED NOT NULL, guild INT UNSIGNED NOT NULL,
user INT UNSIGNED NOT NULL, user INT UNSIGNED NOT NULL,
can_access BOOL NOT NULL DEFAULT 0, can_access BOOL NOT NULL DEFAULT 0,
FOREIGN KEY (guild) REFERENCES reminders.guilds(id) ON DELETE CASCADE, FOREIGN KEY (guild) REFERENCES guilds(id) ON DELETE CASCADE,
FOREIGN KEY (user) REFERENCES reminders.users(id) ON DELETE CASCADE, FOREIGN KEY (user) REFERENCES users(id) ON DELETE CASCADE,
UNIQUE KEY (guild, user) UNIQUE KEY (guild, user)
); );
CREATE EVENT reminders.event_cleanup CREATE EVENT event_cleanup
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 DAY ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 DAY
ON COMPLETION PRESERVE ON COMPLETION PRESERVE
DO DELETE FROM reminders.events WHERE `time` < DATE_SUB(NOW(), INTERVAL 5 DAY); DO DELETE FROM events WHERE `time` < DATE_SUB(NOW(), INTERVAL 5 DAY);

View File

@ -1,5 +1,3 @@
USE reminders;
SET FOREIGN_KEY_CHECKS = 0; SET FOREIGN_KEY_CHECKS = 0;
DROP TABLE IF EXISTS reminders_new; DROP TABLE IF EXISTS reminders_new;
@ -56,8 +54,7 @@ CREATE TABLE reminders_new (
-- , CONSTRAINT interval_enabled_mutin CHECK (`enabled` = 1 OR `interval` IS NULL) -- , CONSTRAINT interval_enabled_mutin CHECK (`enabled` = 1 OR `interval` IS NULL)
# disallow an expiry time if interval is unspecified # disallow an expiry time if interval is unspecified
-- , CONSTRAINT interval_expires_mutin CHECK (`expires` IS NULL OR `interval` IS NOT NULL) -- , CONSTRAINT interval_expires_mutin CHECK (`expires` IS NULL OR `interval` IS NOT NULL)
) );
COLLATE utf8mb4_unicode_ci;
# import data from other tables # import data from other tables
INSERT INTO reminders_new ( INSERT INTO reminders_new (
@ -158,4 +155,9 @@ CREATE TABLE events (
FOREIGN KEY (reminder_id) REFERENCES reminders_new(id) ON DELETE SET NULL FOREIGN KEY (reminder_id) REFERENCES reminders_new(id) ON DELETE SET NULL
); );
DROP TABLE reminders;
DROP TABLE embed_fields;
RENAME TABLE reminders_new TO reminders;
RENAME TABLE embed_fields_new TO embed_fields;
SET FOREIGN_KEY_CHECKS = 1; SET FOREIGN_KEY_CHECKS = 1;

View File

@ -0,0 +1,11 @@
CREATE TABLE macro (
id INT UNSIGNED AUTO_INCREMENT,
guild_id INT UNSIGNED NOT NULL,
name VARCHAR(100) NOT NULL,
description VARCHAR(100),
commands TEXT NOT NULL,
FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE,
PRIMARY KEY (id)
);

View File

@ -0,0 +1,2 @@
ALTER TABLE reminders RENAME COLUMN `interval` TO `interval_seconds`;
ALTER TABLE reminders ADD COLUMN `interval_months` INT UNSIGNED DEFAULT NULL;

View File

@ -0,0 +1,49 @@
CREATE TABLE reminder_template (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`name` VARCHAR(24) NOT NULL DEFAULT 'Reminder',
`guild_id` INT UNSIGNED NOT NULL,
`username` VARCHAR(32) DEFAULT NULL,
`avatar` VARCHAR(512) DEFAULT NULL,
`content` VARCHAR(2048) NOT NULL DEFAULT '',
`tts` BOOL NOT NULL DEFAULT 0,
`attachment` MEDIUMBLOB,
`attachment_name` VARCHAR(260),
`embed_title` VARCHAR(256) NOT NULL DEFAULT '',
`embed_description` VARCHAR(2048) NOT NULL DEFAULT '',
`embed_image_url` VARCHAR(512),
`embed_thumbnail_url` VARCHAR(512),
`embed_footer` VARCHAR(2048) NOT NULL DEFAULT '',
`embed_footer_url` VARCHAR(512),
`embed_author` VARCHAR(256) NOT NULL DEFAULT '',
`embed_author_url` VARCHAR(512),
`embed_color` INT UNSIGNED NOT NULL DEFAULT 0x0,
`embed_fields` JSON,
PRIMARY KEY (id),
FOREIGN KEY (`guild_id`) REFERENCES guilds (`id`) ON DELETE CASCADE
);
ALTER TABLE reminders ADD COLUMN embed_fields JSON;
update reminders
inner join embed_fields as E
on E.reminder_id = reminders.id
set embed_fields = (
select JSON_ARRAYAGG(
JSON_OBJECT(
'title', E.title,
'value', E.value,
'inline',
if(inline = 1, cast(TRUE as json), cast(FALSE as json))
)
)
from embed_fields
group by reminder_id
having reminder_id = reminders.id
);

View File

@ -0,0 +1 @@
ALTER TABLE reminders ADD COLUMN `interval_days` INT UNSIGNED DEFAULT NULL;

View File

@ -0,0 +1 @@
ALTER TABLE reminders ADD COLUMN `thread_id` BIGINT DEFAULT NULL;

View File

@ -0,0 +1 @@
ALTER TABLE guilds ADD COLUMN ephemeral_confirmations BOOL NOT NULL DEFAULT 0;

View File

@ -0,0 +1,2 @@
ALTER TABLE reminders MODIFY `name` VARCHAR(100) NOT NULL DEFAULT 'Reminder';
ALTER TABLE reminder_template MODIFY `name` VARCHAR(100) NOT NULL DEFAULT 'Reminder';

View File

@ -0,0 +1,9 @@
CREATE TABLE stat (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`utc_time` DATETIME NOT NULL DEFAULT NOW(),
`type` ENUM('reminder_sent', 'reminder_failed'),
`reminder_id` INT UNSIGNED,
`message` TEXT,
PRIMARY KEY (`id`)
);

View File

@ -0,0 +1,2 @@
ALTER TABLE reminders ADD COLUMN `status` ENUM ('pending', 'sent', 'failed', 'deleted') NOT NULL DEFAULT 'pending';
ALTER TABLE reminders ADD COLUMN `status_message` TEXT;

View File

@ -0,0 +1,3 @@
ALTER TABLE `reminder_template` ADD COLUMN `interval_seconds` INT UNSIGNED;
ALTER TABLE `reminder_template` ADD COLUMN `interval_days` INT UNSIGNED;
ALTER TABLE `reminder_template` ADD COLUMN `interval_months` INT UNSIGNED;

View File

@ -0,0 +1,50 @@
CREATE TABLE command_macro (
id INT UNSIGNED AUTO_INCREMENT,
guild_id INT UNSIGNED NOT NULL,
name VARCHAR(100) NOT NULL,
description VARCHAR(100),
commands JSON NOT NULL,
FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE,
PRIMARY KEY (id)
);
# New JSON structure is {command_name: "Remind", "<option name>": "<option value>", ...}
INSERT INTO command_macro (guild_id, description, name, commands)
SELECT
guild_id,
description,
name,
(
SELECT JSON_ARRAYAGG(
(
SELECT JSON_OBJECTAGG(t2.name, t2.value)
FROM JSON_TABLE(
JSON_ARRAY_APPEND(t1.options, '$', JSON_OBJECT('name', 'command_name', 'value', t1.command_name)),
'$[*]' COLUMNS (
name VARCHAR(64) PATH '$.name' ERROR ON ERROR,
value TEXT PATH '$.value' ERROR ON ERROR
)) AS t2
)
)
FROM macro m2
JOIN JSON_TABLE(
commands,
'$[*]' COLUMNS (
command_name VARCHAR(64) PATH '$.command_name' ERROR ON ERROR,
options JSON PATH '$.options' ERROR ON ERROR
)) AS t1
WHERE m1.id = m2.id
)
FROM macro m1;
# # Check which commands are used in macros
# SELECT DISTINCT command_name
# FROM macro m2
# JOIN JSON_TABLE(
# commands,
# '$[*]' COLUMNS (
# command_name VARCHAR(64) PATH '$.command_name' ERROR ON ERROR,
# options JSON PATH '$.options' ERROR ON ERROR
# )) AS t1

View File

@ -0,0 +1,5 @@
-- Add migration script here
ALTER TABLE reminders
ADD INDEX `utc_time_index` (`utc_time`);
ALTER TABLE reminders
ADD INDEX `status_index` (`status`);

53
nginx/reminder-bot Normal file
View File

@ -0,0 +1,53 @@
server {
server_name www.reminder-bot.com;
return 301 https://reminder-bot.com$request_uri;
}
server {
listen 80;
server_name beta.reminder-bot.com;
return 301 https://reminder-bot.com$request_uri;
}
server {
listen 443 ssl;
server_name beta.reminder-bot.com;
ssl_certificate /etc/letsencrypt/live/beta.reminder-bot.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/beta.reminder-bot.com/privkey.pem;
return 301 https://reminder-bot.com$request_uri;
}
server {
listen 443 ssl;
server_name reminder-bot.com;
ssl_certificate /etc/letsencrypt/live/reminder-bot.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/reminder-bot.com/privkey.pem;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
client_max_body_size 10M;
location / {
proxy_pass http://localhost:18920;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /static {
alias /var/www/reminder-rs/static;
expires 30d;
}
}

32
private/ca_cert.pem Normal file
View File

@ -0,0 +1,32 @@
-----BEGIN CERTIFICATE-----
MIIFbzCCA1egAwIBAgIUUY2fYP5h41dtqcuNLVUS99N7+jAwDQYJKoZIhvcNAQEL
BQAwRzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQKDAlSb2NrZXQg
Q0ExFzAVBgNVBAMMDlJvY2tldCBSb290IENBMB4XDTIxMDUxNzE5MDIyNFoXDTMx
MDUxNTE5MDIyNFowRzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQK
DAlSb2NrZXQgQ0ExFzAVBgNVBAMMDlJvY2tldCBSb290IENBMIICIjANBgkqhkiG
9w0BAQEFAAOCAg8AMIICCgKCAgEA3MmVOUxbZ0rtBBbXjWkkM2n2IoPuqfiENdrM
NH9IcIbEgFb7AQ/c2vGBLWWw2RFtXxe2ubbgcHyAySQ+ENGEf+W2yox5JpcOOBP+
/KrUQ4ROqxLBWOD6+fA/q5m9/CXXJosWXly3DFE9nUEUlNXcaSxJm/bxIqnxwEMQ
NESWdG1qlsGVhHLi5sflgwAcYH+nZzdtINLyvQ5AGIs8s6LpmvUVbC3I+5oNNSwW
rRsIfCFaYLuUx7jZMaOk8Dfz8MbFL68+7OCGwmhduSHoGW8jVzDXwXv/crAmFtau
zmu43/vPDwKa4w/kQJLNr1QmiGz9FBt8DXVJ0s5bokOIlodEgAvjsL3xwnrKak0F
8ak74IBetfDdVvuC9WWmJ9/dj3z91KvCWLdZGm8ijlYZ2OuaiiJ/oC6iebaPFHEY
IQmF5wzT/nZLIbQsti9wFnOpptgkhDAcKIAk3+pncuMwZ9WRvWO2IFt+ZCqKCCDU
JliDZmm3ow/zob8DD6Txex5OkJGUF6iu8o7fjI9BxsjSc76bLrxIhvR43UXjzoHl
t3lvDH8SzSN/kxzQMXVl48h3xydJ3+ZyDOLle1bqMga8Kcin9GcEUqmL1JcyFafe
CUE/Nk1pHyrlnhpMCYIDVI3pmNLB0/m/IEWLckSr9+hXyUPJQyWiyrixwC86SPxZ
AonU2aUCAwEAAaNTMFEwHQYDVR0OBBYEFJ5uQa9kD5fioNeS/Ff/mxDcMAzhMB8G
A1UdIwQYMBaAFJ5uQa9kD5fioNeS/Ff/mxDcMAzhMA8GA1UdEwEB/wQFMAMBAf8w
DQYJKoZIhvcNAQELBQADggIBABf7adF1J40/8EjfSJ5XdG7OBUrZTas3+nuULh8B
6h1igAq0BgX9v3awmPCaxqDLqwJCsFZCk0s9Vgd62McKVKasyW1TQchWbdvlqeAB
QQfZpJtAZK12dcMl6iznC/JBTPvYl9q1FKS8kYtg19in/xlhxiiEJaL5z+slpTgT
cN12650W2FqjT9sD9jshZI/a8o53d2yUBXBBecCZ49djceBGLbxbteEi/sb9qM4f
IUxeSrvK6owxKMZ5okUqZWaFseFCkdMKpMg9eecyfSTweac8yLlZmeqX5D/H85Hr
hnWHjI5NeVZAoDkdmNeaDbAIv4f3prqC8uFP3ub8D0rG/WiPdOAd79KNgqbUUFyp
NbjSiAesx4Rm1WIgjHljFQKXJdtnxt+v1VkKV9entXJX2tye7+Z1+zurNYyUyM1J
COdXeV4jpq2mP8cLpAqX7ip1tNx9YMy5ctbAE1WsUw7lPyO91+VN2Q6MN5OyemY3
4nBzRJU8X1nsDgeNQWjzb/ZC7eoS916Gywk8Hu6GoIwvYMm3zea25n6mAAOX17vE
1YdvSzUlnVbwnbgd4BgFRGUfBVjO7qvFC7ZWLngWhBzIIYIGUJfO2rippgkxAoHH
dgWYk1MNnk2yM8WSo0VKoe4yuF9NwPlfPqeCE683JHdwGEJKZWMocszzHrGZ2KX2
I4/u
-----END CERTIFICATE-----

51
private/ca_key.pem Normal file
View File

@ -0,0 +1,51 @@
-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----

View File

@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDYTCCAUmgAwIBAgIUA/EaCcXKgOxBiuaMNTackeF9DgcwDQYJKoZIhvcNAQEL
BQAwRzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQKDAlSb2NrZXQg
Q0ExFzAVBgNVBAMMDlJvY2tldCBSb290IENBMB4XDTIxMDUxNzE5MDIyNFoXDTMx
MDUxNTE5MDIyNFowPzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQ8wDQYDVQQK
DAZSb2NrZXQxEjAQBgNVBAMMCWxvY2FsaG9zdDBZMBMGByqGSM49AgEGCCqGSM49
AwEHA0IABOEVdmVd218y3Yy+ocjtt5siaFsNR7tknvS21pzKzxsFc05UrF74YqRx
Ic6/AQq56C48x6gDhUCdf+nMzD0NWTijGDAWMBQGA1UdEQQNMAuCCWxvY2FsaG9z
dDANBgkqhkiG9w0BAQsFAAOCAgEAW32kadHWEIsN5WXacjtgE9jbFii/rWJjrAO/
GWuARwLvjWeEFM5xSTny1cTDEz/7Bmfd9GRRpfgxKCFBGaU7zTmOV/AokrjUay+s
KPj0EV9ZE/84KVfr+MOuhwXhSSv+NvxduFw1sdhTUHs+Wopwjte+bnOUBXmnQH97
ITSQuUvEamPUS4tJD/kQ0qSGJKNv3CShhbe3XkjUd0UKK7lZzn6hY2fy3CrkkJDT
GyzFCRf3ZDfjW5/fGXN/TNoEK65pPQVr5taK/bovDesEO7rR4Y4YgtnGlJ7YToWh
E6I77CbsJCJdmgpjPNwRlwQ8upoWAfxOHqwg32s6uWq20v4TXZTOnEKOPEJG74bh
JHtwqsy2fIdGz4kZksKGerzJffyQPhX4f3qxxrijT9Hj2EoFIQ4u/jTRpK31IW3R
gEeNB8O4iyg3crx0oHRwc6zX63O6wBJCZ+0uxLoyjwtjKCxVYbJpccjoQh6zO3UO
pqAO+NKxQ469QDBV3uRyyQ7DRPu6p67/8gn1E/o8kyU1P7eLFIhBp4T4wCaMQBG6
IpL5W7eCyuOcqfdm471N07Z4KAqiV8eBJcKaAE+WzNIvrFvCZ/zq7Gj8p07nkjK8
+ZGDUieiY0mQEpiXLQZt1xNtT/MmlAzFYrK7t6gSygykQKjO1o4GG4g+eXu3h1YK
avsOwtc=
-----END CERTIFICATE-----

View File

@ -0,0 +1,5 @@
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgeo/7wBIYVax9bj4m
1UEe2H+H4YLsbApDCZMslZbubRuhRANCAAThFXZlXdtfMt2MvqHI7bebImhbDUe7
ZJ70ttacys8bBXNOVKxe+GKkcSHOvwEKueguPMeoA4VAnX/pzMw9DVk4
-----END PRIVATE KEY-----

View File

@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDfjCCAWagAwIBAgIUfmkM99JpQUsQsh8TVILPQDBGOhowDQYJKoZIhvcNAQEM
BQAwRzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQKDAlSb2NrZXQg
Q0ExFzAVBgNVBAMMDlJvY2tldCBSb290IENBMB4XDTIxMDUxNzE5MDIyNFoXDTMx
MDUxNTE5MDIyNFowPzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQ8wDQYDVQQK
DAZSb2NrZXQxEjAQBgNVBAMMCWxvY2FsaG9zdDB2MBAGByqGSM49AgEGBSuBBAAi
A2IABM5rQQNZEGWeDyrg2dVQS15c+JsX/ginN9/ArHpTgGO6xaLfkbekIj7gewUR
VQcQrYulwu02HTFDzGVvf1DdCZzIJLJUpl+jagdI05yMPFg2s5lThzGB6HENyw1I
hU1dMaMYMBYwFAYDVR0RBA0wC4IJbG9jYWxob3N0MA0GCSqGSIb3DQEBDAUAA4IC
AQAzpXFEeCMvTBG+kzmHN/+esjCK8MO/Grrw3Qoa1stX0yjNWzfhlGHfWk9Mgwnp
DnIEFT9lB+nT9uTkKL81Opu2bkWXuL0MyjyYmcCfEgJeDblUbD4HYzPO/s2P7QZu
Oa1pNKBiSWe1xq+F/gkn0+nDfICq3QQOOQMzaAmlFmszN3v7pryz+x21MqTFsfuW
ymycRcwZ3VyYeceZuBxjOhR2l8I5yZbR+KPj8VS7A3vgqvkS8ixTK0LrxcLwrTIz
W9Z1skzVpux4K7iwn3qlWIJ7EbERi9Ydz0MzpjD+CHxGlgHTzT552DGZhPO8D7BE
+4IoS0YeXEGLeC2a9Hmiy3FXbM4nt3aIWMiWyhjslXIbvWOJL1Vi5GNpdQCG+rB7
lvA0IISp/tAi9duufdONjgRceRDsqf7o6TOBQdB3QxHRt9gCB2QquRh5llMUY8YH
PxFJAEzF4X5b0q0k4b50HRVNfDY0SC/XIR7S2xnLDGuoUqrOpUligBzfXV4JHrPv
YKHsWSOiVxU9eY1SYKuKqnOsGvXSSQlYqSK1ol94eOKDjNy8wekaVYAQNsdNL7o5
QSWZUcbZLkaNMFdD9twWGduAs0eTichVgyvRgPdevjTGDPBHKNSUURZRTYeW4aIJ
QzSQUQXwOXsQOmfCkaaTISsDwZAJ0wFqqST1AOhlCWD1OQ==
-----END CERTIFICATE-----

View File

@ -0,0 +1,6 @@
-----BEGIN PRIVATE KEY-----
MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDCRBt7MeJXyJRq7grGQ
jEdBHE31hG5LuKQ5iL8yTVGk75Kc8ISll2LewbvQ3NhR6E+hZANiAATOa0EDWRBl
ng8q4NnVUEteXPibF/4IpzffwKx6U4BjusWi35G3pCI+4HsFEVUHEK2LpcLtNh0x
Q8xlb39Q3QmcyCSyVKZfo2oHSNOcjDxYNrOZU4cxgehxDcsNSIVNXTE=
-----END PRIVATE KEY-----

20
private/ed25519_cert.pem Normal file
View File

@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDMjCCARqgAwIBAgIUSu1CGfaWlNPjjLG7SaEEgxI+AsAwDQYJKoZIhvcNAQEL
BQAwRzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQKDAlSb2NrZXQg
Q0ExFzAVBgNVBAMMDlJvY2tldCBSb290IENBMB4XDTIxMDUxNzE5MDIyNFoXDTMx
MDUxNTE5MDIyNFowPzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQ8wDQYDVQQK
DAZSb2NrZXQxEjAQBgNVBAMMCWxvY2FsaG9zdDAqMAUGAytlcAMhAKNv1fkv5rxY
xGT84C98g2xPpain+jsliHvYrqNkS1zhoxgwFjAUBgNVHREEDTALgglsb2NhbGhv
c3QwDQYJKoZIhvcNAQELBQADggIBAMgWZhr3whYbFPNYm5RZxjgEonMY7cH/Rza1
UrBkyoMRL9v00YKJTEvjl5Xl4ku7jqxY2qjValacDroD8hYTLhhkkbHxH6u+kJSC
cKRQ8QVBZMmoor8qD2Y2BuXZYGWEtgeoXh+DLHWOim7gXDD6GyFPnYFGHnRhNmkE
6fPcfw1FZmBrMM9rk31EeK9nLVRabL+vuLDEddX1LH4LwK4OuV51wQMZGYiC3M2b
JpmuPAUAUyiyN53Utuw/ubeih3cEglOwCyDPFt6XISYHO0UaQdw25uhpjxs8KTZB
qOPJAI4cvGaN3GARXvz2P/QHUiTzsf2XOXXtrO0g+F9P7t6x2xL35c0A8yxKkfsa
RKdCveRuVlsLXVLVBikqLE/OgPC6SIhIFvTYzXTaZyNfge6dRy2Wg6qWPcGwseeA
QpFKgWiY3cuy7nJm6uybR6zOEMj5GgNWIbICGguQ5161MLnv0bEmKh2d9/jQ/XN5
M+nixtGla/G4hL3FQIy9rhHVZzPWwSxl+/eE4qgnInEhmlQ2KrcpgneCt38t0vjJ
dwHZSzVv8yX7fE0OSq/DWQXJraWUcT7Ds0g7T7jWkWfS0qStVd9VJ+rYQ8dBbE9Y
gcmkm1DMUd+y9xDRDjxby7gMDWFiN2Au9FQgaIpIctTNCj0+agFBPnfXPVSIH6gX
10kA2ZVX
-----END CERTIFICATE-----

3
private/ed25519_key.pem Normal file
View File

@ -0,0 +1,3 @@
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIGtLkaYE4D+P5HLkoc3a/tNhPP+gnhPLF3jEtdYcAtpd
-----END PRIVATE KEY-----

114
private/gen_certs.sh Normal file
View File

@ -0,0 +1,114 @@
#! /bin/bash
# Usage:
# ./gen_certs.sh [cert-kind]
#
# [cert-kind]:
# ed25519
# rsa_sha256
# ecdsa_nistp256_sha256
# ecdsa_nistp384_sha384
#
# Generate a certificate of the [cert-kind] key type, or if no cert-kind is
# specified, all of the certificates.
#
# Examples:
# ./gen_certs.sh ed25519
# ./gen_certs.sh rsa_sha256
# TODO: `rustls` (really, `webpki`) doesn't currently use the CN in the subject
# to check if a certificate is valid for a server name sent via SNI. It's not
# clear if this is intended, since certificates _should_ have a `subjectAltName`
# with a DNS name, or if it simply hasn't been implemented yet. See
# https://bugzilla.mozilla.org/show_bug.cgi?id=552346 for a bit more info.
CA_SUBJECT="/C=US/ST=CA/O=Rocket CA/CN=Rocket Root CA"
SUBJECT="/C=US/ST=CA/O=Rocket/CN=localhost"
ALT="DNS:localhost"
function gen_ca() {
openssl genrsa -out ca_key.pem 4096
openssl req -new -x509 -days 3650 -key ca_key.pem \
-subj "${CA_SUBJECT}" -out ca_cert.pem
}
function gen_ca_if_non_existent() {
if ! [ -f ./ca_cert.pem ]; then gen_ca; fi
}
function gen_rsa_sha256() {
gen_ca_if_non_existent
openssl req -newkey rsa:4096 -nodes -sha256 -keyout rsa_sha256_key.pem \
-subj "${SUBJECT}" -out server.csr
openssl x509 -req -sha256 -extfile <(printf "subjectAltName=${ALT}") -days 3650 \
-CA ca_cert.pem -CAkey ca_key.pem -CAcreateserial \
-in server.csr -out rsa_sha256_cert.pem
rm ca_cert.srl server.csr
}
function gen_ed25519() {
gen_ca_if_non_existent
openssl genpkey -algorithm ED25519 > ed25519_key.pem
openssl req -new -key ed25519_key.pem -subj "${SUBJECT}" -out server.csr
openssl x509 -req -extfile <(printf "subjectAltName=${ALT}") -days 3650 \
-CA ca_cert.pem -CAkey ca_key.pem -CAcreateserial \
-in server.csr -out ed25519_cert.pem
rm ca_cert.srl server.csr
}
function gen_ecdsa_nistp256_sha256() {
gen_ca_if_non_existent
openssl ecparam -out ecdsa_nistp256_sha256_key.pem -name prime256v1 -genkey
# Convert to pkcs8 format supported by rustls
openssl pkcs8 -topk8 -nocrypt -in ecdsa_nistp256_sha256_key.pem \
-out ecdsa_nistp256_sha256_key_pkcs8.pem
openssl req -new -nodes -sha256 -key ecdsa_nistp256_sha256_key_pkcs8.pem \
-subj "${SUBJECT}" -out server.csr
openssl x509 -req -sha256 -extfile <(printf "subjectAltName=${ALT}") -days 3650 \
-CA ca_cert.pem -CAkey ca_key.pem -CAcreateserial \
-in server.csr -out ecdsa_nistp256_sha256_cert.pem
rm ca_cert.srl server.csr ecdsa_nistp256_sha256_key.pem
}
function gen_ecdsa_nistp384_sha384() {
gen_ca_if_non_existent
openssl ecparam -out ecdsa_nistp384_sha384_key.pem -name secp384r1 -genkey
# Convert to pkcs8 format supported by rustls
openssl pkcs8 -topk8 -nocrypt -in ecdsa_nistp384_sha384_key.pem \
-out ecdsa_nistp384_sha384_key_pkcs8.pem
openssl req -new -nodes -sha384 -key ecdsa_nistp384_sha384_key_pkcs8.pem \
-subj "${SUBJECT}" -out server.csr
openssl x509 -req -sha384 -extfile <(printf "subjectAltName=${ALT}") -days 3650 \
-CA ca_cert.pem -CAkey ca_key.pem -CAcreateserial \
-in server.csr -out ecdsa_nistp384_sha384_cert.pem
rm ca_cert.srl server.csr ecdsa_nistp384_sha384_key.pem
}
case $1 in
ed25519) gen_ed25519 ;;
rsa_sha256) gen_rsa_sha256 ;;
ecdsa_nistp256_sha256) gen_ecdsa_nistp256_sha256 ;;
ecdsa_nistp384_sha384) gen_ecdsa_nistp384_sha384 ;;
*)
gen_ed25519
gen_rsa_sha256
gen_ecdsa_nistp256_sha256
gen_ecdsa_nistp384_sha384
;;
esac

View File

@ -0,0 +1,30 @@
-----BEGIN CERTIFICATE-----
MIIFLDCCAxSgAwIBAgIUZ461KXDgTT25LP1S6PK6i9ZKtOYwDQYJKoZIhvcNAQEL
BQAwRzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQKDAlSb2NrZXQg
Q0ExFzAVBgNVBAMMDlJvY2tldCBSb290IENBMB4XDTIxMDUxNzE5MDIyNFoXDTMx
MDUxNTE5MDIyNFowPzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQ8wDQYDVQQK
DAZSb2NrZXQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCAiIwDQYJKoZIhvcNAQEBBQAD
ggIPADCCAgoCggIBAKl8Re1dneFB2obYPOuZf3d6BR2xGcMhr2vmHpnVgkeg/xGI
cwHhhB04mEg4TqbZ0Q1wdKN4ruNigdrQAXDqnm4y2IB1q/uTdoXVWNsvAZmDq4N4
rPUDWagpkilV4HOHDQOI27Lo/+30wJX6H8i3J6Nag1y9spuySkL1F1SgQng9uzvP
3SwbsO9R/jS7qTZNEtirnJv3qJlxmT4BCvBuWNALShFlSZYnZk/2csYXEaVkWMUE
rnWGmTmzMZEzDOdQdPC3sJDCPQbbgD4SnQANqhOVjPSdJ6Y6joN6XYtB2EP+6LJ8
UBTICETnUROWeKqMm215Gsen32cyWkaCwEWtTFn7rJHbPvUY4vPYQZAB2/h07lYq
v67W3r5lTq1KuoStD8M/7nCIYIuNhmJLMzzWrSfJrQpYexoMII6kjOxv2kiUgb1y
bYKnFlVf4up3pTpi2/0We4SHRgc7xTcEPNvU5z5hc5JwHZIFjmi5dx50ZoAULrXl
OUhfdUPut7H38UQg3riJiNAzedUiTubek26po8qH1iS/EMgAi5RboaBovjWhgjwq
P/RP0vzpln6OdQIRaRlY9vZiik9HbFybafuA2zEkyc+zc4HqJk3vqX/0hgd9qWeL
zgsHr6A86tNUXsUaoInQbzznjpbFE85klxd6HeqasOyVDCeDUs4lWoDNp0DbAgMB
AAGjGDAWMBQGA1UdEQQNMAuCCWxvY2FsaG9zdDANBgkqhkiG9w0BAQsFAAOCAgEA
sS2TUKKYtNYC68TEQb5KAa8/zi6Lhz9lyn9rpBXAcAVeBdxCqrBU0nRN8EjXEff1
oBeau9Wi9PwdK6J+4xFuer9YrZZWuWLKUBXJL9ZXEfI+USo5WVz7v/puoKZSUSu2
+Ee4+v1eoAsCtaA8IR0Sh1KTc9kg1QZW1nIdBV0zgAERWTzm1iy2Ff+euowM/lUR
FczMAJsNq83O2kaH541fRx3gC2iMN/QLwRalBR2y8hTI5wQa0Yr8moC4IsTfRrKQ
/SZDjFT1XoA9TnrByIvGQNlxK1Rm2hvK1OQn4OL6sdYK6F+MxfUn/atQNyBQ6CF+
oKuvOnE2jces8GGZIU1jYJ2271SQkzp0CW3N1ZKjClSQFAYED+5ERTGyeEL4o3Vr
V/BUd0KC7HhbWBlFc2EDmPOoOTp/ID901Kf499M68pe2Fr/63/nCAON6WFk1NPYA
+sWMfS25hikU5rOHGQgXxXAz90pwpvpoLQvaq0/azsbHvq9B4ubdcLE0xcoK+DGq
+/aOeWlYkalWp93akPYpWN3UJXzDXzzagRID/1LEGS+Ssbh1WVhAhLKVoxzBvkgm
ozVAhR3zHXI2QpQ2FEbOmkcRtpxv2CiaTsgHg2MK8cdmPx3G+ufCZjRbQx/VXVdN
vaFRdmzr/5EQ7DT5Uy8w32h1to4Fm2sAtbAFYaFLXaM=
-----END CERTIFICATE-----

View File

@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
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
ce4G+zZOOYXwvWGJLwNhgsve8C3oqg==
-----END PRIVATE KEY-----

View File

@ -0,0 +1,11 @@
[package]
name = "recordable_derive"
version = "0.1.0"
edition = "2021"
[lib]
proc-macro = true
[dependencies]
quote = "1.0.35"
syn = { version = "2.0.49", features = ["full"] }

View File

@ -0,0 +1,42 @@
use proc_macro::TokenStream;
use syn::{spanned::Spanned, Data};
/// Macro to allow Recordable to be implemented on an enum by dispatching over each variant.
#[proc_macro_derive(Recordable)]
pub fn extract(input: TokenStream) -> TokenStream {
let ast = syn::parse_macro_input!(input);
impl_recordable(&ast)
}
fn impl_recordable(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
match &ast.data {
Data::Enum(en) => {
let extracted = en.variants.iter().map(|var| {
let ident = &var.ident;
quote::quote_spanned! {var.span()=>
Self::#ident (opt) => opt.run(ctx).await?
}
});
TokenStream::from(quote::quote! {
impl Recordable for #name {
async fn run(self, ctx: crate::Context<'_>) -> Result<(), crate::Error> {
match self {
#(#extracted,)*
}
Ok(())
}
}
})
}
_ => {
panic!("Only enums can derive Recordable");
}
}
}

View File

@ -1,14 +0,0 @@
[package]
name = "regex_command_attr"
version = "0.2.0"
authors = ["acdenisSK <acdenissk69@gmail.com>", "jellywx <judesouthworth@pm.me>"]
edition = "2018"
description = "Procedural macros for command creation for the RegexFramework for serenity."
[lib]
proc-macro = true
[dependencies]
quote = "^1.0"
syn = { version = "^1.0", features = ["full", "derive", "extra-traits"] }
proc-macro2 = "1.0"

View File

@ -1,293 +0,0 @@
use proc_macro2::Span;
use syn::parse::{Error, Result};
use syn::spanned::Spanned;
use syn::{Attribute, Ident, Lit, LitStr, Meta, NestedMeta, Path};
use crate::structures::PermissionLevel;
use crate::util::{AsOption, LitExt};
use std::fmt::{self, Write};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ValueKind {
// #[<name>]
Name,
// #[<name> = <value>]
Equals,
// #[<name>([<value>, <value>, <value>, ...])]
List,
// #[<name>(<value>)]
SingleList,
}
impl fmt::Display for ValueKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ValueKind::Name => f.pad("`#[<name>]`"),
ValueKind::Equals => f.pad("`#[<name> = <value>]`"),
ValueKind::List => f.pad("`#[<name>([<value>, <value>, <value>, ...])]`"),
ValueKind::SingleList => f.pad("`#[<name>(<value>)]`"),
}
}
}
fn to_ident(p: Path) -> Result<Ident> {
if p.segments.is_empty() {
return Err(Error::new(
p.span(),
"cannot convert an empty path to an identifier",
));
}
if p.segments.len() > 1 {
return Err(Error::new(
p.span(),
"the path must not have more than one segment",
));
}
if !p.segments[0].arguments.is_empty() {
return Err(Error::new(
p.span(),
"the singular path segment must not have any arguments",
));
}
Ok(p.segments[0].ident.clone())
}
#[derive(Debug)]
pub struct Values {
pub name: Ident,
pub literals: Vec<Lit>,
pub kind: ValueKind,
pub span: Span,
}
impl Values {
#[inline]
pub fn new(name: Ident, kind: ValueKind, literals: Vec<Lit>, span: Span) -> Self {
Values {
name,
literals,
kind,
span,
}
}
}
pub fn parse_values(attr: &Attribute) -> Result<Values> {
let meta = attr.parse_meta()?;
match meta {
Meta::Path(path) => {
let name = to_ident(path)?;
Ok(Values::new(name, ValueKind::Name, Vec::new(), attr.span()))
}
Meta::List(meta) => {
let name = to_ident(meta.path)?;
let nested = meta.nested;
if nested.is_empty() {
return Err(Error::new(attr.span(), "list cannot be empty"));
}
let mut lits = Vec::with_capacity(nested.len());
for meta in nested {
match meta {
NestedMeta::Lit(l) => lits.push(l),
NestedMeta::Meta(m) => match m {
Meta::Path(path) => {
let i = to_ident(path)?;
lits.push(Lit::Str(LitStr::new(&i.to_string(), i.span())))
}
Meta::List(_) | Meta::NameValue(_) => {
return Err(Error::new(attr.span(), "cannot nest a list; only accept literals and identifiers at this level"))
}
},
}
}
let kind = if lits.len() == 1 {
ValueKind::SingleList
} else {
ValueKind::List
};
Ok(Values::new(name, kind, lits, attr.span()))
}
Meta::NameValue(meta) => {
let name = to_ident(meta.path)?;
let lit = meta.lit;
Ok(Values::new(name, ValueKind::Equals, vec![lit], attr.span()))
}
}
}
#[derive(Debug, Clone)]
struct DisplaySlice<'a, T>(&'a [T]);
impl<'a, T: fmt::Display> fmt::Display for DisplaySlice<'a, T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut iter = self.0.iter().enumerate();
match iter.next() {
None => f.write_str("nothing")?,
Some((idx, elem)) => {
write!(f, "{}: {}", idx, elem)?;
for (idx, elem) in iter {
f.write_char('\n')?;
write!(f, "{}: {}", idx, elem)?;
}
}
}
Ok(())
}
}
#[inline]
fn is_form_acceptable(expect: &[ValueKind], kind: ValueKind) -> bool {
if expect.contains(&ValueKind::List) && kind == ValueKind::SingleList {
true
} else {
expect.contains(&kind)
}
}
#[inline]
fn validate(values: &Values, forms: &[ValueKind]) -> Result<()> {
if !is_form_acceptable(forms, values.kind) {
return Err(Error::new(
values.span,
// Using the `_args` version here to avoid an allocation.
format_args!(
"the attribute must be in of these forms:\n{}",
DisplaySlice(forms)
),
));
}
Ok(())
}
#[inline]
pub fn parse<T: AttributeOption>(values: Values) -> Result<T> {
T::parse(values)
}
pub trait AttributeOption: Sized {
fn parse(values: Values) -> Result<Self>;
}
impl AttributeOption for Vec<String> {
fn parse(values: Values) -> Result<Self> {
validate(&values, &[ValueKind::List])?;
Ok(values
.literals
.into_iter()
.map(|lit| lit.to_str())
.collect())
}
}
impl AttributeOption for String {
#[inline]
fn parse(values: Values) -> Result<Self> {
validate(&values, &[ValueKind::Equals, ValueKind::SingleList])?;
Ok(values.literals[0].to_str())
}
}
impl AttributeOption for bool {
#[inline]
fn parse(values: Values) -> Result<Self> {
validate(&values, &[ValueKind::Name, ValueKind::SingleList])?;
Ok(values.literals.get(0).map_or(true, |l| l.to_bool()))
}
}
impl AttributeOption for Ident {
#[inline]
fn parse(values: Values) -> Result<Self> {
validate(&values, &[ValueKind::SingleList])?;
Ok(values.literals[0].to_ident())
}
}
impl AttributeOption for Vec<Ident> {
#[inline]
fn parse(values: Values) -> Result<Self> {
validate(&values, &[ValueKind::List])?;
Ok(values.literals.into_iter().map(|l| l.to_ident()).collect())
}
}
impl AttributeOption for Option<String> {
fn parse(values: Values) -> Result<Self> {
validate(&values, &[ValueKind::Name, ValueKind::Equals, ValueKind::SingleList])?;
Ok(values.literals.get(0).map(|l| l.to_str()))
}
}
impl AttributeOption for PermissionLevel {
fn parse(values: Values) -> Result<Self> {
validate(&values, &[ValueKind::SingleList])?;
Ok(values.literals.get(0).map(|l| PermissionLevel::from_str(&*l.to_str()).unwrap()).unwrap())
}
}
impl<T: AttributeOption> AttributeOption for AsOption<T> {
#[inline]
fn parse(values: Values) -> Result<Self> {
Ok(AsOption(Some(T::parse(values)?)))
}
}
macro_rules! attr_option_num {
($($n:ty),*) => {
$(
impl AttributeOption for $n {
fn parse(values: Values) -> Result<Self> {
validate(&values, &[ValueKind::SingleList])?;
Ok(match &values.literals[0] {
Lit::Int(l) => l.base10_parse::<$n>()?,
l => {
let s = l.to_str();
// Use `as_str` to guide the compiler to use `&str`'s parse method.
// We don't want to use our `parse` method here (`impl AttributeOption for String`).
match s.as_str().parse::<$n>() {
Ok(n) => n,
Err(_) => return Err(Error::new(l.span(), "invalid integer")),
}
}
})
}
}
impl AttributeOption for Option<$n> {
#[inline]
fn parse(values: Values) -> Result<Self> {
<$n as AttributeOption>::parse(values).map(Some)
}
}
)*
}
}
attr_option_num!(u16, u32, usize);

View File

@ -1,5 +0,0 @@
pub mod suffixes {
pub const COMMAND: &str = "COMMAND";
}
pub use self::suffixes::*;

View File

@ -1,102 +0,0 @@
#![deny(rust_2018_idioms)]
// FIXME: Remove this in a foreseeable future.
// Currently exists for backwards compatibility to previous Rust versions.
#![recursion_limit = "128"]
#[allow(unused_extern_crates)]
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse::Error, parse_macro_input, spanned::Spanned, Lit};
pub(crate) mod attributes;
pub(crate) mod consts;
pub(crate) mod structures;
#[macro_use]
pub(crate) mod util;
use attributes::*;
use consts::*;
use structures::*;
use util::*;
macro_rules! match_options {
($v:expr, $values:ident, $options:ident, $span:expr => [$($name:ident);*]) => {
match $v {
$(
stringify!($name) => $options.$name = propagate_err!($crate::attributes::parse($values)),
)*
_ => {
return Error::new($span, format_args!("invalid attribute: {:?}", $v))
.to_compile_error()
.into();
},
}
};
}
#[proc_macro_attribute]
pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream {
let mut fun = parse_macro_input!(input as CommandFun);
let lit_name = if !attr.is_empty() {
parse_macro_input!(attr as Lit).to_str()
} else {
fun.name.to_string()
};
let mut options = Options::new();
for attribute in &fun.attributes {
let span = attribute.span();
let values = propagate_err!(parse_values(attribute));
let name = values.name.to_string();
let name = &name[..];
match_options!(name, values, options, span => [
permission_level;
supports_dm;
can_blacklist
]);
}
let Options {
permission_level,
supports_dm,
can_blacklist,
} = options;
let visibility = fun.visibility;
let name = fun.name.clone();
let body = fun.body;
let n = name.with_suffix(COMMAND);
let cooked = fun.cooked.clone();
let command_path = quote!(crate::framework::Command);
populate_fut_lifetimes_on_refs(&mut fun.args);
let args = fun.args;
(quote! {
#(#cooked)*
pub static #n: #command_path = #command_path {
func: #name,
name: #lit_name,
required_perms: #permission_level,
supports_dm: #supports_dm,
can_blacklist: #can_blacklist,
};
#visibility fn #name<'fut> (#(#args),*) -> ::serenity::futures::future::BoxFuture<'fut, ()> {
use ::serenity::futures::future::FutureExt;
async move { #(#body)* }.boxed()
}
})
.into()
}

View File

@ -1,231 +0,0 @@
use crate::util::{Argument, Parenthesised};
use proc_macro2::Span;
use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, ToTokens};
use syn::{
braced,
parse::{Error, Parse, ParseStream, Result},
spanned::Spanned,
Attribute, Block, FnArg, Ident, Pat, Path, PathSegment, Stmt, Token, Visibility,
};
fn parse_argument(arg: FnArg) -> Result<Argument> {
match arg {
FnArg::Typed(typed) => {
let pat = typed.pat;
let kind = typed.ty;
match *pat {
Pat::Ident(id) => {
let name = id.ident;
let mutable = id.mutability;
Ok(Argument {
mutable,
name,
kind: *kind,
})
}
Pat::Wild(wild) => {
let token = wild.underscore_token;
let name = Ident::new("_", token.spans[0]);
Ok(Argument {
mutable: None,
name,
kind: *kind,
})
}
_ => Err(Error::new(
pat.span(),
format_args!("unsupported pattern: {:?}", pat),
)),
}
}
FnArg::Receiver(_) => Err(Error::new(
arg.span(),
format_args!("`self` arguments are prohibited: {:?}", arg),
)),
}
}
/// Test if the attribute is cooked.
fn is_cooked(attr: &Attribute) -> bool {
const COOKED_ATTRIBUTE_NAMES: &[&str] = &[
"cfg", "cfg_attr", "doc", "derive", "inline", "allow", "warn", "deny", "forbid",
];
COOKED_ATTRIBUTE_NAMES.iter().any(|n| attr.path.is_ident(n))
}
/// Removes cooked attributes from a vector of attributes. Uncooked attributes are left in the vector.
///
/// # Return
///
/// Returns a vector of cooked attributes that have been removed from the input vector.
fn remove_cooked(attrs: &mut Vec<Attribute>) -> Vec<Attribute> {
let mut cooked = Vec::new();
// FIXME: Replace with `Vec::drain_filter` once it is stable.
let mut i = 0;
while i < attrs.len() {
if !is_cooked(&attrs[i]) {
i += 1;
continue;
}
cooked.push(attrs.remove(i));
}
cooked
}
#[derive(Debug)]
pub struct CommandFun {
/// `#[...]`-style attributes.
pub attributes: Vec<Attribute>,
/// Populated cooked attributes. These are attributes outside of the realm of this crate's procedural macros
/// and will appear in generated output.
pub cooked: Vec<Attribute>,
pub visibility: Visibility,
pub name: Ident,
pub args: Vec<Argument>,
pub body: Vec<Stmt>,
}
impl Parse for CommandFun {
fn parse(input: ParseStream<'_>) -> Result<Self> {
let mut attributes = input.call(Attribute::parse_outer)?;
// `#[doc = "..."]` is a cooked attribute but it is special-cased for commands.
for attr in &mut attributes {
// Rename documentation comment attributes (`#[doc = "..."]`) to `#[description = "..."]`.
if attr.path.is_ident("doc") {
attr.path = Path::from(PathSegment::from(Ident::new(
"description",
Span::call_site(),
)));
}
}
let cooked = remove_cooked(&mut attributes);
let visibility = input.parse::<Visibility>()?;
input.parse::<Token![async]>()?;
input.parse::<Token![fn]>()?;
let name = input.parse()?;
// (...)
let Parenthesised(args) = input.parse::<Parenthesised<FnArg>>()?;
// { ... }
let bcont;
braced!(bcont in input);
let body = bcont.call(Block::parse_within)?;
let args = args
.into_iter()
.map(parse_argument)
.collect::<Result<Vec<_>>>()?;
Ok(Self {
attributes,
cooked,
visibility,
name,
args,
body,
})
}
}
impl ToTokens for CommandFun {
fn to_tokens(&self, stream: &mut TokenStream2) {
let Self {
attributes: _,
cooked,
visibility,
name,
args,
body,
} = self;
stream.extend(quote! {
#(#cooked)*
#visibility async fn #name (#(#args),*) -> () {
#(#body)*
}
});
}
}
#[derive(Debug)]
pub enum PermissionLevel {
Unrestricted,
Managed,
Restricted,
}
impl Default for PermissionLevel {
fn default() -> Self {
Self::Unrestricted
}
}
impl PermissionLevel {
pub fn from_str(s: &str) -> Option<Self> {
Some(match s.to_uppercase().as_str() {
"UNRESTRICTED" => Self::Unrestricted,
"MANAGED" => Self::Managed,
"RESTRICTED" => Self::Restricted,
_ => return None,
})
}
}
impl ToTokens for PermissionLevel {
fn to_tokens(&self, stream: &mut TokenStream2) {
let path = quote!(crate::framework::PermissionLevel);
let variant;
match self {
Self::Unrestricted => {
variant = quote!(Unrestricted);
}
Self::Managed => {
variant = quote!(Managed);
}
Self::Restricted => {
variant = quote!(Restricted);
}
}
stream.extend(quote! {
#path::#variant
});
}
}
#[derive(Debug, Default)]
pub struct Options {
pub permission_level: PermissionLevel,
pub supports_dm: bool,
pub can_blacklist: bool,
}
impl Options {
#[inline]
pub fn new() -> Self {
let mut options = Self::default();
options.can_blacklist = true;
options.supports_dm = true;
options
}
}

View File

@ -1,160 +0,0 @@
use proc_macro::TokenStream;
use proc_macro2::Span;
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote, ToTokens};
use syn::{
braced, bracketed, parenthesized,
parse::{Error, Parse, ParseStream, Result as SynResult},
punctuated::Punctuated,
token::{Comma, Mut},
Ident, Lifetime, Lit, Type,
};
pub trait LitExt {
fn to_str(&self) -> String;
fn to_bool(&self) -> bool;
fn to_ident(&self) -> Ident;
}
impl LitExt for Lit {
fn to_str(&self) -> String {
match self {
Lit::Str(s) => s.value(),
Lit::ByteStr(s) => unsafe { String::from_utf8_unchecked(s.value()) },
Lit::Char(c) => c.value().to_string(),
Lit::Byte(b) => (b.value() as char).to_string(),
_ => panic!("values must be a (byte)string or a char"),
}
}
fn to_bool(&self) -> bool {
if let Lit::Bool(b) = self {
b.value
} else {
self.to_str()
.parse()
.unwrap_or_else(|_| panic!("expected bool from {:?}", self))
}
}
#[inline]
fn to_ident(&self) -> Ident {
Ident::new(&self.to_str(), self.span())
}
}
pub trait IdentExt2: Sized {
fn to_uppercase(&self) -> Self;
fn with_suffix(&self, suf: &str) -> Ident;
}
impl IdentExt2 for Ident {
#[inline]
fn to_uppercase(&self) -> Self {
format_ident!("{}", self.to_string().to_uppercase())
}
#[inline]
fn with_suffix(&self, suffix: &str) -> Ident {
format_ident!("{}_{}", self.to_string().to_uppercase(), suffix)
}
}
#[inline]
pub fn into_stream(e: Error) -> TokenStream {
e.to_compile_error().into()
}
macro_rules! propagate_err {
($res:expr) => {{
match $res {
Ok(v) => v,
Err(e) => return $crate::util::into_stream(e),
}
}};
}
#[derive(Debug)]
pub struct Bracketed<T>(pub Punctuated<T, Comma>);
impl<T: Parse> Parse for Bracketed<T> {
fn parse(input: ParseStream<'_>) -> SynResult<Self> {
let content;
bracketed!(content in input);
Ok(Bracketed(content.parse_terminated(T::parse)?))
}
}
#[derive(Debug)]
pub struct Braced<T>(pub Punctuated<T, Comma>);
impl<T: Parse> Parse for Braced<T> {
fn parse(input: ParseStream<'_>) -> SynResult<Self> {
let content;
braced!(content in input);
Ok(Braced(content.parse_terminated(T::parse)?))
}
}
#[derive(Debug)]
pub struct Parenthesised<T>(pub Punctuated<T, Comma>);
impl<T: Parse> Parse for Parenthesised<T> {
fn parse(input: ParseStream<'_>) -> SynResult<Self> {
let content;
parenthesized!(content in input);
Ok(Parenthesised(content.parse_terminated(T::parse)?))
}
}
#[derive(Debug)]
pub struct AsOption<T>(pub Option<T>);
impl<T: ToTokens> ToTokens for AsOption<T> {
fn to_tokens(&self, stream: &mut TokenStream2) {
match &self.0 {
Some(o) => stream.extend(quote!(Some(#o))),
None => stream.extend(quote!(None)),
}
}
}
impl<T> Default for AsOption<T> {
#[inline]
fn default() -> Self {
AsOption(None)
}
}
#[derive(Debug)]
pub struct Argument {
pub mutable: Option<Mut>,
pub name: Ident,
pub kind: Type,
}
impl ToTokens for Argument {
fn to_tokens(&self, stream: &mut TokenStream2) {
let Argument {
mutable,
name,
kind,
} = self;
stream.extend(quote! {
#mutable #name: #kind
});
}
}
#[inline]
pub fn populate_fut_lifetimes_on_refs(args: &mut Vec<Argument>) {
for arg in args {
if let Type::Reference(reference) = &mut arg.kind {
reference.lifetime = Some(Lifetime::new("'fut", Span::call_site()));
}
}
}

View File

@ -0,0 +1,2 @@
printWidth = 100
tabWidth = 4

View File

@ -0,0 +1,19 @@
# reminder-dashboard
The re-re-rewrite of the dashboard.
## Why
The existing beta variant of the dashboard is written using vanilla JavaScript. This is fine,
but annoying to update. This would've been okay if I was more dedicated to updating the vanilla
JavaScript too, but I want to experiment with "new" things.
This also allows me to expand my frontend skills, which is relevant to part of my job.
## Developing
1. Run both `npm run dev` and `cargo run`
2. Symlink assets: assuming cloned
into `$HOME`, `ln -s $HOME/reminder-bot/reminder-dashboard/dist/index.html $HOME/reminder-bot/web/static/index.html`
and
`ln -s $HOME/reminder-bot/reminder-dashboard/dist/static/assets $HOME/reminder-bot/web/static/assets`

View File

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="EN">
<head>
<meta name="description" content="The most powerful Discord Reminders Bot">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="UTF-8">
<meta name="yandex-verification" content="bb77b8681eb64a90"/>
<meta name="google-site-verification" content="7h7UVTeEe0AOzHiH3cFtsqMULYGN-zCZdMT_YCkW1Ho"/>
<!-- <meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src *; font-src fonts.gstatic.com 'self'"> -->
<!-- favicon -->
<link rel="apple-touch-icon" sizes="180x180"
href="/static/favicon/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32"
href="/static/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16"
href="/static/favicon/favicon-16x16.png">
<link rel="manifest" href="/static/site.webmanifest">
<meta name="msapplication-TileColor" content="#da532c">
<meta name="theme-color" content="#ffffff">
<title>Reminder Bot | Dashboard</title>
<!-- styles -->
<link rel="stylesheet" href="/static/css/bulma.min.css">
<link rel="stylesheet" href="/static/css/fa.css">
<link rel="stylesheet" href="/static/css/font.css">
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>

5626
reminder-dashboard/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,32 @@
{
"name": "example",
"private": true,
"type": "module",
"scripts": {
"dev": "vite build --watch --mode development",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.5.1",
"bulma": "^0.9.4",
"luxon": "^3.4.3",
"preact": "^10.13.1",
"react-colorful": "^5.6.1",
"react-query": "^3.39.3",
"tributejs": "^5.1.3",
"use-debounce": "^10.0.0",
"wouter": "^3.0"
},
"devDependencies": {
"@preact/preset-vite": "^2.5.0",
"@types/luxon": "^3.3.2",
"eslint": "^8.50.0",
"eslint-config-preact": "^1.3.0",
"prettier": "^3.0.3",
"react-datepicker": "^4.21.0",
"sass": "^1.71.1",
"typescript": "^5.2.2",
"vite": "^5.1"
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,91 @@
.date-selector-wrapper {
width: 200px;
padding: 3px;
background-color: #fff;
box-shadow: 1px 1px 10px 1px #5c5c5c;
position: absolute;
font-size: 12px;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
/* user-select: none; */
}
.cal-header, .cal-row {
display: flex;
width: 100%;
height: 30px;
line-height: 30px;
text-align: center;
}
.cal-cell, .cal-nav {
cursor: pointer;
}
.cal-day-names {
height: 25px;
line-height: 25px;
}
.cal-day-names .cal-cell {
cursor: default;
font-weight: bold;
}
.cal-cell-prev, .cal-cell-next {
color: #777;
}
.cal-months .cal-row, .cal-years .cal-row {
height: 60px;
line-height: 60px;
}
.cal-nav-prev, .cal-nav-next {
flex: 0.15;
}
.cal-nav-current {
flex: 0.75;
font-weight: bold;
}
.cal-months .cal-cell, .cal-years .cal-cell {
flex: 0.25;
}
.cal-days .cal-cell {
flex: 0.143;
}
.cal-value {
color: #fff;
background-color: #286090;
}
.cal-cell:hover, .cal-nav:hover {
background-color: #eee;
}
.cal-value:hover {
background-color: #204d74;
}
/* time footer */
.cal-time {
display: flex;
justify-content: flex-start;
height: 27px;
line-height: 27px;
}
.cal-time-label, .cal-time-value {
flex: 0.12;
text-align: center;
}
.cal-time-slider {
flex: 0.77;
background-image: linear-gradient(to right, #d1d8dd, #d1d8dd);
background-repeat: no-repeat;
background-size: 100% 1px;
background-position: left 50%;
height: 100%;
}
.cal-time-slider input {
width: 100%;
-webkit-appearance: none;
background: 0 0;
cursor: pointer;
height: 100%;
outline: 0;
user-select: auto;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,63 @@
@font-face {
font-family: 'Source Sans Pro';
font-style: italic;
font-weight: 300;
src: local('Source Sans Pro Light Italic'), local('SourceSansPro-LightItalic'), url(https://fonts.gstatic.com/s/sourcesanspro/v13/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZZMkids18E.ttf) format('truetype');
font-display: swap;
}
@font-face {
font-family: 'Source Sans Pro';
font-style: italic;
font-weight: 400;
src: local('Source Sans Pro Italic'), local('SourceSansPro-Italic'), url(https://fonts.gstatic.com/s/sourcesanspro/v13/6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7nsDc.ttf) format('truetype');
font-display: swap;
}
@font-face {
font-family: 'Source Sans Pro';
font-style: italic;
font-weight: 600;
src: local('Source Sans Pro SemiBold Italic'), local('SourceSansPro-SemiBoldItalic'), url(https://fonts.gstatic.com/s/sourcesanspro/v13/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZY4lCds18E.ttf) format('truetype');
font-display: swap;
}
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 300;
src: local('Source Sans Pro Light'), local('SourceSansPro-Light'), url(https://fonts.gstatic.com/s/sourcesanspro/v13/6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwlxdr.ttf) format('truetype');
font-display: swap;
}
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 400;
src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url(https://fonts.gstatic.com/s/sourcesanspro/v13/6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7g.ttf) format('truetype');
font-display: swap;
}
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 600;
src: local('Source Sans Pro SemiBold'), local('SourceSansPro-SemiBold'), url(https://fonts.gstatic.com/s/sourcesanspro/v13/6xKydSBYKcSV-LCoeQqfX1RYOo3i54rwlxdr.ttf) format('truetype');
font-display: swap;
}
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 700;
src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url(https://fonts.gstatic.com/s/sourcesanspro/v13/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlxdr.ttf) format('truetype');
font-display: swap;
}
@font-face {
font-family: 'Ubuntu';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/ubuntu/v15/4iCs6KVjbNBYlgo6eA.ttf) format('truetype');
font-display: swap;
}
@font-face {
font-family: 'Ubuntu';
font-style: normal;
font-weight: 700;
src: url(https://fonts.gstatic.com/s/ubuntu/v15/4iCv6KVjbNBYlgoCxCvTtw.ttf) format('truetype');
font-display: swap;
}

View File

@ -0,0 +1,888 @@
* {
font-family: "Ubuntu Bold", "Ubuntu", sans-serif;
}
button {
font-weight: 700;
}
/* override styles for when the div is collapsed */
div.reminderContent.is-collapsed .column.discord-frame {
display: none;
}
div.reminderContent.is-collapsed .column.settings {
display: none;
}
div.reminderContent.is-collapsed .reminder-settings {
margin-bottom: 0;
}
div.reminderContent.is-collapsed .button-row {
display: none;
}
div.reminderContent.is-collapsed .button-row-edit {
display: none;
}
div.reminderContent.is-collapsed .reminder-topbar {
padding-bottom: 0;
}
div.reminderContent.is-collapsed .invert-collapses {
display: inline-flex;
}
div.reminderContent .invert-collapses {
display: none;
}
div.reminderContent.is-collapsed input[name="name"] {
display: inline-flex;
flex-grow: 1;
border: none;
background: none;
box-shadow: none;
opacity: 1;
}
div.reminderContent.is-collapsed .hide-box {
display: inline-flex;
}
div.reminderContent.is-collapsed .hide-box i {
transform: rotate(90deg);
}
/* END */
/* dashboard styles */
.hide-box {
border: none;
background: none;
}
.hide-box:focus {
outline: none;
box-shadow: none !important;
}
.channel-bar {
display: flex;
justify-content: center;
flex-direction: column;
font-weight: bold;
}
button.inline-btn {
height: 100%;
padding: 5px;
}
button.change-color {
position: absolute;
left: calc(-1rem - 40px);
}
button.disable-enable[data-action="enable"]:after {
content: "Enable";
}
button.disable-enable[data-action="disable"]:after {
content: "Disable";
}
.media-content {
overflow-x: visible;
}
div.discord-embed {
position: relative;
}
div.split-controls {
display: flex;
flex-direction: column;
justify-content: space-between;
flex-grow: 2;
}
.reminder-topbar > div {
padding-left: 6px;
padding-right: 6px;
}
.settings {
display: flex;
flex-direction: column;
}
.name-bar {
flex-grow: 1;
flex-shrink: 1;
}
.hide-button-bar {
flex-grow: 0;
flex-shrink: 0;
}
.patreon-only {
padding-bottom: 16px;
}
.tts-row {
padding-bottom: 10px;
}
.reminder-topbar {
display: flex;
margin-bottom: 0 !important;
}
.reminder-settings {
margin-top: 0 !important;
}
.reminder-settings > .column {
flex-grow: 0;
flex-shrink: 0;
flex-basis: 50%;
}
div.reminderContent {
margin-top: 10px;
margin-bottom: 10px;
padding: 14px;
background-color: #f5f5f5;
border-radius: 8px;
}
/* Interval inputs */
div.interval-group {
height: unset !important;
}
div.interval-group .clear:focus {
outline: none;
box-shadow: none !important;
}
div.interval-group .no-break {
text-wrap: avoid;
white-space: nowrap;
}
div.interval-group .clear {
border: none;
background: none;
padding: 1px;
margin-right: -3px;
}
div.interval-group > .interval-group-left input {
-webkit-appearance: none;
border-style: none;
background-color: #eee;
font-size: 1rem;
font-family: monospace;
}
div.interval-group > .interval-group-left input.w2 {
width: 3ch;
}
div.interval-group > .interval-group-left input.w3 {
width: 3ch;
}
div.interval-group {
display: flex;
flex-direction: row;
justify-content: space-between;
}
/* !Interval inputs */
.left-pad {
padding-left: 1rem;
padding-right: 0.2rem;
}
.notification {
padding-right: 1.5rem;
}
div.inset-content {
margin-left: 10%;
margin-right: 10%;
}
div.flash-message {
position: fixed;
width: calc(100% - 32px);
margin: 16px !important;
z-index: 99;
bottom: 0;
display: none;
}
div.flash-message.is-active {
display: block;
}
body {
min-height: 100vh;
}
span.spacer {
width: 10px;
}
nav .dashboard-button {
background: white ;
}
span.patreon-color {
color: #f96854;
}
p.pageTitle {
margin-left: 12px;
}
#welcome > div {
height: 100%;
padding-top: 30vh;
}
div#pageNavbar {
background-color: #363636;
}
div#pageNavbar a {
color: #fff;
text-align: center;
}
.navbar-burger {
flex-shrink: 0;
}
.navbar-item.pageTitle {
flex-shrink: 1;
white-space: nowrap;
overflow: hidden;
}
.dashboard-burger, .dashboard-burger:active, .dashboard-burger.is-active {
background-color: #adc99c !important;
border-radius: 14px;
padding: 6px;
background-clip: content-box;
}
div#pageNavbar a:hover {
background-color: #4a4a4a;
}
img.rounded-corners {
border-radius: 12px;
}
div.brand {
text-align: center;
height: 52px;
background-color: #8fb677;
}
img.dashboard-brand {
text-align: center;
height: 100%;
width: auto;
}
div.dashboard-sidebar {
background-color: #363636;
width: 230px !important;
padding-right: 0;
}
ul.guildList {
flex-grow: 1;
flex-shrink: 1;
overflow: auto;
}
div.dashboard-sidebar:not(.mobile-sidebar) .aside-footer {
flex-shrink: 0;
flex-grow: 0;
position: fixed;
bottom: 0;
width: 226px;
}
div.dashboard-sidebar svg {
flex-shrink: 0;
}
div.mobile-sidebar {
z-index: 100;
min-height: 100vh;
position: absolute;
top: 0;
display: none;
flex-direction: column;
}
#expandAll {
width: 60px;
}
div.mobile-sidebar .aside-footer {
margin-top: auto;
}
div.mobile-sidebar.is-active {
display: flex;
}
aside.menu {
display: flex;
flex-direction: column;
flex-grow: 1;
}
div.dashboard-frame {
min-height: 100vh;
margin-bottom: 0 !important;
}
.embed-field-box[data-inlined="0"] .inline-btn > i {
transform: rotate(90deg);
}
.embed-field-box[data-inlined="0"] {
min-width: 100%;
}
.embed-field-box[data-inlined="1"] {
min-width: auto;
}
.menu a {
color: #fff;
}
.menu .menu-label {
color: #bbb;
}
.menu {
padding-left: 4px;
}
.dashboard-navbar {
background-color: #8fb677 !important;
position: absolute;
top: 0;
width: 100%;
}
textarea.autoresize {
resize: none;
}
textarea, input {
width: 100%;
}
input.default-width {
width: initial;
}
.message-input:placeholder-shown {
font-style: italic;
background-color: #40444b;
color: #fff;
}
.message-input {
border: none;
background-color: rgba(0, 0, 0, 0);
color: #fff;
}
.time-input {
border-top: none;
border-left: none;
border-right: none;
border-bottom-style: solid;
background-color: #40444b;
color: #fff;
width: 120px;
font-size: 0.875rem;
}
.message-input::placeholder {
color: #72767b;
}
.discord-title {
font-weight: bold;
font-size: 1rem;
margin: 4px 0 4px 0;
}
.discord-description {
font-size: 0.875rem;
}
.discord-username {
font-size: 1rem;
font-weight: bold;
margin-bottom: 4px;
width: initial;
}
.discord-message-header {
white-space: nowrap;
margin-bottom: 8px;
}
.discord-content {
margin-bottom: 8px;
}
.customizable img {
background-color: #72767b;
border-radius: 8px;
}
.customizable.is-20x20 img {
width: 20px;
height: 20px;
}
.customizable.is-24x24 img {
width: 24px;
height: 24px;
}
.customizable.is-400x300 img {
margin-top: 10px;
width: 100%;
height: 100px;
}
.customizable.is-32x32 img {
width: 32px;
height: 32px;
}
.customizable.thumbnail img {
width: 100px;
height: 100px;
}
.customizable input.imageInput {
display: none;
position: absolute;
top: 0;
left: 36px;
width: 400px;
}
.customizable.thumbnail input.imageInput {
display: none;
position: absolute;
top: 0;
left: -400px;
width: 400px;
}
.customizable input.is-active {
display: block !important;
}
.discord-frame {
color: #fff;
padding: 10px;
border-radius: 8px;
background-color: #36393f;
}
.discord-embed {
padding: 8px 16px 16px 12px;
margin: 0 20px 4px 0;
border-radius: 4px;
border-left: 4px solid #fff;
background-color: #2f3136;
}
.embed-author-box {
display: flex;
align-items: center;
margin-bottom: 8px;
}
.embed-author-box > .a {
flex: initial;
}
.embed-author-box > .b {
flex: auto;
}
.embed-footer-box {
display: flex;
align-items: center;
margin-bottom: 8px;
}
.embed-author-box .image {
margin: 0 8px 0 0 !important;
}
.embed-footer-box .image {
margin: 0 8px 0 0 !important;
}
.discord-embed-author {
display: inline-block;
font-size: 0.875rem;
font-weight: bold;
}
.discord-embed-footer {
font-size: 0.75rem;
}
.embed-body {
display: flex;
}
.embed-body > .a {
flex-grow: 1;
flex-shrink: 1;
flex-basis: auto;
margin-right: 4px;
}
.embed-body input, .embed-body textarea {
min-width: 0;
}
.embed-body > .b {
flex-grow: 0;
flex-shrink: 0;
flex-basis: auto;
}
.discord-field-title, .discord-field-value {
max-width: 120px;
}
.discord-field-title {
font-weight: bold;
}
.embed-field-box {
margin: 12px 8px 0 0;
max-width: 120px;
flex: initial;
}
.field-input {
font-size: 0.875rem;
width: 120px;
}
.embed-multifield-box {
display: flex;
max-width: 100%;
flex-wrap: wrap;
}
.channel-select {
font-size: 1.125rem;
margin-bottom: 4px;
margin-left: 48px;
display: inline-flex;
font-weight: bold;
color: #6e89da;
width: auto;
border-radius: 2px;
border-bottom: 1px solid #fff;
}
.channel-selector {
width: 100%;
}
.select {
width: 100%;
}
li.highlight {
margin-bottom: 0 !important;
}
.button-row {
display: flex;
}
.button-row .button-row-reminder {
flex-grow: 0;
padding: 2px;
}
.button-row-template {
display: flex;
flex-grow: 1;
justify-content: space-between;
}
.button-row .button-row-template > div {
padding: 2px;
}
@media only screen and (max-width: 1023px) {
p.title.pageTitle {
display: none;
}
.dashboard-frame {
margin-top: 4rem !important;
}
.customizable.thumbnail img {
width: 60px;
height: 60px;
}
}
@media only screen and (max-width: 768px) {
.button-row {
display: flex;
flex-direction: column;
}
.button-row .button-row-reminder {
width: 100%;
}
.button-row .button-row-template > div {
flex-basis: 0;
flex-grow: 1;
}
.button-row button {
width: 100%;
}
.reminder-settings {
margin-bottom: 0 !important;
}
.tts-row {
padding-bottom: 0;
}
}
/* loader */
#loader {
position: fixed;
top: 0;
background-color: rgba(255, 255, 255, 0.8);
width: 100vw;
z-index: 999;
}
#loader .title {
font-size: 6rem;
}
/* END */
div.reminderError {
margin: 10px;
padding: 14px;
background-color: #f5f5f5;
border-radius: 8px;
}
div.reminderError .errorHead {
display: flex;
flex-direction: row;
}
div.reminderError .errorIcon {
padding: 8px;
border-radius: 4px;
margin-right: 12px;
}
div.reminderError .errorIcon .fas {
display: none
}
div.reminderError[data-case="deleted"] .errorIcon {
background-color: #e7e5e4;
}
div.reminderError[data-case="failed"] .errorIcon {
background-color: #fecaca;
}
div.reminderError[data-case="sent"] .errorIcon {
background-color: #d9f99d;
}
div.reminderError[data-case="deleted"] .errorIcon .fas.fa-trash {
display: block;
}
div.reminderError[data-case="failed"] .errorIcon .fas.fa-exclamation-triangle {
display: block;
}
div.reminderError[data-case="sent"] .errorIcon .fas.fa-check {
display: block;
}
div.reminderError .errorHead .reminderName {
font-size: 1rem;
display: flex;
flex-direction: column;
justify-content: center;
color: rgb(54, 54, 54);
flex-grow: 1;
}
div.reminderError .errorHead .reminderTime {
font-size: 1rem;
display: flex;
flex-direction: column;
flex-shrink: 1;
justify-content: center;
color: rgb(54, 54, 54);
background-color: #ffffff;
padding: 8px;
border-radius: 4px;
border-color: #e5e5e5;
border-width: 1px;
border-style: solid;
}
div.reminderError .reminderMessage {
font-size: 1rem;
display: flex;
flex-direction: column;
justify-content: center;
color: rgb(54, 54, 54);
flex-grow: 1;
font-style: italic;
}
/* other stuff */
.half-rem {
width: 0.5rem;
}
.pad-left {
width: 12px;
}
#dead {
display: none;
}
.colorpicker-container {
display: flex;
justify-content: center;
}
.create-reminder {
margin: 0 12px 12px 12px;
}
.button.is-success:not(.is-outlined) {
color: white;
}
.button.is-outlined.is-success {
background-color: white;
}
a.switch-pane {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.guild-submenu {
display: none;
}
.guild-submenu li {
font-size: 0.8rem;
}
a.switch-pane.is-active ~ .guild-submenu {
display: block;
}
.feedback {
background-color: #5865F2;
}
.is-locked {
pointer-events: none;
}
.is-locked > :not(.patreon-invert) {
opacity: 0.4;
}
.is-locked .patreon-invert {
display: block;
}
.patreon-invert {
display: none;
}
.is-locked .foreground {
pointer-events: auto;
}
.is-locked .field:last-of-type {
display: none;
}
.stat-row {
display: flex;
flex-direction: row;
}
.stat-box {
flex-grow: 1;
border-radius: 6px;
background-color: #fcfcfc;
border-color: #efefef;
border-style: solid;
border-width: 1px;
margin: 4px;
padding: 4px;
}
.figure {
text-align: center;
}
.figure-num {
font-size: 2rem;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#da532c</TileColor>
</tile>
</msapplication>
</browserconfig>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

View File

@ -0,0 +1,131 @@
document.addEventListener("DOMContentLoaded", () => {
fetch("/admin/data")
.then((resp) => resp.json())
.then((data) => {
document.querySelector("#backlog").textContent = data.backlog;
document.querySelector("#reminders").textContent = data.count.reminders;
document.querySelector("#intervals").textContent = data.count.intervals;
let historySent = data.historyLong.sent.reduce(
(iv, frame) => iv + frame.count,
0
);
let historyFailed = data.historyLong.failed.reduce(
(iv, frame) => iv + frame.count,
0
);
let rate = historyFailed / (historySent + historyFailed);
let formatted = Math.round(rate * 10000) / 100;
document.querySelector("#historySent").textContent = historySent;
document.querySelector("#historyFailed").textContent = historyFailed;
document.querySelector("#failRate").textContent = `${formatted}%`;
new Chart(document.getElementById("schedule"), {
type: "bar",
data: {
labels: [
...data.scheduleShort.once,
...data.scheduleShort.interval,
].map((row) => luxon.DateTime.fromISO(row.time_key)),
datasets: [
{
label: "Reminders",
data: data.scheduleShort.once.map((row) => row.count),
},
{
label: "Intervals",
data: data.scheduleShort.interval.map((row) => row.count),
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
stacked: true,
type: "time",
time: {
unit: "minute",
},
},
y: {
stacked: true,
},
},
},
});
new Chart(document.getElementById("scheduleLong"), {
type: "bar",
data: {
labels: [
...data.scheduleLong.once,
...data.scheduleLong.interval,
].map((row) => luxon.DateTime.fromISO(row.time_key)),
datasets: [
{
label: "Reminders",
data: data.scheduleLong.once.map((row) => row.count),
},
{
label: "Intervals",
data: data.scheduleLong.interval.map((row) => row.count),
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
stacked: true,
type: "time",
time: {
unit: "day",
},
},
y: {
stacked: true,
},
},
},
});
new Chart(document.getElementById("historyLong"), {
type: "bar",
data: {
labels: [...data.historyLong.sent, ...data.historyLong.failed].map(
(row) => luxon.DateTime.fromISO(row.time_key)
),
datasets: [
{
label: "Success",
data: data.historyLong.sent.map((row) => row.count),
},
{
label: "Fail",
data: data.historyLong.failed.map((row) => row.count),
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
stacked: true,
type: "time",
time: {
unit: "day",
},
},
y: {
stacked: true,
},
},
},
});
});
});

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,7 @@
/*!
* chartjs-adapter-luxon v1.3.1
* https://www.chartjs.org
* (c) 2023 chartjs-adapter-luxon Contributors
* Released under the MIT license
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(require("chart.js"),require("luxon")):"function"==typeof define&&define.amd?define(["chart.js","luxon"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).Chart,e.luxon)}(this,(function(e,t){"use strict";const n={datetime:t.DateTime.DATETIME_MED_WITH_SECONDS,millisecond:"h:mm:ss.SSS a",second:t.DateTime.TIME_WITH_SECONDS,minute:t.DateTime.TIME_SIMPLE,hour:{hour:"numeric"},day:{day:"numeric",month:"short"},week:"DD",month:{month:"short",year:"numeric"},quarter:"'Q'q - yyyy",year:{year:"numeric"}};e._adapters._date.override({_id:"luxon",_create:function(e){return t.DateTime.fromMillis(e,this.options)},init(e){this.options.locale||(this.options.locale=e.locale)},formats:function(){return n},parse:function(e,n){const i=this.options,r=typeof e;return null===e||"undefined"===r?null:("number"===r?e=this._create(e):"string"===r?e="string"==typeof n?t.DateTime.fromFormat(e,n,i):t.DateTime.fromISO(e,i):e instanceof Date?e=t.DateTime.fromJSDate(e,i):"object"!==r||e instanceof t.DateTime||(e=t.DateTime.fromObject(e,i)),e.isValid?e.valueOf():null)},format:function(e,t){const n=this._create(e);return"string"==typeof t?n.toFormat(t):n.toLocaleString(t)},add:function(e,t,n){const i={};return i[n]=t,this._create(e).plus(i).valueOf()},diff:function(e,t,n){return this._create(e).diff(this._create(t)).as(n).valueOf()},startOf:function(e,t,n){if("isoWeek"===t){n=Math.trunc(Math.min(Math.max(0,n),6));const t=this._create(e);return t.minus({days:(t.weekday-n+7)%7}).startOf("day").valueOf()}return t?this._create(e).startOf(t).valueOf():e},endOf:function(e,t){return this._create(e).endOf(t).valueOf()}})}));

View File

@ -0,0 +1,931 @@
(function () {
"use strict";
var BODYTYPES = ["DAYS", "MONTHS", "YEARS"];
var MONTHS = [
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
];
var WEEKDAYS = [
"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
];
/** @typedef {Object.<string, Function[]>} Handlers */
/** @typedef {function(String, Function): null} AddHandler */
/** @typedef {("DAYS"|"MONTHS"|"YEARS")} BodyType */
/** @typedef {string|number} StringNum */
/** @typedef {Object.<string, StringNum>} StringNumObj */
/**
* The local state
* @typedef {Object} InstanceState
* @property {Date} value
* @property {Number} year
* @property {Number} month
* @property {Number} day
* @property {Number} time
* @property {Number} hours
* @property {Number} minutes
* @property {Number} seconds
* @property {BodyType} bodyType
* @property {Boolean} visible
* @property {Number} cancelBlur
*/
/**
* @typedef {Object} Config
* @property {String} dateFormat
* @property {String} timeFormat
* @property {Boolean} showDate
* @property {Boolean} showTime
* @property {Number} paddingX
* @property {Number} paddingY
* @property {BodyType} defaultView
* @property {"TOP"|"BOTTOM"} direction
*/
/**
* @class
* @param {HTMLElement} elem
* @param {Config} config
*/
function DTS(elem, config) {
var config = config || {};
/** @type {Config} */
var defaultConfig = {
defaultView: BODYTYPES[0],
dateFormat: "yyyy-mm-dd",
timeFormat: "HH:MM:SS",
showDate: true,
showTime: false,
paddingX: 5,
paddingY: 5,
direction: 'TOP'
}
if (!elem) {
throw TypeError("input element or selector required for contructor");
}
if (Object.getPrototypeOf(elem) === String.prototype) {
var _elem = document.querySelectorAll(elem);
if (!_elem[0]){
throw Error('"' + elem + '" not found.');
}
elem = _elem[0];
}
this.config = setDefaults(config, defaultConfig);
this.dateFormat = this.config.dateFormat;
this.timeFormat = this.config.timeFormat;
this.dateFormatRegEx = new RegExp("yyyy|yy|mm|dd", "gi");
this.timeFormatRegEx = new RegExp("hh|mm|ss|a", "gi");
this.inputElem = elem;
this.dtbox = null;
this.setup();
}
DTS.prototype.setup = function () {
var handler = this.inputElemHandler.bind(this);
this.inputElem.addEventListener("focus", handler, false)
this.inputElem.addEventListener("blur", handler, false);
}
DTS.prototype.inputElemHandler = function (e) {
if (e.type == "focus") {
if (!this.dtbox) {
this.dtbox = new DTBox(e.target, this);
}
this.dtbox.visible = true;
} else if (e.type == "blur" && this.dtbox && this.dtbox.visible) {
var self = this;
setTimeout(function () {
if (self.dtbox.cancelBlur > 0) {
self.dtbox.cancelBlur -= 1;
} else {
self.dtbox.visible = false;
self.inputElem.blur();
}
}, 100);
}
}
/**
* @class
* @param {HTMLElement} elem
* @param {DTS} settings
*/
function DTBox(elem, settings) {
/** @type {DTBox} */
var self = this;
/** @type {Handlers} */
var handlers = {};
/** @type {InstanceState} */
var localState = {};
/**
* @param {String} key
* @param {*} default_val
*/
function getterSetter(key, default_val) {
return {
get: function () {
var val = localState[key];
return val === undefined ? default_val : val;
},
set: function (val) {
var prevState = self.state;
var _handlers = handlers[key] || [];
localState[key] = val;
for (var i = 0; i < _handlers.length; i++) {
_handlers[i].bind(self)(localState, prevState);
}
},
};
};
/** @type {AddHandler} */
function addHandler(key, handlerFn) {
if (!key || !handlerFn) {
return false;
}
if (!handlers[key]) {
handlers[key] = [];
}
handlers[key].push(handlerFn);
}
Object.defineProperties(this, {
visible: getterSetter("visible", false),
bodyType: getterSetter("bodyType", settings.config.defaultView),
value: getterSetter("value"),
year: getterSetter("year", 0),
month: getterSetter("month", 0),
day: getterSetter("day", 0),
hours: getterSetter("hours", 0),
minutes: getterSetter("minutes", 0),
seconds: getterSetter("seconds", 0),
cancelBlur: getterSetter("cancelBlur", 0),
addHandler: {value: addHandler},
month_long: {
get: function () {
return MONTHS[self.month];
},
},
month_short: {
get: function () {
return self.month_long.slice(0, 3);
},
},
state: {
get: function () {
return Object.assign({}, localState);
},
},
time: {
get: function() {
var hours = self.hours * 60 * 60 * 1000;
var minutes = self.minutes * 60 * 1000;
var seconds = self.seconds * 1000;
return hours + minutes + seconds;
}
},
});
this.el = {};
this.settings = settings;
this.elem = elem;
this.setup();
}
DTBox.prototype.setup = function () {
Object.defineProperties(this.el, {
wrapper: { value: null, configurable: true },
header: { value: null, configurable: true },
body: { value: null, configurable: true },
footer: { value: null, configurable: true }
});
this.setupWrapper();
if (this.settings.config.showDate) {
this.setupHeader();
this.setupBody();
}
if (this.settings.config.showTime) {
this.setupFooter();
}
var self = this;
this.addHandler("visible", function (state, prevState) {
if (state.visible && !prevState.visible){
document.body.appendChild(this.el.wrapper);
var parts = self.elem.value.split(/\s*,\s*/);
var startDate = undefined;
var startTime = 0;
if (self.settings.config.showDate) {
startDate = parseDate(parts[0], self.settings);
}
if (self.settings.config.showTime) {
startTime = parseTime(parts[parts.length-1], self.settings);
startTime = startTime || 0;
}
if (!(startDate && startDate.getTime())) {
startDate = new Date();
startDate = new Date(
startDate.getFullYear(),
startDate.getMonth(),
startDate.getDate()
);
}
var value = new Date(startDate.getTime() + startTime);
self.value = value;
self.year = value.getFullYear();
self.month = value.getMonth();
self.day = value.getDate();
self.hours = value.getHours();
self.minutes = value.getMinutes();
self.seconds = value.getSeconds();
if (self.settings.config.showDate) {
self.setHeaderContent();
self.setBodyContent();
}
if (self.settings.config.showTime) {
self.setFooterContent();
}
} else if (!state.visible && prevState.visible) {
document.body.removeChild(this.el.wrapper);
}
});
}
DTBox.prototype.setupWrapper = function () {
if (!this.el.wrapper) {
var el = document.createElement("div");
el.classList.add("date-selector-wrapper");
Object.defineProperty(this.el, "wrapper", { value: el });
}
var self = this;
var htmlRoot = document.getElementsByTagName('html')[0];
function setPosition(e){
var minTopSpace = 300;
var box = getOffset(self.elem);
var config = self.settings.config;
var paddingY = config.paddingY || 5;
var paddingX = config.paddingX || 5;
var top = box.top + self.elem.offsetHeight + paddingY;
var left = box.left + paddingX;
var bottom = htmlRoot.clientHeight - box.top + paddingY;
self.el.wrapper.style.left = `${left}px`;
if (box.top > minTopSpace && config.direction != 'BOTTOM') {
self.el.wrapper.style.bottom = `${bottom}px`;
self.el.wrapper.style.top = '';
} else {
self.el.wrapper.style.top = `${top}px`;
self.el.wrapper.style.bottom = '';
}
}
function handler(e) {
self.cancelBlur += 1;
setTimeout(function(){
self.elem.focus();
}, 50);
}
setPosition();
this.setPosition = setPosition;
this.el.wrapper.addEventListener("mousedown", handler, false);
this.el.wrapper.addEventListener("touchstart", handler, false);
window.addEventListener('resize', this.setPosition);
}
DTBox.prototype.setupHeader = function () {
if (!this.el.header) {
var row = document.createElement("div");
var classes = ["cal-nav-prev", "cal-nav-current", "cal-nav-next"];
row.classList.add("cal-header");
for (var i = 0; i < 3; i++) {
var cell = document.createElement("div");
cell.classList.add("cal-nav", classes[i]);
cell.onclick = this.onHeaderChange.bind(this);
row.appendChild(cell);
}
row.children[0].innerHTML = "&lt;";
row.children[2].innerHTML = "&gt;";
Object.defineProperty(this.el, "header", { value: row });
tryAppendChild(row, this.el.wrapper);
}
this.setHeaderContent();
}
DTBox.prototype.setHeaderContent = function () {
var content = this.year;
if ("DAYS" == this.bodyType) {
content = this.month_long + " " + content;
} else if ("YEARS" == this.bodyType) {
var start = this.year + 10 - (this.year % 10);
content = start - 10 + "-" + (start - 1);
}
this.el.header.children[1].innerText = content;
}
DTBox.prototype.setupBody = function () {
if (!this.el.body) {
var el = document.createElement("div");
el.classList.add("cal-body");
Object.defineProperty(this.el, "body", { value: el });
tryAppendChild(el, this.el.wrapper);
}
var toAppend = null;
function makeGrid(rows, cols, className, firstRowClass, clickHandler) {
var grid = document.createElement("div");
grid.classList.add(className);
for (var i = 1; i < rows + 1; i++) {
var row = document.createElement("div");
row.classList.add("cal-row", "cal-row-" + i);
if (i == 1 && firstRowClass) {
row.classList.add(firstRowClass);
}
for (var j = 1; j < cols + 1; j++) {
var col = document.createElement("div");
col.classList.add("cal-cell", "cal-col-" + j);
col.onclick = clickHandler;
row.appendChild(col);
}
grid.appendChild(row);
}
return grid;
}
if ("DAYS" == this.bodyType) {
toAppend = this.el.body.calDays;
if (!toAppend) {
toAppend = makeGrid(7, 7, "cal-days", "cal-day-names", this.onDateSelected.bind(this));
for (var i = 0; i < 7; i++) {
var cell = toAppend.children[0].children[i];
cell.innerText = WEEKDAYS[i].slice(0, 2);
cell.onclick = null;
}
this.el.body.calDays = toAppend;
}
} else if ("MONTHS" == this.bodyType) {
toAppend = this.el.body.calMonths;
if (!toAppend) {
toAppend = makeGrid(3, 4, "cal-months", null, this.onMonthSelected.bind(this));
for (var i = 0; i < 3; i++) {
for (var j = 0; j < 4; j++) {
var monthShort = MONTHS[4 * i + j].slice(0, 3);
toAppend.children[i].children[j].innerText = monthShort;
}
}
this.el.body.calMonths = toAppend;
}
} else if ("YEARS" == this.bodyType) {
toAppend = this.el.body.calYears;
if (!toAppend) {
toAppend = makeGrid(3, 4, "cal-years", null, this.onYearSelected.bind(this));
this.el.body.calYears = toAppend;
}
}
empty(this.el.body);
tryAppendChild(toAppend, this.el.body);
this.setBodyContent();
}
DTBox.prototype.setBodyContent = function () {
var grid = this.el.body.children[0];
var classes = ["cal-cell-prev", "cal-cell-next", "cal-value"];
if ("DAYS" == this.bodyType) {
var oneDayMilliSecs = 24 * 60 * 60 * 1000;
var start = new Date(this.year, this.month, 1);
var adjusted = new Date(start.getTime() - oneDayMilliSecs * start.getDay());
grid.children[6].style.display = "";
for (var i = 1; i < 7; i++) {
for (var j = 0; j < 7; j++) {
var cell = grid.children[i].children[j];
var month = adjusted.getMonth();
var date = adjusted.getDate();
cell.innerText = date;
cell.classList.remove(classes[0], classes[1], classes[2]);
if (month != this.month) {
if (i == 6 && j == 0) {
grid.children[6].style.display = "none";
break;
}
cell.classList.add(month < this.month ? classes[0] : classes[1]);
} else if (isEqualDate(adjusted, this.value)){
cell.classList.add(classes[2]);
}
adjusted = new Date(adjusted.getTime() + oneDayMilliSecs);
}
}
} else if ("YEARS" == this.bodyType) {
var year = this.year - (this.year % 10) - 1;
for (i = 0; i < 3; i++) {
for (j = 0; j < 4; j++) {
grid.children[i].children[j].innerText = year;
year += 1;
}
}
grid.children[0].children[0].classList.add(classes[0]);
grid.children[2].children[3].classList.add(classes[1]);
}
}
/** @param {Event} e */
DTBox.prototype.onTimeChange = function(e) {
e.stopPropagation();
if (e.type == 'mousedown') {
this.cancelBlur += 1;
return;
}
var el = e.target;
this[el.name] = parseInt(el.value) || 0;
this.setupFooter();
if (e.type == 'change') {
var self = this;
setTimeout(function(){
self.elem.focus();
}, 50);
}
this.setInputValue();
}
DTBox.prototype.setupFooter = function() {
if (!this.el.footer) {
var footer = document.createElement("div");
var handler = this.onTimeChange.bind(this);
var self = this;
function makeRow(label, name, range, changeHandler) {
var row = document.createElement("div");
row.classList.add('cal-time');
var labelCol = row.appendChild(document.createElement("div"));
labelCol.classList.add('cal-time-label');
labelCol.innerText = label;
var valueCol = row.appendChild(document.createElement("div"));
valueCol.classList.add('cal-time-value');
valueCol.innerText = '00';
var inputCol = row.appendChild(document.createElement("div"));
var slider = inputCol.appendChild(document.createElement("input"));
Object.assign(slider, {step:1, min:0, max:range, name:name, type:'range'});
Object.defineProperty(footer, name, {value: slider});
inputCol.classList.add('cal-time-slider');
slider.onchange = changeHandler;
slider.oninput = changeHandler;
slider.onmousedown = changeHandler;
self[name] = self[name] || parseInt(slider.value) || 0;
footer.appendChild(row)
}
makeRow('HH:', 'hours', 23, handler);
makeRow('MM:', 'minutes', 59, handler);
makeRow('SS:', 'seconds', 59, handler);
footer.classList.add("cal-footer");
Object.defineProperty(this.el, "footer", { value: footer });
tryAppendChild(footer, this.el.wrapper);
}
this.setFooterContent();
}
DTBox.prototype.setFooterContent = function() {
if (this.el.footer) {
var footer = this.el.footer;
footer.hours.value = this.hours;
footer.children[0].children[1].innerText = padded(this.hours, 2);
footer.minutes.value = this.minutes;
footer.children[1].children[1].innerText = padded(this.minutes, 2);
footer.seconds.value = this.seconds;
footer.children[2].children[1].innerText = padded(this.seconds, 2);
}
}
DTBox.prototype.setInputValue = function() {
var date = new Date(this.year, this.month, this.day);
var strings = [];
if (this.settings.config.showDate) {
strings.push(renderDate(date, this.settings));
}
if (this.settings.config.showTime) {
var joined = new Date(date.getTime() + this.time);
strings.push(renderTime(joined, this.settings));
}
this.elem.value = strings.join(', ');
}
DTBox.prototype.onDateSelected = function (e) {
var row = e.target.parentNode;
var date = parseInt(e.target.innerText);
if (!(row.nextSibling && row.nextSibling.nextSibling) && date < 8) {
this.month += 1;
} else if (!(row.previousSibling && row.previousSibling.previousSibling) && date > 7) {
this.month -= 1;
}
this.day = parseInt(e.target.innerText);
this.value = new Date(this.year, this.month, this.day);
this.setInputValue();
this.setHeaderContent();
this.setBodyContent();
}
/** @param {Event} e */
DTBox.prototype.onMonthSelected = function (e) {
var col = 0;
var row = 2;
var cell = e.target;
if (cell.parentNode.nextSibling){
row = cell.parentNode.previousSibling ? 1: 0;
}
if (cell.previousSibling) {
col = 3;
if (cell.nextSibling) {
col = cell.previousSibling.previousSibling ? 2 : 1;
}
}
this.month = 4 * row + col;
this.bodyType = "DAYS";
this.setHeaderContent();
this.setupBody();
}
/** @param {Event} e */
DTBox.prototype.onYearSelected = function (e) {
this.year = parseInt(e.target.innerText);
this.bodyType = "MONTHS";
this.setHeaderContent();
this.setupBody();
}
/** @param {Event} e */
DTBox.prototype.onHeaderChange = function (e) {
var cell = e.target;
if (cell.previousSibling && cell.nextSibling) {
var idx = BODYTYPES.indexOf(this.bodyType);
if (idx < 0 || !BODYTYPES[idx + 1]) {
return;
}
this.bodyType = BODYTYPES[idx + 1];
this.setupBody();
} else {
var sign = cell.previousSibling ? 1 : -1;
switch (this.bodyType) {
case "DAYS":
this.month += sign * 1;
break;
case "MONTHS":
this.year += sign * 1;
break;
case "YEARS":
this.year += sign * 10;
}
if (this.month > 11 || this.month < 0) {
this.year += Math.floor(this.month / 11);
this.month = this.month > 11 ? 0 : 11;
}
}
this.setHeaderContent();
this.setBodyContent();
}
/**
* @param {HTMLElement} elem
* @returns {{left:number, top:number}}
*/
function getOffset(elem) {
var box = elem.getBoundingClientRect();
var left = window.pageXOffset !== undefined ? window.pageXOffset :
(document.documentElement || document.body.parentNode || document.body).scrollLeft;
var top = window.pageYOffset !== undefined ? window.pageYOffset :
(document.documentElement || document.body.parentNode || document.body).scrollTop;
return { left: box.left + left, top: box.top + top };
}
function empty(e) {
for (; e.children.length; ) e.removeChild(e.children[0]);
}
function tryAppendChild(newChild, refNode) {
try {
refNode.appendChild(newChild);
return newChild;
} catch (e) {
console.trace(e);
}
}
/** @class */
function hookFuncs() {
/** @type {Handlers} */
this._funcs = {};
}
/**
* @param {string} key
* @param {Function} func
*/
hookFuncs.prototype.add = function(key, func){
if (!this._funcs[key]){
this._funcs[key] = [];
}
this._funcs[key].push(func)
}
/**
* @param {String} key
* @returns {Function[]} handlers
*/
hookFuncs.prototype.get = function(key){
return this._funcs[key] ? this._funcs[key] : [];
}
/**
* @param {Array.<string>} arr
* @param {String} string
* @returns {Array.<string>} sorted string
*/
function sortByStringIndex(arr, string) {
return arr.sort(function(a, b){
var h = string.indexOf(a);
var l = string.indexOf(b);
var rank = 0;
if (h < l) {
rank = -1;
} else if (l < h) {
rank = 1;
} else if (a.length > b.length) {
rank = -1;
} else if (b.length > a.length) {
rank = 1;
}
return rank;
});
}
/**
* Remove keys from array that are not in format
* @param {string[]} keys
* @param {string} format
* @returns {string[]} new filtered array
*/
function filterFormatKeys(keys, format) {
var out = [];
var formatIdx = 0;
for (var i = 0; i<keys.length; i++) {
var key = keys[i];
if (format.slice(formatIdx).indexOf(key) > -1) {
formatIdx += key.length;
out.push(key);
}
}
return out;
}
/**
* @template {StringNumObj} FormatObj
* @param {string} value
* @param {string} format
* @param {FormatObj} formatObj
* @param {function(Object.<string, hookFuncs>): null} setHooks
* @returns {FormatObj} formatObj
*/
function parseData(value, format, formatObj, setHooks) {
var hooks = {
canSkip: new hookFuncs(),
updateValue: new hookFuncs(),
}
var keys = sortByStringIndex(Object.keys(formatObj), format);
var filterdKeys = filterFormatKeys(keys, format);
var vstart = 0; // value start
if (setHooks) {
setHooks(hooks);
}
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var fstart = format.indexOf(key);
var _vstart = vstart; // next value start
var val = null;
var canSkip = false;
var funcs = hooks.canSkip.get(key);
vstart = vstart || fstart;
for (var j = 0; j < funcs.length; j++) {
if (funcs[j](formatObj)){
canSkip = true;
break;
}
}
if (fstart > -1 && !canSkip) {
var sep = null;
var stop = vstart + key.length;
var fnext = -1;
var nextKeyIdx = i + 1;
_vstart += key.length; // set next value start if current key is found
// get next format token used to determine separator
while (fnext == -1 && nextKeyIdx < keys.length){
var nextKey = keys[nextKeyIdx];
nextKeyIdx += 1;
if (filterdKeys.indexOf(nextKey) === -1) {
continue;
}
fnext = nextKey ? format.indexOf(nextKey) : -1; // next format start
}
if (fnext > -1){
sep = format.slice(stop, fnext);
if (sep) {
var _stop = value.slice(vstart).indexOf(sep);
if (_stop && _stop > -1){
stop = _stop + vstart;
_vstart = stop + sep.length;
}
}
}
val = parseInt(value.slice(vstart, stop));
var funcs = hooks.updateValue.get(key);
for (var k = 0; k < funcs.length; k++) {
val = funcs[k](val, formatObj, vstart, stop);
}
}
formatObj[key] = { index: vstart, value: val };
vstart = _vstart; // set next value start
}
return formatObj;
}
/**
* @param {String} value
* @param {DTS} settings
* @returns {Date} date object
*/
function parseDate(value, settings) {
/** @type {{yyyy:number=, yy:number=, mm:number=, dd:number=}} */
var formatObj = {yyyy:null, yy:null, mm:null, dd:null};
var format = ((settings.dateFormat) || '').toLowerCase();
if (!format) {
throw new TypeError('dateFormat not found (' + settings.dateFormat + ')');
}
var formatObj = parseData(value, format, formatObj, function(hooks){
hooks.canSkip.add("yy", function(data){
return data["yyyy"].value;
});
hooks.updateValue.add("yy", function(val){
return 100 * Math.floor(new Date().getFullYear() / 100) + val;
});
});
var year = formatObj["yyyy"].value || formatObj["yy"].value;
var month = formatObj["mm"].value - 1;
var date = formatObj["dd"].value;
var result = new Date(year, month, date);
return result;
}
/**
* @param {String} value
* @param {DTS} settings
* @returns {Number} time in milliseconds <= (24 * 60 * 60 * 1000) - 1
*/
function parseTime(value, settings) {
var format = ((settings.timeFormat) || '').toLowerCase();
if (!format) {
throw new TypeError('timeFormat not found (' + settings.timeFormat + ')');
}
/** @type {{hh:number=, mm:number=, ss:number=, a:string=}} */
var formatObj = {hh:null, mm:null, ss:null, a:null};
var formatObj = parseData(value, format, formatObj, function(hooks){
hooks.updateValue.add("a", function(val, data, start, stop){
return value.slice(start, start + 2);
});
});
var hours = formatObj["hh"].value;
var minutes = formatObj["mm"].value;
var seconds = formatObj["ss"].value;
var am_pm = formatObj["a"].value;
var am_pm_lower = am_pm ? am_pm.toLowerCase() : am_pm;
if (am_pm && ["am", "pm"].indexOf(am_pm_lower) > -1){
if (am_pm_lower == 'am' && hours == 12){
hours = 0;
} else if (am_pm_lower == 'pm') {
hours += 12;
}
}
var time = hours * 60 * 60 * 1000 + minutes * 60 * 1000 + seconds * 1000;
return time;
}
/**
* @param {Date} value
* @param {DTS} settings
* @returns {String} date string
*/
function renderDate(value, settings) {
var format = settings.dateFormat.toLowerCase();
var date = value.getDate();
var month = value.getMonth() + 1;
var year = value.getFullYear();
var yearShort = year % 100;
var formatObj = {
dd: date < 10 ? "0" + date : date,
mm: month < 10 ? "0" + month : month,
yyyy: year,
yy: yearShort < 10 ? "0" + yearShort : yearShort
};
var str = format.replace(settings.dateFormatRegEx, function (found) {
return formatObj[found];
});
return str;
}
/**
* @param {Date} value
* @param {DTS} settings
* @returns {String} date string
*/
function renderTime(value, settings) {
var Format = settings.timeFormat;
var format = Format.toLowerCase();
var hours = value.getHours();
var minutes = value.getMinutes();
var seconds = value.getSeconds();
var am_pm = null;
var hh_am_pm = null;
if (format.indexOf('a') > -1) {
am_pm = hours >= 12 ? 'pm' : 'am';
am_pm = Format.indexOf('A') > -1 ? am_pm.toUpperCase() : am_pm;
hh_am_pm = hours == 0 ? '12' : (hours > 12 ? hours%12 : hours);
}
var formatObj = {
hh: am_pm ? hh_am_pm : (hours < 10 ? "0" + hours : hours),
mm: minutes < 10 ? "0" + minutes : minutes,
ss: seconds < 10 ? "0" + seconds : seconds,
a: am_pm,
};
var str = format.replace(settings.timeFormatRegEx, function (found) {
return formatObj[found];
});
return str;
}
/**
* checks if two dates are equal
* @param {Date} date1
* @param {Date} date2
* @returns {Boolean} true or false
*/
function isEqualDate(date1, date2) {
if (!(date1 && date2)) return false;
return (date1.getFullYear() == date2.getFullYear() &&
date1.getMonth() == date2.getMonth() &&
date1.getDate() == date2.getDate());
}
/**
* @param {Number} val
* @param {Number} pad
* @param {*} default_val
* @returns {String} padded string
*/
function padded(val, pad, default_val) {
var default_val = default_val || 0;
var valStr = '' + (parseInt(val) || default_val);
var diff = Math.max(pad, valStr.length) - valStr.length;
return ('' + default_val).repeat(diff) + valStr;
}
/**
* @template X
* @template Y
* @param {X} obj
* @param {Y} objDefaults
* @returns {X|Y} merged object
*/
function setDefaults(obj, objDefaults) {
var keys = Object.keys(objDefaults);
for (var i=0; i<keys.length; i++) {
var key = keys[i];
if (!Object.prototype.hasOwnProperty.call(obj, key)) {
obj[key] = objDefaults[key];
}
}
return obj;
}
window.dtsel = Object.create({},{
DTS: { value: DTS },
DTObj: { value: DTBox },
fn: {
value: Object.defineProperties({}, {
empty: { value: empty },
appendAfter: {
value: function (newElem, refNode) {
refNode.parentNode.insertBefore(newElem, refNode.nextSibling);
},
},
getOffset: { value: getOffset },
parseDate: { value: parseDate },
renderDate: { value: renderDate },
parseTime: {value: parseTime},
renderTime: {value: renderTime},
setDefaults: {value: setDefaults},
}),
},
});
})();

View File

@ -0,0 +1,23 @@
function collapse_all() {
document.querySelectorAll("div.reminderContent:not(.creator)").forEach((el) => {
el.classList.add("is-collapsed");
});
}
function expand_all() {
document.querySelectorAll("div.reminderContent:not(.creator)").forEach((el) => {
el.classList.remove("is-collapsed");
});
}
const expandAll = document.querySelector("#expandAll");
expandAll.addEventListener("change", (ev) => {
if (ev.target.value === "expand") {
expand_all();
} else if (ev.target.value === "collapse") {
collapse_all();
}
ev.target.value = "";
});

View File

@ -0,0 +1,94 @@
function get_interval(element) {
let months = element.querySelector('input[name="interval_months"]').value;
let days = element.querySelector('input[name="interval_days"]').value;
let hours = element.querySelector('input[name="interval_hours"]').value;
let minutes = element.querySelector('input[name="interval_minutes"]').value;
let seconds = element.querySelector('input[name="interval_seconds"]').value;
return {
months: parseInt(months) || null,
days: parseInt(days) || null,
seconds:
(parseInt(hours) || 0) * 3600 +
(parseInt(minutes) || 0) * 60 +
(parseInt(seconds) || 0) || null,
};
}
function update_interval(element) {
let months = element.querySelector('input[name="interval_months"]');
let days = element.querySelector('input[name="interval_days"]');
let hours = element.querySelector('input[name="interval_hours"]');
let minutes = element.querySelector('input[name="interval_minutes"]');
let seconds = element.querySelector('input[name="interval_seconds"]');
let interval = get_interval(element);
if (interval.months === null && interval.days === null && interval.seconds === null) {
months.value = "";
days.value = "";
hours.value = "";
minutes.value = "";
seconds.value = "";
} else {
months.value = months.value.padStart(1, "0");
days.value = days.value.padStart(1, "0");
hours.value = hours.value.padStart(2, "0");
minutes.value = minutes.value.padStart(2, "0");
seconds.value = seconds.value.padStart(2, "0");
if (seconds.value >= 60) {
let quotient = Math.floor(seconds.value / 60);
let remainder = seconds.value % 60;
seconds.value = String(remainder).padStart(2, "0");
minutes.value = String(Number(minutes.value) + Number(quotient)).padStart(
2,
"0"
);
}
if (minutes.value >= 60) {
let quotient = Math.floor(minutes.value / 60);
let remainder = minutes.value % 60;
minutes.value = String(remainder).padStart(2, "0");
hours.value = String(Number(hours.value) + Number(quotient)).padStart(2, "0");
}
}
}
const $intervalGroup = document.querySelector(".interval-group");
document.querySelector(".interval-group").addEventListener(
"blur",
(ev) => {
if (ev.target.nodeName !== "BUTTON") update_interval($intervalGroup);
},
true
);
$intervalGroup.querySelector("button.clear").addEventListener("click", () => {
$intervalGroup.querySelectorAll("input").forEach((el) => {
el.value = "";
});
});
document.addEventListener("remindersLoaded", (event) => {
for (reminder of event.detail) {
let $intervalGroup = reminder.node.querySelector(".interval-group");
$intervalGroup.addEventListener(
"blur",
(ev) => {
if (ev.target.nodeName !== "BUTTON") update_interval($intervalGroup);
},
true
);
$intervalGroup.querySelector("button.clear").addEventListener("click", () => {
$intervalGroup.querySelectorAll("input").forEach((el) => {
el.value = "";
});
});
}
});

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,2 @@
/*! js-cookie v3.0.0-rc.0 | MIT */
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self,function(){var r=e.Cookies,n=e.Cookies=t();n.noConflict=function(){return e.Cookies=r,n}}())}(this,function(){"use strict";function e(e){for(var t=1;t<arguments.length;t++){var r=arguments[t];for(var n in r)e[n]=r[n]}return e}var t={read:function(e){return e.replace(/%3B/g,";")},write:function(e){return e.replace(/;/g,"%3B")}};return function r(n,i){function o(r,o,u){if("undefined"!=typeof document){"number"==typeof(u=e({},i,u)).expires&&(u.expires=new Date(Date.now()+864e5*u.expires)),u.expires&&(u.expires=u.expires.toUTCString()),r=t.write(r).replace(/=/g,"%3D"),o=n.write(String(o),r);var c="";for(var f in u)u[f]&&(c+="; "+f,!0!==u[f]&&(c+="="+u[f].split(";")[0]));return document.cookie=r+"="+o+c}}return Object.create({set:o,get:function(e){if("undefined"!=typeof document&&(!arguments.length||e)){for(var r=document.cookie?document.cookie.split("; "):[],i={},o=0;o<r.length;o++){var u=r[o].split("="),c=u.slice(1).join("="),f=t.read(u[0]).replace(/%3D/g,"=");if(i[f]=n.read(c,f),e===f)break}return e?i[e]:i}},remove:function(t,r){o(t,"",e({},r,{expires:-1}))},withAttributes:function(t){return r(this.converter,e({},this.attributes,t))},withConverter:function(t){return r(e({},this.converter,t),this.attributes)}},{attributes:{value:Object.freeze(i)},converter:{value:Object.freeze(n)}})}(t,{path:"/"})});

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,16 @@
const REPORTER_ID = crypto.randomUUID();
window.addEventListener("error", async (ev) => {
await fetch("/report", {
method: "POST",
body: JSON.stringify({
reporterId: REPORTER_ID,
url: window.location.href,
relativeTimestamp: ev.timeStamp,
errorMessage: ev.message,
errorLine: ev.lineno,
errorFile: ev.filename,
errorType: ev.type,
}),
});
});

View File

@ -0,0 +1,70 @@
let guildReminders = document.querySelector("#guildReminders");
function sort_by(cond) {
if (cond === "channel") {
[...guildReminders.children]
.sort((a, b) => {
let channel1 = a.querySelector("select.channel-selector").value;
let channel2 = b.querySelector("select.channel-selector").value;
return channel1 > channel2 ? 1 : -1;
})
.forEach((node) => guildReminders.appendChild(node));
// go through and add channel categories
let currentChannelGroup = null;
for (let child of guildReminders.querySelectorAll("div.reminderContent")) {
let thisChannelGroup = child.querySelector("select.channel-selector").value;
if (currentChannelGroup !== thisChannelGroup) {
let newNode = document.createElement("div");
newNode.textContent =
"#" + channels.find((a) => a.id === thisChannelGroup).name;
newNode.classList.add("channel-tag");
guildReminders.insertBefore(newNode, child);
currentChannelGroup = thisChannelGroup;
}
}
} else {
// remove any channel tags if previous ordering was by channel
guildReminders.querySelectorAll("div.channel-tag").forEach((el) => {
el.remove();
});
if (cond === "time") {
[...guildReminders.children]
.sort((a, b) => {
let time1 = luxon.DateTime.fromISO(
a.querySelector('input[name="time"]').value
);
let time2 = luxon.DateTime.fromISO(
b.querySelector('input[name="time"]').value
);
return time1 > time2 ? 1 : -1;
})
.forEach((node) => guildReminders.appendChild(node));
} else {
[...guildReminders.children]
.sort((a, b) => {
let name1 = a.querySelector('input[name="name"]').value;
let name2 = b.querySelector('input[name="name"]').value;
return name1 > name2 ? 1 : -1;
})
.forEach((node) => guildReminders.appendChild(node));
}
}
}
const selector = document.querySelector("#orderBy");
selector.addEventListener("change", () => {
sort_by(selector.value);
});
document.addEventListener("remindersLoaded", () => {
sort_by(selector.value);
});

Some files were not shown because too many files have changed in this diff Show More