add /status command

it took me a while lol
This commit is contained in:
JelNiSlaw 2023-06-19 22:52:11 +02:00
parent e5d5cf98b2
commit 301230d7a6
No known key found for this signature in database
GPG key ID: EA41571A0A88E97E
9 changed files with 276 additions and 12 deletions

39
poetry.lock generated
View file

@ -151,6 +151,24 @@ docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-
tests = ["attrs[tests-no-zope]", "zope-interface"]
tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
[[package]]
name = "beautifulsoup4"
version = "4.12.2"
description = "Screen-scraping library"
optional = false
python-versions = ">=3.6.0"
files = [
{file = "beautifulsoup4-4.12.2-py3-none-any.whl", hash = "sha256:bd2520ca0d9d7d12694a53d44ac482d181b4ec1888909b035a3dbf40d0f57d4a"},
{file = "beautifulsoup4-4.12.2.tar.gz", hash = "sha256:492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da"},
]
[package.dependencies]
soupsieve = ">1.2"
[package.extras]
html5lib = ["html5lib"]
lxml = ["lxml"]
[[package]]
name = "black"
version = "23.3.0"
@ -596,13 +614,13 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-
[[package]]
name = "pluggy"
version = "1.0.0"
version = "1.1.0"
description = "plugin and hook calling mechanisms for python"
optional = false
python-versions = ">=3.6"
python-versions = ">=3.7"
files = [
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
{file = "pluggy-1.1.0-py3-none-any.whl", hash = "sha256:d81d19a3a88d82ed06998353ce5d5c02587ef07ee2d808ae63904ab0ccef0087"},
{file = "pluggy-1.1.0.tar.gz", hash = "sha256:c500b592c5512df35622e4faf2135aa0b7e989c7d31344194b4afb9d5e47b1bf"},
]
[package.extras]
@ -677,6 +695,17 @@ docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-g
testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
[[package]]
name = "soupsieve"
version = "2.4.1"
description = "A modern CSS selector implementation for Beautiful Soup."
optional = false
python-versions = ">=3.7"
files = [
{file = "soupsieve-2.4.1-py3-none-any.whl", hash = "sha256:1c1bfee6819544a3447586c889157365a27e10d88cde3ad3da0cf0ddf646feb8"},
{file = "soupsieve-2.4.1.tar.gz", hash = "sha256:89d12b2d5dfcd2c9e8c22326da9d9aa9cb3dfab0a83a024f05704076ee8d35ea"},
]
[[package]]
name = "yarl"
version = "1.9.2"
@ -767,4 +796,4 @@ multidict = ">=4.0"
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
content-hash = "34afc83c805a39863cc1a214127e30c071d94387ab1efcb398df3c3d67699e46"
content-hash = "0cce046078dad0fc1b3edabda140722614f0e88531f5255581d5de21227f7b6d"

View file

@ -11,6 +11,7 @@ python = "^3.11"
"discord.py" = "^2.3"
aiohttp = "^3.8"
python-dotenv = "^1.0"
beautifulsoup4 = "^4.12"
[tool.poetry.dev-dependencies]
black = "^23.3"

View file

@ -15,6 +15,7 @@ class Wulkabot(commands.Bot):
help_command=None,
intents=discord.Intents(guilds=True, messages=True, message_content=True),
allowed_mentions=discord.AllowedMentions.none(),
max_ratelimit_timeout=10,
)
async def setup_hook(self) -> None:

View file

@ -6,7 +6,7 @@ import discord
from discord.ext import commands
from .. import bot
from ..utils import github
from ..utils import data_utils, github
from ..utils.constants import GITHUB_REPO
from ..utils.views import DeleteButton
@ -149,8 +149,7 @@ class GitHub(commands.Cog):
if message.author.bot:
return
# `dict.fromkeys` allows us to deduplicate the list whilst preserving order
words = dict.fromkeys(message.content.casefold().split()).keys()
words = data_utils.deduplicate_list(message.content.casefold().split())
embeds = []
try:

View file

