Uptime monitoring for jimmy

This commit is contained in:
nex 2023-01-18 20:54:48 +00:00
parent 8f4f616258
commit bb927f4d7b
6 changed files with 436 additions and 215 deletions

1
.gitignore vendored
View file

@ -4,3 +4,4 @@ __pycache__
venv venv
domains.txt domains.txt
geckodriver.log geckodriver.log
targets.json

View file

@ -1321,254 +1321,298 @@
</table> </table>
<table id="486" parent="166" name="starboard"/> <table id="486" parent="166" name="starboard"/>
<table id="487" parent="166" name="students"/> <table id="487" parent="166" name="students"/>
<column id="488" parent="482" name="entry_id"> <table id="488" parent="166" name="uptime"/>
<column id="489" parent="482" name="entry_id">
<DasType>INTEGER|0s</DasType> <DasType>INTEGER|0s</DasType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
<Position>1</Position> <Position>1</Position>
</column> </column>
<column id="489" parent="482" name="created_by"> <column id="490" parent="482" name="created_by">
<DasType>CHAR(32)|0s</DasType> <DasType>CHAR(32)|0s</DasType>
<Position>2</Position> <Position>2</Position>
</column> </column>
<column id="490" parent="482" name="title"> <column id="491" parent="482" name="title">
<DasType>VARCHAR(2000)|0s</DasType> <DasType>VARCHAR(2000)|0s</DasType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
<Position>3</Position> <Position>3</Position>
</column> </column>
<column id="491" parent="482" name="classroom"> <column id="492" parent="482" name="classroom">
<DasType>VARCHAR(4096)|0s</DasType> <DasType>VARCHAR(4096)|0s</DasType>
<Position>4</Position> <Position>4</Position>
</column> </column>
<column id="492" parent="482" name="shared_doc"> <column id="493" parent="482" name="shared_doc">
<DasType>VARCHAR(4096)|0s</DasType> <DasType>VARCHAR(4096)|0s</DasType>
<Position>5</Position> <Position>5</Position>
</column> </column>
<column id="493" parent="482" name="created_at"> <column id="494" parent="482" name="created_at">
<DasType>FLOAT|0s</DasType> <DasType>FLOAT|0s</DasType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
<Position>6</Position> <Position>6</Position>
</column> </column>
<column id="494" parent="482" name="due_by"> <column id="495" parent="482" name="due_by">
<DasType>FLOAT|0s</DasType> <DasType>FLOAT|0s</DasType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
<Position>7</Position> <Position>7</Position>
</column> </column>
<column id="495" parent="482" name="tutor"> <column id="496" parent="482" name="tutor">
<DasType>VARCHAR(7)|0s</DasType> <DasType>VARCHAR(7)|0s</DasType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
<Position>8</Position> <Position>8</Position>
</column> </column>
<column id="496" parent="482" name="reminders"> <column id="497" parent="482" name="reminders">
<DasType>JSON|0s</DasType> <DasType>JSON|0s</DasType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
<Position>9</Position> <Position>9</Position>
</column> </column>
<column id="497" parent="482" name="finished"> <column id="498" parent="482" name="finished">
<DasType>BOOLEAN|0s</DasType> <DasType>BOOLEAN|0s</DasType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
<Position>10</Position> <Position>10</Position>
</column> </column>
<column id="498" parent="482" name="submitted"> <column id="499" parent="482" name="submitted">
<DasType>BOOLEAN|0s</DasType> <DasType>BOOLEAN|0s</DasType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
<Position>11</Position> <Position>11</Position>
</column> </column>
<column id="499" parent="482" name="assignees"> <column id="500" parent="482" name="assignees">
<DasType>JSON|0s</DasType> <DasType>JSON|0s</DasType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
<Position>12</Position> <Position>12</Position>
</column> </column>
<foreign-key id="500" parent="482"> <foreign-key id="501" parent="482">
<ColNames>created_by</ColNames> <ColNames>created_by</ColNames>
<RefColNames>entry_id</RefColNames> <RefColNames>entry_id</RefColNames>
<RefTableName>students</RefTableName> <RefTableName>students</RefTableName>
</foreign-key> </foreign-key>
<key id="501" parent="482"> <key id="502" parent="482">
<ColNames>entry_id</ColNames> <ColNames>entry_id</ColNames>
<Primary>1</Primary> <Primary>1</Primary>
</key> </key>
<column id="502" parent="483" name="entry_id"> <column id="503" parent="483" name="entry_id">
<DasType>CHAR(32)|0s</DasType> <DasType>CHAR(32)|0s</DasType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
<Position>1</Position> <Position>1</Position>
</column> </column>
<column id="503" parent="483" name="student_id"> <column id="504" parent="483" name="student_id">
<DasType>VARCHAR(7)|0s</DasType> <DasType>VARCHAR(7)|0s</DasType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
<Position>2</Position> <Position>2</Position>
</column> </column>
<column id="504" parent="483" name="associated_account"> <column id="505" parent="483" name="associated_account">
<DasType>BIGINT|0s</DasType> <DasType>BIGINT|0s</DasType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
<Position>3</Position> <Position>3</Position>
</column> </column>
<column id="505" parent="483" name="banned_at_timestamp"> <column id="506" parent="483" name="banned_at_timestamp">
<DasType>FLOAT|0s</DasType> <DasType>FLOAT|0s</DasType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
<Position>4</Position> <Position>4</Position>
</column> </column>
<index id="506" parent="483" name="sqlite_autoindex_banned_1"> <index id="507" parent="483" name="sqlite_autoindex_banned_1">
<ColNames>entry_id</ColNames> <ColNames>entry_id</ColNames>
<NameSurrogate>1</NameSurrogate> <NameSurrogate>1</NameSurrogate>
<Unique>1</Unique> <Unique>1</Unique>
</index> </index>
<index id="507" parent="483" name="sqlite_autoindex_banned_2"> <index id="508" parent="483" name="sqlite_autoindex_banned_2">
<ColNames>student_id</ColNames> <ColNames>student_id</ColNames>
<NameSurrogate>1</NameSurrogate> <NameSurrogate>1</NameSurrogate>
<Unique>1</Unique> <Unique>1</Unique>
</index> </index>
<key id="508" parent="483"> <key id="509" parent="483">
<ColNames>entry_id</ColNames> <ColNames>entry_id</ColNames>
<Primary>1</Primary> <Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_banned_1</UnderlyingIndexName> <UnderlyingIndexName>sqlite_autoindex_banned_1</UnderlyingIndexName>
</key> </key>
<key id="509" parent="483"> <key id="510" parent="483">
<ColNames>student_id</ColNames> <ColNames>student_id</ColNames>
<UnderlyingIndexName>sqlite_autoindex_banned_2</UnderlyingIndexName> <UnderlyingIndexName>sqlite_autoindex_banned_2</UnderlyingIndexName>
</key> </key>
<column id="510" parent="484" name="id"> <column id="511" parent="484" name="id">
<DasType>INTEGER|0s</DasType> <DasType>INTEGER|0s</DasType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
<Position>1</Position> <Position>1</Position>
</column> </column>
<column id="511" parent="484" name="code"> <column id="512" parent="484" name="code">
<DasType>VARCHAR(64)|0s</DasType> <DasType>VARCHAR(64)|0s</DasType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
<Position>2</Position> <Position>2</Position>
</column> </column>
<column id="512" parent="484" name="bind"> <column id="513" parent="484" name="bind">
<DasType>BIGINT|0s</DasType> <DasType>BIGINT|0s</DasType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
<Position>3</Position> <Position>3</Position>
</column> </column>
<column id="513" parent="484" name="student_id"> <column id="514" parent="484" name="student_id">
<DasType>VARCHAR(7)|0s</DasType> <DasType>VARCHAR(7)|0s</DasType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
<Position>4</Position> <Position>4</Position>
</column> </column>
<column id="514" parent="484" name="name"> <column id="515" parent="484" name="name">
<DasType>VARCHAR(32)|0s</DasType> <DasType>VARCHAR(32)|0s</DasType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
<Position>5</Position> <Position>5</Position>
</column> </column>
<index id="515" parent="484" name="sqlite_autoindex_codes_1"> <index id="516" parent="484" name="sqlite_autoindex_codes_1">
<ColNames>code</ColNames> <ColNames>code</ColNames>
<NameSurrogate>1</NameSurrogate> <NameSurrogate>1</NameSurrogate>
<Unique>1</Unique> <Unique>1</Unique>
</index> </index>
<key id="516" parent="484"> <key id="517" parent="484">
<ColNames>id</ColNames> <ColNames>id</ColNames>
<Primary>1</Primary> <Primary>1</Primary>
</key> </key>
<key id="517" parent="484"> <key id="518" parent="484">
<ColNames>code</ColNames> <ColNames>code</ColNames>
<UnderlyingIndexName>sqlite_autoindex_codes_1</UnderlyingIndexName> <UnderlyingIndexName>sqlite_autoindex_codes_1</UnderlyingIndexName>
</key> </key>
<column id="518" parent="485" name="type"> <column id="519" parent="485" name="type">
<DasType>TEXT|0s</DasType> <DasType>TEXT|0s</DasType>
<Position>1</Position> <Position>1</Position>
</column> </column>
<column id="519" parent="485" name="name"> <column id="520" parent="485" name="name">
<DasType>TEXT|0s</DasType> <DasType>TEXT|0s</DasType>
<Position>2</Position> <Position>2</Position>
</column> </column>
<column id="520" parent="485" name="tbl_name"> <column id="521" parent="485" name="tbl_name">
<DasType>TEXT|0s</DasType> <DasType>TEXT|0s</DasType>
<Position>3</Position> <Position>3</Position>
</column> </column>
<column id="521" parent="485" name="rootpage"> <column id="522" parent="485" name="rootpage">
<DasType>INT|0s</DasType> <DasType>INT|0s</DasType>
<Position>4</Position> <Position>4</Position>
</column> </column>
<column id="522" parent="485" name="sql"> <column id="523" parent="485" name="sql">
<DasType>TEXT|0s</DasType> <DasType>TEXT|0s</DasType>
<Position>5</Position> <Position>5</Position>
</column> </column>
<column id="523" parent="486" name="entry_id"> <column id="524" parent="486" name="entry_id">
<DasType>CHAR(32)|0s</DasType> <DasType>CHAR(32)|0s</DasType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
<Position>1</Position> <Position>1</Position>
</column> </column>
<column id="524" parent="486" name="id"> <column id="525" parent="486" name="id">
<DasType>BIGINT|0s</DasType> <DasType>BIGINT|0s</DasType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
<Position>2</Position> <Position>2</Position>
</column> </column>
<column id="525" parent="486" name="channel"> <column id="526" parent="486" name="channel">
<DasType>BIGINT|0s</DasType> <DasType>BIGINT|0s</DasType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
<Position>3</Position> <Position>3</Position>
</column> </column>
<column id="526" parent="486" name="starboard_message"> <column id="527" parent="486" name="starboard_message">
<DasType>BIGINT|0s</DasType> <DasType>BIGINT|0s</DasType>
<Position>4</Position> <Position>4</Position>
</column> </column>
<index id="527" parent="486" name="sqlite_autoindex_starboard_1"> <index id="528" parent="486" name="sqlite_autoindex_starboard_1">
<ColNames>entry_id</ColNames> <ColNames>entry_id</ColNames>
<NameSurrogate>1</NameSurrogate> <NameSurrogate>1</NameSurrogate>
<Unique>1</Unique> <Unique>1</Unique>
</index> </index>
<index id="528" parent="486" name="sqlite_autoindex_starboard_2"> <index id="529" parent="486" name="sqlite_autoindex_starboard_2">
<ColNames>id</ColNames> <ColNames>id</ColNames>
<NameSurrogate>1</NameSurrogate> <NameSurrogate>1</NameSurrogate>
<Unique>1</Unique> <Unique>1</Unique>
</index> </index>
<key id="529" parent="486"> <key id="530" parent="486">
<ColNames>entry_id</ColNames> <ColNames>entry_id</ColNames>
<Primary>1</Primary> <Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_starboard_1</UnderlyingIndexName> <UnderlyingIndexName>sqlite_autoindex_starboard_1</UnderlyingIndexName>
</key> </key>
<key id="530" parent="486"> <key id="531" parent="486">
<ColNames>id</ColNames> <ColNames>id</ColNames>
<UnderlyingIndexName>sqlite_autoindex_starboard_2</UnderlyingIndexName> <UnderlyingIndexName>sqlite_autoindex_starboard_2</UnderlyingIndexName>
</key> </key>
<column id="531" parent="487" name="entry_id"> <column id="532" parent="487" name="entry_id">
<DasType>CHAR(32)|0s</DasType> <DasType>CHAR(32)|0s</DasType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
<Position>1</Position> <Position>1</Position>
</column> </column>
<column id="532" parent="487" name="id"> <column id="533" parent="487" name="id">
<DasType>VARCHAR(7)|0s</DasType> <DasType>VARCHAR(7)|0s</DasType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
<Position>2</Position> <Position>2</Position>
</column> </column>
<column id="533" parent="487" name="user_id"> <column id="534" parent="487" name="user_id">
<DasType>BIGINT|0s</DasType> <DasType>BIGINT|0s</DasType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
<Position>3</Position> <Position>3</Position>
</column> </column>
<column id="534" parent="487" name="name"> <column id="535" parent="487" name="name">
<DasType>VARCHAR(32)|0s</DasType> <DasType>VARCHAR(32)|0s</DasType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
<Position>4</Position> <Position>4</Position>
</column> </column>
<index id="535" parent="487" name="sqlite_autoindex_students_1"> <index id="536" parent="487" name="sqlite_autoindex_students_1">
<ColNames>entry_id</ColNames> <ColNames>entry_id</ColNames>
<NameSurrogate>1</NameSurrogate> <NameSurrogate>1</NameSurrogate>
<Unique>1</Unique> <Unique>1</Unique>
</index> </index>
<index id="536" parent="487" name="sqlite_autoindex_students_2"> <index id="537" parent="487" name="sqlite_autoindex_students_2">
<ColNames>id</ColNames> <ColNames>id</ColNames>
<NameSurrogate>1</NameSurrogate> <NameSurrogate>1</NameSurrogate>
<Unique>1</Unique> <Unique>1</Unique>
</index> </index>
<index id="537" parent="487" name="sqlite_autoindex_students_3"> <index id="538" parent="487" name="sqlite_autoindex_students_3">
<ColNames>user_id</ColNames> <ColNames>user_id</ColNames>
<NameSurrogate>1</NameSurrogate> <NameSurrogate>1</NameSurrogate>
<Unique>1</Unique> <Unique>1</Unique>
</index> </index>
<key id="538" parent="487"> <key id="539" parent="487">
<ColNames>entry_id</ColNames> <ColNames>entry_id</ColNames>
<Primary>1</Primary> <Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_students_1</UnderlyingIndexName> <UnderlyingIndexName>sqlite_autoindex_students_1</UnderlyingIndexName>
</key> </key>
<key id="539" parent="487"> <key id="540" parent="487">
<ColNames>id</ColNames> <ColNames>id</ColNames>
<UnderlyingIndexName>sqlite_autoindex_students_2</UnderlyingIndexName> <UnderlyingIndexName>sqlite_autoindex_students_2</UnderlyingIndexName>
</key> </key>
<key id="540" parent="487"> <key id="541" parent="487">
<ColNames>user_id</ColNames> <ColNames>user_id</ColNames>
<UnderlyingIndexName>sqlite_autoindex_students_3</UnderlyingIndexName> <UnderlyingIndexName>sqlite_autoindex_students_3</UnderlyingIndexName>
</key> </key>
<column id="542" parent="488" name="entry_id">
<DasType>CHAR(32)|0s</DasType>
<NotNull>1</NotNull>
<Position>1</Position>
</column>
<column id="543" parent="488" name="target_id">
<DasType>VARCHAR(128)|0s</DasType>
<NotNull>1</NotNull>
<Position>2</Position>
</column>
<column id="544" parent="488" name="target">
<DasType>VARCHAR(128)|0s</DasType>
<NotNull>1</NotNull>
<Position>3</Position>
</column>
<column id="545" parent="488" name="is_up">
<DasType>BOOLEAN|0s</DasType>
<NotNull>1</NotNull>
<Position>4</Position>
</column>
<column id="546" parent="488" name="timestamp">
<DasType>FLOAT|0s</DasType>
<NotNull>1</NotNull>
<Position>5</Position>
</column>
<column id="547" parent="488" name="response_time">
<DasType>INTEGER|0s</DasType>
<Position>6</Position>
</column>
<column id="548" parent="488" name="notes">
<DasType>TEXT|0s</DasType>
<Position>7</Position>
</column>
<index id="549" parent="488" name="sqlite_autoindex_uptime_1">
<ColNames>entry_id</ColNames>
<NameSurrogate>1</NameSurrogate>
<Unique>1</Unique>
</index>
<key id="550" parent="488">
<ColNames>entry_id</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_uptime_1</UnderlyingIndexName>
</key>
</database-model> </database-model>
</dataSource> </dataSource>

