Compare commits

...

122 Commits

Author SHA1 Message Date
renovate[bot]
4169d88acb Update python Docker tag to v3.14 2025-10-08 06:57:12 +00:00
renovate[bot]
e0ecb56b14 Update astral-sh/setup-uv action to v7 (#201)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-08 00:47:50 +00:00
069760fb5e Update Docker actions to latest versions in docker-publish.yml 2025-10-02 22:12:20 +02:00
renovate[bot]
b07d5a8dc5 Update astral-sh/setup-uv digest to 59a0868 (#199)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-02 20:08:53 +00:00
3691a2d812 Add new words to cSpell dictionary in settings.json 2025-10-02 22:04:59 +02:00
7c169f4329 Update pre-commit hooks versions and remove unused imports in main.py 2025-10-02 22:04:50 +02:00
9c2a2e66c5 Remove uv.lock 2025-10-02 22:03:34 +02:00
renovate[bot]
0bb55726fe Update astral-sh/setup-uv digest to f2859da (#198)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-02 18:46:47 +00:00
renovate[bot]
b678e7784a Update python:3.13-slim Docker digest to 5f55cdf (#197)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-02 05:54:03 +00:00
renovate[bot]
90a9a17a63 Update python:3.13-slim Docker digest to 60df8d2 (#196)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-02 01:28:05 +00:00
renovate[bot]
d16da0de6f Update python:3.13-slim Docker digest to a6196d2 (#195)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-01 20:39:26 +00:00
renovate[bot]
d051b96069 Update astral-sh/setup-uv digest to 82f21a5 (#194)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-01 01:45:02 +00:00
renovate[bot]
49dd097948 Update python:3.13-slim Docker digest to 3a6ead7 (#193)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-30 21:42:53 +00:00
renovate[bot]
992e3ae3c4 Update astral-sh/setup-uv digest to d0cc045 (#192)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-30 17:39:39 +00:00
renovate[bot]
ee94587a0b Update python:3.13-slim Docker digest to 6cbc435 (#191)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-30 10:33:28 +00:00
renovate[bot]
03c76f1154 Update docker/login-action digest to 5e57cd1 (#190)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-29 11:08:53 +00:00
renovate[bot]
cf11b1b5a5 Update astral-sh/setup-uv digest to 2841f9f (#189)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-26 10:49:15 +00:00
renovate[bot]
2341932af5 Update astral-sh/setup-uv digest to c7d85d9 (#188)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-23 09:30:56 +00:00
renovate[bot]
02052d5e2a Update astral-sh/setup-uv digest to b75a909 (#187)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-14 17:34:26 +00:00
renovate[bot]
ec17bc35d9 Update astral-sh/setup-uv digest to ffff8aa (#186)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-14 13:04:07 +00:00
renovate[bot]
431c9c1a27 Update python:3.13-slim Docker digest to 58c30f5 (#185)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-14 02:39:00 +00:00
renovate[bot]
35c45fc230 Update docker/login-action digest to 5b7b28b (#184)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-13 23:25:07 +00:00
renovate[bot]
17d2b6956b Update docker/setup-qemu-action digest to e77e806 (#183)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-12 04:52:02 +00:00
renovate[bot]
0e91dcfb82 Update astral-sh/setup-uv digest to f67343a (#182)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-11 09:26:52 +00:00
renovate[bot]
3d6680b4bd Update astral-sh/setup-uv digest to 4dd9f52 (#181)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-11 06:13:33 +00:00
renovate[bot]
81c34a3446 Update python:3.13-slim Docker digest to 1bca020 (#180)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-09 10:39:22 +00:00
renovate[bot]
8565243c4c Update python:3.13-slim Docker digest to 8ebd0ea (#179)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-09 05:59:14 +00:00
renovate[bot]
8ecbce4d12 Update python:3.13-slim Docker digest to f71edd7 (#178)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-09 01:54:13 +00:00
renovate[bot]
6208da86c0 Update docker/login-action digest to bdf14dc (#177)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-08 17:41:52 +00:00
renovate[bot]
b8bdf824a6 Update astral-sh/setup-uv digest to e1e6fe7 (#176)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-06 16:10:31 +00:00
renovate[bot]
cd10e20cab Update astral-sh/setup-uv digest to 26cf676 (#175)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-31 10:40:57 +00:00
renovate[bot]
bfb2b0b81e Update astral-sh/setup-uv digest to 4e1e303 (#174)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-22 11:44:18 +00:00
renovate[bot]
7f84da5b9c Update astral-sh/setup-uv digest to 4959332 (#172)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-22 00:00:37 +00:00
renovate[bot]
79f16a44a3 Update docker/setup-buildx-action digest to 1583c0f (#173)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-21 18:57:16 +00:00
renovate[bot]
18008be8f3 Update astral-sh/setup-uv digest to f758a4a (#171)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-19 23:40:42 +00:00
renovate[bot]
962c369b88 Update python:3.13-slim Docker digest to 27f90d7 (#170)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-18 18:31:07 +00:00
renovate[bot]
87ed08d250 Update docker/setup-buildx-action digest to 4cc794f (#169)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-18 13:16:37 +00:00
renovate[bot]
37ddfd24c3 Update python:3.13-slim Docker digest to 4d55aff (#168)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-16 04:36:35 +00:00
renovate[bot]
3c0638ba47 Update python:3.13-slim Docker digest to 4112a75 (#167)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-16 00:49:59 +00:00
renovate[bot]
5dbac43b27 Update astral-sh/setup-uv digest to c0e7e93 (#165)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-15 02:06:42 +00:00
renovate[bot]
3a268c0cb5 Update python:3.13-slim Docker digest to 2a928e1 (#166)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-14 21:34:12 +00:00
renovate[bot]
d8d57a6581 Update astral-sh/setup-uv digest to fda2399 (#164)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-14 10:32:58 +00:00
renovate[bot]
35b1874714 Update python:3.13-slim Docker digest to 201e1ab (#163)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-13 21:30:58 +00:00
renovate[bot]
1fc3482c36 Update actions/checkout digest to ff7abcd (#162)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-13 16:32:16 +00:00
renovate[bot]
dd7e51fc3f Update python:3.13-slim Docker digest to 6a135f8 (#161)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-13 10:48:19 +00:00
renovate[bot]
ea4f3882f9 Update python:3.13-slim Docker digest to faa4eb6 (#160)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-13 07:07:37 +00:00
renovate[bot]
c89faf85db Update astral-sh/setup-uv digest to d9e0f98 (#159)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-13 04:08:19 +00:00
renovate[bot]
364f1b8aea Update astral-sh/setup-uv digest to e5d42a2 (#158)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-12 20:39:58 +00:00
renovate[bot]
ffaae102c5 Update astral-sh/setup-uv digest to 1463845 (#157)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-12 10:35:48 +00:00
renovate[bot]
3f489edb37 Update docker/setup-buildx-action digest to af1b253 (#156)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-11 16:50:56 +00:00
renovate[bot]
ca29c06c81 Update actions/checkout digest to 08c6903 (#155)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-11 13:53:33 +00:00
renovate[bot]
dd9dab3f8b Update astral-sh/setup-uv digest to ad5ded2 (#154)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-09 09:13:19 +00:00
renovate[bot]
a0a7519f14 Update python:3.13-slim Docker digest to 6f79e7a (#153)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-07 21:45:55 +00:00
renovate[bot]
4ef58a394b Update docker/setup-buildx-action digest to 2c8bcda (#152)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-07 08:49:49 +00:00
renovate[bot]
d2ffd0c9c5 Update docker/setup-buildx-action digest to c65d441 (#151)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-06 17:40:43 +00:00
renovate[bot]
e81db87cd4 Update astral-sh/setup-uv digest to 1422404 (#150)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-06 12:47:16 +00:00
renovate[bot]
c11714fe8c Update docker/setup-buildx-action digest to ae7d689 (#149)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-05 17:31:50 +00:00
renovate[bot]
d46b648134 Update docker/login-action digest to 184bdaa (#148)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-04 16:26:58 +00:00
renovate[bot]
747b031eca Update docker/login-action digest to ef38ec3 (#147)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-01 14:32:52 +00:00
renovate[bot]
ae0a83c1f0 Update astral-sh/setup-uv digest to 6324490 (#146)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-01 01:31:56 +00:00
renovate[bot]
d799845ba7 Update astral-sh/setup-uv digest to 2a967c9 (#145)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-31 08:55:33 +00:00
renovate[bot]
84ec0e673b Update astral-sh/setup-uv digest to 43f3736 (#144)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-26 13:38:51 +00:00
renovate[bot]
963c8f4e3a Update astral-sh/setup-uv digest to 4fb0c07 (#143)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-25 05:27:25 +00:00
renovate[bot]
28bf3679de Update python:3.13-slim Docker digest to 4c2cf99 (#142)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-24 02:09:28 +00:00
renovate[bot]
cb8213aa74 Update astral-sh/setup-uv digest to e92bafb (#141)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-23 23:07:25 +00:00
renovate[bot]
c5de3bfe71 Update actions/checkout digest to 8edcb1b (#140)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-23 17:56:54 +00:00
renovate[bot]
7025fafb72 Update astral-sh/setup-uv digest to 23482a3 (#139)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-23 11:59:04 +00:00
renovate[bot]
453a44f125 Update python:3.13-slim Docker digest to 1020ca4 (#138)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-22 21:10:35 +00:00
renovate[bot]
1cdbcf1d96 Update python:3.13-slim Docker digest to 9248661 (#137)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-22 15:04:51 +00:00
renovate[bot]
e930a3bd9b Update python:3.13-slim Docker digest to 97fe872 (#136)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-22 07:57:32 +00:00
renovate[bot]
3a340d622f Update python:3.13-slim Docker digest to ed8ae2e (#135)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-22 03:33:13 +00:00
c665ea9ea9 Add Sentry log filtering to reduce noise in error reporting 2025-07-19 00:40:41 +02:00
c8d7e059b2 Refactor Sentry integration: move initialization to RemindBotClient and enhance error context logging 2025-07-18 23:03:49 +02:00
a99c381bec Enhance message sending reliability by adding a delay before sending and improving error logging for bot state issues 2025-07-18 21:37:14 +02:00
bb752cd6cc Merge branch 'master' of https://github.com/TheLovinator1/discord-reminder-bot 2025-07-18 21:24:10 +02:00
c3161c2e0a Update dependencies: aiohttp to 3.12.14, certifi to 2025.7.14, orjson to 3.11.0, and sentry-sdk to 2.33.0 2025-07-18 21:24:04 +02:00
ca3c3bbb1b Enhance error handling and logging in Discord message sending functions
- Added detailed bot state information to error messages when sending fails.
- Implemented early validation of bot readiness and closure in `send_to_discord`.
- Introduced `safe_send_to_discord` to manage bot state issues and prevent '_MissingSentinel' errors.
- Improved logging for better debugging of bot state and HTTP client status.
2025-07-18 21:23:35 +02:00
renovate[bot]
4f08d4b73f Update astral-sh/setup-uv digest to 4ac06a0 (#134)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-18 13:15:51 +00:00
renovate[bot]
478138242d Update astral-sh/setup-uv digest to 05273c1 (#133)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-17 23:33:42 +00:00
renovate[bot]
eaa231bf5a Update astral-sh/setup-uv digest to c893ac1 (#132)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-15 10:38:59 +00:00
renovate[bot]
e1910d6cf8 Update astral-sh/setup-uv digest to a905f00 (#131)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-10 07:11:06 +00:00
4f5d62b30d Merge branch 'master' of https://github.com/TheLovinator1/discord-reminder-bot 2025-07-07 06:02:20 +02:00
renovate[bot]
6edc70d6e1 Update astral-sh/setup-uv digest to d4219d1 (#129)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Joakim Hellsén <tlovinator@gmail.com>
2025-07-07 04:00:16 +00:00
renovate[bot]
6f9a2cc995 Update docker/setup-qemu-action digest to 05340d1 (#130)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Joakim Hellsén <tlovinator@gmail.com>
2025-07-07 04:00:02 +00:00
7c39f5fdc8 Merge branch 'master' of https://github.com/TheLovinator1/discord-reminder-bot 2025-07-07 05:57:23 +02:00
8237b3ac46 Add WEBHOOK_URL to environment variables in docker-compose.yml 2025-07-07 05:57:18 +02:00
renovate[bot]
2ff50ba2cd Update actions/checkout digest to 09d2aca (#128)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-07 03:55:54 +00:00
09cccd51cc Update SQLITE_LOCATION environment variable path in Dockerfile 2025-07-07 05:41:24 +02:00
c7c761a8cd Update Docker image reference to use GitHub Container Registry 2025-07-07 05:36:46 +02:00
e58dab8163 Refactor Dockerfile and docker-compose.yml to set environment variables directly and remove unused variables 2025-07-07 05:33:37 +02:00
1508147399 Go back to GitHub workflows 2025-07-07 05:14:13 +02:00
2f6f74c82f Activate .gitea workflow 2025-07-07 04:57:19 +02:00
59edb2a41f Merge pull request 'Rewrite bot' (!1) from kewlrewrite into master
Reviewed-on: #1
2025-07-07 04:50:47 +02:00
fe559c41a9 Enhance missed job notifications with markdown timestamp and improve webhook function signature 2025-07-07 04:46:30 +02:00
d7ea1c9ec4 Improve message sent to Discord for missed reminder 2025-07-06 06:02:05 +02:00
9f2814a3d5 Add message modification support for interval and cron jobs in modals 2025-07-06 04:06:44 +02:00
4dde52ec04 Refactor reminder modification modals to support date, cron, and interval triggers 2025-07-06 02:28:36 +02:00
c6d8df3d80 Add helper functions and modal for modifying APScheduler jobs 2025-07-06 00:38:39 +02:00
77575b0934 Implement modal for modifying APScheduler jobs 2025-07-05 23:28:15 +02:00
e5c662ba20 Add job id to logger.error when failing to format the __getstate__ dictionary for Discord markdown 2025-07-05 04:00:57 +02:00
b97e8260ae Debug with module instead of starting main.py 2025-07-05 03:51:25 +02:00
993a8fd6d9 Start scheduler after syncing commands 2025-07-04 23:39:26 +02:00
480b36ad85 Update Dockerfile to specify Python image digest and add support for build checks 2025-07-04 05:58:36 +02:00
679fedb099 Update Dockerfile to run the main module directly with Python 2025-07-04 02:03:25 +02:00
12f705418a Log timezone; add more data to /remind list 2025-07-03 06:04:24 +02:00
865cd9ba6d Refactor state generation functions for improved markdown formatting; simplify error messages and enhance UI job display 2025-07-02 23:40:32 +02:00
1cf10fc7a9 Update multidict package to version 6.6.3 2025-07-02 06:31:06 +02:00
9299ab800d Refactor reminder job handling and improve UI formatting; add pagination for reminders 2025-07-02 06:30:53 +02:00
acf742c91c Add Discord session ID to logs 2025-07-01 21:45:06 +02:00
fd826da6e4 Stuff and things 2025-07-01 20:10:29 +02:00
b87639910b Update installation instructions and bump Python requirement to 3.13; remove Poetry and nox; update pre-commit hooks. 2025-06-29 04:24:25 +02:00
41c03e10f6 Comment out push, pull_request, and schedule triggers in workflow files 2025-04-12 14:23:39 +02:00
5ec31ba126 Add Nox to development dependencies and format dependency entries 2025-04-12 11:35:50 +02:00
52d8501ef2 Remove SECURITY.md 2025-04-12 11:34:53 +02:00
12c5ece487 Add pause and unpause commands for reminders 2025-04-12 10:59:29 +02:00
7b192fc425 Ignore SQLite database backup files 2025-04-11 21:32:32 +02:00
d55f1993e8 Add remove command for reminders 2025-04-08 17:57:50 +02:00
3e5e23591d Update Copilot instructions 2025-03-04 23:10:24 +01:00
0a2fd88cc0 Make Copilot Instructions shorter 2025-03-02 17:46:47 +01:00
e32d149722 Refactor GitHub Copilot instructions 2025-03-02 05:21:28 +01:00
d583154857 Add GitHub Copilot instructions 2025-03-02 05:17:39 +01:00
36dcf8d376 Add get_human_readable_time function and update reminder summary formatting 2025-03-02 04:37:42 +01:00
20 changed files with 1379 additions and 499 deletions

View File

@@ -15,6 +15,15 @@ TIMEZONE=
# On Linux you will need to use double slashes before the path to get the absolute path.
# SQLITE_LOCATION=//home/lovinator/foo.db
# Additional directory to store data in.
# Note: You will still need to set the SQLITE_LOCATION to a valid path.
# This is used to store markdown files with the reminder data.
# The directory will be created if it does not exist.
# Example: DATA_DIR=C:/Code/discord-reminder-bot/data
# Example: DATA_DIR=/home/lovinator/data
# Example: DATA_DIR=./data
DATA_DIR=./data
# Log level, CRITICAL, ERROR, WARNING, INFO, DEBUG.
LOG_LEVEL=INFO

9
.github/SECURITY.md vendored
View File

@@ -1,9 +0,0 @@
# Reporting a Vulnerability
You can report a vulnerability by creating an issue on this repo. If you want to report it privately, you can email me at [tlovinator@gmail.com](mailto:tlovinator@gmail.com).
There is also [GitHub Security Advisories](https://github.com/TheLovinator1/discord-reminder-bot/security/advisories/new) if you want to be try-hard.
I am also available on Discord at `TheLovinator#9276`.
Thanks :-)

33
.github/copilot-instructions.md vendored Normal file
View File

@@ -0,0 +1,33 @@
This is a Discord.py bot that allows you to set date, cron and interval reminders with APScheduler. Dates are parsed using dateparser.
Use try-except blocks, type hints, f-strings, logging, and Google style docstrings.
Add helpful message when using assert in tests.
Docstrings that doesn't return anything should not have a return section.
A function docstring should describe the function's behavior, arguments, side effects, exceptions, return values, and any other information that may be relevant to the user.
Including the exception object in the log message is redundant.
We use GitHub.
Channel reminders have the following kwargs: "channel_id", "message", "author_id".
User DM reminders have the following kwargs: "user_id", "guild_id", "message".
Bot has the following commands:
"/remind add message:<str> time:<str> dm_and_current_channel:<bool> user:<user> channel:<channel>"
"/remind remove id:<job_id>"
"/remind edit id:<job_id>"
"/remind pause_unpause id:<job_id>"
"/remind list"
"/remind cron message:<str> year:<int> month:<int> day:<int> week:<int> day_of_week:<str> hour:<int> minute:<int> second:<int> start_date:<str> end_date:<str> timezone:<str> jitter:<int> channel:<channel> user:<user> dm_and_current_channel:<bool>"
"/remind interval message:<str> weeks:<int> days:<int> hours:<int> minutes:<int> seconds:<int> start_date:<str> end_date:<str> timezone:<str> jitter:<int> channel:<channel> user:<user> dm_and_current_channel:<bool>"

View File

@@ -5,7 +5,7 @@ on:
pull_request:
workflow_dispatch:
schedule:
- cron: "0 6 * * *"
- cron: '0 16 * * 0'
env:
BOT_TOKEN: ${{ secrets.BOT_TOKEN }}
@@ -20,38 +20,41 @@ jobs:
contents: read
packages: write
steps:
- run: |
if [ -z "${{ env.BOT_TOKEN }}" ]; then
echo "BOT_TOKEN not set"
exit 1
fi
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
with:
python-version: 3.13
- run: uv sync --all-extras --dev
- run: uv run pytest
- uses: docker/setup-qemu-action@v3
with:
platforms: all
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
if: github.event_name != 'pull_request'
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- uses: docker/login-action@v3
if: github.event_name != 'pull_request'
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/checkout@v5
- uses: astral-sh/setup-uv@v7
with:
python-version: 3.13
- run: uv sync --all-extras --dev
- run: uv run pytest
- uses: docker/setup-qemu-action@v3
with:
platforms: all
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64, linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: thelovinator/discord-reminder-bot:latest
- uses: docker/build-push-action@v6
with:
context: .

View File

@@ -1,38 +0,0 @@
name: Poetry
on:
push:
pull_request:
workflow_dispatch:
schedule:
- cron: "0 6 * * *"
env:
BOT_TOKEN: ${{ secrets.BOT_TOKEN }}
TIMEZONE: Europe/Stockholm
LOG_LEVEL: Info
SQLITE_LOCATION: /data/jobs.sqlite
jobs:
test-on-poetry:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
poetry-version: ["latest", "main"]
steps:
- run: |
if [ -z "${{ env.BOT_TOKEN }}" ]; then
echo "BOT_TOKEN not set"
exit 1
fi
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- uses: abatilo/actions-poetry@v4
with:
poetry-version: ${{ matrix.poetry-version }}
- run: poetry install
- run: poetry run pytest

View File

@@ -1,41 +0,0 @@
name: uv
on:
push:
pull_request:
workflow_dispatch:
schedule:
- cron: "0 6 * * *"
env:
BOT_TOKEN: ${{ secrets.BOT_TOKEN }}
TIMEZONE: Europe/Stockholm
LOG_LEVEL: Info
SQLITE_LOCATION: /data/jobs.sqlite
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
jobs:
test-on-uv:
name: Install with uv and run tests on Python ${{ matrix.python-version }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "pypy"]
steps:
- run: |
if [ -z "${{ env.BOT_TOKEN }}" ]; then
echo "BOT_TOKEN not set"
exit 1
fi
- name: Checkout code
uses: actions/checkout@v4
- name: Install uv and set the python version to ${{ matrix.python-version }}
uses: astral-sh/setup-uv@v5
with:
python-version: ${{ matrix.python-version }}
version: "latest"
- name: Install dependencies
run: uv sync --all-extras --dev
- name: Run tests
run: uv run pytest

52
.gitignore vendored
View File

@@ -1,6 +1,6 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*.py[codz]
*$py.class
# C extensions
@@ -46,7 +46,7 @@ htmlcov/
nosetests.xml
coverage.xml
*.cover
*.py,cover
*.py.cover
.hypothesis/
.pytest_cache/
cover/
@@ -106,17 +106,24 @@ uv.lock
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
poetry.lock
poetry.toml
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
pdm.lock
pdm.toml
.pdm-python
.pdm-build/
# pixi
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
#pixi.lock
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
# in the .venv directory. It is recommended not to include this directory in version control.
.pixi
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
@@ -129,6 +136,7 @@ celerybeat.pid
# Environments
.env
.envrc
.venv
env/
venv/
@@ -167,8 +175,38 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Abstra
# Abstra is an AI-powered process automation framework.
# Ignore directories containing user credentials, local state, and settings.
# Learn more at https://abstra.io/docs
.abstra/
# Visual Studio Code
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
# and can be added to the global gitignore or merged into this file. However, if you prefer,
# you could uncomment the following to ignore the entire vscode folder
# .vscode/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
# Cursor
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
# refer to https://docs.cursor.com/context/ignore-files
.cursorignore
.cursorindexingignore
# Marimo
marimo/_static/
marimo/_lsp/
__marimo__/
# SQLite
*.sqlite
*.sqlite.*
data/reminder_data/*

View File

@@ -1,11 +1,11 @@
repos:
- repo: https://github.com/asottile/add-trailing-comma
rev: v3.1.0
rev: v3.2.0
hooks:
- id: add-trailing-comma
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
rev: v6.0.0
hooks:
- id: check-added-large-files
- id: check-ast
@@ -23,13 +23,13 @@ repos:
- id: trailing-whitespace
- repo: https://github.com/asottile/pyupgrade
rev: v3.19.1
rev: v3.20.0
hooks:
- id: pyupgrade
args: ["--py310-plus"]
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.6
rev: v0.13.3
hooks:
- id: ruff-format
- id: ruff

3
.vscode/launch.json vendored
View File

@@ -5,8 +5,7 @@
"name": "Python Debugger: Start bot",
"type": "debugpy",
"request": "launch",
"program": "${workspaceFolder}/discord_reminder_bot/main.py",
"console": "integratedTerminal",
"module": "discord_reminder_bot.main"
}
]
}

View File

@@ -1,6 +1,7 @@
{
"cSpell.words": [
"aiohttp",
"ambiguious",
"apscheduler",
"asctime",
"asyncio",
@@ -24,14 +25,17 @@
"levelname",
"loguru",
"Lovinator",
"McCabe",
"pycodestyle",
"pydocstyle",
"pyproject",
"pypy",
"pytest",
"PYTHONDONTWRITEBYTECODE",
"PYTHONUNBUFFERED",
"pyupgrade",
"sqlalchemy",
"strptime",
"thelovinator",
"uvloop"
],

View File

@@ -1,13 +1,24 @@
FROM python:3.13-slim
# syntax=docker/dockerfile:1
# check=error=true;experimental=all
FROM python:3.14-slim
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
RUN useradd -m botuser && mkdir -p /home/botuser/data
WORKDIR /home/botuser
COPY interactions /home/botuser/interactions
COPY discord_reminder_bot /home/botuser/discord_reminder_bot
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --no-install-project
ENV DATA_DIR=/home/botuser/data
ENV SQLITE_LOCATION=/data/jobs.sqlite
VOLUME ["/home/botuser/data/"]
CMD ["uv", "run", "discord_reminder_bot/main.py"]
CMD ["uv", "run", "python", "-m", "discord_reminder_bot.main"]

View File

@@ -31,30 +31,18 @@ using [Docker](https://hub.docker.com/r/thelovinator/discord-reminder-bot).
### Install directly on your computer
- Install the latest version of needed software:
- [Python](https://www.python.org/)
- You should use the latest version.
- You want to add Python to your PATH.
- Windows: Find `App execution aliases` and disable python.exe and python3.exe
- [Poetry](https://python-poetry.org/docs/master/#installation)
- Windows: You have to add `%appdata%\Python\Scripts` to your PATH for Poetry to work.
- `powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"`
- Download project from GitHub with Git or download
the [ZIP](https://github.com/TheLovinator1/discord-reminder-bot/archive/refs/heads/master.zip).
- If you want to update the bot, you can run `git pull` in the project folder or download the ZIP again.
- Rename .env.example to .env and open it in a text editor (e.g., VSCode, Notepad++, Notepad).
- If you can't see the file extension:
- Windows 10: Click the View Tab in File Explorer and click the box next to File name extensions.
- Windows 11: Click View -> Show -> File name extensions.
- Open a terminal in the repository folder.
- Windows 10: <kbd>Shift</kbd> + <kbd>right-click</kbd> in the folder and select `Open PowerShell window here`
- Windows 11: <kbd>Shift</kbd> + <kbd>right-click</kbd> in the folder and Show more options
and `Open PowerShell window here`
- Install requirements:
- Type `poetry install` into the PowerShell window. Make sure you are
in the repository folder with the [pyproject.toml](pyproject.toml) file.
- You may have to restart your terminal if it can't find the `poetry` command. Also double check it is in
your PATH.
- Start the bot:
- Type `poetry run bot` into the PowerShell window.
- Type `uv run .\discord_reminder_bot\main.py` into the PowerShell window.
- You can stop the bot with <kbd>Ctrl</kbd> + <kbd>c</kbd>.
Note: You will need to run `poetry install` again if poetry.lock has been modified.

View File

@@ -0,0 +1,160 @@
from __future__ import annotations
import datetime
import json
import os
from typing import Any
from zoneinfo import ZoneInfo
import dateparser
from apscheduler.job import Job
from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.date import DateTrigger
from apscheduler.triggers.interval import IntervalTrigger
from loguru import logger
from interactions.api.models.misc import Snowflake
def calculate(job: Job) -> str:
"""Calculate the time left for a job.
Args:
job: The job to calculate the time for.
Returns:
str: The time left for the job or "Paused" if the job is paused or has no next run time.
"""
trigger_time = None
if isinstance(job.trigger, DateTrigger | IntervalTrigger):
trigger_time = job.next_run_time or None
elif isinstance(job.trigger, CronTrigger):
if not job.next_run_time:
logger.debug(f"No next run time found for '{job.id}', probably paused? {job.__getstate__()}")
return "Paused"
trigger_time = job.trigger.get_next_fire_time(None, datetime.datetime.now(tz=job._scheduler.timezone)) # noqa: SLF001
logger.debug(f"{type(job.trigger)=}, {trigger_time=}")
if not trigger_time:
logger.debug("No trigger time found")
return "Paused"
return f"<t:{int(trigger_time.timestamp())}:R>"
def get_human_readable_time(job: Job) -> str:
"""Get the human-readable time for a job.
Args:
job: The job to get the time for.
Returns:
str: The human-readable time.
"""
trigger_time = None
if isinstance(job.trigger, DateTrigger | IntervalTrigger):
trigger_time = job.next_run_time or None
elif isinstance(job.trigger, CronTrigger):
if not job.next_run_time:
logger.debug(f"No next run time found for '{job.id}', probably paused? {job.__getstate__()}")
return "Paused"
trigger_time = job.trigger.get_next_fire_time(None, datetime.datetime.now(tz=job._scheduler.timezone)) # noqa: SLF001
if not trigger_time:
logger.debug("No trigger time found")
return "Paused"
return trigger_time.strftime("%Y-%m-%d %H:%M:%S")
def parse_time(date_to_parse: str | None, timezone: str | None = os.getenv("TIMEZONE")) -> datetime.datetime | None:
"""Parse a date string into a datetime object.
Args:
date_to_parse(str): The date string to parse.
timezone(str, optional): The timezone to use. Defaults to the TIMEZONE environment variable.
Returns:
datetime.datetime: The parsed datetime object.
"""
if not date_to_parse:
logger.error("No date provided to parse.")
return None
if not timezone:
logger.error("No timezone provided to parse date.")
return None
logger.info(f"Parsing date: '{date_to_parse}' with timezone: '{timezone}'")
try:
parsed_date: datetime.datetime | None = dateparser.parse(
date_string=date_to_parse,
settings={
"PREFER_DATES_FROM": "future",
"TIMEZONE": f"{timezone}",
"RETURN_AS_TIMEZONE_AWARE": True,
"RELATIVE_BASE": datetime.datetime.now(tz=ZoneInfo(str(timezone))),
},
)
except (ValueError, TypeError) as e:
logger.error(f"Failed to parse date: '{date_to_parse}' with timezone: '{timezone}'. Error: {e}")
return None
logger.debug(f"Parsed date: {parsed_date} from '{date_to_parse}'")
return parsed_date
def generate_state(state: dict[str, Any], job: Job) -> str:
"""Format the __getstate__ dictionary for Discord markdown.
Args:
state (dict): The __getstate__ dictionary.
job (Job): The APScheduler job.
Returns:
str: The formatted string.
"""
if not state:
logger.error(f"No state found for {job.id}")
return "No state found.\n"
for key, value in state.items():
if isinstance(value, IntervalTrigger):
state[key] = "IntervalTrigger"
elif isinstance(value, DateTrigger):
state[key] = "DateTrigger"
elif isinstance(value, Job):
state[key] = "Job"
elif isinstance(value, Snowflake):
state[key] = str(value)
try:
msg: str = json.dumps(state, indent=4, default=str)
except TypeError as e:
e.add_note("This is likely due to a non-serializable object in the state. Please check the state for any non-serializable objects.")
e.add_note(f"{state=}")
logger.error(f"Failed to serialize state: {e}")
return "Failed to serialize state."
return msg
def generate_markdown_state(state: dict[str, Any], job: Job) -> str:
"""Format the __getstate__ dictionary for Discord markdown.
Args:
state (dict): The __getstate__ dictionary.
job (Job): The APScheduler job.
Returns:
str: The formatted string.
"""
msg: str = generate_state(state=state, job=job)
return "```json\n" + msg + "\n```"

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,405 @@
from __future__ import annotations
import traceback
from typing import TYPE_CHECKING
import discord
from discord.utils import escape_markdown
from loguru import logger
from discord_reminder_bot.helpers import calculate, parse_time
from discord_reminder_bot.settings import scheduler
if TYPE_CHECKING:
import datetime
from apscheduler.job import Job
class DateReminderModifyModal(discord.ui.Modal, title="Modify reminder"):
"""Modal for modifying a date-based APScheduler job (one-time reminder)."""
def __init__(self, job: Job) -> None:
"""Initialize the modal for modifying a date-based reminder.
Args:
job (Job): The APScheduler job to modify. Must be a date-based job.
"""
super().__init__(title="Modify Reminder")
self.job = job
self.job_id = job.id
self.message_input = discord.ui.TextInput(
label="Reminder message",
default=job.kwargs.get("message", ""),
placeholder="What do you want to be reminded of?",
max_length=200,
)
# Only allow editing the date/time for date-based reminders
self.time_input = discord.ui.TextInput(
label="New time",
placeholder="e.g. tomorrow at 3 PM",
required=True,
)
self.add_item(self.message_input)
self.add_item(self.time_input)
def _process_date_trigger(self, new_time_str: str, old_time: datetime.datetime | None) -> tuple[bool, str, Job | None]:
"""Process date trigger modification.
Args:
new_time_str (str): The new time string to parse.
old_time (datetime.datetime | None): The old scheduled time.
Returns:
tuple[bool, str, Job | None]: Success flag, error message, and rescheduled job.
"""
parsed_time: datetime.datetime | None = parse_time(new_time_str)
if not parsed_time:
return False, f"Invalid time format: `{new_time_str}`", None
if old_time and parsed_time == old_time:
return True, "", None # No change needed
logger.info(f"Rescheduling date-based job {self.job_id}")
try:
rescheduled_job = scheduler.reschedule_job(self.job_id, trigger="date", run_date=parsed_time)
except (ValueError, TypeError, AttributeError) as e:
logger.exception(f"Failed to reschedule date-based job: {e}")
return False, f"Failed to reschedule job: {e}", None
else:
return True, "", rescheduled_job
async def _update_message(self, old_message: str, new_message: str) -> bool:
"""Update the message of a job.
Args:
old_message (str): The old message.
new_message (str): The new message.
Returns:
bool: Whether the message was changed.
"""
if new_message == old_message:
return False
job: Job | None = scheduler.get_job(self.job_id)
if not job:
return False
old_kwargs = job.kwargs.copy()
scheduler.modify_job(
self.job_id,
kwargs={
**old_kwargs,
"message": new_message,
},
)
logger.debug(f"Modified job {self.job_id} with new message: {new_message}")
logger.debug(f"Old kwargs: {old_kwargs}, New kwargs: {job.kwargs}")
return True
async def on_submit(self, interaction: discord.Interaction) -> None:
"""Called when the modal is submitted for a date-based reminder.
Args:
interaction (discord.Interaction): The Discord interaction where this modal was triggered from.
"""
old_message: str = self.job.kwargs.get("message", "")
old_time: datetime.datetime | None = self.job.next_run_time
old_time_countdown: str = calculate(self.job)
new_message: str = self.message_input.value
new_time_str: str = self.time_input.value
# Get the job to modify
job_to_modify: Job | None = scheduler.get_job(self.job_id)
if not job_to_modify:
await interaction.response.send_message(
f"Failed to get job.\n{new_message=}\n{new_time_str=}",
ephemeral=True,
)
return
# Defer early for long operations
await interaction.response.defer(ephemeral=True)
# Process date trigger
success, error_msg, rescheduled_job = self._process_date_trigger(new_time_str, old_time)
# If time input is invalid, send error message
if not success and error_msg:
await interaction.followup.send(error_msg, ephemeral=True)
return
# Update the message if changed
msg: str = f"Modified job `{escape_markdown(self.job_id)}`:\n"
changes_made = False
# Add schedule change info to message
if rescheduled_job:
if old_time:
msg += (
f"Old time: `{old_time.strftime('%Y-%m-%d %H:%M:%S')}` (In {old_time_countdown})\n"
f"New time: Next run in {calculate(rescheduled_job)}\n"
)
else:
msg += f"Job unpaused. Next run in {calculate(rescheduled_job)}\n"
changes_made = True
# Update message if changed
message_changed: bool = await self._update_message(old_message, new_message)
if message_changed:
msg += f"Old message: `{escape_markdown(old_message)}`\n"
msg += f"New message: `{escape_markdown(new_message)}`.\n"
changes_made = True
# Send confirmation message
if changes_made:
await interaction.followup.send(content=msg)
else:
await interaction.followup.send(content=f"No changes made to job `{escape_markdown(self.job_id)}`.", ephemeral=True)
async def on_error(self, interaction: discord.Interaction, error: Exception) -> None:
"""A callback that is called when on_submit fails with an error.
Args:
interaction (discord.Interaction): The Discord interaction where this modal was triggered from.
error (Exception): The raised exception.
"""
# Check if the interaction has already been responded to
if not interaction.response.is_done():
await interaction.response.send_message("Oops! Something went wrong.", ephemeral=True)
else:
try:
await interaction.followup.send("Oops! Something went wrong.", ephemeral=True)
except discord.HTTPException:
logger.warning("Failed to send error message via followup")
logger.exception(f"Error in {self.__class__.__name__}: {error}")
traceback.print_exception(type(error), error, error.__traceback__)
class CronReminderModifyModal(discord.ui.Modal, title="Modify reminder"):
"""A modal for modifying a cron-based reminder."""
def __init__(self, job: Job) -> None:
"""Initialize the modal for modifying a date-based reminder.
Args:
job (Job): The APScheduler job to modify. Must be a date-based job.
"""
super().__init__(title="Modify Reminder")
self.job = job
self.job_id = job.id
# message
self.message_input = discord.ui.TextInput(
label="Reminder message",
default=job.kwargs.get("message", ""),
placeholder="What do you want to be reminded of?",
max_length=200,
)
async def _update_message(self, old_message: str, new_message: str) -> bool:
"""Update the message of a job.
Args:
old_message (str): The old message.
new_message (str): The new message.
Returns:
bool: Whether the message was changed.
"""
if new_message == old_message:
return False
job: Job | None = scheduler.get_job(self.job_id)
if not job:
return False
old_kwargs = job.kwargs.copy()
scheduler.modify_job(
self.job_id,
kwargs={
**old_kwargs,
"message": new_message,
},
)
logger.debug(f"Modified job {self.job_id} with new message: {new_message}")
logger.debug(f"Old kwargs: {old_kwargs}, New kwargs: {job.kwargs}")
return True
async def on_submit(self, interaction: discord.Interaction) -> None:
"""Called when the modal is submitted for a cron-based reminder.
Args:
interaction (discord.Interaction): The Discord interaction where this modal was triggered from.
"""
old_message: str = self.job.kwargs.get("message", "")
new_message: str = self.message_input.value
# Get the job to modify
job_to_modify: Job | None = scheduler.get_job(self.job_id)
if not job_to_modify:
await interaction.response.send_message(
f"Failed to get job.\n{new_message=}",
ephemeral=True,
)
return
# Defer early for long operations
await interaction.response.defer(ephemeral=True)
# Update the message if changed
msg: str = f"Modified job `{escape_markdown(self.job_id)}`:\n"
changes_made = False
# Update message if changed
message_changed: bool = await self._update_message(old_message, new_message)
if message_changed:
msg += f"Old message: `{escape_markdown(old_message)}`\n"
msg += f"New message: `{escape_markdown(new_message)}`.\n"
changes_made = True
# Send confirmation message
if changes_made:
await interaction.followup.send(content=msg)
else:
await interaction.followup.send(content=f"No changes made to job `{escape_markdown(self.job_id)}`.", ephemeral=True)
async def on_error(self, interaction: discord.Interaction, error: Exception) -> None:
"""A callback that is called when on_submit fails with an error.
Args:
interaction (discord.Interaction): The Discord interaction where this modal was triggered from.
error (Exception): The raised exception.
"""
# Check if the interaction has already been responded to
if not interaction.response.is_done():
await interaction.response.send_message("Oops! Something went wrong.", ephemeral=True)
else:
try:
await interaction.followup.send("Oops! Something went wrong.", ephemeral=True)
except discord.HTTPException:
logger.warning("Failed to send error message via followup")
logger.exception(f"Error in {self.__class__.__name__}: {error}")
traceback.print_exception(type(error), error, error.__traceback__)
class IntervalReminderModifyModal(discord.ui.Modal, title="Modify reminder"):
"""A modal for modifying an interval-based reminder."""
def __init__(self, job: Job) -> None:
"""Initialize the modal for modifying a date-based reminder.
Args:
job (Job): The APScheduler job to modify. Must be a date-based job.
"""
super().__init__(title="Modify Reminder")
self.job = job
self.job_id = job.id
# message
self.message_input = discord.ui.TextInput(
label="Reminder message",
default=job.kwargs.get("message", ""),
placeholder="What do you want to be reminded of?",
max_length=200,
)
self.add_item(self.message_input)
async def _update_message(self, old_message: str, new_message: str) -> bool:
"""Update the message of a job.
Args:
old_message (str): The old message.
new_message (str): The new message.
Returns:
bool: Whether the message was changed.
"""
if new_message == old_message:
return False
job: Job | None = scheduler.get_job(self.job_id)
if not job:
return False
old_kwargs = job.kwargs.copy()
scheduler.modify_job(
self.job_id,
kwargs={
**old_kwargs,
"message": new_message,
},
)
logger.debug(f"Modified job {self.job_id} with new message: {new_message}")
logger.debug(f"Old kwargs: {old_kwargs}, New kwargs: {job.kwargs}")
return True
async def on_submit(self, interaction: discord.Interaction) -> None:
"""Called when the modal is submitted for an interval-based reminder.
Args:
interaction (discord.Interaction): The Discord interaction where this modal was triggered from.
"""
old_message: str = self.job.kwargs.get("message", "")
new_message: str = self.message_input.value
# Get the job to modify
job_to_modify: Job | None = scheduler.get_job(self.job_id)
if not job_to_modify:
await interaction.response.send_message(
f"Failed to get job.\n{new_message=}",
ephemeral=True,
)
return
# Defer early for long operations
await interaction.response.defer(ephemeral=True)
# Update the message if changed
msg: str = f"Modified job `{escape_markdown(self.job_id)}`:\n"
changes_made = False
# Update message if changed
message_changed: bool = await self._update_message(old_message, new_message)
if message_changed:
msg += f"Old message: `{escape_markdown(old_message)}`\n"
msg += f"New message: `{escape_markdown(new_message)}`.\n"
changes_made = True
# Send confirmation message
if changes_made:
await interaction.followup.send(content=msg)
else:
await interaction.followup.send(content=f"No changes made to job `{escape_markdown(self.job_id)}`.", ephemeral=True)
async def on_error(self, interaction: discord.Interaction, error: Exception) -> None:
"""A callback that is called when on_submit fails with an error.
Args:
interaction (discord.Interaction): The Discord interaction where this modal was triggered from.
error (Exception): The raised exception.
"""
# Check if the interaction has already been responded to
if not interaction.response.is_done():
await interaction.response.send_message("Oops! Something went wrong.", ephemeral=True)
else:
try:
await interaction.followup.send("Oops! Something went wrong.", ephemeral=True)
except discord.HTTPException:
logger.warning("Failed to send error message via followup")
logger.exception(f"Error in {self.__class__.__name__}: {error}")
traceback.print_exception(type(error), error, error.__traceback__)

View File

@@ -0,0 +1,89 @@
from __future__ import annotations
import os
from pathlib import Path
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
import pytz
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from dotenv import load_dotenv
from loguru import logger
from discord_reminder_bot.helpers import generate_state
load_dotenv(verbose=True)
def get_scheduler() -> AsyncIOScheduler:
"""Return the scheduler instance.
Uses the SQLITE_LOCATION environment variable for the SQLite database location.
Raises:
ValueError: If the timezone is missing or invalid.
Returns:
AsyncIOScheduler: The scheduler instance.
"""
config_timezone: str | None = os.getenv("TIMEZONE")
if not config_timezone:
msg = "Missing timezone. Please set the TIMEZONE environment variable."
raise ValueError(msg)
# Test if the timezone is valid
try:
ZoneInfo(config_timezone)
except (ZoneInfoNotFoundError, ModuleNotFoundError) as e:
msg: str = f"Invalid timezone: {config_timezone}. Error: {e}"
raise ValueError(msg) from e
logger.info(f"Using timezone: {config_timezone}. If this is incorrect, please set the TIMEZONE environment variable.")
sqlite_location: str = os.getenv("SQLITE_LOCATION", default="/jobs.sqlite")
logger.info(f"Using SQLite database at: {sqlite_location}")
jobstores: dict[str, SQLAlchemyJobStore] = {"default": SQLAlchemyJobStore(url=f"sqlite://{sqlite_location}")}
job_defaults: dict[str, bool] = {"coalesce": True}
timezone = pytz.timezone(config_timezone)
return AsyncIOScheduler(jobstores=jobstores, timezone=timezone, job_defaults=job_defaults)
scheduler: AsyncIOScheduler = get_scheduler()
def export_reminder_jobs_to_markdown() -> None:
"""Loop through the APScheduler database and save each job's data to a markdown file if changed."""
data_dir: str = os.getenv("DATA_DIR", default="./data")
logger.info(f"Exporting reminder jobs to markdown files in directory: {data_dir}")
for job in scheduler.get_jobs():
job_state: str = generate_state(job.__getstate__(), job)
file_path: Path = Path(data_dir) / "reminder_data" / f"{job.id}.md"
file_path.parent.mkdir(parents=True, exist_ok=True)
try:
if file_path.exists():
existing_content = file_path.read_text(encoding="utf-8")
if existing_content == job_state:
logger.debug(f"No changes for {file_path}, skipping write.")
continue
file_path.write_text(job_state, encoding="utf-8")
logger.info(f"Data saved to {file_path}")
except OSError as e:
logger.error(f"Failed to save data to {file_path}: {e}")
def get_markdown_contents_from_markdown_file(job_id: str) -> str:
"""Get the contents of a markdown file for a specific job ID.
Args:
job_id (str): The ID of the job.
Returns:
str: The contents of the markdown file, or an empty string if the file does not exist.
"""
data_dir: str = os.getenv("DATA_DIR", default="./data")
file_path: Path = Path(data_dir) / "reminder_data" / f"{job_id}.md"
if file_path.exists():
return file_path.read_text(encoding="utf-8")
return ""

View File

@@ -1,14 +1,13 @@
services:
discord-reminder-bot:
image: thelovinator/discord-reminder-bot
image: ghcr.io/thelovinator1/discord-reminder-bot:latest
env_file:
- .env
container_name: discord-reminder-bot
environment:
- BOT_TOKEN=${BOT_TOKEN}
- TIMEZONE=${TIMEZONE}
- LOG_LEVEL=${LOG_LEVEL}
- SQLITE_LOCATION=/data/jobs.sqlite
- WEBHOOK_URL=${WEBHOOK_URL}
restart: unless-stopped
volumes:
- data_folder:/home/botuser/data/

View File

@@ -1,13 +0,0 @@
from __future__ import annotations
import nox # type: ignore[import]
nox.options.default_venv_backend = "uv"
@nox.session(python=["3.10", "3.11", "3.12", "3.13"])
def tests(session: nox.Session) -> None:
"""Run the test suite."""
session.install(".")
session.install("pytest")
session.run("pytest")

View File

@@ -1,91 +1,23 @@
[project]
name = "discord-reminder-bot"
version = "2.0.0"
version = "3.0.0"
description = "Discord bot that allows you to set date, cron and interval reminders."
readme = "README.md"
requires-python = ">=3.10"
requires-python = ">=3.13"
dependencies = [
# The Discord bot library uses discord.py
"discord-py[speed]>=2.5.0", # https://github.com/Rapptz/discord.py
# For parsing dates and times in /remind commands
"dateparser>=1.0.0", # https://github.com/scrapinghub/dateparser
# For sending webhook messages to Discord
"discord-webhook>=1.3.1", # https://github.com/lovvskillz/python-discord-webhook
# For scheduling reminders, sqlalchemy is needed for storing reminders in a database
"apscheduler>=3.11.0", # https://github.com/agronholm/apscheduler
"sqlalchemy>=2.0.37", # https://github.com/sqlalchemy/sqlalchemy
# For loading environment variables from a .env file
"python-dotenv>=1.0.1", # https://github.com/theskumar/python-dotenv
# For error tracking
"sentry-sdk>=2.20.0", # https://github.com/getsentry/sentry-python
# For logging
"loguru>=0.7.3", # https://github.com/Delgan/loguru
"apscheduler",
"dateparser",
"discord-py[speed]",
"discord-webhook",
"loguru",
"python-dotenv",
"sentry-sdk",
"sqlalchemy",
]
[dependency-groups]
dev = ["pytest"]
[tool.poetry]
name = "discord-reminder-bot"
version = "2.0.0"
description = "Discord bot that allows you to set date, cron and interval reminders."
authors = ["Joakim Hellsén <tlovinator@gmail.com>"]
license = "GPL-3.0-or-later"
[tool.poetry.scripts]
bot = "discord_reminder_bot.main:start"
[tool.poetry.dependencies]
python = "^3.10"
# https://github.com/agronholm/apscheduler
# https://github.com/sqlalchemy/sqlalchemy
# For scheduling reminders, sqlalchemy is needed for storing reminders in a database
sqlalchemy = {version = ">=2.0.37,<3.0.0"}
apscheduler = {version = ">=3.11.0,<4.0.0"}
# https://github.com/scrapinghub/dateparser
# For parsing dates and times in /remind commands
dateparser = {version = ">=1.0.0"}
# https://github.com/Rapptz/discord.py
# https://github.com/jackrosenthal/legacy-cgi
# https://github.com/AbstractUmbra/audioop
# The Discord bot library uses discord.py
# legacy-cgi and audioop-lts are because Python 3.13 removed cgi module and audioop module
discord-py = {version = ">=2.4.0,<3.0.0", extras = ["speed"]}
legacy-cgi = {version = ">=2.6.2,<3.0.0", markers = "python_version >= '3.13'"}
audioop-lts = {version = ">=0.2.1,<1.0.0", markers = "python_version >= '3.13'"}
# https://github.com/lovvskillz/python-discord-webhook
# For sending webhook messages to Discord
discord-webhook = {version = ">=1.3.1,<2.0.0"}
# https://github.com/theskumar/python-dotenv
# For loading environment variables from a .env file
python-dotenv = {version = ">=1.0.1,<2.0.0"}
# https://github.com/getsentry/sentry-python
# For error tracking
sentry-sdk = {version = ">=2.20.0,<3.0.0"}
# https://github.com/Delgan/loguru
# For logging
loguru = {version = ">=0.7.3,<1.0.0"}
[tool.poetry.dev-dependencies]
pytest = "*"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.ruff]
preview = true
line-length = 140
@@ -128,10 +60,6 @@ lint.ignore = [
"W191", # Checks for indentation that uses tabs.
]
[tool.ruff.format]
docstring-code-format = true
docstring-code-line-length = 20
[tool.ruff.lint.per-file-ignores]
"**/test_*.py" = [
"ARG", # Unused function args -> fixtures nevertheless are functionally relevant...

View File

@@ -1,7 +1,7 @@
from __future__ import annotations
import zoneinfo
from datetime import datetime, timezone
from datetime import UTC, datetime
from typing import TYPE_CHECKING
import pytest
@@ -12,7 +12,7 @@ from apscheduler.triggers.date import DateTrigger
from apscheduler.triggers.interval import IntervalTrigger
from discord_reminder_bot import main
from discord_reminder_bot.main import calculate, parse_time
from discord_reminder_bot.helpers import calculate, parse_time
if TYPE_CHECKING:
from apscheduler.job import Job
@@ -25,7 +25,7 @@ def dummy_job() -> None:
def test_calculate() -> None:
"""Test the calculate function with various job inputs."""
scheduler = BackgroundScheduler()
scheduler.timezone = timezone.utc
scheduler.timezone = UTC
scheduler.start()
# Create a job with a DateTrigger