@ -0,0 +1,66 @@
import discord
from discord import app_commands
from discord.ext import commands
from .. import bot
from ..utils import constants, vulcan_status
from ..utils.vulcan_status import Result, Status
def status_embed(status: list[tuple[str, Status]]) -> discord.Embed:
ok = []
errors = []
for domain, result in status:
match result.state:
case Result.OK:
ok.append(domain)
continue
case Result.DATABASE_UPDATE:
icon = "🔄"
case Result.BREAK:
icon = "⚠️"
case Result.ERROR:
icon = "‼️"
case Result.TIMEOUT:
icon = ""
case _:
icon = ""
errors.append(f"{icon} {domain}: {result.message or result.status_code}")
if errors:
error_text = "\n".join(errors)
else:
error_text = "🟢 Wszystko działa!"
if ok:
ok_text = ", ".join(ok)
else:
ok_text = "🔥 Nic nie działa"
return (
discord.Embed(title="Status dziennika", colour=constants.ACCENT_COLOR)
.add_field(name="Błędy", value=error_text, inline=False)
.add_field(name="Działające usługi", value=ok_text, inline=False)
)
class VulcanStatus(commands.Cog):
def __init__(self, bot: bot.Wulkabot) -> None:
super().__init__()
self.bot = bot
@app_commands.command()
async def status(self, interaction: discord.Interaction):
"""Sprawdza status dziennika"""
await interaction.response.defer(thinking=True)
status = await vulcan_status.check_all(self.bot.http_client)
embed = status_embed(status)
await interaction.followup.send(embed=embed)
async def setup(bot: bot.Wulkabot):
await bot.add_cog(VulcanStatus(bot))

View file

