From 17eada7e2911d26ffa16902465c62d65fef32521 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Hells=C3=A9n?= Date: Mon, 31 Oct 2022 01:24:32 +0100 Subject: [PATCH] Send errors and missed reminders to webhook New environment variable that you need to use for this to work. --- .env.example | 19 ++++++--- discord_reminder_bot/main.py | 37 ++++++++++++++-- discord_reminder_bot/settings.py | 3 +- poetry.lock | 73 +++++++++++++++++++++++++++++++- pyproject.toml | 1 + 5 files changed, 121 insertions(+), 12 deletions(-) diff --git a/.env.example b/.env.example index a0a7e9c..ca37b9d 100644 --- a/.env.example +++ b/.env.example @@ -1,15 +1,22 @@ # Discord bot token +# https://discord.com/developers/applications BOT_TOKEN=JFIiasfjioFIAOJFOIJIOSAF.AFo-7A.akwFakeopfaWPOKawPOFKOAKFPA -# Timezone +# Time zone that you are in. This is used when typing "Friday 22:00" and more. # https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List # You want to use the TZ databse name. TIMEZONE=Europe/Stockholm -# Optional: Change sqlite database location -# SQLITE_LOCATION=/jobs.sqlite # This will be created in the repo root (This is the default if not set) -# SQLITE_LOCATION=/C:\\Users\\Jocke\\Desktop\\db.sqlite3 # Note the double backslashes and the first slash -# SQLITE_LOCATION=//home/lovinator/foo.db # On Linux you will need to use double slashes before the path to get the absolute path +# Optional: Change sqlite database location. +# SQLITE_LOCATION=/jobs.sqlite # This will be created in the repo root (This is the default if not set). +# SQLITE_LOCATION=/C:\\Users\\Jocke\\Desktop\\db.sqlite3 # Note the double backslashes and the first slash. +# SQLITE_LOCATION=//home/lovinator/foo.db # On Linux you will need to use double slashes before the path to get the +# absolute path. -# Log level, CRITICAL, ERROR, WARNING, INFO, DEBUG +# Log level, CRITICAL, ERROR, WARNING, INFO, DEBUG. LOG_LEVEL=INFO + +# Webhook that discord-reminder-bot will send errors and information about missed reminders. +# https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks +# Right click channel in Discord -> Intergrations -> Webhooks -> Create Webhook. +WEBHOOK_URL=https://discord.com/api/webhooks/582696524044304394/a3CMwZWchmHAXItB_lzSSRYBx0-AlPAHseJWqhHLfsAg_X4erac9-CeVeUDqPI1ac1vT \ No newline at end of file diff --git a/discord_reminder_bot/main.py b/discord_reminder_bot/main.py index bbf987c..2030ae9 100644 --- a/discord_reminder_bot/main.py +++ b/discord_reminder_bot/main.py @@ -5,9 +5,12 @@ from typing import List import dateparser import interactions +from apscheduler import events +from apscheduler.events import EVENT_JOB_ERROR, EVENT_JOB_MISSED from apscheduler.jobstores.base import JobLookupError from apscheduler.triggers.date import DateTrigger from dateparser.conf import SettingValidationError +from discord_webhook import DiscordWebhook from interactions import CommandContext, Embed, Option, OptionType, autodefer from interactions.ext.paginator import Paginator @@ -19,11 +22,27 @@ from discord_reminder_bot.settings import ( log_level, scheduler, sqlite_location, + webhook_url ) bot = interactions.Client(token=bot_token) +def send_webhook(url=webhook_url, message: str = "discord-reminder-bot: Empty message."): + """ + Send a webhook to Discord. + + Args: + url: Our webhook url, defaults to the one from settings. + message: The message that will be sent to Discord. + """ + if not url: + print("ERROR: Tried to send a webhook but you have no webhook url configured.") + return + webhook = DiscordWebhook(url=url, content=message, rate_limit_retry=True) + webhook.execute() + + @bot.command(name="remind") async def base_command(ctx: interactions.CommandContext): """This description isn't seen in the UI (yet?) @@ -639,6 +658,19 @@ async def remind_interval( await ctx.send(message) +def my_listener(event): + """This gets called when something in APScheduler happens.""" + if event.code == events.EVENT_JOB_MISSED: + # TODO: Is it possible to get the message? + scheduled_time = event.scheduled_run_time.strftime("%Y-%m-%d %H:%M:%S") + msg = f"Job {event.job_id} was missed! Was scheduled at {scheduled_time}" + send_webhook(message=msg) + + if event.exception: + send_webhook(f"discord-reminder-bot failed to send message to Discord\n" + f"{event}") + + async def send_to_discord(channel_id: int, message: str, author_id: int): """Send a message to Discord. @@ -647,8 +679,7 @@ async def send_to_discord(channel_id: int, message: str, author_id: int): message: The message. author_id: User we should ping. """ - # TODO: Check if channel exists. - # TODO: Send message to webhook if channel is not found. + channel = await interactions.get( bot, interactions.Channel, @@ -669,8 +700,8 @@ def start(): f"config_timezone = {config_timezone}\n" f"log_level = {log_level}" ) - scheduler.start() + scheduler.add_listener(my_listener, EVENT_JOB_MISSED | EVENT_JOB_ERROR) bot.start() diff --git a/discord_reminder_bot/settings.py b/discord_reminder_bot/settings.py index fd1c0ec..503f3a9 100644 --- a/discord_reminder_bot/settings.py +++ b/discord_reminder_bot/settings.py @@ -9,7 +9,8 @@ load_dotenv(verbose=True) sqlite_location = os.getenv("SQLITE_LOCATION", default="/jobs.sqlite") config_timezone = os.getenv("TIMEZONE", default="UTC") bot_token = os.getenv("BOT_TOKEN", default="") -log_level = os.getenv(key="LOG_LEVEL", default="INFO") +log_level = os.getenv("LOG_LEVEL", default="INFO") +webhook_url = os.getenv("WEBHOOK_URL", default="") if not bot_token: raise ValueError("Missing bot token") diff --git a/poetry.lock b/poetry.lock index 061d97d..5293639 100644 --- a/poetry.lock +++ b/poetry.lock @@ -78,6 +78,14 @@ docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] +[[package]] +name = "certifi" +version = "2022.9.24" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = ">=3.6" + [[package]] name = "charset-normalizer" version = "2.1.1" @@ -157,7 +165,21 @@ readthedocs = ["Sphinx", "enum-tools[sphinx]", "furo", "sphinx-copybutton", "sph type = "git" url = "https://github.com/interactions-py/library.git" reference = "unstable" -resolved_reference = "4cd4538f6d90b0b31471d7e5dcb98d68fc6d7987" +resolved_reference = "596b95f8a97194920dfdb70a55b52493ff63554f" + +[[package]] +name = "discord-webhook" +version = "0.17.0" +description = "execute discord webhooks" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +requests = ">=2.19.1" + +[package.extras] +async = ["httpx (>=0.20.0)"] [[package]] name = "exceptiongroup" @@ -327,6 +349,24 @@ category = "main" optional = false python-versions = ">=3.6" +[[package]] +name = "requests" +version = "2.28.1" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=3.7, <4" + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<3" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + [[package]] name = "setuptools" version = "65.5.0" @@ -412,6 +452,19 @@ tzdata = {version = "*", markers = "platform_system == \"Windows\""} devenv = ["black", "pyroma", "pytest-cov", "zest.releaser"] test = ["pytest (>=4.3)", "pytest-mock (>=3.3)"] +[[package]] +name = "urllib3" +version = "1.26.12" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + [[package]] name = "yarl" version = "1.8.1" @@ -427,7 +480,7 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "c64bd02df0b561448367fecf47b590289f6c3f91cf6e0524e10f3d858854d8cc" +content-hash = "d6a5cb22ff5db1181c1a877bf8bf13852d2f724dba691fc2ae26161b0ddbd8ab" [metadata.files] aiohttp = [ @@ -535,6 +588,10 @@ attrs = [ {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, ] +certifi = [ + {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"}, + {file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"}, +] charset-normalizer = [ {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, @@ -549,6 +606,10 @@ dateparser = [ ] dinteractions-Paginator = [] discord-py-interactions = [] +discord-webhook = [ + {file = "discord-webhook-0.17.0.tar.gz", hash = "sha256:bb47e5fc83f73614d7b2a3764b84359b52c96a94aadf3302bc3c067dd21b43cc"}, + {file = "discord_webhook-0.17.0-py3-none-any.whl", hash = "sha256:d869849c4834f928f5c22597dc7600b1a30f9e797d1aeb1d4196711a243d9a73"}, +] exceptiongroup = [ {file = "exceptiongroup-1.0.0-py3-none-any.whl", hash = "sha256:2ac84b496be68464a2da60da518af3785fff8b7ec0d090a581604bc870bdee41"}, {file = "exceptiongroup-1.0.0.tar.gz", hash = "sha256:affbabf13fb6e98988c38d9c5650e701569fe3c1de3233cfb61c5f33774690ad"}, @@ -863,6 +924,10 @@ regex = [ {file = "regex-2022.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:9efa41d1527b366c88f265a227b20bcec65bda879962e3fc8a2aee11e81266d7"}, {file = "regex-2022.3.2.tar.gz", hash = "sha256:79e5af1ff258bc0fe0bdd6f69bc4ae33935a898e3cbefbbccf22e88a27fa053b"}, ] +requests = [ + {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, + {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, +] setuptools = [ {file = "setuptools-65.5.0-py3-none-any.whl", hash = "sha256:f62ea9da9ed6289bfe868cd6845968a2c854d1427f8548d52cae02a42b4f0356"}, {file = "setuptools-65.5.0.tar.gz", hash = "sha256:512e5536220e38146176efb833d4a62aa726b7bbff82cfbc8ba9eaa3996e0b17"}, @@ -926,6 +991,10 @@ tzlocal = [ {file = "tzlocal-4.2-py3-none-any.whl", hash = "sha256:89885494684c929d9191c57aa27502afc87a579be5cdd3225c77c463ea043745"}, {file = "tzlocal-4.2.tar.gz", hash = "sha256:ee5842fa3a795f023514ac2d801c4a81d1743bbe642e3940143326b3a00addd7"}, ] +urllib3 = [ + {file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"}, + {file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"}, +] yarl = [ {file = "yarl-1.8.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:abc06b97407868ef38f3d172762f4069323de52f2b70d133d096a48d72215d28"}, {file = "yarl-1.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:07b21e274de4c637f3e3b7104694e53260b5fc10d51fb3ec5fed1da8e0f754e3"}, diff --git a/pyproject.toml b/pyproject.toml index c00d164..a5ea4fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ sqlalchemy = "^1.4.42" discord-py-interactions = { git = "https://github.com/interactions-py/library.git", rev = "unstable" } interactions-wait-for = "^1.0.6" dinteractions-paginator = { git = "https://github.com/interactions-py/paginator.git", rev = "unstable" } +discord-webhook = "^0.17.0" [tool.poetry.dev-dependencies] pytest = "^7.1.2"