View file

@ -25,6 +25,7 @@ from selenium.webdriver.chrome.options import Options as ChromeOptions
from selenium.webdriver.chrome.service import Service as ChromeService from selenium.webdriver.chrome.service import Service as ChromeService
from selenium.webdriver.firefox.options import Options as FirefoxOptions from selenium.webdriver.firefox.options import Options as FirefoxOptions
from selenium.webdriver.firefox.service import Service as FirefoxService from selenium.webdriver.firefox.service import Service as FirefoxService
# from selenium.webdriver.ie # from selenium.webdriver.ie
from utils import console from utils import console
@ -56,12 +57,7 @@ class OtherCog(commands.Cog):
"/usr/bin/firefox-esr", "/usr/bin/firefox-esr",
"/usr/bin/firefox", "/usr/bin/firefox",
], ],
"chrome": [ "chrome": ["/usr/bin/chromium", "/usr/bin/chrome", "/usr/bin/chrome-browser", "/usr/bin/google-chrome"],
"/usr/bin/chromium",
"/usr/bin/chrome",
"/usr/bin/chrome-browser",
"/usr/bin/google-chrome"
],
} }
selected_driver = driver selected_driver = driver
arr = drivers.pop(selected_driver) arr = drivers.pop(selected_driver)
@ -128,11 +124,7 @@ class OtherCog(commands.Cog):
start_init = time() start_init = time()
driver, friendly_url = await asyncio.to_thread(_setup) driver, friendly_url = await asyncio.to_thread(_setup)
end_init = time() end_init = time()
console.log( console.log("Driver '{}' initialised in {} seconds.".format(driver_name, round(end_init - start_init, 2)))
"Driver '{}' initialised in {} seconds.".format(
driver_name, round(end_init - start_init, 2)
)
)
async def _edit(content: str): async def _edit(content: str):
self.bot.loop.create_task(ctx.interaction.edit_original_response(content=content)) self.bot.loop.create_task(ctx.interaction.edit_original_response(content=content))
@ -441,7 +433,7 @@ class OtherCog(commands.Cog):
name="capture-full-page", name="capture-full-page",
description="(firefox only) whether to capture the full page or just the viewport.", description="(firefox only) whether to capture the full page or just the viewport.",
default=False, default=False,
) ),
): ):
"""Takes a screenshot of a URL""" """Takes a screenshot of a URL"""
window_width = max(min(1080 * 6, window_width), 1080 // 6) window_width = max(min(1080 * 6, window_width), 1080 // 6)
@ -536,13 +528,13 @@ class OtherCog(commands.Cog):
await asyncio.sleep(0.5) await asyncio.sleep(0.5)
await ctx.edit( await ctx.edit(
content="Here's your screenshot!\n" content="Here's your screenshot!\n"
"Details:\n" "Details:\n"
f"\\* Browser: {driver}\n" f"\\* Browser: {driver}\n"
f"\\* Resolution: {window_height}x{window_width} ({window_width*window_height:,} pixels)\n" f"\\* Resolution: {window_height}x{window_width} ({window_width*window_height:,} pixels)\n"
f"\\* URL: <{friendly_url}>\n" f"\\* URL: <{friendly_url}>\n"
f"\\* Load time: {fetch_time:.2f}ms\n" f"\\* Load time: {fetch_time:.2f}ms\n"
f"\\* Screenshot render time: {screenshot_time:.2f}ms\n", f"\\* Screenshot render time: {screenshot_time:.2f}ms\n",
file=screenshot file=screenshot,
) )
domains = discord.SlashCommandGroup("domains", "Commands for managing domains") domains = discord.SlashCommandGroup("domains", "Commands for managing domains")

View file

@ -1,33 +1,44 @@
import asyncio import asyncio
import datetime import datetime
import json
from datetime import timedelta from datetime import timedelta
from typing import Dict, Tuple from pathlib import Path
from typing import Dict, Tuple, List, Optional
from urllib.parse import urlparse, parse_qs, ParseResult
import discord import discord
import httpx import httpx
from httpx import AsyncClient, Response from httpx import AsyncClient, Response
from discord.ext import commands, tasks from discord.ext import commands, tasks, pages
from utils import UptimeEntry, console from utils import UptimeEntry, console
class UptimeCompetition(commands.Cog): BASE_JSON = """[
USER_ID = 1063875884274163732 {
OUR_ENDPOINT = "http://wg.nexy7574.cyou:9000/" "name": "SHRoNK Bot",
SHRONK_SERVER = "http://shronk.nexy7574.cyou:9000/" "id": "SHRONK",
OUR_DROPLET = "http://droplet.nexy7574.co.uk:9000/" "uri": "user://994710566612500550/1063875884274163732?online=1&idle=0&dnd=0"
TARGETS = { },
"SHRONK": USER_ID, {
"NEX_DROPLET": OUR_DROPLET, "name": "Nex's Droplet",
"PI": OUR_ENDPOINT, "id": "NEX_DROPLET",
"SHRONK_DROPLET": SHRONK_SERVER "uri": "http://droplet.nexy7574.co.uk:9000/"
} },
TARGETS_NAMES = { {
"SHRONK": "SHRoNK Bot", "name": "Nex's Pi",
"NEX_DROPLET": "Nex's Droplet", "id": "PI",
"PI": "Nex's Pi", "uri": "http://wg.nexy7574.cyou:9000/"
"SHRONK_DROPLET": "SHRoNK's Droplet" },
{
"name": "SHRoNK Droplet",
"id": "SHRONK_DROPLET",
"uri": "http://shronkservz.tk:9000/"
} }
]
"""
class UptimeCompetition(commands.Cog):
class CancelTaskView(discord.ui.View): class CancelTaskView(discord.ui.View):
def __init__(self, task: asyncio.Task, expires: datetime.datetime): def __init__(self, task: asyncio.Task, expires: datetime.datetime):
timeout = expires - discord.utils.utcnow() timeout = expires - discord.utils.utcnow()
@ -39,10 +50,7 @@ class UptimeCompetition(commands.Cog):
self.stop() self.stop()
if self.task: if self.task:
self.task.cancel() self.task.cancel()
await interaction.response.edit_message( await interaction.response.edit_message(content="Uptime test cancelled.", view=None)
content="Uptime test cancelled.",
view=None
)
def __init__(self, bot: commands.Bot): def __init__(self, bot: commands.Bot):
self.bot = bot self.bot = bot
@ -52,6 +60,46 @@ class UptimeCompetition(commands.Cog):
self.last_result: list[UptimeEntry] = [] self.last_result: list[UptimeEntry] = []
self.task_lock = asyncio.Lock() self.task_lock = asyncio.Lock()
self.task_event = asyncio.Event() self.task_event = asyncio.Event()
if not (pth := Path("targets.json")).exists():
pth.write_text(BASE_JSON)
self._cached_targets = self.read_targets()
@property
def cached_targets(self) -> List[Dict[str, str]]:
return self._cached_targets.copy()
def get_target(self, name: str = None, target_id: str = None) -> Optional[Dict[str, str]]:
for target in self.cached_targets:
if name and target["name"].lower() == name.lower():
return target
if target["id"] == target_id:
return target
return
def stats_autocomplete(self, ctx: discord.ApplicationContext) -> List[str]:
return [x["name"] for x in self.cached_targets if ctx.value.lower() in x["name"].lower()] + ["ALL"]
@staticmethod
def parse_user_uri(uri: str) -> Dict[str, str | None | List[str]]:
parsed = urlparse(uri)
response = {"guild": parsed.hostname, "user": None, "statuses": []}
if parsed.path:
response["user"] = parsed.path.strip("/")
if parsed.query:
response["statuses"] = [x for x, y in parse_qs(parsed.query).items() if y[0] == "1"]
return response
def read_targets(self) -> List[Dict[str, str]]:
with open("targets.json") as f:
data: list = json.load(f)
data.sort(key=lambda x: x["name"])
self._cached_targets = data.copy()
return data
def write_targets(self, data: List[Dict[str, str]]):
self._cached_targets = data
with open("targets.json", "w") as f:
json.dump(data, f, indent=4, default=str)
def cog_unload(self): def cog_unload(self):
self.test_uptimes.cancel() self.test_uptimes.cancel()
@ -61,14 +109,20 @@ class UptimeCompetition(commands.Cog):
assert response.status_code == 200 assert response.status_code == 200
assert response.text.strip() == "<!DOCTYPE html><html><body>Hello Jimmy!</body></html>" assert response.text.strip() == "<!DOCTYPE html><html><body>Hello Jimmy!</body></html>"
async def _test_url(self, url: str, max_retries: int = 10, timeout: int = 30) -> Tuple[int, Response | Exception]: async def _test_url(
self, url: str, *, max_retries: int | None = 10, timeout: int | None = 30
) -> Tuple[int, Response | Exception]:
attempts = 1 attempts = 1
if max_retries is None:
max_retries = 1
if timeout is None:
timeout = 10
err = RuntimeError("Unknown Error") err = RuntimeError("Unknown Error")
while attempts < max_retries: while attempts < max_retries:
try: try:
response = await self.http.get(url, timeout=timeout) response = await self.http.get(url, timeout=timeout)
response.raise_for_status() response.raise_for_status()
except (httpx.TimeoutException, httpx.HTTPStatusError) as err: except (httpx.TimeoutException, httpx.HTTPStatusError, ConnectionError, TimeoutError) as err:
attempts += 1 attempts += 1
continue continue
else: else:
@ -76,7 +130,7 @@ class UptimeCompetition(commands.Cog):
return attempts, err return attempts, err
async def do_test_uptimes(self): async def do_test_uptimes(self):
console.log("Testing uptimes...") targets = self.cached_targets
# First we need to check that we are online. # First we need to check that we are online.
# If we aren't online, this isn't very fair. # If we aren't online, this isn't very fair.
try: try:
@ -86,16 +140,17 @@ class UptimeCompetition(commands.Cog):
create_tasks = [] create_tasks = []
# We need to collect the tasks in case the sqlite server is being sluggish # We need to collect the tasks in case the sqlite server is being sluggish
for target in targets.copy():
for key, url in self.TARGETS.items(): if not target["uri"].startswith("http"):
if key == "SHRONK":
continue continue
kwargs: Dict[str, str | int | None] = { targets.remove(target)
"target_id": key, request_kwargs = {}
"target": url, if timeout := target.get("timeout"):
"notes": "" request_kwargs["timeout"] = timeout
} if max_retries := target.get("max_retries"):
attempts, response = await self._test_url(url) request_kwargs["max_retries"] = max_retries
kwargs: Dict[str, str | int | None] = {"target_id": target["id"], "target": target["uri"], "notes": ""}
attempts, response = await self._test_url(target["uri"], **request_kwargs)
if isinstance(response, Exception): if isinstance(response, Exception):
kwargs["is_up"] = False kwargs["is_up"] = False
kwargs["response_time"] = None kwargs["response_time"] = None
@ -112,13 +167,7 @@ class UptimeCompetition(commands.Cog):
kwargs["is_up"] = True kwargs["is_up"] = True
kwargs["response_time"] = round(response.elapsed.total_seconds() * 1000) kwargs["response_time"] = round(response.elapsed.total_seconds() * 1000)
kwargs["notes"] += "nothing notable." kwargs["notes"] += "nothing notable."
create_tasks.append( create_tasks.append(self.bot.loop.create_task(UptimeEntry.objects.create(**kwargs)))
self.bot.loop.create_task(
UptimeEntry.objects.create(
**kwargs
)
)
)
# We need to check if the shronk bot is online since matthew # We need to check if the shronk bot is online since matthew
# Won't let us access their server (cough cough) # Won't let us access their server (cough cough)
@ -126,35 +175,46 @@ class UptimeCompetition(commands.Cog):
# If we don't have presences this is useless. # If we don't have presences this is useless.
if not self.bot.is_ready(): if not self.bot.is_ready():
await self.bot.wait_until_ready() await self.bot.wait_until_ready()
for target in targets:
guild: discord.Guild = self.bot.get_guild(994710566612500550) parsed = urlparse(target["uri"])
if guild is None: if parsed.scheme != "user":
console.log( continue
"[yellow]:warning: Unable to locate the LCC server! Can't uptime check shronk." guild_id = int(parsed.hostname)
) user_id = int(parsed.path.strip("/"))
else: okay_statuses = [
shronk_bot: discord.Member | None = discord.utils.get(guild.members, name="SHRoNK Bot") discord.Status.online if "online=1" in parsed.query.lower() else None,
if not shronk_bot: discord.Status.idle if "idle=1" in parsed.query.lower() else None,
# SHRoNK Bot is not in members cache. discord.Status.dnd if "dnd=1" in parsed.query.lower() else None,
shronk_bots = await guild.query_members(query="SHRoNK Bot", limit=1) ]
if not shronk_bots: if "offline=1" in parsed.query:
console.log( okay_statuses = [discord.Status.offline]
"[yellow]:warning: Unable to locate SHRoNK Bot! Can't uptime check shronk." okay_statuses = list(filter(None, okay_statuses))
) guild: discord.Guild = self.bot.get_guild(guild_id)
else: if guild is None:
shronk_bot = shronk_bots[0] console.log(
if shronk_bot: f"[yellow]:warning: Unable to locate the guild for {target['name']!r}! Can't uptime check."
create_tasks.append( )
self.bot.loop.create_task( else:
UptimeEntry.objects.create( user: discord.Member | None = guild.get_member(user_id)
target_id="SHRONK", if not user:
target="SHRoNK Bot", # SHRoNK Bot is not in members cache.
is_up=shronk_bot.status is not discord.Status.offline, try:
response_time=None, user = await guild.fetch_member(user_id)
notes="*Unable to monitor response time, not a HTTP request.*", except discord.HTTPException:
console.log(f"[yellow]:warning: Unable to locate {target['name']!r}! Can't uptime check.")
user = None
if user:
create_tasks.append(
self.bot.loop.create_task(
UptimeEntry.objects.create(
target_id=target["id"],
target=target["name"],
is_up=user.status in okay_statuses,
response_time=None,
notes="*Unable to monitor response time, not a HTTP request.*",
)
) )
) )
)
else: else:
if self._warning_posted is False: if self._warning_posted is False:
console.log( console.log(
@ -167,86 +227,76 @@ class UptimeCompetition(commands.Cog):
return await asyncio.gather(*create_tasks, return_exceptions=True) return await asyncio.gather(*create_tasks, return_exceptions=True)
# All done! # All done!
@tasks.loop(minutes=1) # @tasks.loop(minutes=1)
@tasks.loop(seconds=10)
async def test_uptimes(self): async def test_uptimes(self):
self.task_event.clear() self.task_event.clear()
async with self.task_lock: async with self.task_lock:
self.last_result = await self.do_test_uptimes() self.last_result = await self.do_test_uptimes()
self.task_event.set() self.task_event.set()
uptime = discord.SlashCommandGroup( uptime = discord.SlashCommandGroup("uptime", "Commands for the uptime competition.")
"uptime",
"Commands for the uptime competition."
)
@uptime.command(name="stats") @uptime.command(name="stats")
async def stats( async def stats(
self, self,
ctx: discord.ApplicationContext, ctx: discord.ApplicationContext,
query_target: discord.Option( query_target: discord.Option(
str, str,
name="target", name="target",
description="The target to check the uptime of. Defaults to all.", description="The target to check the uptime of. Defaults to all.",
required=False, required=False,
choices=[ autocomplete=stats_autocomplete,
discord.OptionChoice("SHRoNK Bot", "SHRONK"), default="ALL",
discord.OptionChoice("Nex Droplet", "NEX_DROPLET"), ),
discord.OptionChoice("Shronk Server", "SHRONK_DROPLET"), look_back: discord.Option(
discord.OptionChoice("Nex Pi", "PI"), int, description="How many days to look back. Defaults to a year.", required=False, default=365
discord.OptionChoice("ALL", "ALL"), ),
],
default="ALL"
),
look_back: discord.Option(
int,
description="How many days to look back. Defaults to a year.",
required=False,
default=365
)
): ):
"""View collected uptime stats.""" """View collected uptime stats."""
org_target = query_target
def generate_embed(target, specific_entries: list[UptimeEntry]): def generate_embed(target, specific_entries: list[UptimeEntry]):
targ = target
# targ = self.get_target(target_id=target)
embed = discord.Embed( embed = discord.Embed(
title=f"Uptime stats for {self.TARGETS_NAMES[target]}", title=f"Uptime stats for {targ['name']}",
description=f"Showing uptime stats for the last {look_back:,} days.", description=f"Showing uptime stats for the last {look_back:,} days.",
color=discord.Color.blurple() color=discord.Color.blurple(),
)
first_check = datetime.datetime.fromtimestamp(
specific_entries[-1].timestamp,
datetime.timezone.utc
) )
first_check = datetime.datetime.fromtimestamp(specific_entries[-1].timestamp, datetime.timezone.utc)
last_offline = last_online = None last_offline = last_online = None
online_count = offline_count = 0 online_count = offline_count = 0
for entry in reversed(specific_entries): for entry in reversed(specific_entries):
if entry.is_up is False: if entry.is_up is False:
last_offline = datetime.datetime.fromtimestamp( last_offline = datetime.datetime.fromtimestamp(entry.timestamp, datetime.timezone.utc)
entry.timestamp,
datetime.timezone.utc
)
offline_count += 1 offline_count += 1
else: else:
last_online = datetime.datetime.fromtimestamp( last_online = datetime.datetime.fromtimestamp(entry.timestamp, datetime.timezone.utc)
entry.timestamp,
datetime.timezone.utc
)
online_count += 1 online_count += 1
total_count = online_count + offline_count total_count = online_count + offline_count
online_avg = (online_count / total_count) * 100 online_avg = (online_count / total_count) * 100
average_response_time = ( average_response_time = (
sum(entry.response_time for entry in entries if entry.response_time is not None) / total_count sum(entry.response_time for entry in entries if entry.response_time is not None) / total_count
)
embed.add_field(
name="\u200b",
value=f"*Started monitoring {discord.utils.format_dt(first_check, style='R')}, "
f"{total_count:,} monitoring events collected*\n"
f"**Online:**\n\t\\* {online_avg:.2f}% of the time\n\t\\* Last online: "
f"{discord.utils.format_dt(last_online, 'R') if last_online else 'Never'}\n"
f"\n"
f"**Offline:**\n\t\\* {100 - online_avg:.2f}% of the time\n\t\\* Last offline: "
f"{discord.utils.format_dt(last_offline, 'R') if last_offline else 'Never'}\n"
f"\n"
f"**Average Response Time:**\n\t\\* {average_response_time:.2f}ms",
) )
if org_target != "ALL":
embed.add_field(
name="\u200b",
value=f"*Started monitoring {discord.utils.format_dt(first_check, style='R')}, "
f"{total_count:,} monitoring events collected*\n"
f"**Online:**\n\t\\* {online_avg:.2f}% of the time\n\t\\* Last online: "
f"{discord.utils.format_dt(last_online, 'R') if last_online else 'Never'}\n"
f"\n"
f"**Offline:**\n\t\\* {100 - online_avg:.2f}% of the time\n\t\\* Last offline: "
f"{discord.utils.format_dt(last_offline, 'R') if last_offline else 'Never'}\n"
f"\n"
f"**Average Response Time:**\n\t\\* {average_response_time:.2f}ms",
)
else:
embed.title = None
embed.description = f"{targ['name']}: {online_avg:.2f}% uptime, last offline: "
if last_offline:
embed.description += f"{discord.utils.format_dt(last_offline, 'R')}"
return embed return embed
await ctx.defer() await ctx.defer()
@ -254,27 +304,33 @@ class UptimeCompetition(commands.Cog):
look_back_timestamp = (now - timedelta(days=look_back)).timestamp() look_back_timestamp = (now - timedelta(days=look_back)).timestamp()
targets = [query_target] targets = [query_target]
if query_target == "ALL": if query_target == "ALL":
targets = ["SHRONK", "NEX_DROPLET", "SHRONK_DROPLET", "PI"] targets = [t["id"] for t in self.cached_targets]
embeds = [] embeds = []
for _target in targets: for _target in targets:
query = UptimeEntry.objects.filter(UptimeEntry.columns.timestamp >= look_back_timestamp).filter(target_id=_target) _target = self.get_target(_target, _target)
query = UptimeEntry.objects.filter(UptimeEntry.columns.timestamp >= look_back_timestamp).filter(
target_id=_target["id"]
)
query = query.order_by("-timestamp") query = query.order_by("-timestamp")
entries = await query.all() entries = await query.all()
if not entries: if not entries:
embeds.append( embeds.append(discord.Embed(description=f"No uptime entries found for {_target}."))
discord.Embed(
description=f"No uptime entries found for {_target}."
)
)
else: else:
embeds.append(generate_embed(_target, entries)) embeds.append(generate_embed(_target, entries))
if org_target == "ALL":
new_embed = discord.Embed(
title="Uptime stats for all monitored targets:",
description=f"Showing uptime stats for the last {look_back:,} days.\n\n",
color=discord.Color.blurple(),
)
for embed_ in embeds:
new_embed.description += f"{embed_.description}\n"
embeds = [new_embed]
await ctx.respond(embeds=embeds) await ctx.respond(embeds=embeds)
@uptime.command(name="view-next-run") @uptime.command(name="view-next-run")
async def view_next_run( async def view_next_run(self, ctx: discord.ApplicationContext):
self,
ctx: discord.ApplicationContext
):
"""View when the next uptime test will run.""" """View when the next uptime test will run."""
await ctx.defer() await ctx.defer()
next_run = self.test_uptimes.next_iteration next_run = self.test_uptimes.next_iteration
@ -284,8 +340,7 @@ class UptimeCompetition(commands.Cog):
_wait = self.bot.loop.create_task(discord.utils.sleep_until(next_run)) _wait = self.bot.loop.create_task(discord.utils.sleep_until(next_run))
view = self.CancelTaskView(_wait, next_run) view = self.CancelTaskView(_wait, next_run)
await ctx.respond( await ctx.respond(
f"The next uptime test will run in {discord.utils.format_dt(next_run, 'R')}. Waiting...", f"The next uptime test will run in {discord.utils.format_dt(next_run, 'R')}. Waiting...", view=view
view=view
) )
await _wait await _wait
if not self.task_event.is_set(): if not self.task_event.is_set():
@ -297,19 +352,139 @@ class UptimeCompetition(commands.Cog):
embed = discord.Embed( embed = discord.Embed(
title="Error", title="Error",
description=f"An error occurred while running an uptime test: {result}", description=f"An error occurred while running an uptime test: {result}",
color=discord.Color.red() color=discord.Color.red(),
) )
else: else:
result = await UptimeEntry.objects.get(entry_id=result.entry_id) result = await UptimeEntry.objects.get(entry_id=result.entry_id)
target = self.get_target(target_id=result.target_id) or {"name": result.target_id}
embed = discord.Embed( embed = discord.Embed(
title="Uptime for: " + self.TARGETS_NAMES[result.target_id], title="Uptime for: " + target["name"],
description="Is up: {0.is_up!s}\n" description="Is up: {0.is_up!s}\n"
"Response time: {1:,.2f}ms\n" "Response time: {1:,.2f}ms\n"
"Notes: {0.notes!s}".format(result, result.response_time or -1), "Notes: {0.notes!s}".format(result, result.response_time or -1),
color=discord.Color.green() color=discord.Color.green(),
) )
embeds.append(embed) embeds.append(embed)
await ctx.edit(content="Uptime test complete! Results are in.", embeds=embeds, view=None) if len(embeds) >= 3:
paginator = pages.Paginator(embeds, loop_pages=True)
await ctx.delete(delay=0.1)
await paginator.respond(ctx.interaction)
else:
await ctx.edit(content="Uptime test complete! Results are in:", embeds=embeds, view=None)
monitors = uptime.create_subgroup(name="monitors", description="Manage uptime monitors.")
@monitors.command(name="add")
@commands.is_owner()
async def add_monitor(
self,
ctx: discord.ApplicationContext,
name: discord.Option(str, description="The name of the monitor."),
uri: discord.Option(
str,
description="The URI to monitor. Enter HELP for more info.",
),
http_max_retries: discord.Option(
int,
description="The maximum number of HTTP retries to make if the request fails.",
required=False,
default=None,
),
http_timeout: discord.Option(
int, description="The timeout for the HTTP request.", required=False, default=None
),
):
"""Creates a monitor to... monitor"""
await ctx.defer()
name: str
uri: str
http_max_retries: Optional[int]
http_timeout: Optional[int]
uri: ParseResult = urlparse(uri.lower())
if uri.scheme == "" and uri.path.lower() == "help":
return await ctx.respond(
"URI can be either `HTTP(S)`, or the custom `USER` scheme.\n"
"Examples:\n"
"`http://example.com` - HTTP GET request to example.com\n"
"`https://example.com` - HTTPS GET request to example.com\n\n"
f"`user://{ctx.guild.id}/{ctx.user.id}?dnd=1` - Checks if user `{ctx.user.id}` (you) is"
f" in `dnd` (the red status) in the server `{ctx.guild.id}` (this server)."
f"\nQuery options are: `dnd`, `idle`, `online`, `offline`. `1` means the status is classed as "
f"'online' - anything else means offline.\n"
f"The format is `user://{{server_id}}/{{user_id}}?{{status}}={{1|0}}`\n"
f"Setting `server_id` to `$GUILD` will auto-fill in the current server ID."
)
options = {
"name": name,
"id": name.upper().strip().replace(" ", "_"),
}
if uri.scheme == "user":
uri: ParseResult = uri._replace(netloc=uri.hostname.replace("$guild", str(ctx.guild.id)))
data = self.parse_user_uri(uri.geturl())
if data["guild"] is None or data["guild"].isdigit() is False:
return await ctx.respond("Invalid guild ID in URI.")
if data["user"] is None or data["user"].isdigit() is False:
return await ctx.respond("Invalid user ID in URI.")
if not data["statuses"] or any(
x.lower() not in ("dnd", "idle", "online", "offline") for x in data["statuses"]
):
return await ctx.respond("Invalid status query string in URI. Did you forget to supply one?")
guild = self.bot.get_guild(int(data["guild"]))
if guild is None:
return await ctx.respond("Invalid guild ID in URI (not found).")
try:
guild.get_member(int(data["user"])) or await guild.fetch_member(int(data["user"]))
except discord.HTTPException:
return await ctx.respond("Invalid user ID in URI (not found).")
options["uri"] = uri.geturl()
elif uri.scheme in ["http", "https"]:
attempts, response = await self._test_url(uri.geturl(), max_retries=http_max_retries, timeout=http_timeout)
if response is None:
return await ctx.respond(
f"Failed to connect to {uri.geturl()!r} after {attempts} attempts. Please ensure the target page"
f" is up, and try again."
)
options["uri"] = uri.geturl()
options["http_max_retries"] = http_max_retries
options["http_timeout"] = http_timeout
else:
return await ctx.respond("Invalid URI scheme. Supported: HTTP[S], USER.")
targets = self.cached_targets
for target in targets:
if target["uri"] == options["uri"] or target["name"] == options["name"]:
return await ctx.respond("This monitor already exists.")
targets.append(options)
self.write_targets(targets)
await ctx.respond("Monitor added!")
@monitors.command(name="remove")
@commands.is_owner()
async def remove_monitor(
self,
ctx: discord.ApplicationContext,
name: discord.Option(str, description="The name of the monitor."),
):
"""Removes a monitor."""
await ctx.defer()
name: str
targets = self.cached_targets
for target in targets:
if target["name"] == name or target["id"] == name:
targets.remove(target)
self.write_targets(targets)
return await ctx.respond("Monitor removed.")
await ctx.respond("Monitor not found.")
def setup(bot): def setup(bot):

View file

@ -29,7 +29,7 @@ extensions = [
"cogs.timetable", "cogs.timetable",
"cogs.other", "cogs.other",
"cogs.starboard", "cogs.starboard",
"cogs.uptime" "cogs.uptime",
] ]
for ext in extensions: for ext in extensions:
try: try:

View file

@ -21,7 +21,16 @@ class Tutors(IntEnum):
os.chdir(Path(__file__).parent.parent) os.chdir(Path(__file__).parent.parent)
__all__ = ["registry", "get_or_none", "VerifyCode", "Student", "BannedStudentID", "Assignments", "Tutors", "UptimeEntry"] __all__ = [
"registry",
"get_or_none",
"VerifyCode",
"Student",
"BannedStudentID",
"Assignments",
"Tutors",
"UptimeEntry",
]
T = TypeVar("T") T = TypeVar("T")
T_co = TypeVar("T_co", covariant=True) T_co = TypeVar("T_co", covariant=True)