@ -5,9 +5,9 @@ from discord import app_commands
from discord.ext import commands
from .. import bot
from ..utils import github, wulkanowy_manager
from ..utils import data_utils, github, wulkanowy_manager
from ..utils.constants import ACCENT_COLOR, BUILDS_CHANNEL_ID, GITHUB_REPO
from ..utils.wulkanowy_manager import WulkanowyBuild, WulkanowyManagerException
from ..utils.wulkanowy_manager import WulkanowyBuild
OTHER_DOWNLOADS = " | ".join(
(
@ -46,7 +46,8 @@ class Wulkanowy(commands.Cog):
pulls = await self.github.fetch_open_pulls(*GITHUB_REPO)
branches = ["develop"]
branches.extend((pull["head"]["ref"] for pull in pulls))
builds: list[WulkanowyBuild | WulkanowyManagerException] = await asyncio.gather(
branches = data_utils.deduplicate_list(branches)
builds = await asyncio.gather(
*map(self.wulkanowy_manager.fetch_branch_build, branches), return_exceptions=True
)
lines = "\n".join(

View file

@ -0,0 +1,7 @@
from typing import TypeVar
_T = TypeVar("_T")
def deduplicate_list(data: list[_T]) -> list[_T]:
return list(dict.fromkeys(data).keys())

View file

@ -1,4 +1,4 @@
from typing import Any, Literal
from typing import Any
import aiohttp

View file

@ -0,0 +1,160 @@
import asyncio
from enum import Enum
import aiohttp
from bs4 import BeautifulSoup
class Domain:
def __init__(self, name: str, host: str, expected_title: str) -> None:
self.name = name
self.host = host
self.expected_title = expected_title
DOMAINS: list[Domain] = [
Domain("vulcan.net.pl", "https://uonetplus.vulcan.net.pl/warszawa/", "Dziennik UONET+"),
Domain("vulcan.net.pl: Uczeń", "https://uonetplus-uczen.vulcan.net.pl/warszawa", "Uczeń"),
Domain(
"vulcan.net.pl: Wiadomości Plus",
"https://uonetplus-wiadomosciplus.vulcan.net.pl/warszawa",
"Wiadomości Plus",
),
Domain("vulcan.net.pl: Aplikacja mobilna", "https://lekcjaplus.vulcan.net.pl", "Eduone"),
Domain("umt.tarnow.pl", "https://uonetplus.umt.tarnow.pl/tarnow", "Zaloguj"),
Domain("umt.tarnow.pl: Uczeń", "https://uonetplus-uczen.umt.tarnow.pl/tarnow", "Zaloguj"),
Domain(
"umt.tarnow.pl: Wiadomości Plus",
"https://uonetplus-wiadomosciplus.umt.tarnow.pl/tarnow",
"Zaloguj",
),
Domain(
"umt.tarnow.pl: Aplikacja mobilna",
"https://uonetplus-komunikacja.umt.tarnow.pl/tarnow",
"UONET+ dla urządzeń mobilnych",
),
Domain(
"eszkola.opolskie.pl",
"https://uonetplus.eszkola.opolskie.pl/opole",
"Logowanie do systemu Opolska e-Szkola",
),
Domain(
"eszkola.opolskie.pl: Uczeń",
"https://uonetplus-uczen.eszkola.opolskie.pl/opole",
"Logowanie do systemu Opolska e-Szkola",
),
Domain(
"eszkola.opolskie.pl: Wiadomości Plus",
"https://uonetplus-wiadomosciplus.eszkola.opolskie.pl/opole",
"Logowanie do systemu Opolska e-Szkola",
),
Domain(
"eszkola.opolskie.pl: Aplikacja mobilna",
"https://uonetplus-komunikacja.eszkola.opolskie.pl/opole",
"UONET+ dla urządzeń mobilnych",
),
Domain("Rzeszów", "https://portal.vulcan.net.pl/rzeszowprojekt", "Platforma VULCAN"),
Domain(
"resman.pl: Uczeń",
"https://uonetplus-uczen.vulcan.net.pl/rzeszowprojekt",
"Logowanie do systemu",
),
Domain(
"Rzeszów: Wiadomości Plus",
"https://uonetplus-wiadomosciplus.vulcan.net.pl/rzeszowprojekt",
"Logowanie do systemu",
),
Domain("edu.gdansk.pl", "https://uonetplus.edu.gdansk.pl/gdansk", "Logowanie do systemu"),
Domain(
"edu.gdansk.pl: Uczeń",
"https://uonetplus-uczen.edu.gdansk.pl/gdansk",
"Logowanie do systemu",
),
Domain(
"edu.gdansk.pl: Wiadomości Plus",
"https://uonetplus-wiadomosciplus.edu.gdansk.pl/gdansk",
"Logowanie do systemu",
),
Domain(
"edu.gdansk.pl: Aplikacja mobilna",
"https://uonetplus-komunikacja.edu.gdansk.pl/gdansk",
"UONET+ dla urządzeń mobilnych",
),
Domain("edu.lublin.eu", "https://uonetplus.edu.lublin.eu/lublin", "Logowanie do systemu"),
Domain(
"edu.lublin.eu: Uczeń",
"https://uonetplus-uczen.edu.lublin.eu/lublin",
"Logowanie do systemu",
),
Domain(
"edu.lublin.eu: Wiadomości Plus",
"https://uonetplus-wiadomosciplus.edu.lublin.eu/lublin",
"Logowanie do systemu",
),
Domain(
"edu.lublin.eu: Aplikacja mobilna",
"https://uonetplus-komunikacja.edu.lublin.eu/lublin",
"UONET+ dla urządzeń mobilnych",
),
Domain("eduportal.koszalin.pl", "https://uonetplus.eduportal.koszalin.pl/koszalin", "Zaloguj"),
Domain(
"eduportal.koszalin.pl: Uczeń",
"https://uonetplus-uczen.eduportal.koszalin.pl/koszalin",
"Zaloguj",
),
Domain(
"eduportal.koszalin.pl: Wiadomości Plus",
"https://uonetplus-wiadomosciplus.eduportal.koszalin.pl/koszalin",
"Zaloguj",
),
Domain(
"eduportal.koszalin.pl: Aplikacja mobilna",
"https://uonetplus-komunikacja.eduportal.koszalin.pl/koszalin",
"UONET+ dla urządzeń mobilnych",
),
]
class Result(Enum):
OK = 0
DATABASE_UPDATE = 1
BREAK = 2
ERROR = 3
TIMEOUT = 4
UNKNOWN = 5
class Status:
def __init__(self, state: Result, status_code: int | None, message: str | None) -> None:
self.state = state
self.status_code = status_code
self.message = message
async def check_status(http_client: aiohttp.ClientSession, url: str, expected_title: str) -> Status:
try:
response = await http_client.get(url, timeout=10)
except asyncio.TimeoutError as e:
return Status(Result.TIMEOUT, None, "Timeout")
except aiohttp.ClientError as e:
return Status(Result.ERROR, None, str(e))
soup = BeautifulSoup(await response.text(), "html.parser")
if error_div := soup.find(id="MainPage_ErrorDiv"):
return Status(Result.ERROR, response.status, error_div.get_text())
title = soup.title.string if soup.title else None
if title == expected_title:
return Status(Result.OK, response.status, None)
return Status(Result.UNKNOWN, response.status, title)
async def check_all(http_client: aiohttp.ClientSession) -> list[tuple[str, Status]]:
status = await asyncio.gather(
*(check_status(http_client, domain.host, domain.expected_title) for domain in DOMAINS)
)
return [(domain.name, result) for (domain, result) in zip(DOMAINS, status)]