diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index e0186099..f1188c95 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -14,7 +14,7 @@ jobs: matrix: python_version: - "3.8" - - "3.9" + - "3.11" tox_env: - style-black - style-isort @@ -40,17 +40,17 @@ jobs: name: Tox - ${{ matrix.python_version }} - ${{ matrix.friendly_name }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: ref: ${{ env.ref }} - name: Set up Python ${{ matrix.python_version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python_version }} # caching cuts down time for tox (for example black) from ~40 secs to 4 - name: Cache tox - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: .tox key: tox-${{ matrix.python_version }}-${{ matrix.tox_env }}-${{ hashFiles('tox.ini') }} diff --git a/.github/workflows/loadcheck.yml b/.github/workflows/loadcheck.yml index 21cf1319..31777ad5 100644 --- a/.github/workflows/loadcheck.yml +++ b/.github/workflows/loadcheck.yml @@ -10,30 +10,26 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8", "3.9"] - red-version: - # this workflow required pr #5453 commit d27dbde, which is in dev & pypi 3.4.15+ - - "git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=Red-DiscordBot" - - "Red-DiscordBot==3.4.16" + python-version: ["3.8", "3.11"] include: - red-version: "git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=Red-DiscordBot" friendly-red: "Red (dev version)" - - red-version: "Red-DiscordBot==3.4.16" - friendly-red: "Red 3.4.16" + - red-version: "Red-DiscordBot==3.4.19" + friendly-red: "Red 3.4.19" fail-fast: false name: Cog load test - Python ${{ matrix.python-version }} & ${{ matrix.friendly-red }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Cache venv id: cache-venv - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: .venv key: ${{ matrix.red-version }}-${{ matrix.python-version }}-${{ hashFiles('dev-requirements.txt') }}-${{ secrets.CACHE_V }} @@ -62,7 +58,7 @@ jobs: - name: Save Red output as Artifact if: always() # still run if prev step failed - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: "Red log - Python ${{ matrix.python-version }} & ${{ matrix.friendly-red }}" path: red.log diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ed2d3904..00e2c8ca 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,16 +10,16 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8"] + python-version: ["3.11"] name: Release steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: - python-version: 3.9 + python-version: 3.11 - name: Install dependencies run: | diff --git a/.github/workflows/scripts/loadcheck.py b/.github/workflows/scripts/loadcheck.py index 533b5c5d..d5bc20fb 100644 --- a/.github/workflows/scripts/loadcheck.py +++ b/.github/workflows/scripts/loadcheck.py @@ -30,21 +30,19 @@ # let Red boot up time.sleep(10) -# not compatible with dpy 1.x -# ["buttonpoll", "ghissues"] - cogs = [ "aliases", "anotherpingcog", "beautify", "betteruptime", + "buttonpoll", "birthday", + "calc", "caseinsensitive", "cmdlog", - "covidgraph", "fivemstatus", - "github", "googletrends", + "ghissues", "madtranslate", "roleplay", "stattrack", @@ -53,10 +51,6 @@ "timechannel", "uptimeresponder", "wol", - # dpy 2 cogs: - # "buttonpoll", - # "ghissues", - # "calc", ] diff --git a/.github/workflows/scripts/syncutils.py b/.github/workflows/scripts/syncutils.py index 31946042..1b6736e9 100644 --- a/.github/workflows/scripts/syncutils.py +++ b/.github/workflows/scripts/syncutils.py @@ -59,9 +59,7 @@ "calc", "caseinsensitive", "cmdlog", - "covidgraph", "fivemstatus", - "github", "googletrends", "ghissues", "madtranslate", diff --git a/.github/workflows/syncutils.yml b/.github/workflows/syncutils.yml index 99edc5e7..3a9d3c0d 100644 --- a/.github/workflows/syncutils.yml +++ b/.github/workflows/syncutils.yml @@ -7,28 +7,28 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8"] + python-version: ["3.11"] name: Sync utils steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - pip install gitpython requests + - name: Install dependencies + run: | + pip install gitpython requests - - name: Run script syncutils.py - run: | - python .github/workflows/scripts/syncutils.py - env: - CF_KV: ${{ secrets.CF_KV}} + - name: Run script syncutils.py + run: | + python .github/workflows/scripts/syncutils.py + env: + CF_KV: ${{ secrets.CF_KV}} - - name: "Commit and push changes" - uses: stefanzweifel/git-auto-commit-action@v4 - with: - commit_message: Automated utils sync + - name: "Commit and push changes" + uses: stefanzweifel/git-auto-commit-action@v4 + with: + commit_message: Automated utils sync diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..e88e6c7a --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,13 @@ +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.11" + +sphinx: + configuration: docs/conf.py + +python: + install: + - requirements: docs/requirements.txt diff --git a/README.md b/README.md index 3e4535d7..aade6a3c 100644 --- a/README.md +++ b/README.md @@ -30,11 +30,13 @@ There's a list of cogs below, or you can use `[p]cog list vex-cogs` if you've ad | [beautify](https://go.vexcodes.com/c/beautify) | Beautify and minify JSON. | | [betteruptime](https://go.vexcodes.com/c/betteruptime) | See your bot's uptime percentages and check when downtime happened. | | [birthday](https://go.vexcodes.com/c/birthday) | Birthday cog with customisable messages and roles. | +| [buttonpoll](https://go.vexcodes.com/c/buttonpoll) | Create polls with buttons! | +| [calc](https://go.vexcodes.com/c/calc) | A simple button-based calculator. | | [caseinsensitive](https://go.vexcodes.com/c/caseinsensitive) | Make all prefixes and commands case insensitive. | | [cmdlog](https://go.vexcodes.com/c/cmdlog) | Track command usage, searchable by user, server or command name. | | [covidgraph](https://go.vexcodes.com/c/covidgraph) | Get graphs of COVID-19 data. | | [fivemstatus](https://go.vexcodes.com/c/fivemstatus) | View the live status of a FiveM server, in a updating Discord message. | -| [github](https://go.vexcodes.com/c/github) | Create, comment, labelify and close GitHub issues, with partial PR support. | +| [ghissues](https://go.vexcodes.com/c/ghissues) | Create, comment, labelify and close GitHub issues, with some PR support. | | [googletrends](https://go.vexcodes.com/c/googletrends) | Find out what the world is searching, right from Discord. | | [madtranslate](https://go.vexcodes.com/c/madtranslate) | Translate text through lots of languages. Get some funny results! | | [roleplay](https://go.vexcodes.com/c/roleplay) | Create an anonymous role play in your server. | diff --git a/aliases/__init__.py b/aliases/__init__.py index ded8517d..74bf58cd 100644 --- a/aliases/__init__.py +++ b/aliases/__init__.py @@ -14,6 +14,4 @@ async def setup(bot: Red) -> None: cog = Aliases(bot) await out_of_date_check("aliases", cog.__version__) - r = bot.add_cog(cog) - if r is not None: - await r + await bot.add_cog(cog) diff --git a/aliases/info.json b/aliases/info.json index 6a4392f1..73036052 100644 --- a/aliases/info.json +++ b/aliases/info.json @@ -6,8 +6,7 @@ "description": "Get all the information you could ever need about a command's aliases. IF YOU ARE TRYING TO *MAKE* ALIASES, JUST USE THE CORE 'alias` COG.", "end_user_data_statement": "This cog does not persistently store data or metadata about users.", "install_msg": "\\N{WARNING SIGN}\\N{VARIATION SELECTOR-16}\nThis cog reads the config of the alias cog. This means it might break without much notice.\n\nIf you are just trying to **make aliases**: this cog isn't for that. Use the core `alias` cog to make and manage aliases with the `[p]load alias` command, then `[p]help alias`.\n\nThis cog has docs! Check them out at ", - "max_bot_version": "3.6.0.dev0", - "min_bot_version": "3.4.0", + "min_bot_version": "3.5.1", "min_python_version": [ 3, 8, diff --git a/aliases/vexutils/meta.py b/aliases/vexutils/meta.py index cae0ae73..1e963e4c 100644 --- a/aliases/vexutils/meta.py +++ b/aliases/vexutils/meta.py @@ -2,11 +2,12 @@ import asyncio import json -from logging import Logger, getLogger from pathlib import Path from typing import Literal, NamedTuple import aiohttp +from red_commons.logging import RedTraceLogger +from red_commons.logging import getLogger as red_get_logger from redbot.core import VersionInfo, commands from redbot.core import version_info as cur_red_version from rich import box as rich_box @@ -16,13 +17,13 @@ from .consts import DOCS_BASE, GREEN_CIRCLE, RED_CIRCLE from .loop import VexLoop -log = getLogger("red.vex-utils") +log = red_get_logger("red.vex-utils") cog_ver_lock = asyncio.Lock() -def get_vex_logger(name: str) -> Logger: +def get_vex_logger(name: str) -> RedTraceLogger: """Get a logger for the given name. Parameters @@ -42,7 +43,7 @@ def get_vex_logger(name: str) -> Logger: else: # otherwise use full path final_name += name - return getLogger(final_name) + return red_get_logger(final_name) def format_help(self: commands.Cog, ctx: commands.Context) -> str: diff --git a/anotherpingcog/anotherpingcog.py b/anotherpingcog/anotherpingcog.py index abbaa103..41424e42 100644 --- a/anotherpingcog/anotherpingcog.py +++ b/anotherpingcog/anotherpingcog.py @@ -1,5 +1,6 @@ from __future__ import annotations +import contextlib from time import monotonic import discord @@ -43,7 +44,7 @@ class AnotherPingCog(commands.Cog): You can customise the emojis, colours or force embeds with `[p]pingset`. """ - __version__ = "1.1.7" + __version__ = "1.1.8" __author__ = "Vexed#0714" def __init__(self, bot: Red) -> None: @@ -53,22 +54,21 @@ def __init__(self, bot: Red) -> None: self.config.register_global(force_embed=True, footer="default") self.config.register_global(custom_settings=DEFAULT_CONF) - async def async_init(self) -> None: + async def cog_load(self) -> None: self.cache = Cache( await self.config.custom_settings(), await self.config.force_embed(), await self.config.footer(), self.bot, ) + log.trace("Cache loaded: %s", self.cache) - def cog_unload(self) -> None: + async def cog_unload(self) -> None: global old_ping if old_ping: - try: + with contextlib.suppress(Exception): self.bot.remove_command("ping") - except Exception: - pass - self.bot.add_command(old_ping) + self.bot.add_command(old_ping) def format_help_for_context(self, ctx: commands.Context) -> str: """Thanks Sinbad.""" @@ -83,7 +83,7 @@ async def apcinfo(self, ctx: commands.Context): await ctx.send(await format_info(ctx, self.qualified_name, self.__version__)) # cspell:disable-next-line - @commands.command(aliases=["pinf", "pig", "png", "pign", "pjgn", "ipng", "pgn", "pnig"]) + @commands.hybrid_command(aliases=["pinf", "pig", "png", "pign", "pjgn", "ipng", "pgn", "pnig"]) async def ping(self, ctx: commands.Context): """ A rich embed ping command with timings. @@ -128,7 +128,9 @@ async def ping(self, ctx: commands.Context): elif settings.footer != "none": embed.set_footer(text=settings.footer) start = monotonic() - message: discord.Message = await ctx.send(embed=embed) + message: discord.Message = await ctx.send( + embed=embed, + ) else: msg = f"**{title}**\nDiscord WS: {ws_latency} ms" start = monotonic() @@ -150,7 +152,16 @@ async def ping(self, ctx: commands.Context): extra = box(f"{m_latency} ms", "py") embed.add_field(name="Message Send", value=f"{m_latency_text}{extra}") embed.colour = colour - await message.edit(embed=embed) + await message.edit( + content=( + "Message Send is worse for slash commands. Try using the text command for " + "a better result." + ) + if ctx.interaction + else None, + embed=embed, + ) + else: data = [ ["Discord WS", "Message Send"], @@ -475,7 +486,8 @@ async def settings(self, ctx: commands.Context): "non-embed version." ) settings = self.cache - embed = discord.Embed( + log.debug("Raw cached settings: %s", settings) + main_embed = discord.Embed( title="Global settings for the `ping` command.", color=await ctx.embed_color() ) embeds = "**Force embed setting:**\n" @@ -484,7 +496,7 @@ async def settings(self, ctx: commands.Context): if settings.force_embed else "False - `embedset` is how embeds will be determined (defaults to True)." ) - embed.add_field(name="Embeds", value=embeds, inline=False) + main_embed.add_field(name="Embeds", value=embeds, inline=False) footer = "**Embed footer setting:**\n" footer += ( "Default - the default text will be used in the embed footer." @@ -493,36 +505,32 @@ async def settings(self, ctx: commands.Context): if settings.footer == "none" else f"Custom - {settings.footer}" ) - embed.add_field(name="Footer", value=footer, inline=False) + main_embed.add_field(name="Footer", value=footer, inline=False) # these 3 are alright with the 5/5 rate limit, plus it's owner only. # if anyone wants to PR something with image generation, don't as it's wayyyyy to complex # for this - await ctx.send(embed=embed) - await ctx.send( - embed=discord.Embed( - title=f"Emoji for green: {self.cache.green.emoji}", - description=f"{LEFT_ARROW} Colour for green", - colour=self.cache.green.colour, - ) + + green_embed = discord.Embed( + title=f"Emoji for green: {self.cache.green.emoji}", + description=f"{LEFT_ARROW} Colour for green", + colour=self.cache.green.colour, ) - await ctx.send( - embed=discord.Embed( - title=f"Emoji for orange: {self.cache.orange.emoji}", - description=f"{LEFT_ARROW} Colour for orange", - colour=self.cache.orange.colour, - ) + orange_embed = discord.Embed( + title=f"Emoji for orange: {self.cache.orange.emoji}", + description=f"{LEFT_ARROW} Colour for orange", + colour=self.cache.orange.colour, ) - await ctx.send( - embed=discord.Embed( - title=f"Emoji for red: {self.cache.red.emoji}", - description=f"{LEFT_ARROW} Colour for red", - colour=self.cache.red.colour, - ) + red_embed = discord.Embed( + title=f"Emoji for red: {self.cache.red.emoji}", + description=f"{LEFT_ARROW} Colour for red", + colour=self.cache.red.colour, ) + await ctx.send(embeds=(main_embed, green_embed, orange_embed, red_embed)) + async def setup(bot: Red) -> None: global old_ping @@ -531,8 +539,5 @@ async def setup(bot: Red) -> None: bot.remove_command(old_ping.name) cog = AnotherPingCog(bot) - await cog.async_init() await out_of_date_check("anotherpingcog", cog.__version__) - r = bot.add_cog(cog) - if r is not None: - await r + await bot.add_cog(cog) diff --git a/anotherpingcog/info.json b/anotherpingcog/info.json index 6dfcdbea..44db9198 100644 --- a/anotherpingcog/info.json +++ b/anotherpingcog/info.json @@ -6,8 +6,7 @@ "description": "Replace the ping command with a rich embed that shows ping time and message time. It is colour coded: red, orange and green, and the bot owner can customise the colours and emojis for different latency levels, as well as force an embed.", "end_user_data_statement": "This cog does not persistently store data or metadata about users.", "install_msg": "Thanks for installing! This cog will replace the normal `ping` command. The bot owner can customise the colours and emojis for different latency levels, as well as force an embed with use `[p]pingset`.\n\nThis cog has docs! Check them out at ", - "max_bot_version": "3.6.0.dev0", - "min_bot_version": "3.4.0", + "min_bot_version": "3.5.1", "min_python_version": [ 3, 8, diff --git a/anotherpingcog/vexutils/meta.py b/anotherpingcog/vexutils/meta.py index cae0ae73..1e963e4c 100644 --- a/anotherpingcog/vexutils/meta.py +++ b/anotherpingcog/vexutils/meta.py @@ -2,11 +2,12 @@ import asyncio import json -from logging import Logger, getLogger from pathlib import Path from typing import Literal, NamedTuple import aiohttp +from red_commons.logging import RedTraceLogger +from red_commons.logging import getLogger as red_get_logger from redbot.core import VersionInfo, commands from redbot.core import version_info as cur_red_version from rich import box as rich_box @@ -16,13 +17,13 @@ from .consts import DOCS_BASE, GREEN_CIRCLE, RED_CIRCLE from .loop import VexLoop -log = getLogger("red.vex-utils") +log = red_get_logger("red.vex-utils") cog_ver_lock = asyncio.Lock() -def get_vex_logger(name: str) -> Logger: +def get_vex_logger(name: str) -> RedTraceLogger: """Get a logger for the given name. Parameters @@ -42,7 +43,7 @@ def get_vex_logger(name: str) -> Logger: else: # otherwise use full path final_name += name - return getLogger(final_name) + return red_get_logger(final_name) def format_help(self: commands.Cog, ctx: commands.Context) -> str: diff --git a/beautify/__init__.py b/beautify/__init__.py index aeada71c..ee06295e 100644 --- a/beautify/__init__.py +++ b/beautify/__init__.py @@ -17,6 +17,4 @@ async def setup(bot: Red): cog = Beautify(bot) await out_of_date_check("beautify", cog.__version__) - r = bot.add_cog(cog) - if r is not None: - await r + await bot.add_cog(cog) diff --git a/beautify/beautify.py b/beautify/beautify.py index f802822d..a1b6e3b2 100644 --- a/beautify/beautify.py +++ b/beautify/beautify.py @@ -6,15 +6,19 @@ from .errors import JSONDecodeError, NoData from .utils import decode_json, get_data, send_output -from .vexutils import format_help, format_info +from .vexutils import format_help, format_info, get_vex_logger + +log = get_vex_logger(__name__) # dont want to force this as can be a pain on windows try: import pyjson5 # noqa # import otherwise unused use_pyjson = True + log.debug("pyjson5 available") except ImportError: use_pyjson = False + log.debug("pyjson5 not available") # NOTE FOR DOCSTRINGS: # They don't use a normal space character, if you're editing them make sure to copy and paste @@ -118,7 +122,8 @@ async def com_minify(self, ctx: commands.Context, *, data: Optional[str]): """ try: raw_json = await get_data(ctx, data) - except NoData: + except NoData as e: + log.debug("No data found for msg %s", ctx.message.id, exc_info=e) return try: diff --git a/beautify/info.json b/beautify/info.json index a06a6b67..827df6fd 100644 --- a/beautify/info.json +++ b/beautify/info.json @@ -6,8 +6,7 @@ "description": "Beautify and minify JSON. Supports attachments, codeblocks and replies.", "end_user_data_statement": "This cog does not persistently store data or metadata about users.", "install_msg": "This cog has two commands - `beautify` and `minify`.\n\n**If you would like this cog to support Python dicts as well as normal JSON, please run this command with your prefix ([p]) replaced: `[p]pipinstall pyjson5`. This should work on Linux, but it can error on Windows. It is not required for the cog so don't worry if it does error.**\n\nThis cog has docs! Check them out at ", - "max_bot_version": "3.6.0.dev0", - "min_bot_version": "3.4.0", + "min_bot_version": "3.5.1", "min_python_version": [ 3, 8, diff --git a/beautify/utils.py b/beautify/utils.py index 8118c428..bc2ab774 100644 --- a/beautify/utils.py +++ b/beautify/utils.py @@ -16,7 +16,7 @@ except ImportError: use_pyjson = False -_log = get_vex_logger(__name__) +log = get_vex_logger(__name__) def cleanup_json(json: str) -> str: @@ -98,7 +98,7 @@ def decode_json(str_json: str) -> DecodeReturn: if isinstance(json_pyjson, dict): return DecodeReturn(json_pyjson, changed_input) except Exception: # cant just catch pyjson5 as might not be imported... sad - _log.debug( + log.debug( "Exception caught. If the bellow information doesn't mention 'pyjson5' please " "report this to Vexed.", exc_info=True, diff --git a/beautify/vexutils/meta.py b/beautify/vexutils/meta.py index cae0ae73..1e963e4c 100644 --- a/beautify/vexutils/meta.py +++ b/beautify/vexutils/meta.py @@ -2,11 +2,12 @@ import asyncio import json -from logging import Logger, getLogger from pathlib import Path from typing import Literal, NamedTuple import aiohttp +from red_commons.logging import RedTraceLogger +from red_commons.logging import getLogger as red_get_logger from redbot.core import VersionInfo, commands from redbot.core import version_info as cur_red_version from rich import box as rich_box @@ -16,13 +17,13 @@ from .consts import DOCS_BASE, GREEN_CIRCLE, RED_CIRCLE from .loop import VexLoop -log = getLogger("red.vex-utils") +log = red_get_logger("red.vex-utils") cog_ver_lock = asyncio.Lock() -def get_vex_logger(name: str) -> Logger: +def get_vex_logger(name: str) -> RedTraceLogger: """Get a logger for the given name. Parameters @@ -42,7 +43,7 @@ def get_vex_logger(name: str) -> Logger: else: # otherwise use full path final_name += name - return getLogger(final_name) + return red_get_logger(final_name) def format_help(self: commands.Cog, ctx: commands.Context) -> str: diff --git a/betteruptime/abc.py b/betteruptime/abc.py index 50344b7f..9457e828 100644 --- a/betteruptime/abc.py +++ b/betteruptime/abc.py @@ -3,8 +3,8 @@ from typing import TYPE_CHECKING import pandas +from redbot.core import commands from redbot.core.bot import Red -from redbot.core.commands import CogMeta from redbot.core.config import Config from .vexutils.loop import VexLoop @@ -13,7 +13,7 @@ from betteruptime.utils import UptimeData -class CompositeMetaClass(CogMeta, ABCMeta): +class CompositeMetaClass(commands.CogMeta, ABCMeta): """ This allows the metaclass used for proper type detection to coexist with discord.py's metaclass @@ -44,5 +44,13 @@ async def get_data(self, num_days: int) -> "UptimeData": raise NotImplementedError @abstractmethod - async def async_init(self) -> None: + async def uptime_command(self, ctx: commands.Context, days: int = 30) -> None: + raise NotImplementedError + + @abstractmethod + async def downtime(self, ctx: commands.Context, days: int = 30) -> None: + raise NotImplementedError + + @abstractmethod + async def uptimegraph(self, ctx: commands.Context, days: int = 30) -> None: raise NotImplementedError diff --git a/betteruptime/betteruptime.py b/betteruptime/betteruptime.py index 9d8eaea6..9b3e022a 100644 --- a/betteruptime/betteruptime.py +++ b/betteruptime/betteruptime.py @@ -11,6 +11,7 @@ from .abc import CompositeMetaClass from .commands import BUCommands from .loop import BULoop +from .slash import BUSlash from .utils import Utils from .vexutils import format_help, format_info, get_vex_logger from .vexutils.chat import humanize_bytes @@ -23,7 +24,7 @@ # THIS COG WILL BE REWRITTEN/REFACTORED AT SOME POINT (#23) -class BetterUptime(commands.Cog, BUCommands, BULoop, Utils, metaclass=CompositeMetaClass): +class BetterUptime(commands.Cog, BUCommands, BUSlash, BULoop, Utils, metaclass=CompositeMetaClass): """ Replaces the core `uptime` command to show the uptime percentage over the last 30 days. @@ -32,7 +33,7 @@ class BetterUptime(commands.Cog, BUCommands, BULoop, Utils, metaclass=CompositeM data to become available. """ - __version__ = "2.1.3" + __version__ = "2.1.4" __author__ = "Vexed#0714" def __init__(self, bot: Red) -> None: @@ -68,7 +69,10 @@ async def red_delete_data_for_user(self, **kwargs) -> None: """Nothing to delete""" return - def cog_unload(self) -> None: + async def cog_load(self) -> None: + await self.setup_loop() + + async def cog_unload(self) -> None: log.info("BetterUptime is now unloading. Cleaning up...") if self.main_loop: @@ -117,8 +121,5 @@ async def setup(bot: Red) -> None: bot.remove_command(old_uptime.name) cog = BetterUptime(bot) - await cog.async_init() await out_of_date_check("betteruptime", cog.__version__) - r = bot.add_cog(cog) - if r is not None: - await r + await bot.add_cog(cog) diff --git a/betteruptime/commands.py b/betteruptime/commands.py index a6c849f5..fb592b1d 100644 --- a/betteruptime/commands.py +++ b/betteruptime/commands.py @@ -15,8 +15,11 @@ from .abc import MixinMeta from .consts import SECONDS_IN_DAY, WARN from .plot import plot +from .vexutils import get_vex_logger from .vexutils.chat import datetime_to_timestamp +log = get_vex_logger(__name__) + old_uptime = None @@ -57,6 +60,7 @@ async def uptime_command(self, ctx: commands.Context, num_days: int = 30): data = await self.get_data(num_days) else: data = await self.get_data(num_days) + log.trace("pd data obj:\n%s", data) embed = discord.Embed(description=description, colour=await ctx.embed_colour()) @@ -109,6 +113,7 @@ async def downtime(self, ctx: commands.Context, num_days: int = 30): data = await self.get_data(num_days) else: data = await self.get_data(num_days) + log.trace("pd data obj:\n%s", data) midnight = datetime.datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0) if data.first_load > midnight: # cog was first loaded today @@ -139,7 +144,10 @@ async def downtime(self, ctx: commands.Context, num_days: int = 30): f"downtime today._\n\n{msg}" ) paged = pagify(full, page_length=1000) - await ctx.send_interactive(paged) + if ctx.interaction: + await ctx.send([i for i in paged][0]) + else: + await ctx.send_interactive(paged) @commands.command() async def uptimegraph(self, ctx: commands.Context, num_days: int = 30): @@ -164,6 +172,7 @@ async def uptimegraph(self, ctx: commands.Context, num_days: int = 30): data = await self.get_data(num_days) else: data = await self.get_data(num_days) + log.trace("pd data obj:\n%s", data) sr = data.daily_connected_percentages() diff --git a/betteruptime/info.json b/betteruptime/info.json index a004be6d..429433c8 100644 --- a/betteruptime/info.json +++ b/betteruptime/info.json @@ -6,8 +6,7 @@ "description": "Replace the uptime command with a rich embed that shows the bot's percentage uptime (both time of the bot being on and time connected to Discord). There is also a new `downtime` command which shows when downtime happened. This cog writes to config every 60 seconds to prevent data loss. It is also very storage efficient, using under 150 bytes each day the cog runs.", "end_user_data_statement": "This cog does not persistently store data or metadata about users.", "install_msg": "Thanks for installing! This cog will replace the default `uptime` command once you load it.\n\nWhilst the cog will start showing data from first load, it will ignore today's data from tomorrow onwards. Once the cog's been running for a while, data over 30 days old will no longer be counted in the `uptime` command.\n\nThis cog has docs! Check them out at ", - "max_bot_version": "3.6.0.dev0", - "min_bot_version": "3.4.0", + "min_bot_version": "3.5.1", "min_python_version": [ 3, 8, diff --git a/betteruptime/loop.py b/betteruptime/loop.py index d2c4961f..41b6a103 100644 --- a/betteruptime/loop.py +++ b/betteruptime/loop.py @@ -11,13 +11,11 @@ from .vexutils import get_vex_logger from .vexutils.loop import VexLoop -_log = get_vex_logger(__name__) +log = get_vex_logger(__name__) class BULoop(MixinMeta): - async def async_init(self) -> None: - _log.debug("[BU SETUP] Starting setup...") - + async def setup_loop(self) -> None: self.first_load = await self.config.first_load() # want to make sure its actually written if self.first_load is None: @@ -25,22 +23,24 @@ async def async_init(self) -> None: self.first_load = time() if await self.config.version() == 1: - _log.info("Migrating BetterUptime config to new format (1 -> 3)...") + log.info("Migrating BetterUptime config to new format (1 -> 3)...") await self.migrate_v1_to_v3() await self.config.version.set(3) elif await self.config.version() == 2: - _log.info("Migrating BetterUptime config to new format (2 -> 3)...") + log.info("Migrating BetterUptime config to new format (2 -> 3)...") await self.migate_v2_to_v3() await self.config.version.set(3) else: self.cog_loaded_cache = pandas.Series( pandas.read_json(json.dumps(await self.config.cog_loaded()), typ="series") ) + log.trace("pd obj for cog loaded cache:\n%s", self.cog_loaded_cache) self.connected_cache = pandas.Series( pandas.read_json(json.dumps(await self.config.connected()), typ="series") ) + log.trace("pd obj for connected cache:\n%s", self.connected_cache) - _log.debug("[BU SETUP] Config setup finished, waiting to start loops") + log.debug("Config setup finished, waiting to start loops") self.main_loop = self.bot.loop.create_task(self.betteruptime_main_loop()) @@ -86,6 +86,8 @@ def convert(data: Dict[str, float]) -> pandas.Series: await self.write_to_config() async def betteruptime_main_loop(self): + await self.bot.wait_until_red_ready() + self.last_known_ping = self.bot.latency self.last_ping_change = time() @@ -98,33 +100,38 @@ async def betteruptime_main_loop(self): self.main_loop_meta = VexLoop("BetterUptime Main Loop", 60.0) - _log.debug("[BU SETUP] Starting loop") - _log.debug("[BU SETUP] BetterUptime is now fully initialised. Setup complete.") + log.debug("[BU SETUP] Starting loop") + log.debug("[BU SETUP] BetterUptime is now fully initialised. Setup complete.") self.ready.set() while True: - _log.debug("Loop has started next iteration") + log.verbose("Loop has started next iteration") try: self.main_loop_meta.iter_start() await self.update_uptime() self.main_loop_meta.iter_finish() - _log.debug("Loop has finished, saved to config") - except Exception: - _log.exception( + log.verbose("Loop has finished") + except Exception as e: + self.main_loop_meta.iter_error(e) + log.exception( "Something went wrong in the main BetterUptime loop. The loop will try again " - "in 60 seconds. Please report this and the below information to Vexed." + "in 60 seconds. Please report this and the below information to Vexed.", + exc_info=e, ) await self.main_loop_meta.sleep_until_next() async def write_to_config(self) -> None: + log.trace("write to config called") if not self.cog_loaded_cache.empty: data = json.loads(self.cog_loaded_cache.to_json()) # type: ignore await self.config.cog_loaded.set(data) + log.trace("written cog loaded cache to config") if not self.connected_cache.empty: data = json.loads(self.connected_cache.to_json()) # type: ignore await self.config.connected.set(data) + log.trace("written connected cache to config") async def update_uptime(self): utcdatetoday = datetime.datetime.utcnow().replace( diff --git a/betteruptime/slash.py b/betteruptime/slash.py new file mode 100644 index 00000000..69dcd04d --- /dev/null +++ b/betteruptime/slash.py @@ -0,0 +1,26 @@ +import discord +from redbot.core import app_commands, commands + +from .abc import MixinMeta + + +class BUSlash(MixinMeta): + uptime = app_commands.Group(name="uptime", description="Get my uptime data") + + @uptime.command(name="data", description="Get my uptime data in an embed") + @app_commands.describe(days="Days of data to show, use 0 for all-time data. Default: 30") + async def uptime_slash(self, interaction: discord.Interaction, days: int = 30): + context: commands.Context = await self.bot.get_context(interaction) + await self.uptime_command(context) + + @uptime.command(name="graph", description="Get my uptime graph in an embed") + @app_commands.describe(days="Days of data to show, use 0 for all-time data. Default: 30") + async def uptimegraph_slash(self, interaction: discord.Interaction, days: int = 30): + context: commands.Context = await self.bot.get_context(interaction) + await self.uptimegraph(context) + + @uptime.command(name="downtime", description="Get my downtime data in an embed") + @app_commands.describe(days="Days of data to show, use 0 for all-time data. Default: 30") + async def downtime_slash(self, interaction: discord.Interaction, days: int = 30): + context: commands.Context = await self.bot.get_context(interaction) + await self.downtime(context) diff --git a/betteruptime/utils.py b/betteruptime/utils.py index 85fff515..b041125b 100644 --- a/betteruptime/utils.py +++ b/betteruptime/utils.py @@ -9,6 +9,9 @@ from .abc import MixinMeta from .consts import SECONDS_IN_DAY +from .vexutils import get_vex_logger + +log = get_vex_logger(__name__) def round_up_to_min(num: float): @@ -140,6 +143,20 @@ async def get_data(self, num_days: int) -> UptimeData: midnight = now.replace(hour=0, minute=0, second=0, microsecond=0) seconds_since_midnight = float((now - midnight).total_seconds()) + log.trace( + "data filter things: %s", + { + "num_days": num_days, + "now": now, + "midnight": midnight, + "seconds_since_midnight": seconds_since_midnight, + "seconds_cog_loaded": seconds_cog_loaded, + "seconds_connected": seconds_connected, + "expected_index": expected_index, + "conf_first_loaded": conf_first_loaded, + }, + ) + if len(expected_index) >= num_days: # need to cut down from days collected expected_index = expected_index[-(num_days):] seconds_data_collected = float( diff --git a/betteruptime/vexutils/meta.py b/betteruptime/vexutils/meta.py index cae0ae73..1e963e4c 100644 --- a/betteruptime/vexutils/meta.py +++ b/betteruptime/vexutils/meta.py @@ -2,11 +2,12 @@ import asyncio import json -from logging import Logger, getLogger from pathlib import Path from typing import Literal, NamedTuple import aiohttp +from red_commons.logging import RedTraceLogger +from red_commons.logging import getLogger as red_get_logger from redbot.core import VersionInfo, commands from redbot.core import version_info as cur_red_version from rich import box as rich_box @@ -16,13 +17,13 @@ from .consts import DOCS_BASE, GREEN_CIRCLE, RED_CIRCLE from .loop import VexLoop -log = getLogger("red.vex-utils") +log = red_get_logger("red.vex-utils") cog_ver_lock = asyncio.Lock() -def get_vex_logger(name: str) -> Logger: +def get_vex_logger(name: str) -> RedTraceLogger: """Get a logger for the given name. Parameters @@ -42,7 +43,7 @@ def get_vex_logger(name: str) -> Logger: else: # otherwise use full path final_name += name - return getLogger(final_name) + return red_get_logger(final_name) def format_help(self: commands.Cog, ctx: commands.Context) -> str: diff --git a/birthday/__init__.py b/birthday/__init__.py index 2318dda4..ba4f9449 100644 --- a/birthday/__init__.py +++ b/birthday/__init__.py @@ -17,7 +17,4 @@ async def setup(bot: Red) -> None: cog = Birthday(bot) await out_of_date_check("birthday", cog.__version__) - await cog.async_init() - r = bot.add_cog(cog) - if r is not None: - await r + await bot.add_cog(cog) diff --git a/birthday/birthday.py b/birthday/birthday.py index cbb4dfc0..c60201a1 100644 --- a/birthday/birthday.py +++ b/birthday/birthday.py @@ -29,7 +29,7 @@ class Birthday( Set yours and get a message and role on your birthday! """ - __version__ = "1.2.0" + __version__ = "1.2.1" __author__ = "Vexed#0714" def __init__(self, bot: Red) -> None: @@ -57,13 +57,13 @@ def __init__(self, bot: Red) -> None: self.ready = asyncio.Event() - bot.add_dev_env_value("birthday", lambda x: self) + bot.add_dev_env_value("birthday", lambda _: self) def format_help_for_context(self, ctx: commands.Context) -> str: """Thanks Sinbad.""" return format_help(self, ctx) - def cog_unload(self): + async def cog_unload(self): self.loop.cancel() self.role_manager.cancel() @@ -93,7 +93,7 @@ async def red_delete_data_for_user(self, **kwargs) -> None: if not hit: log.debug("No user data found for user with ID %s.", target_u_id) - async def async_init(self) -> None: + async def cog_load(self) -> None: version = await self.config.version() if version == 0: # first load so no need to update await self.config.version.set(1) @@ -102,9 +102,13 @@ async def async_init(self) -> None: self.ready.set() + log.trace("birthday ready") + @commands.command(hidden=True, aliases=["birthdayinfo"]) async def bdayinfo(self, ctx: commands.Context): await ctx.send(await format_info(ctx, self.qualified_name, self.__version__)) async def check_if_setup(self, guild: discord.Guild) -> bool: - return await self.config.guild(guild).setup_state() == 5 + state = await self.config.guild(guild).setup_state() + log.trace("setup state: %s", state) + return state == 5 diff --git a/birthday/commands.py b/birthday/commands.py index 47eca29e..922ef3f2 100644 --- a/birthday/commands.py +++ b/birthday/commands.py @@ -6,21 +6,21 @@ from typing import TYPE_CHECKING import discord -from dateutil.parser import ParserError -from dateutil.parser import parse as time_parser from redbot.core import Config, commands from redbot.core.commands import CheckFailure from redbot.core.utils import AsyncIter from redbot.core.utils.chat_formatting import box, pagify, warning from redbot.core.utils.menus import start_adding_reactions -from redbot.core.utils.predicates import MessagePredicate, ReactionPredicate +from redbot.core.utils.predicates import ReactionPredicate from rich.table import Table # type:ignore from .abc import MixinMeta +from .components.setup import SetupView from .consts import MAX_BDAY_MSG_LEN, MIN_BDAY_YEAR from .converters import BirthdayConverter, TimeConverter from .utils import channel_perm_check, format_bday_message, role_perm_check from .vexutils import get_vex_logger, no_colour_rich_markup +from .vexutils.button_pred import wait_for_yes_no log = get_vex_logger(__name__) @@ -41,7 +41,7 @@ async def setup_check(self, ctx: commands.Context) -> None: @commands.guild_only() # type:ignore @commands.before_invoke(setup_check) # type:ignore - @commands.group(aliases=["bday"]) + @commands.hybrid_group(aliases=["bday"]) async def birthday(self, ctx: commands.Context): """Set and manage your birthday.""" @@ -134,6 +134,8 @@ async def upcoming(self, ctx: commands.Context, days: int = 7): all_birthdays: dict[int, dict[str, dict]] = await self.config.all_members(ctx.guild) + log.trace("raw data for all bdays: %s", all_birthdays) + parsed_bdays: dict[int, list[str]] = defaultdict(list) number_day_mapping: dict[int, str] = {} @@ -182,6 +184,8 @@ async def upcoming(self, ctx: commands.Context, days: int = 7): ) number_day_mapping[diff.days] = next_birthday_dt.strftime("%B %d") + log.trace("bdays parsed: %s", parsed_bdays) + if len(parsed_bdays) == 0: await ctx.send(f"No upcoming birthdays in the next {days} days.") return @@ -224,240 +228,11 @@ async def bdset(self, ctx: commands.Context): @bdset.command() async def interactive(self, ctx: commands.Context): """Start interactive setup""" - # group has guild check + # guild only check in group if TYPE_CHECKING: - assert ctx.guild is not None - assert isinstance(ctx.me, discord.Member) assert isinstance(ctx.author, discord.Member) - m: discord.Message = await ctx.send( - "Just a heads up, you'll be asked for a message for when the user provided their birth" - " year, a message for when they didn't, the channel to sent notifications, the role," - " and the time of day to send them.\n\nWhen you're ready, press the tick." - ) - start_adding_reactions(m, ReactionPredicate.YES_OR_NO_EMOJIS) - pred = ReactionPredicate.yes_or_no(m, ctx.author) # type:ignore - try: - await self.bot.wait_for("reaction_add", check=pred, timeout=300) - except asyncio.TimeoutError: - await m.edit( - content=( - f"Took too long to react, cancelling setup. Run `{ctx.clean_prefix}bdset" - " interactive` to start again." - ) - ) - - if pred.result is not True: - await ctx.send("Okay, I'll cancel setup.") - return - - # ============================== MSG WITH YEAR ============================== - - m = await ctx.send( - "What message should I send if the user provided their birth year?\n\nYou can use the" - " following variables: `mention`, `name`, `new_age`. Put curly brackets `{}` around" - " them, for example: {mention} is now {new_age} years old!\n\nYou have 5 minutes." - ) - - try: - pred = MessagePredicate.same_context(ctx) - message = await self.bot.wait_for("message", check=pred, timeout=300) - except asyncio.TimeoutError: - await ctx.send( - f"Took too long to react, cancelling setup. Run `{ctx.clean_prefix}bdset" - " interactive` to start again." - ) - return - - message_w_year = message.content - - if len(message_w_year) > MAX_BDAY_MSG_LEN: - await ctx.send( - "That message is too long, please try again. Stay under" - f" {MAX_BDAY_MSG_LEN} characters." - ) - return - - # ============================== MSG WITHOUT YEAR ============================== - - m = await ctx.send( - "What message should I send if the user didn't provide their birth year?\n\nYou can" - " use the following variables: `mention`, `name`. Put curly brackets `{}` around them," - " for example: {mention}'s birthday is today! Happy birthday {name}\n\nYou have 5" - " minutes." - ) - - try: - pred = MessagePredicate.same_context(ctx) - message = await self.bot.wait_for("message", check=pred, timeout=300) - except asyncio.TimeoutError: - await ctx.send( - f"Took too long to react, cancelling setup. Run `{ctx.clean_prefix}bdset" - " interactive` to start again." - ) - return - - message_wo_year = message.content - - if len(message_wo_year) > MAX_BDAY_MSG_LEN: - await ctx.send( - f"That message is too long, please try again. Stay under {MAX_BDAY_MSG_LEN}" - " characters." - ) - return - - # ============================== CHANNEL ============================== - - await ctx.send( - "If you intend to mention a role with any of these messages, you need to run the " - f"`{ctx.clean_prefix}bdset rolemention true` command." - ) - - m = await ctx.send( - "Where would you like to send notifications? I will ignore any message with an invalid" - " channel.\n\nYou have 5 minutes." - ) - - try: - pred = MessagePredicate.valid_text_channel(ctx) - await self.bot.wait_for("message", check=pred, timeout=300) - except asyncio.TimeoutError: - await ctx.send( - f"Took too long to react, cancelling setup. Run `{ctx.clean_prefix}bdset" - " interactive` to start again." - ) - return - - channel: discord.TextChannel = pred.result # type:ignore - if error := channel_perm_check(ctx.me, channel): - await ctx.send( - warning( - f"{error} Please make sure" - " you rectify this as soon as possible, but I'll let you continue the setup." - ) - ) - - channel_id = pred.result.id # type:ignore - - # ============================== ROLE ============================== - - m = await ctx.send( - "What role should I assign to users who have their birthday today? I will ignore any" - " message which isn't a role.\n\nYou can mention the role, give its exact name, or its" - " ID.\n\nYou have 5 minutes." - ) - - try: - pred = MessagePredicate.valid_role(ctx) - await self.bot.wait_for("message", check=pred, timeout=300) - except asyncio.TimeoutError: - await ctx.send( - f"Took too long to react, cancelling setup. Run `{ctx.clean_prefix}bdset" - " interactive` to start again." - ) - return - - # no need to check hierarchy for author, since command is locked to admins - if error := role_perm_check(ctx.me, pred.result): # type:ignore - await ctx.send( - warning( - f"{error} Please make" - " sure you rectify this as soon as possible, but I'll let you continue the" - " setup." - ) - ) - - role_id = pred.result.id # type:ignore - - # ============================== TIME ============================== - - def time_check(m: discord.Message): - if m.author == ctx.author and m.channel == ctx.channel is False: - return False - - try: - time_parser(m.content) - except ParserError: - return False - - return True - - m = await ctx.send( - "What time of day should I send the birthday message? Please use the UTC time, for" - " example `12AM` for midnight or `7:00`. I will ignore any invalid input. I will" - " ignore minutes.\n\nYou have 5 minutes." - ) - - try: - ret = await self.bot.wait_for("message", check=time_check, timeout=300) - except asyncio.TimeoutError: - await ctx.send( - f"Took too long to react, cancelling setup. Run `{ctx.clean_prefix}bdset" - " interactive` to start again." - ) - return - - full_time = time_parser(ret.content) - full_time = full_time.replace(tzinfo=datetime.timezone.utc, year=1, month=1, day=1) - - midnight = datetime.datetime.now(tz=datetime.timezone.utc).replace( - year=1, month=1, day=1, hour=0, minute=0, second=0, microsecond=0 - ) - - time_utc_s = int((full_time - midnight).total_seconds()) - - try: - await ctx.trigger_typing() # dpy 1 - except AttributeError: - await ctx.typing() # dpy 2 - - p = ctx.clean_prefix - - setup_state = 5 - errors = "" - try: - format_bday_message(message_w_year, ctx.author, 1) - except KeyError as e: - setup_state -= 1 - errors += warning( - "You birthday message **with year** can only include `{mention}`, `{name}`" - " and `{new_age}`. You can't have anything else in `{}`. You did" - f" `{{{e.args[0]}}}` which is invalid.\nYou can correct this with `{p}bdset" - " msgwithyear`\n\n" - ) - - try: - format_bday_message(message_wo_year, ctx.author) - except KeyError as e: - e = e - setup_state -= 1 - errors += warning( - "You birthday message **without year** can only include `{mention}` and" - f" `{{name}}`. You can't have anything else in `{{}}`. You did `{{{e.args[0]}}}`" - f" which is invalid.\nYou can correct this with `{p}bdset msgwithoutyear`\n\n" - ) - - async with self.config.guild(ctx.guild).all() as conf: - conf["time_utc_s"] = time_utc_s - conf["message_w_year"] = message_w_year - conf["message_wo_year"] = message_wo_year - conf["channel_id"] = channel_id - conf["role_id"] = role_id - conf["setup_state"] = setup_state - - if errors: - await ctx.send( - errors - + f"Once you fix this, members will be able to use `{p}birthday add` to add their" - " birthday and messages will be sent." - ) - return - - await ctx.send( - f"All set! You can change these settings at any time with `{p}bdset` and view them" - f" with `{p}bdset settings`. Members can now use `{p}birthday add` to add their" - " birthday." - ) + await ctx.send("Click bellow to start.", view=SetupView(ctx.author, self.bot, self.config)) @bdset.command() async def settings(self, ctx: commands.Context): @@ -470,6 +245,8 @@ async def settings(self, ctx: commands.Context): table = Table("Name", "Value", title="Settings for this server") async with self.config.guild(ctx.guild).all() as conf: + log.trace("raw config: %s", conf) + channel = ctx.guild.get_channel(conf["channel_id"]) table.add_row("Channel", channel.name if channel else "Channel deleted") @@ -555,7 +332,7 @@ async def time(self, ctx: commands.Context, *, time: TimeConverter): @bdset.command() async def msgwithoutyear(self, ctx: commands.Context, *, message: str): """ - Set the message to be send when the user did not provide a year. + Set the message to send when the user did not provide a year. If you would like to mention a role, you will need to run `[p]bdset rolemention true`. @@ -603,7 +380,7 @@ async def msgwithoutyear(self, ctx: commands.Context, *, message: str): @bdset.command() async def msgwithyear(self, ctx: commands.Context, *, message: str): """ - Set the message to be send when the user did provide a year. + Set the message to send when the user did provide a year. If you would like to mention a role, you will need to run `[p]bdset rolemention true` @@ -850,8 +627,27 @@ async def stop(self, ctx: commands.Context): """ Stop the cog from sending birthday messages and giving roles in the server. """ + # group has guild check + if TYPE_CHECKING: + assert ctx.guild is not None + + confirm = await wait_for_yes_no( + ctx, "Are you sure you want to stop sending updates and giving roles?" + ) + if confirm is False: + await ctx.send("Okay, nothing's changed.") + return + await self.config.guild(ctx.guild).clear() - await ctx.send( - "Birthday messages and roles have been stopped. Configuration has been reset, but the" - " birthdays of users have been kept in case you need them again." + + confirm = await wait_for_yes_no( + ctx, + "I've deleted your configuration. Would you also like to delete the data about when" + " users birthdays are?", ) + if confirm is False: + await ctx.send("I'll keep that.") + return + + await self.config.clear_all_members(ctx.guild) + await ctx.send("Deleted.") diff --git a/birthday/components/setup.py b/birthday/components/setup.py new file mode 100644 index 00000000..0e39008e --- /dev/null +++ b/birthday/components/setup.py @@ -0,0 +1,124 @@ +from __future__ import annotations + +import discord +from redbot.core import Config +from redbot.core.bot import Red +from redbot.core.utils.chat_formatting import box, warning + +from ..consts import MAX_BDAY_MSG_LEN +from ..utils import format_bday_message + + +class SetupView(discord.ui.View): + def __init__(self, author: discord.Member, bot: Red, config: Config): + super().__init__() + + self.author = author + + self.bot = bot + self.config = config + + @discord.ui.button(label="Start setup", style=discord.ButtonStyle.blurple) + async def btn_start(self, interaction: discord.Interaction, button: discord.ui.Button): + await interaction.response.send_modal(SetupModal(self.bot, self.config)) + + async def interaction_check(self, interaction: discord.Interaction) -> bool: + if interaction.user != self.author: + await interaction.response.send_message("You are not authorized to use this button.") + return False + else: + return True + + +class SetupModal(discord.ui.Modal): + message_w_year = discord.ui.TextInput( + label="Birthday message with a new age", + max_length=MAX_BDAY_MSG_LEN, + style=discord.TextStyle.long, + placeholder=( + "You can use {mention}, {name} and {new_age}\nExample:\n{mention}" + " is now {new_age} years old!" + ), + ) + + message_wo_year = discord.ui.TextInput( + label="Birthday message without an age", + max_length=MAX_BDAY_MSG_LEN, + style=discord.TextStyle.long, + placeholder=( + "You can use {mention} and {name}\nExample:\n{mention}'s birthday" + " is today! Happy Birthday!" + ), + ) + + time = discord.ui.TextInput( + label="Time of day to send messages", + style=discord.TextStyle.short, + placeholder="Times in UTC. Examples: 12AM, 5AM", + ) + + # bellow was valid then discord decided nope + + # time = discord.ui.Select( + # placeholder="Time of day to send messages", + # options=[ + # discord.SelectOption(label=str(i) + ":00 UTC", value=str(i * 60 * 60)) + # for i in range(24) + # ], + # ) + + def __init__(self, bot: Red, config: Config): + super().__init__(title="Birthday setup") + + self.bot = bot + self.config = config + + async def on_submit(self, interaction: discord.Interaction) -> None: + def get_reminder() -> str: + return ( + "Nothing's been set, try again.\n\nHere are your messages so you don't have to" + " type them again.\n\nWith age:\n" + + box(self.message_w_year.value or "Not set") + + "\nWithout age:\n" + + box(self.message_wo_year.value or "Not set") + ) + + time_utc_s = int(self.time.values[0]) + + try: + format_bday_message(self.message_w_year.value, interaction.user, 1) + except KeyError as e: + await interaction.response.send_message( + warning( + "You birthday message **with year** can only include `{mention}`, `{name}`" + " and `{new_age}`. You can't have anything else in `{}`. You did" + f" `{{{e.args[0]}}}` which is invalid.\n\n{get_reminder()}" + ), + ephemeral=True, + ) + return + + try: + format_bday_message(self.message_wo_year.value, interaction.user) + except KeyError as e: + await interaction.response.send_message( + warning( + "You birthday message **without year** can only include `{mention}` and" + " `{name}`. You can't have anything else in `{}`. You did" + f" `{{{e.args[0]}}}` which is invalid.\n\n{get_reminder()}" + ), + ephemeral=True, + ) + return + + async with self.config.guild(interaction.guild).all() as conf: + conf["time_utc_s"] = time_utc_s + conf["message_w_year"] = self.message_w_year.value + conf["message_wo_year"] = self.message_wo_year.value + conf["setup_state"] = 3 + + await interaction.response.send_message( + "All set, but you're not quite ready yet. Just set up the channel and role with `bdset" + " role` and `bdset channel` then birthdays will be sent and assigned. You can check" + " with `bdset settings`" + ) diff --git a/birthday/converters.py b/birthday/converters.py index 7dbe452d..8da3f331 100644 --- a/birthday/converters.py +++ b/birthday/converters.py @@ -4,6 +4,11 @@ from dateutil.parser import ParserError, parse from redbot.core.commands import BadArgument, Context, Converter +from .vexutils import get_vex_logger + +log = get_vex_logger(__name__) + + if TYPE_CHECKING: BirthdayConverter = datetime.datetime TimeConverter = datetime.datetime @@ -11,13 +16,18 @@ class BirthdayConverter(Converter): async def convert(self, ctx: Context, argument: str) -> datetime.datetime: + log.trace("attempting to parse date %s", argument) try: default = datetime.datetime(year=1, month=1, day=1) + log.trace("parsed date: %s", argument) out = parse(argument, default=default, ignoretz=True).replace( hour=0, minute=0, second=0, microsecond=0 ) + return out except ParserError: + if ctx.interaction: + raise BadArgument("That's not a valid date. Example: `1 Jan` or `1 Jan 2000`.") raise BadArgument( f"That's not a valid date. See {ctx.clean_prefix}help" f" {ctx.command.qualified_name} for more information." @@ -25,11 +35,16 @@ async def convert(self, ctx: Context, argument: str) -> datetime.datetime: class TimeConverter(Converter): async def convert(self, ctx: Context, argument: str) -> datetime.datetime: + log.trace("attempting to parse time %s", argument) try: - return parse(argument, ignoretz=True).replace( + out = parse(argument, ignoretz=True).replace( year=1, month=1, day=1, minute=0, second=0, microsecond=0 ) + log.trace("parsed time: %s", argument) + return out except ParserError: + if ctx.interaction: + raise BadArgument("That's not a valid time.") raise BadArgument( f"That's not a valid time. See {ctx.clean_prefix}help" f" {ctx.command.qualified_name} for more information." diff --git a/birthday/info.json b/birthday/info.json index e77813b2..c37d5f20 100644 --- a/birthday/info.json +++ b/birthday/info.json @@ -6,8 +6,7 @@ "description": "Get users to add their birthday, then on their birthday they will get a special role. Server owners can set the time of day, announcement message and role.", "end_user_data_statement": "This cog will associate a User ID with a birthday if the user explicitly sets it. This data is also associated with a specific guild ID. No other data or metadata about users is stored.", "install_msg": "Thanks for installing! You can set the cog up with `[p]bdset interactive`, then your members can use `[p]bday set `.\n\nThis cog has docs! Check them out at ", - "max_bot_version": "3.6.0.dev0", - "min_bot_version": "3.4.0", + "min_bot_version": "3.5.1", "min_python_version": [ 3, 8, diff --git a/birthday/loop.py b/birthday/loop.py index aa582c46..66e0fde4 100644 --- a/birthday/loop.py +++ b/birthday/loop.py @@ -30,6 +30,7 @@ async def birthday_role_manager(self) -> None: try: coro = await self.coro_queue.get() await coro + log.trace("ran coro %s", coro) except discord.HTTPException as e: log.warning("A queued coro failed to run.", exc_info=e) @@ -47,7 +48,7 @@ async def add_role(self, me: discord.Member, member: discord.Member, role: disco error, ) return - log.debug("Queued birthday role add for %s in guild %s", member.id, member.guild.id) + log.trace("Queued birthday role add for %s in guild %s", member.id, member.guild.id) self.coro_queue.put_nowait( member.add_roles(role, reason="Birthday cog - birthday starts today") ) @@ -61,7 +62,7 @@ async def remove_role(self, me: discord.Member, member: discord.Member, role: di error, ) return - log.debug("Queued birthday role remove for %s in guild %s", member.id, member.guild.id) + log.trace("Queued birthday role remove for %s in guild %s", member.id, member.guild.id) self.coro_queue.put_nowait( member.remove_roles(role, reason="Birthday cog - birthday ends today") ) @@ -78,6 +79,8 @@ async def send_announcement( ) return + log.trace("Queued birthday announcement for %s in guild %s", channel.id, channel.guild.id) + log.trace("Message: %s", message) self.coro_queue.put_nowait( channel.send( message, @@ -92,12 +95,14 @@ async def birthday_loop(self) -> NoReturn: await self.bot.wait_until_red_ready() await self.ready.wait() + log.verbose("Birthday task started") + # 1st loop try: self.loop_meta.iter_start() await self._update_birthdays() self.loop_meta.iter_finish() - log.debug("Initial loop has finished") + log.verbose("Initial loop has finished") except Exception as e: self.loop_meta.iter_error(e) log.exception( @@ -116,12 +121,12 @@ async def birthday_loop(self) -> NoReturn: # all further iterations while True: - log.debug("Loop has started next iteration") + log.verbose("Loop has started next iteration") try: self.loop_meta.iter_start() await self._update_birthdays() self.loop_meta.iter_finish() - log.debug("Loop has finished") + log.verbose("Loop has finished") except Exception as e: self.loop_meta.iter_error(e) log.exception( @@ -140,15 +145,15 @@ async def _update_birthdays(self): async for guild_id, guild_data in AsyncIter(all_birthdays.items(), steps=5): guild = self.bot.get_guild(int(guild_id)) if guild is None: - log.debug("Guild %s is not in cache, skipping", guild_id) + log.trace("Guild %s is not in cache, skipping", guild_id) continue if all_settings.get(guild.id) is None: # can happen with migration from ZeLarp's cog - log.debug("Guild %s is not setup, skipping", guild_id) + log.trace("Guild %s is not setup, skipping", guild_id) continue if await self.check_if_setup(guild) is False: - log.debug("Guild %s is not setup, skipping", guild_id) + log.trace("Guild %s is not setup, skipping", guild_id) continue birthday_members: dict[discord.Member, datetime.datetime] = {} @@ -160,7 +165,7 @@ async def _update_birthdays(self): ) - datetime.datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0) if since_midnight.total_seconds() != hour_td.total_seconds(): - log.debug("Not correct time for update for guild %s, skipping", guild_id) + log.trace("Not correct time for update for guild %s, skipping", guild_id) continue today_dt = (datetime.datetime.utcnow() - hour_td).replace( @@ -174,7 +179,7 @@ async def _update_birthdays(self): birthday = data["birthday"] member = guild.get_member(int(member_id)) if member is None: - log.debug( + log.trace( "Member %s for guild %s is not in cache, skipping", member_id, guild_id ) continue @@ -207,7 +212,7 @@ async def _update_birthdays(self): ) continue - log.debug("Members with birthdays in guild %s: %s", guild_id, birthday_members) + log.trace("Members with birthdays in guild %s: %s", guild_id, birthday_members) for member, dt in birthday_members.items(): if member not in role.members: @@ -220,12 +225,6 @@ async def _update_birthdays(self): all_settings[guild.id]["allow_role_mention"], ) - log.debug( - "Queued birthday message wo year for %s in guild %s", - member.id, - guild_id, - ) - else: age = today_dt.year - dt.year await self.send_announcement( @@ -236,14 +235,8 @@ async def _update_birthdays(self): all_settings[guild.id]["allow_role_mention"], ) - log.debug( - "Queued birthday message w year for %s in guild %s", - member.id, - guild_id, - ) - for member in role.members: if member not in birthday_members: await self.remove_role(guild.me, member, role) - log.debug("Potential updates for %s have been queued", guild_id) + log.trace("Potential updates for %s have been queued", guild_id) diff --git a/birthday/vexutils/meta.py b/birthday/vexutils/meta.py index cae0ae73..1e963e4c 100644 --- a/birthday/vexutils/meta.py +++ b/birthday/vexutils/meta.py @@ -2,11 +2,12 @@ import asyncio import json -from logging import Logger, getLogger from pathlib import Path from typing import Literal, NamedTuple import aiohttp +from red_commons.logging import RedTraceLogger +from red_commons.logging import getLogger as red_get_logger from redbot.core import VersionInfo, commands from redbot.core import version_info as cur_red_version from rich import box as rich_box @@ -16,13 +17,13 @@ from .consts import DOCS_BASE, GREEN_CIRCLE, RED_CIRCLE from .loop import VexLoop -log = getLogger("red.vex-utils") +log = red_get_logger("red.vex-utils") cog_ver_lock = asyncio.Lock() -def get_vex_logger(name: str) -> Logger: +def get_vex_logger(name: str) -> RedTraceLogger: """Get a logger for the given name. Parameters @@ -42,7 +43,7 @@ def get_vex_logger(name: str) -> Logger: else: # otherwise use full path final_name += name - return getLogger(final_name) + return red_get_logger(final_name) def format_help(self: commands.Cog, ctx: commands.Context) -> str: diff --git a/bump.py b/bump.py index 761e0d88..6289cab4 100644 --- a/bump.py +++ b/bump.py @@ -14,9 +14,7 @@ "caseinsensitive", "channeltrack", "cmdlog", - "covidgraph", "fivemstatus", - "github", "ghissues", "madtranslate", "roleplay", diff --git a/buttonpoll/__init__.py b/buttonpoll/__init__.py index 1b53cd7e..9d17590d 100644 --- a/buttonpoll/__init__.py +++ b/buttonpoll/__init__.py @@ -25,8 +25,5 @@ async def setup(bot: Red): cog = ButtonPoll(bot) - await cog.async_init() await out_of_date_check("buttonpoll", cog.__version__) - r = bot.add_cog(cog) - if r is not None: - await r + await bot.add_cog(cog) diff --git a/buttonpoll/buttonpoll.py b/buttonpoll/buttonpoll.py index edc2bf89..acbed99c 100644 --- a/buttonpoll/buttonpoll.py +++ b/buttonpoll/buttonpoll.py @@ -62,7 +62,7 @@ def format_help_for_context(self, ctx: commands.Context) -> str: """Thanks Sinbad.""" return format_help(self, ctx) - def cog_unload(self) -> None: + async def cog_unload(self) -> None: self.loop.cancel() self.bot.remove_dev_env_value("bpoll") @@ -72,12 +72,14 @@ def cog_unload(self) -> None: self.plot_executor.shutdown(wait=False) + log.verbose("buttonpoll successfully unloaded") + @commands.command(hidden=True) async def buttonpollinfo(self, ctx: commands.Context): main = await format_info(ctx, self.qualified_name, self.__version__) return await ctx.send(main) - async def async_init(self) -> None: + async def cog_load(self) -> None: # re-initialise views all_polls = await self.config.all_guilds() for guild_polls in all_polls.values(): @@ -128,17 +130,18 @@ async def buttonpoll_loop(self): await self.bot.wait_until_red_ready() while True: try: - log.debug("ButtonPoll loop starting.") + log.verbose("ButtonPoll loop starting.") self.loop_meta.iter_start() await self.check_for_finished_polls() self.loop_meta.iter_finish() - log.debug("ButtonPoll loop finished.") + log.verbose("ButtonPoll loop finished.") except Exception as e: - log.error( + log.exception( "Something went wrong with the ButtonPoll loop. Please report this to Vexed.", exc_info=e, ) self.loop_meta.iter_error(e) + await self.loop_meta.sleep_until_next() async def check_for_finished_polls(self): diff --git a/buttonpoll/components/setup.py b/buttonpoll/components/setup.py index 246e9beb..18315521 100644 --- a/buttonpoll/components/setup.py +++ b/buttonpoll/components/setup.py @@ -75,13 +75,15 @@ def __init__( ) description = ui.TextInput( label="Description", - placeholder="Optionally add a description.", + placeholder="Optionally add a description", style=discord.TextStyle.paragraph, required=False, max_length=4000, ) time = ui.TextInput( - label="Length", placeholder="How long should the poll last?", default="1 day" + label="Poll duration", + placeholder="Examples - '1 day', '1 minute', '4 hours'", + max_length=32, ) options = ui.TextInput( label="Options", @@ -260,6 +262,8 @@ async def btn_submit(self, interaction: discord.Interaction, button: discord.ui. ) return + await interaction.response.defer() + self.stop() unique_poll_id = ( # msg ID and first 25 chars of sanitised question diff --git a/buttonpoll/info.json b/buttonpoll/info.json index 048a3632..3192e8f8 100644 --- a/buttonpoll/info.json +++ b/buttonpoll/info.json @@ -4,10 +4,8 @@ "Vexed (Vexed#3211)" ], "end_user_data_statement": "This cog stores user IDs paired with guild IDs and how they voted if a user participated in a poll. This is to ensure no double voting can happen. This data is removed once a poll finishes. No other data or metadata about users is persistently stored. This cog respects data deletion requests.", - "hidden": true, - "install_msg": "\u26a0 This cog requires Red 3.5/discord.py 2, which is unstable and incompatibe with some other cogs. \u26a0", - "max_bot_version": "3.6.0.dev0", - "min_bot_version": "3.4.0", + "install_msg": "Thanks for installing! Load the cog and get started with the `calculator` command.\n\nThis cog has docs! Check them out at ", + "min_bot_version": "3.5.1", "min_python_version": [ 3, 8, @@ -19,7 +17,7 @@ "kaleido", "pandas" ], - "short": "[DPY 2 ONLY] A poll in Discord, but with powered by buttons and with a pie chart at the end!\n\n\u26a0 This cog requires Red 3.5/discord.py 2, which is unstable and incompatibe with some other cogs. \u26a0", + "short": "A poll in Discord, but with powered by buttons and with a pie chart at the end!", "tags": [ "utility", "poll", diff --git a/buttonpoll/poll.py b/buttonpoll/poll.py index 66a5fd75..1e226f12 100644 --- a/buttonpoll/poll.py +++ b/buttonpoll/poll.py @@ -76,6 +76,8 @@ def __init__( self.cog = cog + log.trace("poll created: %s", self) + def __eq__(self, __o: object) -> bool: if isinstance(__o, Poll): return self.unique_poll_id == __o.unique_poll_id @@ -111,6 +113,9 @@ def from_dict(cls, data: dict, cog: "ButtonPoll"): view=None, # type:ignore ) cls.view = PollView(cog.config, cls) + + log.trace("poll created from dict: %s", cls) + return cls def to_dict(self) -> dict: @@ -147,6 +152,8 @@ async def get_results(self) -> Dict[str, int]: for str_option in raw_vote_data.values(): results[str_option] += 1 + log.trace("poll results: %s", results) + return results async def finish(self): @@ -172,6 +179,8 @@ async def finish(self): except KeyError: pass + log.trace("invalid poll %s removed", self.unique_poll_id) + return channel = guild.get_channel(self.channel_id) @@ -195,6 +204,8 @@ async def finish(self): except KeyError: pass + log.trace("invalid poll %s removed", self.unique_poll_id) + return poll_msg = channel.get_partial_message(self.message_id) @@ -224,6 +235,8 @@ async def finish(self): ) return + log.trace("edited old poll message") + if self.send_msg_when_over: embed_2 = discord.Embed( title="Poll finished", @@ -259,6 +272,8 @@ async def finish(self): except KeyError: pass + log.trace("Finished poll %s", self.unique_poll_id) + async def plot(self) -> discord.File: results = await self.get_results() df = pd.DataFrame.from_dict(results, orient="index", columns=["count"]) # type:ignore diff --git a/buttonpoll/vexutils/meta.py b/buttonpoll/vexutils/meta.py index cae0ae73..1e963e4c 100644 --- a/buttonpoll/vexutils/meta.py +++ b/buttonpoll/vexutils/meta.py @@ -2,11 +2,12 @@ import asyncio import json -from logging import Logger, getLogger from pathlib import Path from typing import Literal, NamedTuple import aiohttp +from red_commons.logging import RedTraceLogger +from red_commons.logging import getLogger as red_get_logger from redbot.core import VersionInfo, commands from redbot.core import version_info as cur_red_version from rich import box as rich_box @@ -16,13 +17,13 @@ from .consts import DOCS_BASE, GREEN_CIRCLE, RED_CIRCLE from .loop import VexLoop -log = getLogger("red.vex-utils") +log = red_get_logger("red.vex-utils") cog_ver_lock = asyncio.Lock() -def get_vex_logger(name: str) -> Logger: +def get_vex_logger(name: str) -> RedTraceLogger: """Get a logger for the given name. Parameters @@ -42,7 +43,7 @@ def get_vex_logger(name: str) -> Logger: else: # otherwise use full path final_name += name - return getLogger(final_name) + return red_get_logger(final_name) def format_help(self: commands.Cog, ctx: commands.Context) -> str: diff --git a/calc/__init__.py b/calc/__init__.py index a12cd81c..2b1ad5b9 100644 --- a/calc/__init__.py +++ b/calc/__init__.py @@ -26,6 +26,4 @@ async def setup(bot: Red) -> None: cog = Calc(bot) await out_of_date_check("calculator", cog.__version__) - r = bot.add_cog(cog) - if r is not None: - await r + await bot.add_cog(cog) diff --git a/calc/info.json b/calc/info.json index 7a07724d..32cb4ccf 100644 --- a/calc/info.json +++ b/calc/info.json @@ -5,10 +5,8 @@ ], "description": "Calculate simple mathematical expressions, right in Discord with buttons.", "end_user_data_statement": "This cog does not persistently store data or metadata about users.", - "hidden": true, - "install_msg": "\\N{WARNING SIGN}\\N{VARIATION SELECTOR-16} This cog requires Red 3.5/discord.py 2, which is unstable and incompatibe with some other cogs. \\N{WARNING SIGN}\\N{VARIATION SELECTOR-16}\n\nThanks for installing! Load the cog and get started with the `calculator` command.\n\nThis cog has docs! Check them out at ", - "max_bot_version": "3.6.0.dev0", - "min_bot_version": "3.4.0", + "install_msg": "Thanks for installing! Load the cog and get started with the `calculator` command.\n\nThis cog has docs! Check them out at ", + "min_bot_version": "3.5.1", "min_python_version": [ 3, 8, @@ -19,7 +17,7 @@ "expr.py", "asyncache" ], - "short": "Calculate simple mathematical expressions, right in Discord with buttons. This cog requires Red 3.5/discord.py 2, which is unstable and incompatibe with some other cogs.", + "short": "Calculate simple mathematical expressions, right in Discord with buttons.", "tags": [ "utility", "calc", diff --git a/calc/vexutils/meta.py b/calc/vexutils/meta.py index cae0ae73..1e963e4c 100644 --- a/calc/vexutils/meta.py +++ b/calc/vexutils/meta.py @@ -2,11 +2,12 @@ import asyncio import json -from logging import Logger, getLogger from pathlib import Path from typing import Literal, NamedTuple import aiohttp +from red_commons.logging import RedTraceLogger +from red_commons.logging import getLogger as red_get_logger from redbot.core import VersionInfo, commands from redbot.core import version_info as cur_red_version from rich import box as rich_box @@ -16,13 +17,13 @@ from .consts import DOCS_BASE, GREEN_CIRCLE, RED_CIRCLE from .loop import VexLoop -log = getLogger("red.vex-utils") +log = red_get_logger("red.vex-utils") cog_ver_lock = asyncio.Lock() -def get_vex_logger(name: str) -> Logger: +def get_vex_logger(name: str) -> RedTraceLogger: """Get a logger for the given name. Parameters @@ -42,7 +43,7 @@ def get_vex_logger(name: str) -> Logger: else: # otherwise use full path final_name += name - return getLogger(final_name) + return red_get_logger(final_name) def format_help(self: commands.Cog, ctx: commands.Context) -> str: diff --git a/caseinsensitive/__init__.py b/caseinsensitive/__init__.py index acfc93df..967d59c7 100644 --- a/caseinsensitive/__init__.py +++ b/caseinsensitive/__init__.py @@ -26,8 +26,4 @@ async def setup(bot: Red): cog = CaseInsensitive(bot) await out_of_date_check("caseinsensitive", cog.__version__) - cog.plug_core() - cog.plug_alias() - r = bot.add_cog(cog) - if r is not None: - await r + await bot.add_cog(cog) diff --git a/caseinsensitive/caseinsensitive.py b/caseinsensitive/caseinsensitive.py index 83b242ae..ebd1cede 100644 --- a/caseinsensitive/caseinsensitive.py +++ b/caseinsensitive/caseinsensitive.py @@ -13,7 +13,9 @@ from redbot.core.commands.context import Context from .fakealias import FakeAlias -from .vexutils import format_help, format_info +from .vexutils import format_help, format_info, get_vex_logger + +log = get_vex_logger(__name__) class CaseInsensitiveStringView(StringView): @@ -258,25 +260,38 @@ async def red_delete_data_for_user(self, **kwargs) -> None: async def caseinsensitiveinfo(self, ctx: commands.Context): await ctx.send(await format_info(ctx, self.qualified_name, self.__version__)) + async def cog_load(self) -> None: + self.plug_core() + self.plug_alias() + + log.info("CaseInsensitive methods have been plugged") + def plug_core(self) -> None: """Plug the case-insensitive shit.""" if hasattr(discord_ext_commands, "HybridCommand"): new_method = types.MethodType(ci_get_context_dpy2, self.bot) + ver = "dpy 2 >= hybrid commands" else: new_method = types.MethodType(ci_get_context_dpy1, self.bot) + ver = "dpy 1 & dpy 2 < hybrid commands" self.old_get_context = self.bot.get_context self.bot.get_context = new_method + log.trace("patched get_context for %s", ver) + def unplug_core(self) -> None: """Unplug case-insensitive stuff.""" if self.old_get_context is not None: self.bot.get_context = self.old_get_context + log.trace("unpatched get_context") + def plug_alias(self) -> None: """Plug the alias magic.""" alias_cog: Optional[commands.Cog] = self.bot.get_cog("Alias") if alias_cog is None: + log.trace("not patching alias - not loaded") return if TYPE_CHECKING: @@ -286,19 +301,26 @@ def plug_alias(self) -> None: self.old_alias_get = alias_cog._aliases.get_alias alias_cog._aliases.get_alias = new_method + log.trace("patched get_alias") + def unplug_alias(self) -> None: alias_cog = self.bot.get_cog("Alias") if alias_cog is None or self.old_alias_get is None: + log.trace("not unpatched get_alias - not loaded") return if TYPE_CHECKING: assert isinstance(alias_cog, FakeAlias) alias_cog._aliases.get_alias = self.old_alias_get - def cog_unload(self): + log.trace("unpatched get_alias") + + async def cog_unload(self): self.unplug_core() self.unplug_alias() + log.info("CaseInsensitive methods have been unplugged") + @commands.Cog.listener() async def on_cog_add(self, cog: commands.Cog): if cog.qualified_name == "Alias": diff --git a/caseinsensitive/info.json b/caseinsensitive/info.json index 6cae46dc..40779946 100644 --- a/caseinsensitive/info.json +++ b/caseinsensitive/info.json @@ -6,8 +6,7 @@ "description": "Make all prefixes and commands case insensitive (so that something like !Ping works). This will have a negative performance impact.", "end_user_data_statement": "This cog does not persistently store data or metadata about users.", "install_msg": "Thanks for installing! This cog is **active whenever it is loaded** and has no commands. Try out `[p]piNG`! (once you load the cog)\n\nPlease be aware that this cog:\n1) may affect other cogs that call or change bot.get_context\n2) has a negative performance impact\n3) may break at any time\n\nThis cog has docs! Check them out at ", - "max_bot_version": "3.6.0.dev0", - "min_bot_version": "3.4.11", + "min_bot_version": "3.5.1", "min_python_version": [ 3, 8, diff --git a/caseinsensitive/vexutils/meta.py b/caseinsensitive/vexutils/meta.py index cae0ae73..1e963e4c 100644 --- a/caseinsensitive/vexutils/meta.py +++ b/caseinsensitive/vexutils/meta.py @@ -2,11 +2,12 @@ import asyncio import json -from logging import Logger, getLogger from pathlib import Path from typing import Literal, NamedTuple import aiohttp +from red_commons.logging import RedTraceLogger +from red_commons.logging import getLogger as red_get_logger from redbot.core import VersionInfo, commands from redbot.core import version_info as cur_red_version from rich import box as rich_box @@ -16,13 +17,13 @@ from .consts import DOCS_BASE, GREEN_CIRCLE, RED_CIRCLE from .loop import VexLoop -log = getLogger("red.vex-utils") +log = red_get_logger("red.vex-utils") cog_ver_lock = asyncio.Lock() -def get_vex_logger(name: str) -> Logger: +def get_vex_logger(name: str) -> RedTraceLogger: """Get a logger for the given name. Parameters @@ -42,7 +43,7 @@ def get_vex_logger(name: str) -> Logger: else: # otherwise use full path final_name += name - return getLogger(final_name) + return red_get_logger(final_name) def format_help(self: commands.Cog, ctx: commands.Context) -> str: diff --git a/cmdlog/__init__.py b/cmdlog/__init__.py index 976aac6d..b8c2984d 100644 --- a/cmdlog/__init__.py +++ b/cmdlog/__init__.py @@ -17,6 +17,4 @@ async def setup(bot: Red): cog = CmdLog(bot) await out_of_date_check("cmdlog", cog.__version__) - r = bot.add_cog(cog) - if r is not None: - await r + await bot.add_cog(cog) diff --git a/cmdlog/channellogger.py b/cmdlog/channellogger.py index ac842a7d..8bffddbd 100644 --- a/cmdlog/channellogger.py +++ b/cmdlog/channellogger.py @@ -33,12 +33,17 @@ def stop(self) -> None: if self.task: self.task.cancel() + log.verbose("CmdLog channel logger task stopped.") + def start(self) -> None: """Start the channel logger task.""" self._queue = Queue() self.task = self.bot.loop.create_task(self._cmdlog_channel_task()) + log.verbose("CmdLog channel logger task started.") + def add_command(self, command: Log): + log.trace("command added to channel logger queue: %s", command) self._queue.put_nowait(command) @staticmethod @@ -46,7 +51,6 @@ def _utc_now() -> datetime.datetime: return datetime.datetime.now(datetime.timezone.utc) async def _cmdlog_channel_task(self) -> NoReturn: - log.debug("CmdLog channel logger task started.") while True: try: await self._wait_to_next_safe_send_time() @@ -54,12 +58,16 @@ async def _cmdlog_channel_task(self) -> NoReturn: while self._queue.empty() is False: to_send.append(self._queue.get_nowait()) + log.trace("got %s commands to send", len(to_send)) + self.last_send = self._utc_now() msg = "\n".join(str(i) for i in to_send) for page in pagify(msg): await self.channel.send(box(page, "css")) + log.trace("sent %s commands", len(to_send)) + except Exception as e: log.warning( "Something went wrong preparing and sending the messages for the CmdLog " @@ -74,11 +82,11 @@ async def _wait_to_next_safe_send_time(self) -> None: if last_send < 60: to_wait = 60 - last_send - log.debug( + log.trace( f"Waiting {to_wait}s for next safe sendable time, last send was {last_send}s ago." ) await asyncio.sleep(to_wait) # else: # log.debug(f"Last send was {last_send}s ago, only waiting 5 seconds.") # await asyncio.sleep(5) - log.debug("Wait finished") + log.trace("Wait finished") diff --git a/cmdlog/cmdlog.py b/cmdlog/cmdlog.py index 378d1b1c..b801c064 100644 --- a/cmdlog/cmdlog.py +++ b/cmdlog/cmdlog.py @@ -19,7 +19,7 @@ if discord.__version__.startswith("2"): from discord import Interaction, InteractionType -_log = get_vex_logger(__name__) +log = get_vex_logger(__name__) class CmdLog(commands.Cog): @@ -70,12 +70,14 @@ async def start_channel_logger(self) -> None: self.channel_logger = ChannelLogger(self.bot, chan) # type:ignore self.channel_logger.start() else: - _log.warning("Commands will NOT be sent to a channel because it appears invalid.") + log.warning("Commands will NOT be sent to a channel because it appears invalid.") - def cog_unload(self): + async def cog_unload(self): if self.channel_logger: self.channel_logger.stop() + log.trace("cmdlog unload") + def format_help_for_context(self, ctx: commands.Context) -> str: """Thanks Sinbad.""" return format_help(self, ctx) @@ -94,7 +96,7 @@ def log_com(self, ctx: commands.Context) -> None: log_content=self.log_content, content=ctx.message.content, ) - _log.info(logged_com) + log.info(logged_com) self.log_cache.append(logged_com) if self.channel_logger: self.channel_logger.add_command(logged_com) @@ -105,7 +107,7 @@ def log_ce(self, ctx: commands.Context, error: commands.CommandError) -> None: ( commands.ConversionError, commands.MissingRequiredArgument, - # commands.MissingRequiredAttachment, # waiting for red 3.5 + commands.MissingRequiredAttachment, commands.TooManyArguments, commands.BadArgument, commands.UserInputError, @@ -135,7 +137,7 @@ def log_ce(self, ctx: commands.Context, error: commands.CommandError) -> None: error_info=error_info, ) - _log.info(logged_com) + log.info(logged_com) self.log_cache.append(logged_com) if self.channel_logger: self.channel_logger.add_command(logged_com) @@ -154,12 +156,13 @@ def get_track_start(self) -> str: return f"Log started {ago} ago." def log_list_error(self, e): - _log.exception( + log.exception( "Something went wrong processing a command. See below for more info.", exc_info=e ) @commands.Cog.listener() async def on_command_completion(self, ctx: commands.Context): + log.trace("command completion received for %s", ctx.command.qualified_name) try: if self.log_content is None: self.log_content = await self.config.log_content() @@ -170,6 +173,7 @@ async def on_command_completion(self, ctx: commands.Context): @commands.Cog.listener() async def on_command_error(self, ctx: commands.Context, error: commands.CommandError): + log.trace("command error received: %s", error) try: if self.log_content is None: self.log_content = await self.config.log_content() @@ -180,11 +184,13 @@ async def on_command_error(self, ctx: commands.Context, error: commands.CommandE @commands.Cog.listener() async def on_interaction(self, inter: "Interaction"): + log.trace("received interaction %s", inter) try: if inter.data is None: return if inter.type in (InteractionType.autocomplete, InteractionType.ping): + log.trace("skipping logging of %s - type is %s", inter, inter.type) return elif inter.type == InteractionType.application_command: if inter.command is None: @@ -212,17 +218,25 @@ async def on_interaction(self, inter: "Interaction"): ) elif inter.type == InteractionType.component: + log.trace("skipping logging of %s - type is %s", inter, inter.type) # TODO: add support for component interaction return elif inter.type == InteractionType.modal_submit: + log.trace("skipping logging of %s - type is %s", inter, inter.type) # TODO: MAYBE add support for modals return else: # we should never get here + log.warning( + "Skipping logging of %s - unknown type - type is %s. Please report this to" + " Vexed, it should never happen.", + inter, + inter.type, + ) return - _log.info(log_obj) + log.info(log_obj) self.log_cache.append(log_obj) if self.channel_logger: self.channel_logger.add_command(log_obj) @@ -303,7 +317,7 @@ async def channel(self, ctx: commands.Context, channel: Optional[TextChannel]): async def cache(self, ctx: commands.Context): """Show the size of the internal command cache.""" cache_bytes = self.cache_size() - _log.debug(f"Cache size is exactly {cache_bytes} bytes.") + log.debug(f"Cache size is exactly {cache_bytes} bytes.") cache_size = humanize_bytes(cache_bytes, 1) cache_count = humanize_number(len(self.log_cache)) await ctx.send(f"\nCache size: {cache_size} with {cache_count} commands.") diff --git a/cmdlog/info.json b/cmdlog/info.json index c7045617..db4ec6cf 100644 --- a/cmdlog/info.json +++ b/cmdlog/info.json @@ -6,8 +6,7 @@ "description": "Track who used what commands. All command usage is logged to the bot's console/log and also internally cached. You can view the activity of an individual user, server or command. Supports slash and text commands.", "end_user_data_statement": "This cog may persistently store data or metadata about users. Operational data is temporarily collected and cached whenever a command is used and may be stored.\n\nWhen a command is used, the following data is collected: the message invoking the command, the user that invoked it, the server it was invoked in, and the channel it was invoked in. This includes application commands, as well as old-style text commands.\n\nThis cog does not support data deletion requests, as it handles operational data.", "install_msg": "This cog will immediately start tracking commands once you have loaded it. They are logged with the `logging` module, so they'll appear in the bot's console or main logs (search for `red.vex.cmdlog`). An internal cache is kept, which you can see in Discord with the `[p]cmdlog` command.\n\nIf you want to store the whole message content, including arguments, instead of just the command invoked, run `[p]cmdlog content true` once you've loaded the cog.\n\nThis cog has docs! Check them out at ", - "max_bot_version": "3.6.0.dev0", - "min_bot_version": "3.4.0", + "min_bot_version": "3.5.1", "min_python_version": [ 3, 8, diff --git a/cmdlog/vexutils/meta.py b/cmdlog/vexutils/meta.py index cae0ae73..1e963e4c 100644 --- a/cmdlog/vexutils/meta.py +++ b/cmdlog/vexutils/meta.py @@ -2,11 +2,12 @@ import asyncio import json -from logging import Logger, getLogger from pathlib import Path from typing import Literal, NamedTuple import aiohttp +from red_commons.logging import RedTraceLogger +from red_commons.logging import getLogger as red_get_logger from redbot.core import VersionInfo, commands from redbot.core import version_info as cur_red_version from rich import box as rich_box @@ -16,13 +17,13 @@ from .consts import DOCS_BASE, GREEN_CIRCLE, RED_CIRCLE from .loop import VexLoop -log = getLogger("red.vex-utils") +log = red_get_logger("red.vex-utils") cog_ver_lock = asyncio.Lock() -def get_vex_logger(name: str) -> Logger: +def get_vex_logger(name: str) -> RedTraceLogger: """Get a logger for the given name. Parameters @@ -42,7 +43,7 @@ def get_vex_logger(name: str) -> Logger: else: # otherwise use full path final_name += name - return getLogger(final_name) + return red_get_logger(final_name) def format_help(self: commands.Cog, ctx: commands.Context) -> str: diff --git a/covidgraph/__init__.py b/covidgraph/__init__.py index 1cb2695d..b14aab62 100644 --- a/covidgraph/__init__.py +++ b/covidgraph/__init__.py @@ -1,22 +1,8 @@ -import contextlib -import importlib -import json -from pathlib import Path +from redbot.core.errors import CogLoadError -from redbot.core import VersionInfo -from redbot.core.bot import Red -from . import vexutils -from .covidgraph import CovidGraph -from .vexutils.meta import out_of_date_check - -with open(Path(__file__).parent / "info.json") as fp: - __red_end_user_data_statement__ = json.load(fp)["end_user_data_statement"] - - -async def setup(bot: Red) -> None: - cog = CovidGraph(bot) - await out_of_date_check("covidgraph", cog.__version__) - r = bot.add_cog(cog) - if r is not None: - await r +async def setup(_): + raise CogLoadError( + "This cog will not be updated to be compatible with Red 3.5 due to irrelevance and data " + "issues, and has therefore been removed." + ) diff --git a/covidgraph/abc.py b/covidgraph/abc.py deleted file mode 100644 index c42b673b..00000000 --- a/covidgraph/abc.py +++ /dev/null @@ -1,19 +0,0 @@ -from abc import ABC, ABCMeta -from concurrent.futures.thread import ThreadPoolExecutor - -from redbot.core.bot import Red -from redbot.core.commands import CogMeta - - -class CompositeMetaClass(CogMeta, ABCMeta): - """ - This allows the metaclass used for proper type detection to - coexist with discord.py's metaclass - """ - - -class MixinMeta(ABC): - """A wonderful class for typehinting :tada:""" - - bot: Red - executor: ThreadPoolExecutor diff --git a/covidgraph/covidgraph.py b/covidgraph/covidgraph.py deleted file mode 100644 index 9a3b2a34..00000000 --- a/covidgraph/covidgraph.py +++ /dev/null @@ -1,154 +0,0 @@ -from concurrent.futures.thread import ThreadPoolExecutor -from typing import Optional - -import discord -from aiohttp.client import ClientSession -from redbot.core import commands - -from .abc import CompositeMetaClass -from .data import CovidData -from .errors import CovidError -from .plot import GraphPlot -from .vexutils.meta import format_help, format_info - - -class CovidGraph(commands.Cog, GraphPlot, CovidData, metaclass=CompositeMetaClass): - """ - Get COVID-19 graphs. - """ - - __version__ = "1.2.0" - __author__ = "Vexed#0714" - - def __init__(self, bot): - self.bot = bot - self.executor = ThreadPoolExecutor(16, thread_name_prefix="covidgraph") - self.session = ClientSession() - - def format_help_for_context(self, ctx: commands.Context) -> str: - """Thanks Sinbad.""" - return format_help(self, ctx) - - async def red_delete_data_for_user(self, **kwargs) -> None: - """Nothing to delete""" - return - - def cog_unload(self) -> None: - self.executor.shutdown(wait=False) - - @commands.command(hidden=True) - async def covidgraphinfo(self, ctx: commands.Context): - await ctx.send(await format_info(ctx, self.qualified_name, self.__version__)) - - @commands.cooldown(2, 10, commands.BucketType.user) # 2 per 10 seconds - @commands.group() - async def covidgraph(self, ctx: commands.Context): - """Get graphs of COVID-19 data.""" - - @covidgraph.command(aliases=["c"], usage="[days] ") - async def cases(self, ctx: commands.Context, days: Optional[int], *, country: str): - """ - Get the number of confirmed cases in a country. - - You can optionally specify the number of days to get data for, - otherwise it will be all-time. - - `country` can also be `world` to get the worldwide data. - - **Examples:** - - `[p]covidgraph cases US` - All time data for the US - - `[p]covidgraph cases 7 US` - Last 7 days for the US - - `[p]covidgraph cases world` - Worldwide data - """ - if days and days < 7: - await ctx.send("`days` must be at least 7.") - return - - async with ctx.typing(): - try: - country, ts = await self.get_cases(country, days) - except CovidError: - await ctx.send("Something went wrong. It's probably an invalid country.") - return - - file = await self.plot_graph(ts, "Daily cases") - - embed = discord.Embed( - title=f"Daily COVID-19 cases - {country}", - colour=await ctx.embed_colour(), - ) - embed.set_footer(text="Times are in UTC\nData from disease.sh and John Hopkins University") - embed.set_image(url="attachment://plot.png") - await ctx.send(file=file, embed=embed) - - @covidgraph.command(aliases=["d"], usage="[days] ") - async def deaths(self, ctx: commands.Context, days: Optional[int], *, country: str): - """ - Get the number of deaths in a country. - - You can optionally specify the number of days to get data for, - otherwise it will be all-time. - - `country` can also be `world` to get the worldwide data. - - **Examples:** - - `[p]covidgraph deaths US` - All time data for the US - - `[p]covidgraph deaths 7 US` - Last 7 days for the US - - `[p]covidgraph deaths world` - Worldwide data - """ - if days and days < 7: - await ctx.send("`days` must be at least 7.") - return - - async with ctx.typing(): - try: - country, ts = await self.get_deaths(country, days) - except CovidError: - await ctx.send("Something went wrong. It's probably an invalid country.") - return - - file = await self.plot_graph(ts, "Daily deaths") - - embed = discord.Embed( - title=f"Daily COVID-19 deaths - {country}", - colour=await ctx.embed_colour(), - ) - embed.set_footer(text="Times are in UTC\nData from disease.sh and John Hopkins University") - embed.set_image(url="attachment://plot.png") - await ctx.send(file=file, embed=embed) - - @covidgraph.command(aliases=["v"], usage="[days] ") - async def vaccines(self, ctx: commands.Context, days: Optional[int], *, country: str): - """ - Get the number of vaccine doses administered in a country. - - You can optionally specify the number of days to get data for, - otherwise it will be all-time. - - `country` can also be `world` to get the worldwide data. - - **Examples:** - - `[p]covidgraph vaccines US` - All time data for the US - - `[p]covidgraph vaccines 7 US` - Last 7 days for the US - - `[p]covidgraph vaccines world` - Worldwide data - """ - if days and days < 7: - await ctx.send("`days` must be at least 7.") - return - - async with ctx.typing(): - try: - country, ts = await self.get_vaccines(country, days) - except CovidError: - await ctx.send("Something went wrong. It's probably an invalid country.") - return - - file = await self.plot_graph(ts, "Total vaccine doses") - - embed = discord.Embed( - title=f"Total COVID-19 vaccine doses - {country}", - colour=await ctx.embed_colour(), - ) - embed.set_footer(text="Times are in UTC\nData from disease.sh and Our World in Data") - embed.set_image(url="attachment://plot.png") - await ctx.send(file=file, embed=embed) diff --git a/covidgraph/data.py b/covidgraph/data.py deleted file mode 100644 index e3b64e44..00000000 --- a/covidgraph/data.py +++ /dev/null @@ -1,83 +0,0 @@ -from __future__ import annotations - -import aiohttp -import pandas as pd -from asyncache import cached -from cachetools import TTLCache - -from .abc import MixinMeta -from .errors import CovidError - -API_BASE = "https://disease.sh" - - -class CovidData(MixinMeta): - """Asynchronously get COVID data.""" - - async def get_cases(self, country: str, days: int | None) -> tuple[str, pd.Series]: - if days is None: - d = "all" - else: - d = str(days) - - if country in ("global", "world", "worldwide"): - country = "all" - - return await self.get( - f"{API_BASE}/v3/covid-19/historical/{country}?lastdays={d}", - extra_key="cases", - convert_to_daily=True, - ) - - async def get_deaths(self, country: str, days: int | None) -> tuple[str, pd.Series]: - if days is None: - d = "all" - else: - d = str(days) - - if country in ("global", "world", "worldwide"): - country = "all" - - return await self.get( - f"{API_BASE}/v3/covid-19/historical/{country}?lastdays={d}", - extra_key="deaths", - convert_to_daily=True, - ) - - async def get_vaccines(self, country: str, days: int | None) -> tuple[str, pd.Series]: - if days is None: - d = "all" - else: - d = str(days) - - if country in ("global", "world", "worldwide", "all"): - return await self.get( - f"{API_BASE}/v3/covid-19/vaccine/coverage?lastdays={d}", - ) - - return await self.get( - f"{API_BASE}/v3/covid-19/vaccine/coverage/countries/{country}?lastdays={d}", - ) - - @cached(TTLCache(maxsize=64, ttl=3600)) # 1 hour - async def get( - self, url: str, extra_key: str | None = None, convert_to_daily: bool = False - ) -> tuple[str, pd.Series]: - """Get data from an endpoint as a Series""" - async with aiohttp.ClientSession() as session: - resp = await session.get(url) - if resp.status != 200: - raise CovidError # usually invalid country - - data: dict = await resp.json() - - ts_dict = data.get("timeline", data) # fallback to data if no timeline key - - ts = pd.Series(ts_dict[extra_key] if extra_key else ts_dict) - - ts.index = pd.to_datetime(ts.index, utc=True) - - if convert_to_daily: # cumulative to daily and ty so much copolit - ts = ts.diff().dropna() - - return data.get("country", "worldwide"), ts diff --git a/covidgraph/errors.py b/covidgraph/errors.py deleted file mode 100644 index 75ab96ad..00000000 --- a/covidgraph/errors.py +++ /dev/null @@ -1,2 +0,0 @@ -class CovidError(Exception): - """General CovidGraph exception.""" diff --git a/covidgraph/info.json b/covidgraph/info.json index da740de1..e36dead9 100644 --- a/covidgraph/info.json +++ b/covidgraph/info.json @@ -3,27 +3,9 @@ "author": [ "Vexed (Vexed#0714)" ], - "description": "Get graphs of COVID-19 data.", - "end_user_data_statement": "This cog does not persistently store data or metadata about users.", - "install_msg": "Thanks for installing! Get started with the `[p]covidgraph` command once you've loaded the cog.\n\nThis cog has docs! Check them out at ", - "max_bot_version": "3.6.0.dev0", - "min_bot_version": "3.4.0", - "min_python_version": [ - 3, - 8, - 1 - ], + "description": "This cog has been removed, please uninstall.", + "disabled": true, + "hidden": true, "name": "CovidGraph", - "requirements": [ - "plotly", - "kaleido", - "asyncache" - ], - "short": "Get graphs of COVID-19 data.", - "tags": [ - "utility", - "data", - "graph", - "covid" - ] + "short": "This cog has been removed, please uninstall." } diff --git a/covidgraph/plot.py b/covidgraph/plot.py deleted file mode 100644 index aba53743..00000000 --- a/covidgraph/plot.py +++ /dev/null @@ -1,75 +0,0 @@ -import functools -import io -from typing import TYPE_CHECKING - -import discord -import pandas as pd -import plotly.express as px -from asyncache import cached -from cachetools import TTLCache - -if TYPE_CHECKING: # plotly does dynamic imports - from plotly.graph_objs._figure import Figure -else: - from plotly.graph_objs import Figure - -from .abc import MixinMeta - -# yes i am using private import, atm plotly does dynamic imports which are not supported by mypy - - -# need to specially handle pandas' series because it is unhashable with hash() -def custom_key(*args, **kwargs): - ret = [] - everything = args + tuple(kwargs.values()) - for arg in everything: - if isinstance(arg, pd.Series): - ret.append(str(arg)) # a series is unhashable, so we need to convert it to a string - # and the string will contain the first few and last few elements of the series - # which is good enough for our purposes - else: - ret.append(arg) - return tuple(ret) - - -class GraphPlot(MixinMeta): - # the graph generation can be cached but the discord.File returned is single use - # cacheing is deffo faster. - async def plot_graph(self, data: pd.Series, label: str) -> discord.File: - """Get a graph of the trends.""" - func = functools.partial(self._plot_graph, ts=data, label=label) - b = await self.bot.loop.run_in_executor(self.executor, func) - return self.bytes_to_file(b) - - @cached(TTLCache(maxsize=64, ttl=86400), custom_key) - # graphs are ~50KB so this is only 3 MB which is basically nothing for the speed improvement - # and 1 day TTL should mean new data is available when it stops being used - def _plot_graph(self, ts: pd.Series, label: str) -> bytes: - """Blocking""" - ts.name = "Raw data" - df = pd.DataFrame(ts) - df["7-day avg"] = ts.rolling(7, center=True).mean() - - fig: Figure = px.line( - df, - template="plotly_dark", - color_discrete_map={ - "Raw data": "#3d3e59", - "7-day avg": "#636efa", - }, - labels={"index": "Date", "value": label, "variable": "Key"}, - ) - fig.update_layout( - title_x=0.5, - font_size=14, - ) - fig.update_yaxes(rangemode="tozero") - bytes = fig.to_image(format="png", width=800, height=500, scale=1) - return bytes - - def bytes_to_file(self, b: bytes) -> discord.File: - """Convert bytes to discord.File.""" - fp = io.BytesIO(b) - file = discord.File(fp, filename="plot.png") - fp.close() - return file diff --git a/covidgraph/vexutils/README.md b/covidgraph/vexutils/README.md deleted file mode 100644 index 19b02100..00000000 --- a/covidgraph/vexutils/README.md +++ /dev/null @@ -1,13 +0,0 @@ -## My utils - -Hello there! If you're contributing or taking a look, everything in this folder -is synced from a master repo at https://github.com/Vexed01/vex-cog-utils by GitHub Actions - -so it's probably best to look/edit there. - ---- - -Last sync at: 2023-02-16 14:37:06 UTC - -Version: `2.6.1` - -Commit: [`b98072829ca902ef207688334da34f8e6c1da1e8`](https://github.com/Vexed01/vex-cog-utils/commit/b98072829ca902ef207688334da34f8e6c1da1e8) diff --git a/covidgraph/vexutils/__init__.py b/covidgraph/vexutils/__init__.py deleted file mode 100644 index 77c446e4..00000000 --- a/covidgraph/vexutils/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from typing import TYPE_CHECKING, Optional - -import discord -from redbot.core.bot import Red - -from .chat import humanize_bytes, inline_hum_list, no_colour_rich_markup -from .meta import format_help, format_info, get_vex_logger, out_of_date_check -from .version import __version__ diff --git a/covidgraph/vexutils/button_pred.py b/covidgraph/vexutils/button_pred.py deleted file mode 100644 index 5ed75881..00000000 --- a/covidgraph/vexutils/button_pred.py +++ /dev/null @@ -1,187 +0,0 @@ -# type:ignore -# until dpy2 - -import asyncio -from dataclasses import dataclass -from typing import Any, List, Optional - -import discord -from redbot.core import commands - -if discord.__version__.startswith("1"): - raise RuntimeError("This requires discord.py 2.X") -from discord import ButtonStyle, Embed, Interaction, ui - - -@dataclass -class PredItem: - """ - `ref` is what you want to be returned from the predicate if this button is clicked, though it - cannot be None - - `label` and `style` are what the button will look like. - - `row` is optional if you want to change how it will look in Discord - """ - - ref: Any - style: ButtonStyle - label: str - row: Optional[int] = None - - -class _PredView(ui.View): - def __init__(self, timeout: Optional[float], author_id: int): - super().__init__(timeout=timeout) - self.ref: Any = None - self.author_id = author_id - - self.pressed = asyncio.Event() - - async def interaction_check(self, interaction: Interaction) -> bool: - if interaction.user.id == self.author_id: - return True - - await interaction.response.send_message( - "You don't have have permission to do this.", ephemeral=True - ) - return False - - -class _PredButton(ui.Button): - def __init__(self, ref: Any, style: ButtonStyle, label: str, row: Optional[int] = None): - super().__init__(style=style, label=label, row=row) - self.ref = ref - - async def callback(self, interaction: Interaction): - assert isinstance(self.view, _PredView) - self.view.stop() - self.view.ref = self.ref - self.view.pressed.set() - - -async def wait_for_press( - ctx: commands.Context, - items: List[PredItem], - content: Optional[str] = None, - embed: Optional[Embed] = None, - *, - timeout: float = 180.0, -) -> Any: - """Wait for a single button press with customisable buttons. - - Only the original author will be allowed to use this. - - Parameters - ---------- - ctx : commands.Context - Context to send message to - items : List[PredItem] - List of items to send as buttons - content : Optional[str], optional - Content of the message, by default None - embed : Optional[Embed], optional - Embed of the message, by default None - timeout : float, optional - Button timeout, by default 180.0 - - Returns - ------- - Any - The defined reference of the clicked button - - Raises - ------ - ValueError - An empty list was supplied - asyncio.TimeoutError - A button was not pressed in time. - """ - if not items: - raise ValueError("The `items` argument cannot contain an empty list.") - - view = _PredView(timeout, ctx.author.id) # type:ignore - for i in items: - button = _PredButton(i.ref, i.style, i.label, i.row) - view.add_item(button) - message = await ctx.send(content=content, embed=embed, view=view) - - await asyncio.wait_for(view.pressed.wait(), timeout=timeout) - - emptyview = ui.View() - for i in items: - button = ui.Button( - style=i.style if i.ref == view.ref else ButtonStyle.gray, - label=i.label, - row=i.row, - disabled=True, - ) - emptyview.add_item(button) - await message.edit(view=emptyview) - emptyview.stop() - - return view.ref - - -async def wait_for_yes_no( - ctx: commands.Context, - content: Optional[str] = None, - embed: Optional[Embed] = None, - *, - timeout: float = 180.0, -) -> bool: - """Wait for a single button press of pre-defined yes and no buttons, returning True for yes - and False for no. - - If you want to customise the buttons, I recommend you use the more generic `wait_for_press`. - - Only the original author will be allowed to use this. - - Parameters - ---------- - ctx : commands.Context - Context to send message to - content : Optional[str], optional - Content of the message, by default None - embed : Optional[Embed], optional - Embed of the message, by default None - timeout : float, optional - Button timeout, by default 180.0 - - Returns - ------- - bool - True or False, depending on the clicked button. - - Raises - ------ - asyncio.TimeoutError - A button was not pressed in time. - """ - view = _PredView(timeout, ctx.author.id) # type:ignore - view.add_item(_PredButton(True, ButtonStyle.blurple, "Yes")) - view.add_item(_PredButton(False, ButtonStyle.blurple, "No")) - - message = await ctx.send(content=content, embed=embed, view=view) - - await asyncio.wait_for(view.pressed.wait(), timeout=timeout) - - emptyview = ui.View() - emptyview.add_item( - ui.Button( - style=ButtonStyle.grey if view.ref is False else ButtonStyle.blurple, - label="Yes", - disabled=True, - ) - ) - emptyview.add_item( - ui.Button( - style=ButtonStyle.grey if view.ref is True else ButtonStyle.blurple, - label="No", - disabled=True, - ) - ) - await message.edit(view=emptyview) - emptyview.stop() - - return view.ref diff --git a/covidgraph/vexutils/chat.py b/covidgraph/vexutils/chat.py deleted file mode 100644 index 9208ed74..00000000 --- a/covidgraph/vexutils/chat.py +++ /dev/null @@ -1,99 +0,0 @@ -import datetime -from io import StringIO -from typing import Any, Literal, Sequence, Union - -from redbot.core.utils.chat_formatting import box, humanize_list, humanize_number, inline -from rich.console import Console - -TimestampFormat = Literal["f", "F", "d", "D", "t", "T", "R"] - - -def no_colour_rich_markup(*objects: Any, lang: str = "") -> str: - """ - Slimmed down version of rich_markup which ensure no colours (/ANSI) can exist - https://github.com/Cog-Creators/Red-DiscordBot/pull/5538/files (Kowlin) - """ - temp_console = Console( # Prevent messing with STDOUT's console - color_system=None, - file=StringIO(), - force_terminal=True, - width=80, - ) - temp_console.print(*objects) - return box(temp_console.file.getvalue(), lang=lang) # type: ignore - - -def _hum(num: Union[int, float], unit: str, ndigits: int) -> str: - """Round a number, then humanize.""" - return humanize_number(round(num, ndigits)) + f" {unit}" - - -def humanize_bytes(bytes: Union[int, float], ndigits: int = 0) -> str: - """Humanize a number of bytes, rounding to ndigits. Only supports up to GB. - - This assumes 1GB = 1000MB, 1MB = 1000KB, 1KB = 1000B""" - if bytes > 10000000000: # 10GB - gb = bytes / 1000000000 - return _hum(gb, "GB", ndigits) - if bytes > 10000000: # 10MB - mb = bytes / 1000000 - return _hum(mb, "MB", ndigits) - if bytes > 10000: # 10KB - kb = bytes / 1000 - return _hum(kb, "KB", ndigits) - return _hum(bytes, "B", 0) # no point in rounding - - -# maybe think about adding to core -def inline_hum_list(items: Sequence[str], *, style: str = "standard") -> str: - """Similar to core's humanize_list, but all items are in inline code blocks. **Can** be used - outside my cogs. - - Strips leading and trailing whitespace. - - Does not support locale. - - Does support style (see core's docs for available styles) - - Parameters - ---------- - items : Sequence[str] - The items to humanize - style : str, optional - The style. See core's docs, by default "standard" - - Returns - ------- - str - Humanized inline list. - """ - inline_list = [inline(i.strip()) for i in items] - return humanize_list(inline_list, style=style) - - -def datetime_to_timestamp(dt: datetime.datetime, format: TimestampFormat = "f") -> str: - """Generate a Discord timestamp from a datetime object. - - - - Parameters - ---------- - dt : datetime.datetime - The datetime object to use - format : TimestampFormat, by default `f` - The format to pass to Discord. - - `f` short date time | `18 June 2021 02:50` - - `F` long date time | `Friday, 18 June 2021 02:50` - - `d` short date | `18/06/2021` - - `D` long date | `18 June 2021` - - `t` short time | `02:50` - - `T` long time | `02:50:15` - - `R` relative time | `8 days ago` - - Returns - ------- - str - Formatted timestamp - """ - t = str(int(dt.timestamp())) - return f"" diff --git a/covidgraph/vexutils/commit.json b/covidgraph/vexutils/commit.json deleted file mode 100644 index 32cea625..00000000 --- a/covidgraph/vexutils/commit.json +++ /dev/null @@ -1 +0,0 @@ -{"latest_commit": "b98072829ca902ef207688334da34f8e6c1da1e8"} \ No newline at end of file diff --git a/covidgraph/vexutils/consts.py b/covidgraph/vexutils/consts.py deleted file mode 100644 index a88f5850..00000000 --- a/covidgraph/vexutils/consts.py +++ /dev/null @@ -1,9 +0,0 @@ -DOCS_BASE = "This cog has docs! Check them out at\nhttps://s.vexcodes.com/c/{}" - -CHECK = "\N{HEAVY CHECK MARK}\N{VARIATION SELECTOR-16}" -CROSS = "\N{CROSS MARK}" - -RED_CIRCLE = "\N{LARGE RED CIRCLE}" -GREEN_CIRCLE = "\N{LARGE GREEN CIRCLE}" - -SNOWFLAKE_REGEX = r"\b\d{17,20}\b" diff --git a/covidgraph/vexutils/loop.py b/covidgraph/vexutils/loop.py deleted file mode 100644 index 2a14a1ac..00000000 --- a/covidgraph/vexutils/loop.py +++ /dev/null @@ -1,131 +0,0 @@ -import asyncio -import datetime -import traceback -from typing import Optional - -import discord -from redbot.core.utils.chat_formatting import box, pagify -from rich.table import Table # type:ignore - -from .chat import no_colour_rich_markup -from .consts import CHECK, CROSS - - -class VexLoop: - """ - A class with some utilities for logging the state of a loop. - - Note iter_count increases at the start of an iteration. - - This does not log anything itself. - """ - - def __init__(self, friendly_name: str, expected_interval: float) -> None: - self.friendly_name = friendly_name - self.expected_interval = datetime.timedelta(seconds=expected_interval) - - self.iter_count: int = 0 - self.currently_running: bool = False # whether the loop is running or sleeping - self.last_exc: str = "No exception has occurred yet." - self.last_exc_raw: Optional[BaseException] = None - - self.last_iter: Optional[datetime.datetime] = None - self.next_iter: Optional[datetime.datetime] = None - - def __repr__(self) -> str: - return ( - f"" - ) - - @property - def integrity(self) -> bool: - """ - If the loop is running on time (whether or not next expected iteration is in the future) - """ - if self.next_iter is None: # not started yet - return False - return self.next_iter > datetime.datetime.utcnow() - - @property - def until_next(self) -> float: - """ - Positive float with the seconds until the next iteration, based off the last - iteration and the interval. - - If the expected time of the next iteration is in the past, this will return `0.0` - """ - if self.next_iter is None: # not started yet - return 0.0 - - raw_until_next = (self.next_iter - datetime.datetime.utcnow()).total_seconds() - if raw_until_next > self.expected_interval.total_seconds(): # should never happen - return self.expected_interval.total_seconds() - elif raw_until_next > 0.0: - return raw_until_next - else: - return 0.0 - - async def sleep_until_next(self) -> None: - """Sleep until the next iteration. Basically an "all-in-one" version of `until_next`.""" - await asyncio.sleep(self.until_next) - - def iter_start(self) -> None: - """Register an iteration as starting.""" - self.iter_count += 1 - self.currently_running = True - self.last_iter = datetime.datetime.utcnow() - self.next_iter = datetime.datetime.utcnow() + self.expected_interval - # this isn't accurate, it will be "corrected" when finishing is called - - def iter_finish(self) -> None: - """Register an iteration as finished successfully.""" - self.currently_running = False - # now this is accurate. imo its better to have something than nothing - - def iter_error(self, error: BaseException) -> None: - """Register an iteration's exception.""" - self.currently_running = False - self.last_exc_raw = error - self.last_exc = "".join( - traceback.format_exception(type(error), error, error.__traceback__) - ) - - def get_debug_embed(self) -> discord.Embed: - """Get an embed with infomation on this loop.""" - table = Table("Key", "Value") - - table.add_row("expected_interval", str(self.expected_interval)) - table.add_row("iter_count", str(self.iter_count)) - table.add_row("currently_running", str(self.currently_running)) - table.add_row("last_iterstr", str(self.last_iter) or "Loop not started") - table.add_row("next_iterstr", str(self.next_iter) or "Loop not started") - - raw_table_str = no_colour_rich_markup(table) - - now = datetime.datetime.utcnow() - - if self.next_iter and self.last_iter: - table = Table("Key", "Value") - table.add_row("Seconds until next", str((self.next_iter - now).total_seconds())) - table.add_row("Seconds since last", str((now - self.last_iter).total_seconds())) - processed_table_str = no_colour_rich_markup(table) - - else: - processed_table_str = "Loop hasn't started yet." - - emoji = CHECK if self.integrity else CROSS - embed = discord.Embed(title=f"{self.friendly_name}: `{emoji}`") - embed.add_field(name="Raw data", value=raw_table_str, inline=False) - embed.add_field( - name="Processed data", - value=processed_table_str, - inline=False, - ) - exc = self.last_exc - if len(exc) > 1024: - exc = list(pagify(exc, page_length=1024))[0] + "\n..." - embed.add_field(name="Exception", value=box(exc), inline=False) - - return embed diff --git a/covidgraph/vexutils/meta.py b/covidgraph/vexutils/meta.py deleted file mode 100644 index cae0ae73..00000000 --- a/covidgraph/vexutils/meta.py +++ /dev/null @@ -1,238 +0,0 @@ -from __future__ import annotations - -import asyncio -import json -from logging import Logger, getLogger -from pathlib import Path -from typing import Literal, NamedTuple - -import aiohttp -from redbot.core import VersionInfo, commands -from redbot.core import version_info as cur_red_version -from rich import box as rich_box -from rich.table import Table # type:ignore - -from .chat import no_colour_rich_markup -from .consts import DOCS_BASE, GREEN_CIRCLE, RED_CIRCLE -from .loop import VexLoop - -log = getLogger("red.vex-utils") - - -cog_ver_lock = asyncio.Lock() - - -def get_vex_logger(name: str) -> Logger: - """Get a logger for the given name. - - Parameters - ---------- - name : str - The ``__name__`` of the file - - Returns - ------- - Logger - The logger - """ - final_name = "red.vex." - split = name.split(".") - if len(split) == 2 and split[0] == split[1]: # for example make `cmdlog.cmdlog` just `cmdlog` - final_name += split[0] - else: # otherwise use full path - final_name += name - - return getLogger(final_name) - - -def format_help(self: commands.Cog, ctx: commands.Context) -> str: - """Wrapper for format_help_for_context. **Not** currently for use outside my cogs. - - Thanks Sinbad. - - Parameters - ---------- - self : commands.Cog - The Cog class - context : commands.Context - Context - - Returns - ------- - str - Formatted help - """ - docs = DOCS_BASE.format(self.qualified_name.lower()) - pre_processed = super(type(self), self).format_help_for_context(ctx) # type:ignore - - return ( - f"{pre_processed}\n\nAuthor: **`{self.__author__}`**\nCog Version: " # type:ignore - f"**`{self.__version__}`**\n{docs}" # type:ignore - ) - # adding docs link here so doesn't show up in auto generated docs - - -# TODO: stop using red internal util - - -async def format_info( - ctx: commands.Context, - qualified_name: str, - cog_version: str, - extras: dict[str, str | bool] = {}, - loops: list[VexLoop] = [], -) -> str: - """Generate simple info text about the cog. **Not** currently for use outside my cogs. - - Parameters - ---------- - ctx : commands.Context - Context - qualified_name : str - The name you want to show, eg "BetterUptime" - cog_version : str - The version of the cog - extras : Dict[str, Union[str, bool]], optional - Dict which is foramtted as key: value\\n. Bools as a value will be replaced with - check/cross emojis, by default {} - loops : List[VexLoop], optional - List of VexLoops you want to show, by default [] - - Returns - ------- - str - Simple info text. - """ - cog_name = qualified_name.lower() - current = _get_current_vers(cog_version, qualified_name) - try: - latest = await _get_latest_vers(cog_name) - - cog_updated = current.cog >= latest.cog - utils_updated = current.utils == latest.utils - red_updated = current.red >= latest.red - except Exception: # anything and everything, eg aiohttp error or version parsing error - log.warning("Unable to parse versions.", exc_info=True) - cog_updated, utils_updated, red_updated = "Unknown", "Unknown", "Unknown" - latest = UnknownVers() - - start = f"{qualified_name} by Vexed.\n\n\n" - - main_table = Table( - "", "Current", "Latest", "Up to date?", title="Versions", box=rich_box.MINIMAL - ) - - main_table.add_row( - "This Cog", - str(current.cog), - str(latest.cog), - GREEN_CIRCLE if cog_updated else RED_CIRCLE, - ) - main_table.add_row( - "Bundled Utils", - current.utils, - latest.utils, - GREEN_CIRCLE if utils_updated else RED_CIRCLE, - ) - main_table.add_row( - "Red", - str(current.red), - str(latest.red), - GREEN_CIRCLE if red_updated else RED_CIRCLE, - ) - - update_msg = "\n" - if not cog_updated: - update_msg += f"To update this cog, use the `{ctx.clean_prefix}cog update` command.\n" - if not utils_updated: - update_msg += ( - f"To update the bundled utils, use the `{ctx.clean_prefix}cog update` command.\n" - ) - if not red_updated: - update_msg += "To update Red, see https://docs.discord.red/en/stable/update_red.html\n" - - extra_table = Table("Key", "Value", title="Extras", box=rich_box.MINIMAL) - - data = [] - if loops: - for loop in loops: - extra_table.add_row(loop.friendly_name, GREEN_CIRCLE if loop.integrity else RED_CIRCLE) - - if extras: - if data: - extra_table.add_row("", "") - for key, value in extras.items(): - if isinstance(value, bool): - str_value = GREEN_CIRCLE if value else RED_CIRCLE - else: - assert isinstance(value, str) - str_value = value - extra_table.add_row(key, str_value) - - boxed = no_colour_rich_markup(main_table) - boxed += update_msg - if loops or extras: - boxed += no_colour_rich_markup(extra_table) - - return f"{start}{boxed}" - - -async def out_of_date_check(cogname: str, currentver: str) -> None: - """Send a log at warning level if the cog is out of date.""" - try: - async with cog_ver_lock: - vers = await _get_latest_vers(cogname) - if VersionInfo.from_str(currentver) < vers.cog: - log.warning( - f"Your {cogname} cog, from Vex, is out of date. You can update your cogs with the " - "'cog update' command in Discord." - ) - else: - log.debug(f"{cogname} cog is up to date") - except Exception as e: - log.debug( - f"Something went wrong checking if {cogname} cog is up to date. See below.", exc_info=e - ) - # really doesn't matter if this fails so fine with debug level - return - - -class Vers(NamedTuple): - cogname: str - cog: VersionInfo - utils: str - red: VersionInfo - - -class UnknownVers(NamedTuple): - cogname: str = "Unknown" - cog: VersionInfo | Literal["Unknown"] = "Unknown" - utils: str = "Unknown" - red: VersionInfo | Literal["Unknown"] = "Unknown" - - -async def _get_latest_vers(cogname: str) -> Vers: - data: dict - async with aiohttp.ClientSession() as session: - async with session.get(f"https://api.vexcodes.com/v2/vers/{cogname}", timeout=3) as r: - data = await r.json() - latest_utils = data["utils"][:7] - latest_cog = VersionInfo.from_str(data.get(cogname, "0.0.0")) - async with session.get("https://pypi.org/pypi/Red-DiscordBot/json", timeout=3) as r: - data = await r.json() - latest_red = VersionInfo.from_str(data.get("info", {}).get("version", "0.0.0")) - - return Vers(cogname, latest_cog, latest_utils, latest_red) - - -def _get_current_vers(curr_cog_ver: str, qual_name: str) -> Vers: - with open(Path(__file__).parent / "commit.json") as fp: - data = json.load(fp) - latest_utils = data.get("latest_commit", "Unknown")[:7] - - return Vers( - qual_name, - VersionInfo.from_str(curr_cog_ver), - latest_utils, - cur_red_version, - ) diff --git a/covidgraph/vexutils/py.typed b/covidgraph/vexutils/py.typed deleted file mode 100644 index e69de29b..00000000 diff --git a/covidgraph/vexutils/sqldriver.py b/covidgraph/vexutils/sqldriver.py deleted file mode 100644 index a8d52544..00000000 --- a/covidgraph/vexutils/sqldriver.py +++ /dev/null @@ -1,98 +0,0 @@ -import concurrent.futures -import functools -import os -import sqlite3 -from asyncio.events import AbstractEventLoop -from typing import Optional - -from redbot.core.bot import Red -from redbot.core.data_manager import cog_data_path - -# (comparisons from red config, mainly to make me feel like i didn't waste an evening) -# (these are with the stattrack cog, this driver is also used in betteruptime) -# this compares a write for config to appending to the SQL. i've not compared writes because they -# will only happend once, for migration from config. SQL includes copying DF + executor overhead -# basically this is the raw speed changes for the loop itself -# ~1.3 sec to ~0.03 sec, dataset of ~1 week on windows -# ~5-6 sec to ~0.04 sec, dataset of ~1 month on linux -# reads are insignificant as only happen on cog load - -try: - import pandas -except ImportError: - raise RuntimeError("Pandas must be installed for this driver to work.") - - -class PandasSQLiteDriver: - """An asynchronous SQLite driver for Pandas dataframes.""" - - def __init__(self, bot: Red, cog_name: str, filename: str, table: str = "main_df") -> None: - """Get a driver object for interacting with a table in the given cog's datapath. - - Parameters - ---------- - bot : Red - Bot object - cog_name : str - Full cog name, LikeThis - filename : str - The full file name to use for the database, for example `timeseries.db` - table : str, optional - The SQLite table to use, by default "main_df" - """ - self.bot = bot - self.table = table - - self.sql_executor = concurrent.futures.ThreadPoolExecutor(1, f"{cog_name.lower()}_sql") - self.sql_path = str(cog_data_path(raw_name=cog_name) / filename) - - def _write(self, df: pandas.DataFrame, table: Optional[str] = None) -> None: - connection = sqlite3.connect(self.sql_path) - try: - df.to_sql(table or self.table, con=connection, if_exists="replace") # type:ignore - connection.commit() - finally: - connection.close() - - def _append(self, df: pandas.DataFrame, table: Optional[str] = None) -> None: - connection = sqlite3.connect(self.sql_path) - try: - df.to_sql(table or self.table, con=connection, if_exists="append") # type:ignore - connection.commit() - finally: - connection.close() - - def _read(self, table: Optional[str] = None) -> pandas.DataFrame: - connection = sqlite3.connect(self.sql_path) - try: - df = pandas.read_sql( - f"SELECT * FROM {table or self.table}", - connection, - index_col="index", - parse_dates=["index"], - ) - return df - finally: - connection.close() - - async def write(self, df: pandas.DataFrame, table: Optional[str] = None) -> None: - """Write a dataframe to the database. Replaces and old data.""" - assert isinstance(self.bot.loop, AbstractEventLoop) - func = functools.partial(self._write, df.copy(True), table) - await self.bot.loop.run_in_executor(self.sql_executor, func) - - async def append(self, df: pandas.DataFrame, table: Optional[str] = None) -> None: - """Append a dataframe to the database.""" - assert isinstance(self.bot.loop, AbstractEventLoop) - func = functools.partial(self._append, df.copy(True), table) - await self.bot.loop.run_in_executor(self.sql_executor, func) - - async def read(self, table: Optional[str] = None) -> pandas.DataFrame: - """Read the database, returning as a pandas dataframe.""" - assert isinstance(self.bot.loop, AbstractEventLoop) - func = functools.partial(self._read, table) - return await self.bot.loop.run_in_executor(self.sql_executor, func) - - def storage_usage(self) -> int: - """Return the size of the database file in bytes.""" - return os.path.getsize(self.sql_path) diff --git a/covidgraph/vexutils/url_buttons.py b/covidgraph/vexutils/url_buttons.py deleted file mode 100644 index 3fad6e0c..00000000 --- a/covidgraph/vexutils/url_buttons.py +++ /dev/null @@ -1,64 +0,0 @@ -from typing import Optional - -import discord -from discord.http import Route -from redbot.core.bot import Red - - -class URLButton: - def __init__(self, label: str, url: str) -> None: - if not isinstance(label, str): - raise TypeError("Label must be a string") - if not isinstance(url, str): - raise TypeError("URL must be a string") - - self.label = label - self.url = url - - def to_dict(self) -> dict: - return { - "label": self.label, - "style": 5, - "type": 2, - "url": self.url, - } - - -async def send_message( - bot: Red, - channel_id: int, - *, - content: Optional[str] = None, - embed: Optional[discord.Embed] = None, - file: Optional[discord.File] = None, - url_button: Optional[URLButton] = None, -): - """Send a message with a URL button, with pure dpy 1.7.""" - payload = {} - - if content: - payload["content"] = content - - if embed: - payload["embed"] = embed.to_dict() - - if url_button: - payload["components"] = [{"type": 1, "components": [url_button.to_dict()]}] # type:ignore - - if file: - form = [ - { - "name": "file", - "value": file.fp, - "filename": file.filename, - "content_type": "application/octet-stream", - }, - {"name": "payload_json", "value": discord.utils.to_json(payload)}, - ] - - r = Route("POST", "/channels/{channel_id}/messages", channel_id=channel_id) - await bot._connection.http.request(r, form=form, files=[file]) - - else: - r = Route("POST", "/channels/{channel_id}/messages", channel_id=channel_id) - await bot._connection.http.request(r, json=payload) diff --git a/covidgraph/vexutils/version.py b/covidgraph/vexutils/version.py deleted file mode 100644 index fab833f3..00000000 --- a/covidgraph/vexutils/version.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = "2.6.1" diff --git a/docs/changelog.rst b/docs/changelog.rst index 7ae4fba3..d3b302b9 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -80,6 +80,14 @@ Aliases AnotherPingCog ============== +********* +``1.1.8`` +********* + +2023-05-04 + +- Add slash commands + ********* ``1.1.7`` ********* @@ -193,6 +201,14 @@ Internal Changes BetterUptime ============ +********* +``2.1.4`` +********* + +2023-05-04 + +- Add slash commands + ********* ``2.1.3`` ********* @@ -362,6 +378,14 @@ BetterUptime Birthday ======== +********* +``1.2.1`` +********* + +2023-05-04 + +- Add a modal for setup + ********* ``1.2.0`` ********* @@ -872,6 +896,14 @@ RolePlay StatTrack ========= +********** +``1.10.0`` +********** + +2023-05-04 + +- Add a select menu for changing the graph, metrics, and timespan + ********* ``1.9.1`` ********* @@ -1036,6 +1068,15 @@ StatTrack Status ====== +********* +``2.5.7`` +********* + +2023-05-04 + +- Thread support +- Select menu in statusset add + ********* ``2.5.6`` ********* @@ -1351,6 +1392,14 @@ Internal changes System ====== +********* +``1.4.0`` +********* + +2023-05-04 + +- Add a select menu for changing the shown metric + ********** ``1.3.10`` ********** diff --git a/docs/cogs/aliases.rst b/docs/cogs/aliases.rst index 7e27d32e..c5f10d2c 100644 --- a/docs/cogs/aliases.rst +++ b/docs/cogs/aliases.rst @@ -50,5 +50,5 @@ This will show the main command, built-in aliases, global aliases and server aliases. **Examples:** - - ``[p]aliases foo`` - - ``[p]aliases foo bar`` +- ``[p]aliases foo`` +- ``[p]aliases foo bar`` diff --git a/docs/cogs/anotherpingcog.rst b/docs/cogs/anotherpingcog.rst index 53065784..b918606f 100644 --- a/docs/cogs/anotherpingcog.rst +++ b/docs/cogs/anotherpingcog.rst @@ -148,10 +148,10 @@ If you want to go back to the defaults, just do ``[p]pingset green default defau same colour as the emoji. Google "hex colour" if you need help with this. **Examples:** - - ``[p]pingset green :emoji: #43B581`` - - ``[p]pingset green :emoji: default`` - - ``[p]pingset green default #43B581`` - - ``[p]pingset green default default`` +- ``[p]pingset green :emoji: #43B581`` +- ``[p]pingset green :emoji: default`` +- ``[p]pingset green default #43B581`` +- ``[p]pingset green default default`` .. _anotherpingcog-command-pingset-orange: @@ -183,10 +183,10 @@ If you want to go back to the defaults, just do ``[p]pingset orange default defa same colour as the emoji. Google "hex colour" if you need help with this. **Examples:** - - ``[p]pingset orange :emoji: #FAA61A`` - - ``[p]pingset orange :emoji: default`` - - ``[p]pingset orange default #FAA61A`` - - ``[p]pingset orange default default`` +- ``[p]pingset orange :emoji: #FAA61A`` +- ``[p]pingset orange :emoji: default`` +- ``[p]pingset orange default #FAA61A`` +- ``[p]pingset orange default default`` .. _anotherpingcog-command-pingset-red: @@ -218,10 +218,10 @@ If you want to go back to the defaults, just do ``[p]pingset red default default same colour as the emoji. Google "hex colour" if you need help with this. **Examples:** - - ``[p]pingset red :emoji: #F04747`` - - ``[p]pingset red :emoji: default`` - - ``[p]pingset red default #F04747`` - - ``[p]pingset red default default`` +- ``[p]pingset red :emoji: #F04747`` +- ``[p]pingset red :emoji: default`` +- ``[p]pingset red default #F04747`` +- ``[p]pingset red default default`` .. _anotherpingcog-command-pingset-settings: diff --git a/docs/cogs/beautify.rst b/docs/cogs/beautify.rst index 2ded1938..c475c652 100644 --- a/docs/cogs/beautify.rst +++ b/docs/cogs/beautify.rst @@ -54,16 +54,16 @@ Beautify some JSON. This command accepts it in a few forms. 1. Upload the JSON as a file (it can be .txt or .json) -​ ​ ​ ​ - Note that if you upload multiple files I will only scan the first one + - Note that if you upload multiple files I will only scan the first one 2. Paste the JSON in the command -​ ​ ​ ​ - You send it raw, in inline code or a codeblock -​3. Reply to a message with JSON -​ ​ ​ ​ - I will search for attachments and any codeblocks in the message + - You can send it raw, in inline code or a codeblock +3. Reply to a message with JSON + - I will search for attachments and any codeblocks in the message **Examples:** - - ``[p]beautify {"1": "One", "2": "Two"}`` - - ``[p]beautify`` (with file uploaded) - - ``[p]beautify`` (while replying to a messsage) +- ``[p]beautify {"1": "One", "2": "Two"}`` +- ``[p]beautify`` (with file uploaded) +- ``[p]beautify`` (while replying to a messsage) .. _beautify-command-minify: @@ -84,13 +84,13 @@ Minify some JSON. This command accepts it in a few forms. 1. Upload the JSON as a file (it can be .txt or .json) -​ ​ ​ ​ - Note that if you upload multiple files I will only scan the first one + - Note that if you upload multiple files I will only scan the first one 2. Paste the JSON in the command -​ ​ ​ ​ - You send it raw, in inline code or a codeblock -​3. Reply to a message with JSON -​ ​ ​ ​ - I will search for attachments and any codeblocks in the message + - You can send it raw, in inline code or a codeblock +3. Reply to a message with JSON + - I will search for attachments and any codeblocks in the message **Examples:** - - ``[p]minify {"1": "One", "2": "Two"}`` - - ``[p]minify`` (with file uploaded) - - ``[p]minify`` (while replying to a messsage) +- ``[p]minify {"1": "One", "2": "Two"}`` +- ``[p]minify`` (with file uploaded) +- ``[p]minify`` (while replying to a messsage) diff --git a/docs/cogs/betteruptime.rst b/docs/cogs/betteruptime.rst index 55b8acfa..cae0d747 100644 --- a/docs/cogs/betteruptime.rst +++ b/docs/cogs/betteruptime.rst @@ -54,9 +54,9 @@ The default value for ``num_days`` is ``30``. You can put ``0`` days for all-tim Otherwise, it needs to be ``5`` or more. **Examples:** - - ``[p]uptime`` - - ``[p]uptime 0`` (for all-time data) - - ``[p]uptime 7`` +- ``[p]uptime`` +- ``[p]uptime 0`` (for all-time data) +- ``[p]uptime 7`` .. _betteruptime-command-resetbu: @@ -99,9 +99,9 @@ Otherwise, it needs to be ``5`` or more. **Examples:** - - ``[p]uptime`` - - ``[p]uptime 0`` (for all-time data) - - ``[p]uptime 7`` +- ``[p]uptime`` +- ``[p]uptime 0`` (for all-time data) +- ``[p]uptime 7`` .. _betteruptime-command-uptimeexport: @@ -151,6 +151,6 @@ The default value for ``num_days`` is ``30``. You can put ``0`` days for all-tim Otherwise, it needs to be ``5`` or more. **Examples:** - - ``[p]uptime`` - for the default of 30 days - - ``[p]uptime 0`` - for all-time data - - ``[p]uptime 7`` - 7 days +- ``[p]uptime`` - for the default of 30 days +- ``[p]uptime 0`` - for all-time data +-]uptime 7`` - 7 days diff --git a/docs/cogs/birthday.rst b/docs/cogs/birthday.rst index 95528fed..fe02b57b 100644 --- a/docs/cogs/birthday.rst +++ b/docs/cogs/birthday.rst @@ -44,7 +44,7 @@ bdset .. code-block:: none - [p]bdset + [p]bdset **Description** @@ -69,7 +69,7 @@ bdset channel Set the channel where the birthday message will be sent. **Example:** - - ``[p]bdset channel #birthdays`` - set the channel to #birthdays +- ``[p]bdset channel #birthdays`` - set the channel to #birthdays .. _birthday-command-bdset-force: @@ -91,11 +91,11 @@ You can @ mention any user or type out their exact name. If you're typing out a spaces, make sure to put quotes around it (``"``). **Examples:** - - ``[p]bdset set @User 1-1-2000`` - set the birthday of ``@User`` to 1/1/2000 - - ``[p]bdset set User 1/1`` - set the birthday of ``@User`` to 1/1/2000 - - ``[p]bdset set "User with spaces" 1-1`` - set the birthday of ``@User with spaces`` +- ``[p]bdset set @User 1-1-2000`` - set the birthday of ``@User`` to 1/1/2000 +- ``[p]bdset set User 1/1`` - set the birthday of ``@User`` to 1/1/2000 +- ``[p]bdset set "User with spaces" 1-1`` - set the birthday of ``@User with spaces`` to 1/1 - - ``[p]bdset set 354125157387344896 1/1/2000`` - set the birthday of ``@User`` to 1/1/2000 +- ``[p]bdset set 354125157387344896 1/1/2000`` - set the birthday of ``@User`` to 1/1/2000 .. _birthday-command-bdset-interactive: @@ -107,7 +107,7 @@ bdset interactive .. code-block:: none - [p]bdset interactive + [p]bdset interactive **Description** @@ -132,14 +132,14 @@ Set the message to be send when the user did not provide a year. If you would like to mention a role, you will need to run ``[p]bdset rolemention true``. **Placeholders:** - - ``{name}`` - the user's name - - ``{mention}`` - an @ mention of the user +- ``{name}`` - the user's name +- ``{mention}`` - an @ mention of the user All the placeholders are optional. **Examples:** - - ``[p]bdset msgwithoutyear Happy birthday {mention}!`` - - ``[p]bdset msgwithoutyear {mention}'s birthday is today! Happy birthday {name}.`` +- ``[p]bdset msgwithoutyear Happy birthday {mention}!`` +- ``[p]bdset msgwithoutyear {mention}'s birthday is today! Happy birthday {name}.`` .. _birthday-command-bdset-msgwithyear: @@ -160,15 +160,15 @@ Set the message to be send when the user did provide a year. If you would like to mention a role, you will need to run ``[p]bdset rolemention true`` **Placeholders:** - - ``{name}`` - the user's name - - ``{mention}`` - an @ mention of the user - - ``{new_age}`` - the user's new age +- ``{name}`` - the user's name +- ``{mention}`` - an @ mention of the user +- ``{new_age}`` - the user's new age All the placeholders are optional. **Examples:** - - ``[p]bdset msgwithyear {mention} has turned {new_age}, happy birthday!`` - - ``[p]bdset msgwithyear {name} is {new_age} today! Happy birthday {mention}!`` +- ``[p]bdset msgwithyear {mention} has turned {new_age}, happy birthday!`` +- ``[p]bdset msgwithyear {name} is {new_age} today! Happy birthday {mention}!`` .. _birthday-command-bdset-role: @@ -189,9 +189,9 @@ Set the role that will be given to the user on their birthday. You can give the exact name or a mention. **Example:** - - ``[p]bdset role @Birthday`` - set the role to @Birthday - - ``[p]bdset role Birthday`` - set the role to @Birthday without a mention - - ``[p]bdset role 418058139913063657`` - set the role with an ID +- ``[p]bdset role @Birthday`` - set the role to @Birthday +- ``[p]bdset role Birthday`` - set the role to @Birthday without a mention +- ``[p]bdset role 418058139913063657`` - set the role with an ID .. _birthday-command-bdset-rolemention: @@ -224,7 +224,7 @@ bdset settings .. code-block:: none - [p]bdset settings + [p]bdset settings **Description** @@ -240,7 +240,7 @@ bdset stop .. code-block:: none - [p]bdset stop + [p]bdset stop **Description** @@ -265,9 +265,9 @@ Set the time of day for the birthday message. Minutes are ignored. **Examples:** - - ``[p]bdset time 7:00`` - set the time to 7:45AM UTC - - ``[p]bdset time 12AM`` - set the time to midnight UTC - - ``[p]bdset time 3PM`` - set the time to 3:00PM UTC +- ``[p]bdset time 7:00`` - set the time to 7:45AM UTC +- ``[p]bdset time 12AM`` - set the time to midnight UTC +- ``[p]bdset time 3PM`` - set the time to 3:00PM UTC .. _birthday-command-bdset-zemigrate: @@ -281,7 +281,7 @@ bdset zemigrate .. code-block:: none - [p]bdset zemigrate + [p]bdset zemigrate **Description** @@ -297,7 +297,7 @@ birthday .. code-block:: none - [p]birthday + [p]birthday .. tip:: Alias: ``bday`` @@ -315,7 +315,7 @@ birthday remove .. code-block:: none - [p]birthday remove + [p]birthday remove .. tip:: Aliases: ``birthday delete``, ``birthday del`` @@ -346,11 +346,11 @@ You can optionally add in the year, if you are happy to share this. If you use a date in the format xx/xx/xx or xx-xx-xx MM-DD-YYYY is assumed. **Examples:** - - ``[p]bday set 24th September`` - - ``[p]bday set 24th Sept 2002`` - - ``[p]bday set 9/24/2002`` - - ``[p]bday set 9-24-2002`` - - ``[p]bday set 9-24`` +- ``[p]bday set 24th September`` +- ``[p]bday set 24th Sept 2002`` +- ``[p]bday set 9/24/2002`` +- ``[p]bday set 9-24-2002`` +- ``[p]bday set 9-24`` .. _birthday-command-birthday-upcoming: @@ -369,17 +369,7 @@ birthday upcoming View upcoming birthdays, defaults to 7 days. **Examples:** - - ``[p]birthday upcoming`` - default of 7 days - - ``[p]birthday upcoming 14`` - 14 days +- ``[p]birthday upcoming`` - default of 7 days +- ``[p]birthday upcoming 14`` - 14 days .. _birthday-command-birthdaydebug-upcoming: - -"""""""""""""""""""""" -birthdaydebug upcoming -"""""""""""""""""""""" - -**Syntax** - -.. code-block:: none - - [p]birthdaydebug upcoming diff --git a/docs/cogs/buttonpoll.rst b/docs/cogs/buttonpoll.rst new file mode 100644 index 00000000..83e26cc3 --- /dev/null +++ b/docs/cogs/buttonpoll.rst @@ -0,0 +1,57 @@ +.. _buttonpoll: + +========== +ButtonPoll +========== + +This is the cog guide for the buttonpoll cog. You will +find detailed docs about usage and commands. + +``[p]`` is considered as your prefix. + +.. note:: + + To use this cog, you will need to install and load it. + + See the :ref:`getting_started` page. + +.. _buttonpoll-usage: + +----- +Usage +----- + +Create polls with buttons, and get a pie chart afterwards! + + +.. _buttonpoll-commands: + +-------- +Commands +-------- + +.. _buttonpoll-command-buttonpoll: + +^^^^^^^^^^ +buttonpoll +^^^^^^^^^^ + +**Syntax** + +.. code-block:: none + + [p]buttonpoll [chan] + +.. tip:: Alias: ``bpoll`` + +**Description** + +Start a button-based poll + +This is an interactive setup. By default the current channel will be used, +but if you want to start a poll remotely you can send the channel name +along with the buttonpoll command. + +**Examples:** +- ``[p]buttonpoll`` - start a poll in the current channel +- ``[p]buttonpoll #polls`` start a poll somewhere else diff --git a/docs/cogs/calc.rst b/docs/cogs/calc.rst new file mode 100644 index 00000000..79614110 --- /dev/null +++ b/docs/cogs/calc.rst @@ -0,0 +1,47 @@ +.. _calc: + +==== +Calc +==== + +This is the cog guide for the calc cog. You will +find detailed docs about usage and commands. + +``[p]`` is considered as your prefix. + +.. note:: + + To use this cog, you will need to install and load it. + + See the :ref:`getting_started` page. + +.. _calc-usage: + +----- +Usage +----- + +Calculate simple mathematical expressions. + + +.. _calc-commands: + +-------- +Commands +-------- + +.. _calc-command-calc: + +^^^^ +calc +^^^^ + +**Syntax** + +.. code-block:: none + + [p]calc + +**Description** + +Start an interactive calculator using buttons. diff --git a/docs/cogs/cmdlog.rst b/docs/cogs/cmdlog.rst index a56d07e3..ba096f55 100644 --- a/docs/cogs/cmdlog.rst +++ b/docs/cogs/cmdlog.rst @@ -94,8 +94,8 @@ Set the channel to send logs to, this is optional. Run the comand without a channel to stop sending. **Example:** - - ``[p]cmdlog channel #com-log`` - set the log channel to #com-log - - ``[p]cmdlog channel`` - stop sending logs +- ``[p]cmdlog channel #com-log`` - set the log channel to #com-log +- ``[p]cmdlog channel`` - stop sending logs .. _cmdlog-command-cmdlog-command: @@ -120,9 +120,9 @@ You can search for a group command (eg ``cmdlog``) or a full command (eg ``cmdlo As arguments are not stored, you cannot search for them. **Examples:** - - ``[p]cmdlog command ping`` - - ``[p]cmdlog command playlist`` - - ``[p]cmdlog command playlist create`` +- ``[p]cmdlog command ping`` +- ``[p]cmdlog command playlist`` +- ``[p]cmdlog command playlist create`` .. _cmdlog-command-cmdlog-content: @@ -175,7 +175,7 @@ cmdlog server Upload all the logs that are stored for for a specific server ID in the cache. **Example:** - - ``[p]cmdlog server 527961662716772392`` +- ``[p]cmdlog server 527961662716772392`` .. _cmdlog-command-cmdlog-user: @@ -194,4 +194,4 @@ cmdlog user Upload all the logs that are stored for a specific User ID in the cache. **Example:** - - ``[p]cmdlog user 418078199982063626`` +- ``[p]cmdlog user 418078199982063626`` diff --git a/docs/cogs/covidgraph.rst b/docs/cogs/covidgraph.rst index 67b309de..7e2ab436 100644 --- a/docs/cogs/covidgraph.rst +++ b/docs/cogs/covidgraph.rst @@ -4,128 +4,6 @@ CovidGraph ========== -This is the cog guide for the covidgraph cog. You will -find detailed docs about usage and commands. +This cog will not be updated to Red 3.5 and has been removed due to irrelevance and data issues. -``[p]`` is considered as your prefix. - -.. note:: - - To use this cog, you will need to install and load it. - - See the :ref:`getting_started` page. - -.. _covidgraph-usage: - ------ -Usage ------ - -Get COVID-19 graphs. - - -.. _covidgraph-commands: - --------- -Commands --------- - -.. _covidgraph-command-covidgraph: - -^^^^^^^^^^ -covidgraph -^^^^^^^^^^ - -**Syntax** - -.. code-block:: none - - [p]covidgraph - -**Description** - -Get graphs of COVID-19 data. - -.. _covidgraph-command-covidgraph-cases: - -"""""""""""""""" -covidgraph cases -"""""""""""""""" - -**Syntax** - -.. code-block:: none - - [p]covidgraph cases [days] - -.. tip:: Alias: ``covidgraph c`` - -**Description** - -Get the number of confirmed cases in a country. - -You can optionally specify the number of days to get data for, -otherwise it will be all-time. - -``country`` can also be ``world`` to get the worldwide data. - -**Examples:** - - ``[p]covidgraph cases US`` - All time data for the US - - ``[p]covidgraph cases 7 US`` - Last 7 days for the US - - ``[p]covidgraph cases world`` - Worldwide data - -.. _covidgraph-command-covidgraph-deaths: - -""""""""""""""""" -covidgraph deaths -""""""""""""""""" - -**Syntax** - -.. code-block:: none - - [p]covidgraph deaths [days] - -.. tip:: Alias: ``covidgraph d`` - -**Description** - -Get the number of deaths in a country. - -You can optionally specify the number of days to get data for, -otherwise it will be all-time. - -``country`` can also be ``world`` to get the worldwide data. - -**Examples:** - - ``[p]covidgraph deaths US`` - All time data for the US - - ``[p]covidgraph deaths 7 US`` - Last 7 days for the US - - ``[p]covidgraph deaths world`` - Worldwide data - -.. _covidgraph-command-covidgraph-vaccines: - -""""""""""""""""""" -covidgraph vaccines -""""""""""""""""""" - -**Syntax** - -.. code-block:: none - - [p]covidgraph vaccines [days] - -.. tip:: Alias: ``covidgraph v`` - -**Description** - -Get the number of vaccine doses administered in a country. - -You can optionally specify the number of days to get data for, -otherwise it will be all-time. - -``country`` can also be ``world`` to get the worldwide data. - -**Examples:** - - ``[p]covidgraph vaccines US`` - All time data for the US - - ``[p]covidgraph vaccines 7 US`` - Last 7 days for the US - - ``[p]covidgraph vaccines world`` - Worldwide data +The previous version may still be installed on Red 3.4, however it may break at any time. diff --git a/docs/cogs/fivemstatus.rst b/docs/cogs/fivemstatus.rst index 01c3a2d9..a180f499 100644 --- a/docs/cogs/fivemstatus.rst +++ b/docs/cogs/fivemstatus.rst @@ -85,4 +85,4 @@ fivemstatus setup Set up a FiveM status message. **Examples:** - - ``[p]fivemstatus setup #status 1.0.1.0:30120`` +- ``[p]fivemstatus setup #status 1.0.1.0:30120`` diff --git a/docs/cogs/ghissues.rst b/docs/cogs/ghissues.rst new file mode 100644 index 00000000..5e060ee0 --- /dev/null +++ b/docs/cogs/ghissues.rst @@ -0,0 +1,126 @@ +.. _ghissues: + +======== +GHIssues +======== + +This is the cog guide for the ghissues cog. You will +find detailed docs about usage and commands. + +``[p]`` is considered as your prefix. + +.. note:: + + To use this cog, you will need to install and load it. + + See the :ref:`getting_started` page. + +.. _ghissues-usage: + +----- +Usage +----- + +Create, comment, labelify and close GitHub issues. + +This cog is only for bot owners. +I made it for managing issues on my cog repo as a small project, +but it certainly could be used for other situations where you want +to manage GitHub issues from Discord. + +If you would like a way to search or view issues, I highly recommend +Kowlin's approved ``githubcards`` cog (on the repo +https://github.com/Kowlin/Sentinel) + +At present, this cannot support multiple repos. + +PRs are mostly supported. You can comment on them or close them +but not merge them or create them. + +Get started with the ``ghi howtoken`` command to set your GitHub token. +You don't have to do this if you have already set it for a different +cog, eg ``githubcards``. Then set up with ``ghi setrepo``. + + +.. _ghissues-commands: + +-------- +Commands +-------- + +.. _ghissues-command-ghi: + +^^^ +ghi +^^^ + +.. note:: |owner-lock| + +**Syntax** + +.. code-block:: none + + [p]ghi + +.. tip:: Alias: ``ghissues`` + +**Description** + +Command to interact with this cog. + +All commands are owner only. + +To open the interactive issue view, run ``[p]ghi ``. + +**Examples:** +- ``[p]ghi 11`` +- ``[p]ghi howtoken`` +- ``[p]ghi newissue`` + +.. _ghissues-command-ghi-howtoken: + +"""""""""""" +ghi howtoken +"""""""""""" + +**Syntax** + +.. code-block:: none + + [p]ghi howtoken + +**Description** + +Instructions on how to set up a token. + +.. _ghissues-command-ghi-newissue: + +"""""""""""" +ghi newissue +"""""""""""" + +**Syntax** + +.. code-block:: none + + [p]ghi newissue + +**Description** + +Open a new issue. If you want to reopen, then use the normal interactive view. + +.. _ghissues-command-ghi-setrepo: + +""""""""""" +ghi setrepo +""""""""""" + +**Syntax** + +.. code-block:: none + + [p]ghi setrepo <slug> + +**Description** + +Set up a repo to use as a slug (``USERNAME/REPO``). diff --git a/docs/cogs/github.rst b/docs/cogs/github.rst index cc3bb185..83964fb6 100644 --- a/docs/cogs/github.rst +++ b/docs/cogs/github.rst @@ -4,200 +4,4 @@ GitHub ====== -This is the cog guide for the github cog. You will -find detailed docs about usage and commands. - -``[p]`` is considered as your prefix. - -.. note:: - - To use this cog, you will need to install and load it. - - See the :ref:`getting_started` page. - -.. _github-usage: - ------ -Usage ------ - -Create, comment, labelify and close GitHub issues. - -This cog is only for bot owners. -I made it for managing issues on my cog repo as a small project, -but it certainly could be used for other situations where you want -to manage GitHub issues from Discord. - -If you would like a way to search or view issues, I highly recommend -Kowlin's approved ``githubcards`` cog (on the repo -https://github.com/Kowlin/Sentinel) - -At present, this cannot support multiple repos. - -PRs are mostly supported. You can comment on them or close them -but not merge them or create them. - -Get started with the ``gh howtoken`` command to set your GitHub token. -You don't have to do this if you have already set it for a different -cog, eg ``githubcards``. Then set up with ``gh setrepo``. - - -.. _github-commands: - --------- -Commands --------- - -.. _github-command-gh: - -^^ -gh -^^ - -.. note:: |owner-lock| - -**Syntax** - -.. code-block:: none - - [p]gh - -.. tip:: Alias: ``github`` - -**Description** - -Command to interact with this cog. - -All commands are owner only. - -.. _github-command-gh-addlabels: - -"""""""""""" -gh addlabels -"""""""""""" - -**Syntax** - -.. code-block:: none - - [p]gh addlabels <issue> - -.. tip:: Alias: ``gh addlabel`` - -**Description** - -Interactive command to add labels to an issue or PR. - -.. _github-command-gh-close: - -"""""""" -gh close -"""""""" - -**Syntax** - -.. code-block:: none - - [p]gh close <issue> - -**Description** - -Close an issue or PR. - -.. _github-command-gh-comment: - -"""""""""" -gh comment -"""""""""" - -**Syntax** - -.. code-block:: none - - [p]gh comment <issue> <text> - -**Description** - -Comment on an issue or PR. - -.. _github-command-gh-commentclose: - -""""""""""""""" -gh commentclose -""""""""""""""" - -**Syntax** - -.. code-block:: none - - [p]gh commentclose <issue> <text> - -**Description** - -Comment on, then close, an issue or PR. - -.. _github-command-gh-howtoken: - -""""""""""" -gh howtoken -""""""""""" - -**Syntax** - -.. code-block:: none - - [p]gh howtoken - -**Description** - -Instructions on how to set up a token. - -.. _github-command-gh-open: - -""""""" -gh open -""""""" - -**Syntax** - -.. code-block:: none - - [p]gh open <title> - -**Description** - -Open a new issue. Does NOT reopen. - -.. _github-command-gh-removelabels: - -""""""""""""""" -gh removelabels -""""""""""""""" - -**Syntax** - -.. code-block:: none - - [p]gh removelabels <issue> - -.. tip:: Alias: ``gh removelabel`` - -**Description** - -Interactive command to remove labels from an issue or PR. - -.. _github-command-gh-setrepo: - -"""""""""" -gh setrepo -"""""""""" - -**Syntax** - -.. code-block:: none - - [p]gh setrepo <slug> - -**Description** - -Set up a repo to use as a slug (``USERNAME/REPO``). +This cog has been removed in favour of my :ref:`GHIssues <ghissues>` cog. diff --git a/docs/cogs/madtranslate.rst b/docs/cogs/madtranslate.rst index 161b0d0b..2c3d6732 100644 --- a/docs/cogs/madtranslate.rst +++ b/docs/cogs/madtranslate.rst @@ -51,8 +51,8 @@ madtranslate Translate something into lots of languages, then back to English! **Examples:** - - ``[p]mtrans This is a sentence.`` - - ``[p]mtrans 25 Here's another one.`` +- ``[p]mtrans This is a sentence.`` +- ``[p]mtrans 25 Here's another one.`` At the bottom of the output embed is a count-seed pair. You can use this with the ``mtransseed`` command to use the same language set. @@ -78,5 +78,5 @@ They may be unreproducible if Google Translate changes its translations. The count-seed pair is obtained from the main command, ``mtrans``, in the embed footer. **Examples:** - - ``[p]mtrans 15-111111 This is a sentence.`` - - ``[p]mtrans 25-000000 Here's another one.`` +- ``[p]mtrans 15-111111 This is a sentence.`` +- ``[p]mtrans 25-000000 Here's another one.`` diff --git a/docs/cogs/roleplay.rst b/docs/cogs/roleplay.rst index 1d8b2b92..986ed790 100644 --- a/docs/cogs/roleplay.rst +++ b/docs/cogs/roleplay.rst @@ -73,8 +73,8 @@ Set the channel for the roleplay. Leave blank to disable. **Examples:** - - ``[p]roleplay channel`` - disable roleplay - - ``[p]roleplay channel #roleplay`` - set the channel to #roleplay +- ``[p]roleplay channel`` - disable roleplay +- ``[p]roleplay channel #roleplay`` - set the channel to #roleplay .. _roleplay-command-roleplay-delete: @@ -97,10 +97,9 @@ The time is in minutes. The default is disabled. **Examples:** - - - ``[p]roleplay delete 5`` - delete after 5 mins - - ``[p]roleplay delete 30`` - delete after 30 mins - - ``[p]roleplay delete`` - disable deletion +- ``[p]roleplay delete 5`` - delete after 5 mins +- ``[p]roleplay delete 30`` - delete after 30 mins +- ``[p]roleplay delete`` - disable deletion .. _roleplay-command-roleplay-embed: @@ -121,8 +120,8 @@ Enable or disable embeds. The default is disabled. **Examples:** - - ``[p]roleplay embed true`` - enable - - ``[p]roleplay embed false`` - disable +- ``[p]roleplay embed true`` - enable +- ``[p]roleplay embed false`` - disable .. _roleplay-command-roleplay-log: @@ -143,8 +142,8 @@ Set a channel to log role play messages to. If you do not specify a channel logging will be disabled. **Examples:** - - ``[p]roleplay log #logs`` - set to a channel called logs - - ``[p]roleplay log`` - disable logging +- ``[p]roleplay log #logs`` - set to a channel called logs +- ``[p]roleplay log`` - disable logging .. _roleplay-command-roleplay-radio: @@ -165,8 +164,8 @@ Enable or disable radio. The default is disabled. **Examples:** - - ``[p]roleplay radio true`` - enable radio mode - - ``[p]roleplay radio false`` - disable radio mode +- ``[p]roleplay radio true`` - enable radio mode +- ``[p]roleplay radio false`` - disable radio mode .. _roleplay-command-roleplay-radiofooter: @@ -187,9 +186,8 @@ Set a footer for radio mode (embed only) This only applies to embeds. **Example:** - - - ``[p]roleplay radiofooter Transmission over`` - - ``[p]roleplay radiofooter`` - reset to none +- ``[p]roleplay radiofooter Transmission over`` +- ``[p]roleplay radiofooter`` - reset to none .. _roleplay-command-roleplay-radioimage: @@ -210,9 +208,8 @@ Set an image for radio mode (embed only) This only applies to embeds. **Example:** - - - ``[p]roleplay radioimage https://i.imgur.com/example.png`` - - ``[p]roleplay radioimage`` - reset to none +- ``[p]roleplay radioimage https://i.imgur.com/example.png`` +- ``[p]roleplay radioimage`` - reset to none .. _roleplay-command-roleplay-radiotitle: @@ -233,8 +230,7 @@ Set a title for radio mode (embed only) This only applies to embeds. **Example:** - - - ``[p]roleplay radiotitle New radio transmission detected`` - the default +- ``[p]roleplay radiotitle New radio transmission detected`` - the default .. _roleplay-command-roleplay-settings: diff --git a/docs/cogs/stattrack.rst b/docs/cogs/stattrack.rst index 698bc6d9..3e23fdd3 100644 --- a/docs/cogs/stattrack.rst +++ b/docs/cogs/stattrack.rst @@ -98,9 +98,9 @@ Red, while ``unique`` will only count them once. **Examples:** **Examples:** - - ``[p]stattrack servers 3w2d`` - - ``[p]stattrack servers 5d`` - - ``[p]stattrack servers all`` +- ``[p]stattrack servers 3w2d`` +- ``[p]stattrack servers 5d`` +- ``[p]stattrack servers all`` .. _stattrack-command-stattrack-commands: @@ -124,9 +124,9 @@ Get command usage stats. at least 1 hour. **Examples:** - - ``[p]stattrack commands 3w2d`` - - ``[p]stattrack commands 5d`` - - ``[p]stattrack commands all`` +- ``[p]stattrack commands 3w2d`` +- ``[p]stattrack commands 5d`` +- ``[p]stattrack commands all`` .. _stattrack-command-stattrack-export: @@ -202,9 +202,9 @@ Get my latency stats. at least 1 hour. **Examples:** - - ``[p]stattrack latency 3w2d`` - - ``[p]stattrack latency 5d`` - - ``[p]stattrack latency all`` +- ``[p]stattrack latency 3w2d`` +- ``[p]stattrack latency 5d`` +- ``[p]stattrack latency all`` .. _stattrack-command-stattrack-looptime: @@ -230,9 +230,9 @@ Get my loop time stats. at least 1 hour. **Examples:** - - ``[p]stattrack looptime 3w2d`` - - ``[p]stattrack looptime 5d`` - - ``[p]stattrack looptime all`` +- ``[p]stattrack looptime 3w2d`` +- ``[p]stattrack looptime 5d`` +- ``[p]stattrack looptime all`` .. _stattrack-command-stattrack-maxpoints: @@ -263,12 +263,11 @@ Set maxpoints to -1 to disable this feature, therefore always plotting all point Otherwise, maxpoints must be at least 1k (1440). **Examples:** - - - ``[p]stattrack maxpoints 10000`` - plot up to 10k points - - ``[p]stattrack maxpoints 75000`` - plot up to 75k points - - ``[p]stattrack maxpoints 1440`` - the minimum value possible - - ``[p]stattrack maxpoints 25000`` - the default value - - ``[p]stattrack maxpoints -1`` - disable, always plot all points +- ``[p]stattrack maxpoints 10000`` - plot up to 10k points +- ``[p]stattrack maxpoints 75000`` - plot up to 75k points +- ``[p]stattrack maxpoints 1440`` - the minimum value possible +- ``[p]stattrack maxpoints 25000`` - the default value +- ``[p]stattrack maxpoints -1`` - disable, always plot all points .. _stattrack-command-stattrack-messages: @@ -292,9 +291,9 @@ Get message stats. at least 1 hour. **Examples:** - - ``[p]stattrack messages 3w2d`` - - ``[p]stattrack messages 5d`` - - ``[p]stattrack messages all`` +- ``[p]stattrack messages 3w2d`` +- ``[p]stattrack messages 5d`` +- ``[p]stattrack messages all`` .. _stattrack-command-stattrack-servers: @@ -320,9 +319,9 @@ Get server stats. at least 1 hour. **Examples:** - - ``[p]stattrack servers 3w2d`` - - ``[p]stattrack servers 5d`` - - ``[p]stattrack servers all`` +- ``[p]stattrack servers 3w2d`` +- ``[p]stattrack servers 5d`` +- ``[p]stattrack servers all`` .. _stattrack-command-stattrack-status: @@ -352,10 +351,10 @@ at least 1 hour. Defaults to all of them. **Examples:** - - ``[p]stattrack status`` - show all metrics, 1 day - - ``[p]stattrack status 3w2d`` - show all metrics, 3 weeks 2 days - - ``[p]stattrack status 5d dnd online`` - show dnd & online, 5 days - - ``[p]stattrack status all online idle`` - show online & idle, all time +- ``[p]stattrack status`` - show all metrics, 1 day +- ``[p]stattrack status 3w2d`` - show all metrics, 3 weeks 2 days +- ``[p]stattrack status 5d dnd online`` - show dnd & online, 5 days +- ``[p]stattrack status all online idle`` - show online & idle, all time .. _stattrack-command-stattrack-system: @@ -397,9 +396,9 @@ Get CPU stats. at least 1 hour. **Examples:** - - ``[p]stattrack system cpu 3w2d`` - - ``[p]stattrack system cpu 5d`` - - ``[p]stattrack system cpu all`` +- ``[p]stattrack system cpu 3w2d`` +- ``[p]stattrack system cpu 5d`` +- ``[p]stattrack system cpu all`` .. _stattrack-command-stattrack-system-mem: @@ -425,9 +424,9 @@ Get memory usage stats. at least 1 hour. **Examples:** - - ``[p]stattrack system mem 3w2d`` - - ``[p]stattrack system mem 5d`` - - ``[p]stattrack system mem all`` +- ``[p]stattrack system mem 3w2d`` +- ``[p]stattrack system mem 5d`` +- ``[p]stattrack system mem all`` .. _stattrack-command-stattrack-users: @@ -460,7 +459,7 @@ Note that ``total`` will count users multiple times if they share multiple serve Red, while ``unique`` will only count them once. **Examples:** - - ``[p]stattrack users`` - show all metrics, 1 day - - ``[p]stattrack users 3w2d`` - show all metrics, 3 weeks 2 days - - ``[p]stattrack users 5d total unique`` - show total & unique, 5 days - - ``[p]stattrack users all humans bots`` - show humans & bots, all time +- ``[p]stattrack users`` - show all metrics, 1 day +- ``[p]stattrack users 3w2d`` - show all metrics, 3 weeks 2 days +- ``[p]stattrack users 5d total unique`` - show total & unique, 5 days +- ``[p]stattrack users all humans bots`` - show humans & bots, all time diff --git a/docs/cogs/status.rst b/docs/cogs/status.rst index 41a73f3a..822fcb4d 100644 --- a/docs/cogs/status.rst +++ b/docs/cogs/status.rst @@ -27,7 +27,7 @@ When there is one, it will send the update to all channels that have registered to recieve updates from that service. There's also the ``status`` command which anyone can use to check -updates whereever they want. +updates wherever they want. If there's a service that you want added, contact Vexed#0714 or make an issue on the GitHub repo (or even better a PR!). @@ -56,7 +56,7 @@ status Check for the status of a variety of services, eg Discord. **Example:** - - ``[p]status discord`` +- ``[p]status discord`` .. _status-command-statusset: @@ -125,8 +125,8 @@ Remove all feeds from a channel. If you don't specify a channel, I will use the current channel **Examples:** - - ``[p]statusset clear #testing`` - - ``[p]statusset clear`` (for using current channel) +- ``[p]statusset clear #testing`` +- ``[p]statusset clear`` (for using current channel) .. _status-command-statusset-edit: @@ -174,8 +174,8 @@ status channel. If you don't specify a channel, I will use the current channel. **Examples:** - - ``[p]statusset edit mode #testing discord latest`` - - ``[p]statusset edit mode discord edit`` (for current channel) +- ``[p]statusset edit mode #testing discord latest`` +- ``[p]statusset edit mode discord edit`` (for current channel) .. _status-command-statusset-edit-restrict: @@ -198,8 +198,8 @@ Enabling this will reduce spam. Instead of sending the whole update that automatically receive the status updates, that they have permission to to view. **Examples:** - - ``[p]statusset edit restrict #testing discord true`` - - ``[p]statusset edit restrict discord false`` (for current channel) +- ``[p]statusset edit restrict #testing discord true`` +- ``[p]statusset edit restrict discord false`` (for current channel) .. _status-command-statusset-edit-webhook: @@ -223,8 +223,8 @@ logo and the name will be ``[service] Status Update``, instead of my avatar and If you don't specify a channel, I will use the current channel. **Examples:** - - ``[p]statusset edit webhook #testing discord true`` - - ``[p]statusset edit webhook discord false`` (for current channel) +- ``[p]statusset edit webhook #testing discord true`` +- ``[p]statusset edit webhook discord false`` (for current channel) .. _status-command-statusset-list: @@ -248,8 +248,8 @@ Optionally add a service at the end of the command to view detailed settings for service. **Examples:** - - ``[p]statusset list discord`` - - ``[p]statusset list`` +- ``[p]statusset list discord`` +- ``[p]statusset list`` .. _status-command-statusset-preview: @@ -297,8 +297,8 @@ You can also see this at https://go.vexcodes.com/c/statusref of my avatar and name. **Examples:** - - ``[p]statusset preview discord all true`` - - ``[p]statusset preview discord latest false`` +- ``[p]statusset preview discord all true`` +- ``[p]statusset preview discord latest false`` .. _status-command-statusset-remove: @@ -321,5 +321,5 @@ Stop status updates for a specific service in this server. If you don't specify a channel, I will use the current channel. **Examples:** - - ``[p]statusset remove discord #testing`` - - ``[p]statusset remove discord`` (for using current channel) +- ``[p]statusset remove discord #testing`` +- ``[p]statusset remove discord`` (for using current channel) diff --git a/docs/cogs/system.rst b/docs/cogs/system.rst index 094960b1..4889cb24 100644 --- a/docs/cogs/system.rst +++ b/docs/cogs/system.rst @@ -134,6 +134,7 @@ sensitive if running the command a public space). If ``ignore_loop`` is set to ``True``, this will ignore any loop (fake) devices on Linux. Platforms: Windows, Linux, Mac OS + .. note:: Mount point is basically useless on Windows as it's the same as the drive name, though it's still shown. diff --git a/docs/cogs/timechannel.rst b/docs/cogs/timechannel.rst index a2039aa2..896ce082 100644 --- a/docs/cogs/timechannel.rst +++ b/docs/cogs/timechannel.rst @@ -95,10 +95,10 @@ The default is 12 hour time, but you can use ``{shortid-24h}`` for 24 hour time, eg ``{ni-24h}`` **More Examples:** - - ``[p]tcset create 🕑️ New York: {fv}`` - - ``[p]tcset create 🌐 UTC: {qw}`` - - ``[p]tcset create {ni-24h} in London`` - - ``[p]tcset create US Pacific: {qv-24h}`` +- ``[p]tcset create 🕑️ New York: {fv}`` +- ``[p]tcset create 🌐 UTC: {qw}`` +- ``[p]tcset create {ni-24h} in London`` +- ``[p]tcset create US Pacific: {qv-24h}`` .. _timechannel-command-timechannelset-remove: @@ -119,8 +119,8 @@ Delete and stop updating a channel. For the <channel> argument, you can use its ID or mention (type #!channelname) **Example:** - - ``[p]tcset remove #!channelname`` (the ! is how to mention voice channels) - - ``[p]tcset remove 834146070094282843`` +- ``[p]tcset remove #!channelname`` (the ! is how to mention voice channels) +- ``[p]tcset remove 834146070094282843`` .. _timechannel-command-timechannelset-short: @@ -146,10 +146,10 @@ There is a fuzzy search, so you shouldn't need to enter the region. Please look at ``[p]help tcset create`` for more information. **Examples:** - - ``[p]tcset short New York`` - - ``[p]tcset short UTC`` - - ``[p]tcset short London`` - - ``[p]tcset short Europe/London`` +- ``[p]tcset short New York`` +- ``[p]tcset short UTC`` +- ``[p]tcset short London`` +- ``[p]tcset short Europe/London`` .. _timechannel-command-timezones: diff --git a/docs/cogs/wol.rst b/docs/cogs/wol.rst index e27559ab..a5f5bedb 100644 --- a/docs/cogs/wol.rst +++ b/docs/cogs/wol.rst @@ -60,8 +60,8 @@ write out the MAC each time, or just send the MAC. The IP is optional and only used if you don't use the short name. **Examples:** - - ``[p]wol main_pc`` - - ``[p]wol 11:22:33:44:55:66 192.168.1.15`` +- ``[p]wol main_pc`` +- ``[p]wol 11:22:33:44:55:66 192.168.1.15`` .. _wol-command-wolset: @@ -100,8 +100,8 @@ Add a machine for easy use with ``[p]wol``. ``<friendly_name>`` **cannot** include spaces. **Examples:** - - ``wolset add main_pc 11:22:33:44:55:66`` - - ``wolset add main_pc 11-22-33-44-55-66 192.168.1.15`` +- ``wolset add main_pc 11:22:33:44:55:66`` +- ``wolset add main_pc 11-22-33-44-55-66 192.168.1.15`` .. _wol-command-wolset-list: @@ -140,4 +140,4 @@ wolset remove Remove a machine from my list of machines. **Examples:** - - ``wolset remove main_pc`` +- ``wolset remove main_pc`` diff --git a/docs/index.rst b/docs/index.rst index 2ddd31d8..f52ea5c8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -29,11 +29,12 @@ in #support_vex-cogs. cogs/beautify cogs/betteruptime cogs/birthday + cogs/betteruptime + cogs/calc cogs/caseinsensitive cogs/cmdlog - cogs/covidgraph cogs/fivemstatus - cogs/github + cogs/ghissues cogs/googletrends cogs/madtranslate cogs/roleplay diff --git a/docs/telemetry.rst b/docs/telemetry.rst deleted file mode 100644 index 7ccb5d15..00000000 --- a/docs/telemetry.rst +++ /dev/null @@ -1,8 +0,0 @@ -.. _telemetry: - -Opt-in Telemetry and Error Reporting -==================================== - -Telemetry was removed on the 23rd October 2021 in `pull request #53 <https://github.com/Vexed01/Vex-Cogs/pull/53>`_ for multiple reasons. - -You can view the previous version of this page at https://cogdocs.vexcodes.com/en/archive-telemetry/telemetry.html diff --git a/fivemstatus/fivemstatus.py b/fivemstatus/fivemstatus.py index 856f0070..79128b55 100644 --- a/fivemstatus/fivemstatus.py +++ b/fivemstatus/fivemstatus.py @@ -44,9 +44,9 @@ async def red_delete_data_for_user(self, **kwargs) -> None: """Nothing to delete""" return - def cog_unload(self) -> None: + async def cog_unload(self) -> None: self.loop.cancel() - log.debug("Loop stopped as cog unloaded.") + log.verbose("Loop stopped as cog unloaded.") @commands.command(hidden=True) async def fivemstatusinfo(self, ctx: commands.Context): @@ -76,7 +76,8 @@ async def get_data(self, original_server: str) -> ServerData: ) player_count = players.count('"endpoint":') # i know this is stupid but from my # testing many servers have invalid players.json files on random occurrences. - except aiohttp.ClientError: + except aiohttp.ClientError as e: + log.trace("error getting data", exc_info=e) raise ServerUnreachable(f"Server at {url} is unreachable.") # strip colour data @@ -93,12 +94,14 @@ async def get_data(self, original_server: str) -> ServerData: if name == "": name = "FiveM Server" - return ServerData( + final = ServerData( current_users=player_count, max_users=info["vars"]["sv_maxClients"], name=name, ip=original_server.lstrip("http://").lstrip("https://").rstrip("/"), ) + log.trace("got data for %s: %s", server, final) + return final async def generate_embed( self, data: ServerData | None, config_data: MessageData, maintenance: bool diff --git a/fivemstatus/info.json b/fivemstatus/info.json index b2300579..603b055c 100644 --- a/fivemstatus/info.json +++ b/fivemstatus/info.json @@ -6,8 +6,7 @@ "description": "View the live status of a FiveM server, in an auto updating Discord message.", "end_user_data_statement": "This cog does not persistently store data or metadata about users.", "install_msg": "Thanks for installing! You can get started with the `fivemstatus setup` command, to send the message and start automatic updates.\n\nThis cog has docs! Check them out at <https://go.vexcodes.com/c/fivemstatus>", - "max_bot_version": "3.6.0.dev0", - "min_bot_version": "3.4.0", + "min_bot_version": "3.5.1", "min_python_version": [ 3, 8, diff --git a/fivemstatus/loop.py b/fivemstatus/loop.py index 933747d8..bdfc91c7 100644 --- a/fivemstatus/loop.py +++ b/fivemstatus/loop.py @@ -19,15 +19,16 @@ def __init__(self) -> None: async def fivemstatus_loop(self) -> NoReturn: await self.bot.wait_until_red_ready() await asyncio.sleep(1) - _log.debug("FiveMStatus loop has started.") while True: try: + _log.verbose("FiveMStatus l oop has started.") self.loop_meta.iter_start() await self.update_messages() self.loop_meta.iter_finish() - _log.debug("FiveMStatus iteration finished") + _log.verbose("FiveMStatus iteration finished") except Exception as e: + self.loop_meta.iter_error(e) _log.exception( "Something went wrong in the FiveMStatus loop. Some channels may have been " "missed. The loop will run again at the next hour.", diff --git a/fivemstatus/vexutils/meta.py b/fivemstatus/vexutils/meta.py index cae0ae73..1e963e4c 100644 --- a/fivemstatus/vexutils/meta.py +++ b/fivemstatus/vexutils/meta.py @@ -2,11 +2,12 @@ import asyncio import json -from logging import Logger, getLogger from pathlib import Path from typing import Literal, NamedTuple import aiohttp +from red_commons.logging import RedTraceLogger +from red_commons.logging import getLogger as red_get_logger from redbot.core import VersionInfo, commands from redbot.core import version_info as cur_red_version from rich import box as rich_box @@ -16,13 +17,13 @@ from .consts import DOCS_BASE, GREEN_CIRCLE, RED_CIRCLE from .loop import VexLoop -log = getLogger("red.vex-utils") +log = red_get_logger("red.vex-utils") cog_ver_lock = asyncio.Lock() -def get_vex_logger(name: str) -> Logger: +def get_vex_logger(name: str) -> RedTraceLogger: """Get a logger for the given name. Parameters @@ -42,7 +43,7 @@ def get_vex_logger(name: str) -> Logger: else: # otherwise use full path final_name += name - return getLogger(final_name) + return red_get_logger(final_name) def format_help(self: commands.Cog, ctx: commands.Context) -> str: diff --git a/ghissues/__init__.py b/ghissues/__init__.py index 1f745e57..abfa6e4b 100644 --- a/ghissues/__init__.py +++ b/ghissues/__init__.py @@ -23,8 +23,5 @@ async def setup(bot: Red) -> None: cog = GHIssues(bot) - await cog.async_init() await out_of_date_check("ghissues", cog.__version__) - r = bot.add_cog(cog) - if r is not None: - await r + await bot.add_cog(cog) diff --git a/ghissues/ghissues.py b/ghissues/ghissues.py index 6b5b311e..d3fb361d 100644 --- a/ghissues/ghissues.py +++ b/ghissues/ghissues.py @@ -56,7 +56,7 @@ def __init__(self, bot: Red) -> None: self.setup = False - async def async_init(self) -> None: + async def cog_load(self) -> None: token = (await self.bot.get_shared_api_tokens("github")).get("token", "") repo = await self.config.repo() diff --git a/ghissues/info.json b/ghissues/info.json index 27cc9435..23e075c3 100644 --- a/ghissues/info.json +++ b/ghissues/info.json @@ -3,12 +3,10 @@ "author": [ "Vexed (Vexed#0714)" ], - "description": "Create, comment, labelify and close GitHub issues, with partial PR support.\n\n\u26a0 This cog requires Red 3.5/discord.py 2, which is unstable and incompatibe with some other cogs. \u26a0", + "description": "Create, comment, labelify and close GitHub issues, with partial PR support.\n\n", "end_user_data_statement": "This cog does not persistently store data or metadata about users.", - "hidden": true, - "install_msg": "\\N{WARNING SIGN}\\N{VARIATION SELECTOR-16} This cog requires Red 3.5/discord.py 2, which is unstable and incompatibe with some other cogs. \\N{WARNING SIGN}\\N{VARIATION SELECTOR-16}", - "max_bot_version": "3.6.0.dev0", - "min_bot_version": "3.4.0", + "install_msg": "Thanks for installing! Load the cog and get started with the `calculator` command.\n\nThis cog has docs! Check them out at <https://go.vexcodes.com/c/ghissues>", + "min_bot_version": "3.5.1", "min_python_version": [ 3, 8, @@ -18,7 +16,7 @@ "requirements": [ "gidgethub>=5.0.0" ], - "short": "[DPY 2 ONLY] Create, comment, labelify and close GitHub issues.", + "short": "Create, comment, labelify and close GitHub issues.", "tags": [ "utility", "github", diff --git a/ghissues/vexutils/meta.py b/ghissues/vexutils/meta.py index cae0ae73..1e963e4c 100644 --- a/ghissues/vexutils/meta.py +++ b/ghissues/vexutils/meta.py @@ -2,11 +2,12 @@ import asyncio import json -from logging import Logger, getLogger from pathlib import Path from typing import Literal, NamedTuple import aiohttp +from red_commons.logging import RedTraceLogger +from red_commons.logging import getLogger as red_get_logger from redbot.core import VersionInfo, commands from redbot.core import version_info as cur_red_version from rich import box as rich_box @@ -16,13 +17,13 @@ from .consts import DOCS_BASE, GREEN_CIRCLE, RED_CIRCLE from .loop import VexLoop -log = getLogger("red.vex-utils") +log = red_get_logger("red.vex-utils") cog_ver_lock = asyncio.Lock() -def get_vex_logger(name: str) -> Logger: +def get_vex_logger(name: str) -> RedTraceLogger: """Get a logger for the given name. Parameters @@ -42,7 +43,7 @@ def get_vex_logger(name: str) -> Logger: else: # otherwise use full path final_name += name - return getLogger(final_name) + return red_get_logger(final_name) def format_help(self: commands.Cog, ctx: commands.Context) -> str: diff --git a/ghissues/views/label.py b/ghissues/views/label.py index bebd3b17..ae0b9b17 100644 --- a/ghissues/views/label.py +++ b/ghissues/views/label.py @@ -51,7 +51,7 @@ async def regen(self, page: int, interaction: Interaction): ) @button(emoji="◀", style=ButtonStyle.blurple, row=4) - async def page_left(self, button: Button, interaction: Interaction): + async def page_left(self, interaction: Interaction, button: Button): # pages start from 0 if self.page == 0: return @@ -60,7 +60,7 @@ async def page_left(self, button: Button, interaction: Interaction): await self.regen(self.page, interaction) @button(emoji="▶", style=ButtonStyle.blurple, row=4) - async def page_right(self, button: Button, interaction: Interaction): + async def page_right(self, interaction: Interaction, button: Button): max_pages = len(list(get_menu_sets(self.raw_labels))) # pages start from 0 if self.page + 1 >= max_pages: diff --git a/ghissues/views/master.py b/ghissues/views/master.py index f01dfba1..e6d8b604 100644 --- a/ghissues/views/master.py +++ b/ghissues/views/master.py @@ -72,7 +72,7 @@ async def regen_viw(self): await self.master_msg.edit(embed=embed, view=self) @button(label="Manage labels", style=ButtonStyle.grey) - async def btn_add_label(self, button: Button, interaction: Interaction): + async def btn_add_label(self, interaction: Interaction, button: Button): repo_labels = await self.api.get_repo_labels() issue_labels = await self.api.get_issue_labels(self.issue_data["number"]) @@ -87,7 +87,7 @@ async def btn_add_label(self, button: Button, interaction: Interaction): ) @button(label="Close issue", style=ButtonStyle.red) - async def btn_close(self, button: Button, interaction: Interaction): + async def btn_close(self, interaction: Interaction, button: Button): await self.api.close(self.issue_data["number"]) button.disabled = True self.btn_open.disabled = False @@ -96,7 +96,7 @@ async def btn_close(self, button: Button, interaction: Interaction): await self.regen_viw() @button(label="Reopen issue", style=ButtonStyle.green) - async def btn_open(self, button: Button, interaction: Interaction): + async def btn_open(self, interaction: Interaction, button: Button): await self.api.open(self.issue_data["number"]) button.disabled = True self.btn_close.disabled = False @@ -105,13 +105,13 @@ async def btn_open(self, button: Button, interaction: Interaction): await self.regen_viw() @button(label="Merge", style=ButtonStyle.blurple) - async def btn_merge(self, button: Button, interaction: Interaction): + async def btn_merge(self, interaction: Interaction, button: Button): await interaction.response.send_message( "Please choose the merge method. You'll be able to choose a commit message later.", view=MergeView(self), ) @button(emoji="❌", row=1, style=ButtonStyle.grey) - async def btn_del(self, button: Button, interaction: Interaction): + async def btn_del(self, interaction: Interaction, button: Button): assert self.master_msg is not None await self.master_msg.delete() diff --git a/ghissues/views/merge.py b/ghissues/views/merge.py index ddf9bb29..877e7088 100644 --- a/ghissues/views/merge.py +++ b/ghissues/views/merge.py @@ -72,21 +72,21 @@ def check(m: Message): ) @button(label="Merge") - async def btn_merge(self, button: Button, interaction: Interaction): + async def btn_merge(self, interaction: Interaction, button: Button): button.style = ButtonStyle.green await interaction.response.edit_message(view=self) self.stop() await self.get_commit_and_confirm("merge") @button(label="Squash") - async def btn_squash(self, button: Button, interaction: Interaction): + async def btn_squash(self, interaction: Interaction, button: Button): button.style = ButtonStyle.green await interaction.response.edit_message(view=self) self.stop() await self.get_commit_and_confirm("squash") @button(label="Rebase") - async def btn_rebase(self, button: Button, interaction: Interaction): + async def btn_rebase(self, interaction: Interaction, button: Button): button.style = ButtonStyle.green await interaction.response.edit_message(view=self) self.stop() diff --git a/ghissues/views/merge_confirm.py b/ghissues/views/merge_confirm.py index e86fc271..079bcb7e 100644 --- a/ghissues/views/merge_confirm.py +++ b/ghissues/views/merge_confirm.py @@ -37,7 +37,7 @@ async def interaction_check(self, interaction: Interaction) -> bool: return False @button(label="Confirm merge", style=ButtonStyle.green) - async def btn_confirm(self, button: Button, interaction: Interaction): + async def btn_confirm(self, interaction: Interaction, button: Button): self.stop() try: await self.master.api.merge( @@ -56,6 +56,6 @@ async def btn_confirm(self, button: Button, interaction: Interaction): await interaction.response.send_message("Pull request merged.") @button(label="Cancel merge", style=ButtonStyle.red) - async def btn_cancel(self, button: Button, interaction: Interaction): + async def btn_cancel(self, interaction: Interaction, button: Button): self.stop() await interaction.response.send_message("Merge cancelled.") diff --git a/github/__init__.py b/github/__init__.py index 1b793956..46f8f757 100644 --- a/github/__init__.py +++ b/github/__init__.py @@ -1,22 +1,9 @@ -import contextlib -import importlib -import json -from pathlib import Path - -from redbot.core import VersionInfo from redbot.core.bot import Red - -from . import vexutils -from .github import GitHub -from .vexutils.meta import out_of_date_check - -with open(Path(__file__).parent / "info.json") as fp: - __red_end_user_data_statement__ = json.load(fp)["end_user_data_statement"] +from redbot.core.errors import CogLoadError async def setup(bot: Red) -> None: - cog = GitHub(bot) - await out_of_date_check("github", cog.__version__) - r = bot.add_cog(cog) - if r is not None: - await r + raise CogLoadError( + "This cog has been replaced by my `ghissues` cog. Please uninstall this cog and install " + "that instead." + ) diff --git a/github/api.py b/github/api.py deleted file mode 100644 index d1ddc75c..00000000 --- a/github/api.py +++ /dev/null @@ -1,68 +0,0 @@ -from __future__ import annotations - -import aiohttp -import gidgethub.aiohttp - -# cspell:ignore resp - - -class GitHubAPI: - @staticmethod - async def repo_info(token: str, slug: str) -> dict | bool: - async with aiohttp.ClientSession() as session: - gh = gidgethub.aiohttp.GitHubAPI(session, "GHCog", oauth_token=token) - try: - resp = await gh.getitem(f"/repos/{slug}") - return resp - except Exception: - return False - - @staticmethod - async def get_issue(token: str, repo: str, issue: int) -> dict: - async with aiohttp.ClientSession() as session: - gh = gidgethub.aiohttp.GitHubAPI(session, "GHCog", oauth_token=token) - return await gh.getitem(f"/repos/{repo}/issues/{issue}") - - @staticmethod - async def get_repo_labels(token: str, repo: str) -> dict: - async with aiohttp.ClientSession() as session: - gh = gidgethub.aiohttp.GitHubAPI(session, "GHCog", oauth_token=token) - return await gh.getitem(f"/repos/{repo}/labels") - - @staticmethod - async def get_issue_labels(token: str, repo: str, issue: int) -> dict: - async with aiohttp.ClientSession() as session: - gh = gidgethub.aiohttp.GitHubAPI(session, "GHCog", oauth_token=token) - return await gh.getitem(f"/repos/{repo}/issues/{issue}/labels") - - @staticmethod - async def add_labels(token: str, repo: str, issue: int, labels: list) -> dict: - async with aiohttp.ClientSession() as session: - gh = gidgethub.aiohttp.GitHubAPI(session, "GHCog", oauth_token=token) - return await gh.post(f"/repos/{repo}/issues/{issue}/labels", data={"labels": labels}) - - @staticmethod - async def remove_label(token: str, repo: str, issue: int, label: str) -> None: - async with aiohttp.ClientSession() as session: - gh = gidgethub.aiohttp.GitHubAPI(session, "GHCog", oauth_token=token) - return await gh.delete(f"/repos/{repo}/issues/{issue}/labels/{label}") - - @staticmethod - async def create_issue(token: str, repo: str, title: str, body: str, labels: list) -> dict: - async with aiohttp.ClientSession() as session: - gh = gidgethub.aiohttp.GitHubAPI(session, "GHCog", oauth_token=token) - return await gh.post( - f"/repos/{repo}/issues", data={"title": title, "body": body, "labels": labels} - ) - - @staticmethod - async def comment(token: str, repo: str, issue: int, body: str) -> dict: - async with aiohttp.ClientSession() as session: - gh = gidgethub.aiohttp.GitHubAPI(session, "GHCog", oauth_token=token) - return await gh.post(f"/repos/{repo}/issues/{issue}/comments", data={"body": body}) - - @staticmethod - async def close(token: str, repo: str, issue: int) -> dict: - async with aiohttp.ClientSession() as session: - gh = gidgethub.aiohttp.GitHubAPI(session, "GHCog", oauth_token=token) - return await gh.post(f"/repos/{repo}/issues/{issue}", data={"state": "closed"}) diff --git a/github/consts.py b/github/consts.py deleted file mode 100644 index e5234d6c..00000000 --- a/github/consts.py +++ /dev/null @@ -1,19 +0,0 @@ -from gidgethub import HTTPException - -from .errors import CustomError - -GET_ISSUE = "get_issue" -GET_REPO_LABELS = "get_repo_labels" -GET_ISSUE_LABELS = "get_issue_labels" - -ADD_LABEL = "add_label" -REMOVE_LABEL = "remove_label" - -CREATE_ISSUE = "create_issue" -COMMENT = "comment" -CLOSE = "close" -CHECK_REAL = "check_real" - -EXCEPTIONS = (HTTPException, CustomError) - -CROSS = "\N{CROSS MARK}" diff --git a/github/errors.py b/github/errors.py deleted file mode 100644 index c8e88805..00000000 --- a/github/errors.py +++ /dev/null @@ -1,2 +0,0 @@ -class CustomError(Exception): - pass diff --git a/github/github.py b/github/github.py deleted file mode 100644 index 26bd7905..00000000 --- a/github/github.py +++ /dev/null @@ -1,430 +0,0 @@ -from asyncio import TimeoutError -from typing import Mapping - -import discord -from gidgethub import HTTPException -from redbot.core import Config, commands -from redbot.core.bot import Red -from redbot.core.utils.predicates import MessagePredicate - -from .api import GitHubAPI -from .consts import CROSS, EXCEPTIONS -from .errors import CustomError -from .vexutils import format_help, format_info, get_vex_logger, inline_hum_list - -# cspell:ignore labelify kowlin's resp - -log = get_vex_logger(__name__) - - -class GitHub(commands.Cog): - """ - Create, comment, labelify and close GitHub issues. - - This cog is only for bot owners. - I made it for managing issues on my cog repo as a small project, - but it certainly could be used for other situations where you want - to manage GitHub issues from Discord. - - If you would like a way to search or view issues, I highly recommend - Kowlin's approved `githubcards` cog (on the repo - https://github.com/Kowlin/Sentinel) - - At present, this cannot support multiple repos. - - PRs are mostly supported. You can comment on them or close them - but not merge them or create them. - - Get started with the `gh howtoken` command to set your GitHub token. - You don't have to do this if you have already set it for a different - cog, eg `githubcards`. Then set up with `gh setrepo`. - """ - - __version__ = "1.0.1" - __author__ = "Vexed#0714" - - def __init__(self, bot: Red) -> None: - self.bot = bot - - self.config: Config = Config.get_conf( - self, identifier=418078199982063626, force_registration=True - ) - self.config.register_global(repo=None) - - self.repo = "" - self.token = "" - - def format_help_for_context(self, ctx: commands.Context) -> str: - """Thanks Sinbad.""" - return format_help(self, ctx) - - async def red_delete_data_for_user(self, **kwargs) -> None: - """Nothing to delete""" - return - - async def _handle_error(self, ctx: commands.Context, error: Exception) -> None: - if isinstance(error, HTTPException): - if error.status_code == 404: - await ctx.send("It looks like that isn't a valid issue or PR number.") - else: - await ctx.send(f"HTTP error occurred: `{error.status_code}`") - - elif not isinstance(error, CustomError): - raise error - - async def _get_repo(self, ctx: commands.Context) -> str: - """Get the repo. Return immediately on CustomError.""" - if self.repo: - return self.repo - repo = await self.config.repo() - if not repo: - await ctx.send("The bot owner must decide what repo to use (`gh setrepo`).") - raise CustomError - self.repo = repo - return repo - - async def _get_token(self, ctx: commands.Context) -> str: - """Get the token. Return immediately on CustomError.""" - if self.token: - return self.token - token = (await self.bot.get_shared_api_tokens("github")).get("token") - if not token: - await ctx.send("The bot owner must set the token (`gh howtoken`).") - raise CustomError - self.token = token - return token - - @commands.Cog.listener() - async def on_red_api_tokens_update(self, service_name: str, api_tokens: Mapping[str, str]): - if service_name != "github": - return - - self.token = api_tokens.get("api_key", "") # want it to directly reflect shared thingy - - @commands.command(hidden=True) - async def githubinfo(self, ctx: commands.Context): - extras = {"Token": bool(self.token), "Repo": bool(self.repo)} - main = await format_info( - ctx, self.qualified_name, self.__version__, extras=extras # type:ignore - ) - - if not (self.token and self.repo): - extra = ( - f"\nIt is expected these are `{CROSS}` if no commands have been used since " - "the cog was last loaded." - ) - else: - extra = "" - - await ctx.send(f"{main}{extra}") - - @commands.group(aliases=["github"]) - @commands.is_owner() - async def gh(self, ctx): - """ - Command to interact with this cog. - - All commands are owner only. - """ - - @gh.command() - async def howtoken(self, ctx: commands.Context): - """Instructions on how to set up a token.""" - p = ctx.clean_prefix - await ctx.send( - "Note: if you have already set up a GH API token with your bot (eg for `githubcards`) " - "then this cog will already work.\n\n" - "1. Create a new token at <https://github.com/settings/tokens> and tick the `repo` " - "option at the top.\n" - "2. Copy the token and, in my DMs, run this command: " - f"`{p}set api github token PUTYOURTOKENHERE`\n" - f"3. Set up a repo with `{p}gh setrepo`" - ) - - @gh.command() - async def setrepo(self, ctx: commands.Context, slug: str): - """Set up a repo to use as a slug (`USERNAME/REPO`).""" - try: - await GitHubAPI.repo_info(await self._get_token(ctx), slug) - except HTTPException: - return await ctx.send( - "That looks like a invalid slug or a private repo my token doesn't let me view." - ) - except CustomError: - return - - self.repo = slug - await self.config.repo.set(slug) - await ctx.send(f"Set the repo to use as `{slug}`") - - @gh.command() - async def comment(self, ctx: commands.Context, issue: int, *, text: str): - """Comment on an issue or PR.""" - try: - repo = await self._get_repo(ctx) - token = await self._get_token(ctx) - await GitHubAPI.comment(token, repo, issue, text) - issue_info = await GitHubAPI.get_issue(token, repo, issue) - except EXCEPTIONS as e: - return await self._handle_error(ctx, e) - - await ctx.send( - "Added comment to issue `{}` by `{}`".format( - issue_info.get("title"), issue_info.get("user", {}).get("login") - ) - ) - - @gh.command() - async def close(self, ctx: commands.Context, issue: int): - """Close an issue or PR.""" - try: - repo = await self._get_repo(ctx) - token = await self._get_token(ctx) - await GitHubAPI.close(token, repo, issue) - issue_info = await GitHubAPI.get_issue(token, repo, issue) - except EXCEPTIONS as e: - return await self._handle_error(ctx, e) - await ctx.send( - "Closed `{}` by `{}`".format( - issue_info.get("title"), issue_info.get("user", {}).get("login") - ) - ) - - @gh.command() - async def commentclose(self, ctx: commands.Context, issue: int, *, text: str): - """Comment on, then close, an issue or PR.""" - try: - repo = await self._get_repo(ctx) - token = await self._get_token(ctx) - await GitHubAPI.comment(token, repo, issue, text) - await GitHubAPI.close(token, repo, issue) - issue_info = await GitHubAPI.get_issue(token, repo, issue) - except EXCEPTIONS as e: - return await self._handle_error(ctx, e) - await ctx.send( - "Commented on and closed issue `{}` by `{}`".format( - issue_info.get("title"), issue_info.get("user", {}).get("login") - ) - ) - - @gh.command(aliases=["addlabel"]) - async def addlabels(self, ctx: commands.Context, issue: int): - """Interactive command to add labels to an issue or PR.""" - try: - token = await self._get_token(ctx) - repo = await self._get_repo(ctx) - repo_labels = await GitHubAPI.get_repo_labels(token, repo) - issue_labels = await GitHubAPI.get_issue_labels(token, repo, issue) - except EXCEPTIONS as e: - return await self._handle_error(ctx, e) - - rl_names = [] - for label in repo_labels: - rl_names.append(label["name"]) - - il_names = [] - for label in issue_labels: - il_names.append(label["name"]) - - avaliable_labels = inline_hum_list([label for label in rl_names if label not in il_names]) - used_labels = inline_hum_list(il_names) - await ctx.send( - "You have 30 seconds, please say what label you want to add. Any invalid input will " - "be ignored. This is case sensitive.\n\n" - f"Available labels: {avaliable_labels}\nLabels currently on issue: {used_labels}" - ) - - def check(msg): - return ( - msg.author == ctx.author - and msg.channel == ctx.channel - and (msg.content in rl_names or msg.content.casefold() in ["save", "exit"]) - ) - - to_add: list[str] = [] - while True: - try: - answer: discord.Message = await self.bot.wait_for( - "message", check=check, timeout=30.0 - ) - except TimeoutError: - return await ctx.send("Timeout. No changes were saved.") - if answer.content.casefold() == "save": - break - elif answer.content.casefold() == "exit": - to_add = [] - break - elif answer.content in il_names: - await ctx.send( - "It looks like that label's already on the issue. Choose another, 30 seconds." - ) - continue - to_add.append(answer.content) - il_names.append(answer.content) - rl_names.remove(answer.content) - - avaliable_labels = inline_hum_list( - [label for label in rl_names if label not in il_names] - ) - used_labels = inline_hum_list(il_names) - await ctx.send( - "Label added. Again, 30 seconds. Say another label name if you want to add more, " - "**`save` to save your changes** or **`exit` to exit without saving.**\n\n" - f"Available labels: {avaliable_labels}\nLabels currently on issue: {used_labels}" - ) - if to_add: - try: - await GitHubAPI.add_labels(token, repo, issue, to_add) - issue_info = await GitHubAPI.get_issue(token, repo, issue) - except EXCEPTIONS as e: - return await self._handle_error(ctx, e) - await ctx.send( - "Added labels to issue `{}` by `{}`".format( - issue_info.get("title"), issue_info.get("user", {}).get("login") - ) - ) - else: - await ctx.send("No changes were saved.") - - @gh.command(aliases=["removelabel"]) - async def removelabels(self, ctx: commands.Context, issue: int): - """Interactive command to remove labels from an issue or PR.""" - try: - token = await self._get_token(ctx) - repo = await self._get_repo(ctx) - issue_labels = await GitHubAPI.get_issue_labels(token, repo, issue) - except EXCEPTIONS as e: - return await self._handle_error(ctx, e) - - il_names = [] - for label in issue_labels: - il_names.append(label["name"]) - - used_labels = inline_hum_list(il_names) - await ctx.send( - "You have 30 seconds, please say what label you want to add. Any invalid input will " - "be ignored. This is case sensitive.\n\n" - f"Labels currently on issue: {used_labels}" - ) - - def check(msg: discord.Message): - return ( - msg.author == ctx.author - and msg.channel == ctx.channel - and (msg.content in il_names or msg.content.casefold() == "exit") - ) - - while True: - try: - answer: discord.Message = await self.bot.wait_for( - "message", check=check, timeout=30.0 - ) - except TimeoutError: - return await ctx.send("Timeout.") - if answer.content.casefold() == "exit": - return await ctx.send("Done.") - try: - await GitHubAPI.remove_label(token, repo, issue, answer.content) - except EXCEPTIONS as e: - return await self._handle_error(ctx, e) - - il_names.remove(answer.content) - - used_labels = inline_hum_list(il_names) - await ctx.send( - "Label removed. Again, 30 seconds. Say another label name if you want to remove " - f"one, or `exit` to finish.\n\nLabels currently on issue: {used_labels}" - ) - - @gh.command() - async def open(self, ctx: commands.Context, *, title: str): - """Open a new issue. Does NOT reopen.""" - try: - token = await self._get_token(ctx) - repo = await self._get_repo(ctx) - except EXCEPTIONS as e: - return await self._handle_error(ctx, e) - - await ctx.send( - "Your next message will be the description of the issue. If you answer exactly " - "`cancel` I won't make an issue.\n" - "You've got 5 minutes, remember the 2000 Discord character limit!" - ) - try: - answer: discord.Message = await self.bot.wait_for( - "message", check=MessagePredicate.same_context(ctx), timeout=300.0 - ) - except TimeoutError: - return await ctx.send("Aborting.") - if answer.content.casefold() == "cancel": - return await ctx.send("Aborting.") - else: - description = answer.content - - await ctx.send( - "Do you want to add one or more labels to this issue? (yes or no, 15 seconds)" - ) - pred = MessagePredicate.yes_or_no(ctx) - try: - answer = await self.bot.wait_for("message", check=pred, timeout=15.0) - except TimeoutError: - return await ctx.send("Aborting.") - - to_add: list[str] = [] - if pred.result is True: - repo_labels = await GitHubAPI.get_repo_labels(token, repo) - rl_names = [] - for label in repo_labels: - rl_names.append(label["name"]) - - avaliable_labels = inline_hum_list(rl_names) - await ctx.send( - "You have 30 seconds, please say what label you want to add. Any invalid input " - "will be ignored. This is case sensitive. Say `exit` to abort creating the issue, " - f"or **`create` to make the issue**.\n\nAvaliable labels: {avaliable_labels}" - ) - - def check(msg: discord.Message): - return ( - msg.author == ctx.author - and msg.channel == ctx.channel - and (msg.content in rl_names or msg.content.casefold() in ["create", "exit"]) - ) - - to_add = [] - while True: - try: - answer = await self.bot.wait_for("message", check=check, timeout=30.0) - except TimeoutError: - await ctx.send("Timeout on this label.") - break - if answer.content.casefold() == "exit": - await ctx.send("Exiting. No changes were saved.") - return - if answer.content.casefold() == "create": - break - elif answer.content in to_add: - await ctx.send( - "It looks like that label's already on the issue. Choose another, 30 " - "seconds." - ) - continue - to_add.append(answer.content) - rl_names.remove(answer.content) - - avaliable_labels = inline_hum_list(rl_names) - used_labels = inline_hum_list(to_add) - await ctx.send( - "Label added. Again, 30 seconds. Say another label name if you want to add " - "more, `create` to create the issue or `exit` to exit without saving.\n\n" - f"Avaliable labels: {avaliable_labels}\nLabels currently on issue: " - f"{used_labels}" - ) - try: - resp = await GitHubAPI.create_issue(token, repo, title, description, to_add) - except EXCEPTIONS as e: - return await self._handle_error(ctx, e) - - await ctx.send( - "Created issue {}: {}".format(resp.get("number"), "<{}>".format(resp.get("html_url"))) - ) diff --git a/github/info.json b/github/info.json index 3a813c9f..9aa54b03 100644 --- a/github/info.json +++ b/github/info.json @@ -4,23 +4,9 @@ "Vexed (Vexed#0714)" ], "description": "Create, comment, labelify and close GitHub issues, with partial PR support.", - "end_user_data_statement": "This cog does not persistently store data or metadata about users.", - "install_msg": "Thanks for installing! This cog was made for bot owners to manage issues on a specific repo. Get started with `gh howtoken` and `gh setrepo`.\n\nThis cog has docs! Check them out at <https://go.vexcodes.com/c/github>", - "max_bot_version": "3.6.0.dev0", - "min_bot_version": "3.4.0", - "min_python_version": [ - 3, - 8, - 1 - ], + "disabled": true, + "hidden": true, + "install_msg": "This cog has been removed in favour of my `ghissues` cog. Please uninstall this one and install that.", "name": "GitHub", - "requirements": [ - "gidgethub>=5.0.0" - ], - "short": "Create, comment, labelify and close GitHub issues.", - "tags": [ - "utility", - "github", - "issues" - ] + "short": "Create, comment, labelify and close GitHub issues." } diff --git a/github/vexutils/README.md b/github/vexutils/README.md deleted file mode 100644 index 19b02100..00000000 --- a/github/vexutils/README.md +++ /dev/null @@ -1,13 +0,0 @@ -## My utils - -Hello there! If you're contributing or taking a look, everything in this folder -is synced from a master repo at https://github.com/Vexed01/vex-cog-utils by GitHub Actions - -so it's probably best to look/edit there. - ---- - -Last sync at: 2023-02-16 14:37:06 UTC - -Version: `2.6.1` - -Commit: [`b98072829ca902ef207688334da34f8e6c1da1e8`](https://github.com/Vexed01/vex-cog-utils/commit/b98072829ca902ef207688334da34f8e6c1da1e8) diff --git a/github/vexutils/__init__.py b/github/vexutils/__init__.py deleted file mode 100644 index 77c446e4..00000000 --- a/github/vexutils/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from typing import TYPE_CHECKING, Optional - -import discord -from redbot.core.bot import Red - -from .chat import humanize_bytes, inline_hum_list, no_colour_rich_markup -from .meta import format_help, format_info, get_vex_logger, out_of_date_check -from .version import __version__ diff --git a/github/vexutils/button_pred.py b/github/vexutils/button_pred.py deleted file mode 100644 index 5ed75881..00000000 --- a/github/vexutils/button_pred.py +++ /dev/null @@ -1,187 +0,0 @@ -# type:ignore -# until dpy2 - -import asyncio -from dataclasses import dataclass -from typing import Any, List, Optional - -import discord -from redbot.core import commands - -if discord.__version__.startswith("1"): - raise RuntimeError("This requires discord.py 2.X") -from discord import ButtonStyle, Embed, Interaction, ui - - -@dataclass -class PredItem: - """ - `ref` is what you want to be returned from the predicate if this button is clicked, though it - cannot be None - - `label` and `style` are what the button will look like. - - `row` is optional if you want to change how it will look in Discord - """ - - ref: Any - style: ButtonStyle - label: str - row: Optional[int] = None - - -class _PredView(ui.View): - def __init__(self, timeout: Optional[float], author_id: int): - super().__init__(timeout=timeout) - self.ref: Any = None - self.author_id = author_id - - self.pressed = asyncio.Event() - - async def interaction_check(self, interaction: Interaction) -> bool: - if interaction.user.id == self.author_id: - return True - - await interaction.response.send_message( - "You don't have have permission to do this.", ephemeral=True - ) - return False - - -class _PredButton(ui.Button): - def __init__(self, ref: Any, style: ButtonStyle, label: str, row: Optional[int] = None): - super().__init__(style=style, label=label, row=row) - self.ref = ref - - async def callback(self, interaction: Interaction): - assert isinstance(self.view, _PredView) - self.view.stop() - self.view.ref = self.ref - self.view.pressed.set() - - -async def wait_for_press( - ctx: commands.Context, - items: List[PredItem], - content: Optional[str] = None, - embed: Optional[Embed] = None, - *, - timeout: float = 180.0, -) -> Any: - """Wait for a single button press with customisable buttons. - - Only the original author will be allowed to use this. - - Parameters - ---------- - ctx : commands.Context - Context to send message to - items : List[PredItem] - List of items to send as buttons - content : Optional[str], optional - Content of the message, by default None - embed : Optional[Embed], optional - Embed of the message, by default None - timeout : float, optional - Button timeout, by default 180.0 - - Returns - ------- - Any - The defined reference of the clicked button - - Raises - ------ - ValueError - An empty list was supplied - asyncio.TimeoutError - A button was not pressed in time. - """ - if not items: - raise ValueError("The `items` argument cannot contain an empty list.") - - view = _PredView(timeout, ctx.author.id) # type:ignore - for i in items: - button = _PredButton(i.ref, i.style, i.label, i.row) - view.add_item(button) - message = await ctx.send(content=content, embed=embed, view=view) - - await asyncio.wait_for(view.pressed.wait(), timeout=timeout) - - emptyview = ui.View() - for i in items: - button = ui.Button( - style=i.style if i.ref == view.ref else ButtonStyle.gray, - label=i.label, - row=i.row, - disabled=True, - ) - emptyview.add_item(button) - await message.edit(view=emptyview) - emptyview.stop() - - return view.ref - - -async def wait_for_yes_no( - ctx: commands.Context, - content: Optional[str] = None, - embed: Optional[Embed] = None, - *, - timeout: float = 180.0, -) -> bool: - """Wait for a single button press of pre-defined yes and no buttons, returning True for yes - and False for no. - - If you want to customise the buttons, I recommend you use the more generic `wait_for_press`. - - Only the original author will be allowed to use this. - - Parameters - ---------- - ctx : commands.Context - Context to send message to - content : Optional[str], optional - Content of the message, by default None - embed : Optional[Embed], optional - Embed of the message, by default None - timeout : float, optional - Button timeout, by default 180.0 - - Returns - ------- - bool - True or False, depending on the clicked button. - - Raises - ------ - asyncio.TimeoutError - A button was not pressed in time. - """ - view = _PredView(timeout, ctx.author.id) # type:ignore - view.add_item(_PredButton(True, ButtonStyle.blurple, "Yes")) - view.add_item(_PredButton(False, ButtonStyle.blurple, "No")) - - message = await ctx.send(content=content, embed=embed, view=view) - - await asyncio.wait_for(view.pressed.wait(), timeout=timeout) - - emptyview = ui.View() - emptyview.add_item( - ui.Button( - style=ButtonStyle.grey if view.ref is False else ButtonStyle.blurple, - label="Yes", - disabled=True, - ) - ) - emptyview.add_item( - ui.Button( - style=ButtonStyle.grey if view.ref is True else ButtonStyle.blurple, - label="No", - disabled=True, - ) - ) - await message.edit(view=emptyview) - emptyview.stop() - - return view.ref diff --git a/github/vexutils/chat.py b/github/vexutils/chat.py deleted file mode 100644 index 9208ed74..00000000 --- a/github/vexutils/chat.py +++ /dev/null @@ -1,99 +0,0 @@ -import datetime -from io import StringIO -from typing import Any, Literal, Sequence, Union - -from redbot.core.utils.chat_formatting import box, humanize_list, humanize_number, inline -from rich.console import Console - -TimestampFormat = Literal["f", "F", "d", "D", "t", "T", "R"] - - -def no_colour_rich_markup(*objects: Any, lang: str = "") -> str: - """ - Slimmed down version of rich_markup which ensure no colours (/ANSI) can exist - https://github.com/Cog-Creators/Red-DiscordBot/pull/5538/files (Kowlin) - """ - temp_console = Console( # Prevent messing with STDOUT's console - color_system=None, - file=StringIO(), - force_terminal=True, - width=80, - ) - temp_console.print(*objects) - return box(temp_console.file.getvalue(), lang=lang) # type: ignore - - -def _hum(num: Union[int, float], unit: str, ndigits: int) -> str: - """Round a number, then humanize.""" - return humanize_number(round(num, ndigits)) + f" {unit}" - - -def humanize_bytes(bytes: Union[int, float], ndigits: int = 0) -> str: - """Humanize a number of bytes, rounding to ndigits. Only supports up to GB. - - This assumes 1GB = 1000MB, 1MB = 1000KB, 1KB = 1000B""" - if bytes > 10000000000: # 10GB - gb = bytes / 1000000000 - return _hum(gb, "GB", ndigits) - if bytes > 10000000: # 10MB - mb = bytes / 1000000 - return _hum(mb, "MB", ndigits) - if bytes > 10000: # 10KB - kb = bytes / 1000 - return _hum(kb, "KB", ndigits) - return _hum(bytes, "B", 0) # no point in rounding - - -# maybe think about adding to core -def inline_hum_list(items: Sequence[str], *, style: str = "standard") -> str: - """Similar to core's humanize_list, but all items are in inline code blocks. **Can** be used - outside my cogs. - - Strips leading and trailing whitespace. - - Does not support locale. - - Does support style (see core's docs for available styles) - - Parameters - ---------- - items : Sequence[str] - The items to humanize - style : str, optional - The style. See core's docs, by default "standard" - - Returns - ------- - str - Humanized inline list. - """ - inline_list = [inline(i.strip()) for i in items] - return humanize_list(inline_list, style=style) - - -def datetime_to_timestamp(dt: datetime.datetime, format: TimestampFormat = "f") -> str: - """Generate a Discord timestamp from a datetime object. - - <t:TIMESTAMP:FORMAT> - - Parameters - ---------- - dt : datetime.datetime - The datetime object to use - format : TimestampFormat, by default `f` - The format to pass to Discord. - - `f` short date time | `18 June 2021 02:50` - - `F` long date time | `Friday, 18 June 2021 02:50` - - `d` short date | `18/06/2021` - - `D` long date | `18 June 2021` - - `t` short time | `02:50` - - `T` long time | `02:50:15` - - `R` relative time | `8 days ago` - - Returns - ------- - str - Formatted timestamp - """ - t = str(int(dt.timestamp())) - return f"<t:{t}:{format}>" diff --git a/github/vexutils/commit.json b/github/vexutils/commit.json deleted file mode 100644 index 32cea625..00000000 --- a/github/vexutils/commit.json +++ /dev/null @@ -1 +0,0 @@ -{"latest_commit": "b98072829ca902ef207688334da34f8e6c1da1e8"} \ No newline at end of file diff --git a/github/vexutils/consts.py b/github/vexutils/consts.py deleted file mode 100644 index a88f5850..00000000 --- a/github/vexutils/consts.py +++ /dev/null @@ -1,9 +0,0 @@ -DOCS_BASE = "This cog has docs! Check them out at\nhttps://s.vexcodes.com/c/{}" - -CHECK = "\N{HEAVY CHECK MARK}\N{VARIATION SELECTOR-16}" -CROSS = "\N{CROSS MARK}" - -RED_CIRCLE = "\N{LARGE RED CIRCLE}" -GREEN_CIRCLE = "\N{LARGE GREEN CIRCLE}" - -SNOWFLAKE_REGEX = r"\b\d{17,20}\b" diff --git a/github/vexutils/loop.py b/github/vexutils/loop.py deleted file mode 100644 index 2a14a1ac..00000000 --- a/github/vexutils/loop.py +++ /dev/null @@ -1,131 +0,0 @@ -import asyncio -import datetime -import traceback -from typing import Optional - -import discord -from redbot.core.utils.chat_formatting import box, pagify -from rich.table import Table # type:ignore - -from .chat import no_colour_rich_markup -from .consts import CHECK, CROSS - - -class VexLoop: - """ - A class with some utilities for logging the state of a loop. - - Note iter_count increases at the start of an iteration. - - This does not log anything itself. - """ - - def __init__(self, friendly_name: str, expected_interval: float) -> None: - self.friendly_name = friendly_name - self.expected_interval = datetime.timedelta(seconds=expected_interval) - - self.iter_count: int = 0 - self.currently_running: bool = False # whether the loop is running or sleeping - self.last_exc: str = "No exception has occurred yet." - self.last_exc_raw: Optional[BaseException] = None - - self.last_iter: Optional[datetime.datetime] = None - self.next_iter: Optional[datetime.datetime] = None - - def __repr__(self) -> str: - return ( - f"<friendly_name={self.friendly_name} iter_count={self.iter_count} " - f"currently_running={self.currently_running} last_iter={self.last_iter} " - f"next_iter={self.next_iter} integrity={self.integrity}>" - ) - - @property - def integrity(self) -> bool: - """ - If the loop is running on time (whether or not next expected iteration is in the future) - """ - if self.next_iter is None: # not started yet - return False - return self.next_iter > datetime.datetime.utcnow() - - @property - def until_next(self) -> float: - """ - Positive float with the seconds until the next iteration, based off the last - iteration and the interval. - - If the expected time of the next iteration is in the past, this will return `0.0` - """ - if self.next_iter is None: # not started yet - return 0.0 - - raw_until_next = (self.next_iter - datetime.datetime.utcnow()).total_seconds() - if raw_until_next > self.expected_interval.total_seconds(): # should never happen - return self.expected_interval.total_seconds() - elif raw_until_next > 0.0: - return raw_until_next - else: - return 0.0 - - async def sleep_until_next(self) -> None: - """Sleep until the next iteration. Basically an "all-in-one" version of `until_next`.""" - await asyncio.sleep(self.until_next) - - def iter_start(self) -> None: - """Register an iteration as starting.""" - self.iter_count += 1 - self.currently_running = True - self.last_iter = datetime.datetime.utcnow() - self.next_iter = datetime.datetime.utcnow() + self.expected_interval - # this isn't accurate, it will be "corrected" when finishing is called - - def iter_finish(self) -> None: - """Register an iteration as finished successfully.""" - self.currently_running = False - # now this is accurate. imo its better to have something than nothing - - def iter_error(self, error: BaseException) -> None: - """Register an iteration's exception.""" - self.currently_running = False - self.last_exc_raw = error - self.last_exc = "".join( - traceback.format_exception(type(error), error, error.__traceback__) - ) - - def get_debug_embed(self) -> discord.Embed: - """Get an embed with infomation on this loop.""" - table = Table("Key", "Value") - - table.add_row("expected_interval", str(self.expected_interval)) - table.add_row("iter_count", str(self.iter_count)) - table.add_row("currently_running", str(self.currently_running)) - table.add_row("last_iterstr", str(self.last_iter) or "Loop not started") - table.add_row("next_iterstr", str(self.next_iter) or "Loop not started") - - raw_table_str = no_colour_rich_markup(table) - - now = datetime.datetime.utcnow() - - if self.next_iter and self.last_iter: - table = Table("Key", "Value") - table.add_row("Seconds until next", str((self.next_iter - now).total_seconds())) - table.add_row("Seconds since last", str((now - self.last_iter).total_seconds())) - processed_table_str = no_colour_rich_markup(table) - - else: - processed_table_str = "Loop hasn't started yet." - - emoji = CHECK if self.integrity else CROSS - embed = discord.Embed(title=f"{self.friendly_name}: `{emoji}`") - embed.add_field(name="Raw data", value=raw_table_str, inline=False) - embed.add_field( - name="Processed data", - value=processed_table_str, - inline=False, - ) - exc = self.last_exc - if len(exc) > 1024: - exc = list(pagify(exc, page_length=1024))[0] + "\n..." - embed.add_field(name="Exception", value=box(exc), inline=False) - - return embed diff --git a/github/vexutils/meta.py b/github/vexutils/meta.py deleted file mode 100644 index cae0ae73..00000000 --- a/github/vexutils/meta.py +++ /dev/null @@ -1,238 +0,0 @@ -from __future__ import annotations - -import asyncio -import json -from logging import Logger, getLogger -from pathlib import Path -from typing import Literal, NamedTuple - -import aiohttp -from redbot.core import VersionInfo, commands -from redbot.core import version_info as cur_red_version -from rich import box as rich_box -from rich.table import Table # type:ignore - -from .chat import no_colour_rich_markup -from .consts import DOCS_BASE, GREEN_CIRCLE, RED_CIRCLE -from .loop import VexLoop - -log = getLogger("red.vex-utils") - - -cog_ver_lock = asyncio.Lock() - - -def get_vex_logger(name: str) -> Logger: - """Get a logger for the given name. - - Parameters - ---------- - name : str - The ``__name__`` of the file - - Returns - ------- - Logger - The logger - """ - final_name = "red.vex." - split = name.split(".") - if len(split) == 2 and split[0] == split[1]: # for example make `cmdlog.cmdlog` just `cmdlog` - final_name += split[0] - else: # otherwise use full path - final_name += name - - return getLogger(final_name) - - -def format_help(self: commands.Cog, ctx: commands.Context) -> str: - """Wrapper for format_help_for_context. **Not** currently for use outside my cogs. - - Thanks Sinbad. - - Parameters - ---------- - self : commands.Cog - The Cog class - context : commands.Context - Context - - Returns - ------- - str - Formatted help - """ - docs = DOCS_BASE.format(self.qualified_name.lower()) - pre_processed = super(type(self), self).format_help_for_context(ctx) # type:ignore - - return ( - f"{pre_processed}\n\nAuthor: **`{self.__author__}`**\nCog Version: " # type:ignore - f"**`{self.__version__}`**\n{docs}" # type:ignore - ) - # adding docs link here so doesn't show up in auto generated docs - - -# TODO: stop using red internal util - - -async def format_info( - ctx: commands.Context, - qualified_name: str, - cog_version: str, - extras: dict[str, str | bool] = {}, - loops: list[VexLoop] = [], -) -> str: - """Generate simple info text about the cog. **Not** currently for use outside my cogs. - - Parameters - ---------- - ctx : commands.Context - Context - qualified_name : str - The name you want to show, eg "BetterUptime" - cog_version : str - The version of the cog - extras : Dict[str, Union[str, bool]], optional - Dict which is foramtted as key: value\\n. Bools as a value will be replaced with - check/cross emojis, by default {} - loops : List[VexLoop], optional - List of VexLoops you want to show, by default [] - - Returns - ------- - str - Simple info text. - """ - cog_name = qualified_name.lower() - current = _get_current_vers(cog_version, qualified_name) - try: - latest = await _get_latest_vers(cog_name) - - cog_updated = current.cog >= latest.cog - utils_updated = current.utils == latest.utils - red_updated = current.red >= latest.red - except Exception: # anything and everything, eg aiohttp error or version parsing error - log.warning("Unable to parse versions.", exc_info=True) - cog_updated, utils_updated, red_updated = "Unknown", "Unknown", "Unknown" - latest = UnknownVers() - - start = f"{qualified_name} by Vexed.\n<https://github.com/Vexed01/Vex-Cogs>\n\n" - - main_table = Table( - "", "Current", "Latest", "Up to date?", title="Versions", box=rich_box.MINIMAL - ) - - main_table.add_row( - "This Cog", - str(current.cog), - str(latest.cog), - GREEN_CIRCLE if cog_updated else RED_CIRCLE, - ) - main_table.add_row( - "Bundled Utils", - current.utils, - latest.utils, - GREEN_CIRCLE if utils_updated else RED_CIRCLE, - ) - main_table.add_row( - "Red", - str(current.red), - str(latest.red), - GREEN_CIRCLE if red_updated else RED_CIRCLE, - ) - - update_msg = "\n" - if not cog_updated: - update_msg += f"To update this cog, use the `{ctx.clean_prefix}cog update` command.\n" - if not utils_updated: - update_msg += ( - f"To update the bundled utils, use the `{ctx.clean_prefix}cog update` command.\n" - ) - if not red_updated: - update_msg += "To update Red, see https://docs.discord.red/en/stable/update_red.html\n" - - extra_table = Table("Key", "Value", title="Extras", box=rich_box.MINIMAL) - - data = [] - if loops: - for loop in loops: - extra_table.add_row(loop.friendly_name, GREEN_CIRCLE if loop.integrity else RED_CIRCLE) - - if extras: - if data: - extra_table.add_row("", "") - for key, value in extras.items(): - if isinstance(value, bool): - str_value = GREEN_CIRCLE if value else RED_CIRCLE - else: - assert isinstance(value, str) - str_value = value - extra_table.add_row(key, str_value) - - boxed = no_colour_rich_markup(main_table) - boxed += update_msg - if loops or extras: - boxed += no_colour_rich_markup(extra_table) - - return f"{start}{boxed}" - - -async def out_of_date_check(cogname: str, currentver: str) -> None: - """Send a log at warning level if the cog is out of date.""" - try: - async with cog_ver_lock: - vers = await _get_latest_vers(cogname) - if VersionInfo.from_str(currentver) < vers.cog: - log.warning( - f"Your {cogname} cog, from Vex, is out of date. You can update your cogs with the " - "'cog update' command in Discord." - ) - else: - log.debug(f"{cogname} cog is up to date") - except Exception as e: - log.debug( - f"Something went wrong checking if {cogname} cog is up to date. See below.", exc_info=e - ) - # really doesn't matter if this fails so fine with debug level - return - - -class Vers(NamedTuple): - cogname: str - cog: VersionInfo - utils: str - red: VersionInfo - - -class UnknownVers(NamedTuple): - cogname: str = "Unknown" - cog: VersionInfo | Literal["Unknown"] = "Unknown" - utils: str = "Unknown" - red: VersionInfo | Literal["Unknown"] = "Unknown" - - -async def _get_latest_vers(cogname: str) -> Vers: - data: dict - async with aiohttp.ClientSession() as session: - async with session.get(f"https://api.vexcodes.com/v2/vers/{cogname}", timeout=3) as r: - data = await r.json() - latest_utils = data["utils"][:7] - latest_cog = VersionInfo.from_str(data.get(cogname, "0.0.0")) - async with session.get("https://pypi.org/pypi/Red-DiscordBot/json", timeout=3) as r: - data = await r.json() - latest_red = VersionInfo.from_str(data.get("info", {}).get("version", "0.0.0")) - - return Vers(cogname, latest_cog, latest_utils, latest_red) - - -def _get_current_vers(curr_cog_ver: str, qual_name: str) -> Vers: - with open(Path(__file__).parent / "commit.json") as fp: - data = json.load(fp) - latest_utils = data.get("latest_commit", "Unknown")[:7] - - return Vers( - qual_name, - VersionInfo.from_str(curr_cog_ver), - latest_utils, - cur_red_version, - ) diff --git a/github/vexutils/py.typed b/github/vexutils/py.typed deleted file mode 100644 index e69de29b..00000000 diff --git a/github/vexutils/sqldriver.py b/github/vexutils/sqldriver.py deleted file mode 100644 index a8d52544..00000000 --- a/github/vexutils/sqldriver.py +++ /dev/null @@ -1,98 +0,0 @@ -import concurrent.futures -import functools -import os -import sqlite3 -from asyncio.events import AbstractEventLoop -from typing import Optional - -from redbot.core.bot import Red -from redbot.core.data_manager import cog_data_path - -# (comparisons from red config, mainly to make me feel like i didn't waste an evening) -# (these are with the stattrack cog, this driver is also used in betteruptime) -# this compares a write for config to appending to the SQL. i've not compared writes because they -# will only happend once, for migration from config. SQL includes copying DF + executor overhead -# basically this is the raw speed changes for the loop itself -# ~1.3 sec to ~0.03 sec, dataset of ~1 week on windows -# ~5-6 sec to ~0.04 sec, dataset of ~1 month on linux -# reads are insignificant as only happen on cog load - -try: - import pandas -except ImportError: - raise RuntimeError("Pandas must be installed for this driver to work.") - - -class PandasSQLiteDriver: - """An asynchronous SQLite driver for Pandas dataframes.""" - - def __init__(self, bot: Red, cog_name: str, filename: str, table: str = "main_df") -> None: - """Get a driver object for interacting with a table in the given cog's datapath. - - Parameters - ---------- - bot : Red - Bot object - cog_name : str - Full cog name, LikeThis - filename : str - The full file name to use for the database, for example `timeseries.db` - table : str, optional - The SQLite table to use, by default "main_df" - """ - self.bot = bot - self.table = table - - self.sql_executor = concurrent.futures.ThreadPoolExecutor(1, f"{cog_name.lower()}_sql") - self.sql_path = str(cog_data_path(raw_name=cog_name) / filename) - - def _write(self, df: pandas.DataFrame, table: Optional[str] = None) -> None: - connection = sqlite3.connect(self.sql_path) - try: - df.to_sql(table or self.table, con=connection, if_exists="replace") # type:ignore - connection.commit() - finally: - connection.close() - - def _append(self, df: pandas.DataFrame, table: Optional[str] = None) -> None: - connection = sqlite3.connect(self.sql_path) - try: - df.to_sql(table or self.table, con=connection, if_exists="append") # type:ignore - connection.commit() - finally: - connection.close() - - def _read(self, table: Optional[str] = None) -> pandas.DataFrame: - connection = sqlite3.connect(self.sql_path) - try: - df = pandas.read_sql( - f"SELECT * FROM {table or self.table}", - connection, - index_col="index", - parse_dates=["index"], - ) - return df - finally: - connection.close() - - async def write(self, df: pandas.DataFrame, table: Optional[str] = None) -> None: - """Write a dataframe to the database. Replaces and old data.""" - assert isinstance(self.bot.loop, AbstractEventLoop) - func = functools.partial(self._write, df.copy(True), table) - await self.bot.loop.run_in_executor(self.sql_executor, func) - - async def append(self, df: pandas.DataFrame, table: Optional[str] = None) -> None: - """Append a dataframe to the database.""" - assert isinstance(self.bot.loop, AbstractEventLoop) - func = functools.partial(self._append, df.copy(True), table) - await self.bot.loop.run_in_executor(self.sql_executor, func) - - async def read(self, table: Optional[str] = None) -> pandas.DataFrame: - """Read the database, returning as a pandas dataframe.""" - assert isinstance(self.bot.loop, AbstractEventLoop) - func = functools.partial(self._read, table) - return await self.bot.loop.run_in_executor(self.sql_executor, func) - - def storage_usage(self) -> int: - """Return the size of the database file in bytes.""" - return os.path.getsize(self.sql_path) diff --git a/github/vexutils/url_buttons.py b/github/vexutils/url_buttons.py deleted file mode 100644 index 3fad6e0c..00000000 --- a/github/vexutils/url_buttons.py +++ /dev/null @@ -1,64 +0,0 @@ -from typing import Optional - -import discord -from discord.http import Route -from redbot.core.bot import Red - - -class URLButton: - def __init__(self, label: str, url: str) -> None: - if not isinstance(label, str): - raise TypeError("Label must be a string") - if not isinstance(url, str): - raise TypeError("URL must be a string") - - self.label = label - self.url = url - - def to_dict(self) -> dict: - return { - "label": self.label, - "style": 5, - "type": 2, - "url": self.url, - } - - -async def send_message( - bot: Red, - channel_id: int, - *, - content: Optional[str] = None, - embed: Optional[discord.Embed] = None, - file: Optional[discord.File] = None, - url_button: Optional[URLButton] = None, -): - """Send a message with a URL button, with pure dpy 1.7.""" - payload = {} - - if content: - payload["content"] = content - - if embed: - payload["embed"] = embed.to_dict() - - if url_button: - payload["components"] = [{"type": 1, "components": [url_button.to_dict()]}] # type:ignore - - if file: - form = [ - { - "name": "file", - "value": file.fp, - "filename": file.filename, - "content_type": "application/octet-stream", - }, - {"name": "payload_json", "value": discord.utils.to_json(payload)}, - ] - - r = Route("POST", "/channels/{channel_id}/messages", channel_id=channel_id) - await bot._connection.http.request(r, form=form, files=[file]) - - else: - r = Route("POST", "/channels/{channel_id}/messages", channel_id=channel_id) - await bot._connection.http.request(r, json=payload) diff --git a/github/vexutils/version.py b/github/vexutils/version.py deleted file mode 100644 index fab833f3..00000000 --- a/github/vexutils/version.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = "2.6.1" diff --git a/googletrends/__init__.py b/googletrends/__init__.py index 6cab8ff3..d6a4ef65 100644 --- a/googletrends/__init__.py +++ b/googletrends/__init__.py @@ -17,6 +17,4 @@ async def setup(bot: Red) -> None: cog = GoogleTrends(bot) await out_of_date_check("googletrends", cog.__version__) - r = bot.add_cog(cog) - if r is not None: - await r + await bot.add_cog(cog) diff --git a/googletrends/googletrends.py b/googletrends/googletrends.py index 8bd536ea..49e65234 100644 --- a/googletrends/googletrends.py +++ b/googletrends/googletrends.py @@ -37,7 +37,7 @@ async def red_delete_data_for_user(self, **kwargs) -> None: """Nothing to delete""" return - def cog_unload(self) -> None: + async def cog_unload(self) -> None: self.executor.shutdown(wait=False) @commands.command(hidden=True) diff --git a/googletrends/info.json b/googletrends/info.json index 46e7449f..e3a74fd8 100644 --- a/googletrends/info.json +++ b/googletrends/info.json @@ -6,8 +6,7 @@ "description": "Find out what the world is searching, right from Discord. Includes charts.", "end_user_data_statement": "This cog does not persistently store data or metadata about users.", "install_msg": "Thanks for installing! Find out what the world is searching, right from Discord. Once you've loaded the cog, start with `[p]trends help`.\n\nPlease note there is no Google Trends API, so therefore this is a scraper and may break at any time.\n\nThis cog has docs! Check them out at <https://go.vexcodes.com/c/googletrends>", - "max_bot_version": "3.6.0.dev0", - "min_bot_version": "3.4.0", + "min_bot_version": "3.5.1", "min_python_version": [ 3, 8, diff --git a/googletrends/vexutils/meta.py b/googletrends/vexutils/meta.py index cae0ae73..1e963e4c 100644 --- a/googletrends/vexutils/meta.py +++ b/googletrends/vexutils/meta.py @@ -2,11 +2,12 @@ import asyncio import json -from logging import Logger, getLogger from pathlib import Path from typing import Literal, NamedTuple import aiohttp +from red_commons.logging import RedTraceLogger +from red_commons.logging import getLogger as red_get_logger from redbot.core import VersionInfo, commands from redbot.core import version_info as cur_red_version from rich import box as rich_box @@ -16,13 +17,13 @@ from .consts import DOCS_BASE, GREEN_CIRCLE, RED_CIRCLE from .loop import VexLoop -log = getLogger("red.vex-utils") +log = red_get_logger("red.vex-utils") cog_ver_lock = asyncio.Lock() -def get_vex_logger(name: str) -> Logger: +def get_vex_logger(name: str) -> RedTraceLogger: """Get a logger for the given name. Parameters @@ -42,7 +43,7 @@ def get_vex_logger(name: str) -> Logger: else: # otherwise use full path final_name += name - return getLogger(final_name) + return red_get_logger(final_name) def format_help(self: commands.Cog, ctx: commands.Context) -> str: diff --git a/madtranslate/__init__.py b/madtranslate/__init__.py index 034ebb19..b966aaad 100644 --- a/madtranslate/__init__.py +++ b/madtranslate/__init__.py @@ -17,6 +17,4 @@ async def setup(bot: Red) -> None: cog = MadTranslate(bot) await out_of_date_check("madtranslate", cog.__version__) - r = bot.add_cog(cog) - if r is not None: - await r + await bot.add_cog(cog) diff --git a/madtranslate/info.json b/madtranslate/info.json index 9436cef4..ec19eabf 100644 --- a/madtranslate/info.json +++ b/madtranslate/info.json @@ -6,8 +6,7 @@ "description": "Translate something from English into a gazillion different languages then back to English. Uses an undocumented Google Translate API.", "end_user_data_statement": "This cog does not persistently store data or metadata about users.", "install_msg": "Thanks for installing! Note that this cog uses an undocumented API for Google Translate. As such, this cog may break at short notice.\n\nThis cog has docs! Check them out at <https://go.vexcodes.com/c/madtranslate>", - "max_bot_version": "3.6.0.dev0", - "min_bot_version": "3.4.0", + "min_bot_version": "3.5.1", "min_python_version": [ 3, 8, diff --git a/madtranslate/madtranslate.py b/madtranslate/madtranslate.py index 75820f8a..898ddded 100644 --- a/madtranslate/madtranslate.py +++ b/madtranslate/madtranslate.py @@ -43,6 +43,7 @@ async def get_translation(session: aiohttp.ClientSession, sl: str, tl: str, q: s raise ForbiddenExc as_json = await resp.json() + log.trace("raw JSON query result: %s", as_json) return as_json[0] diff --git a/madtranslate/vexutils/meta.py b/madtranslate/vexutils/meta.py index cae0ae73..1e963e4c 100644 --- a/madtranslate/vexutils/meta.py +++ b/madtranslate/vexutils/meta.py @@ -2,11 +2,12 @@ import asyncio import json -from logging import Logger, getLogger from pathlib import Path from typing import Literal, NamedTuple import aiohttp +from red_commons.logging import RedTraceLogger +from red_commons.logging import getLogger as red_get_logger from redbot.core import VersionInfo, commands from redbot.core import version_info as cur_red_version from rich import box as rich_box @@ -16,13 +17,13 @@ from .consts import DOCS_BASE, GREEN_CIRCLE, RED_CIRCLE from .loop import VexLoop -log = getLogger("red.vex-utils") +log = red_get_logger("red.vex-utils") cog_ver_lock = asyncio.Lock() -def get_vex_logger(name: str) -> Logger: +def get_vex_logger(name: str) -> RedTraceLogger: """Get a logger for the given name. Parameters @@ -42,7 +43,7 @@ def get_vex_logger(name: str) -> Logger: else: # otherwise use full path final_name += name - return getLogger(final_name) + return red_get_logger(final_name) def format_help(self: commands.Cog, ctx: commands.Context) -> str: diff --git a/roleplay/info.json b/roleplay/info.json index a25e39ef..6dc449be 100644 --- a/roleplay/info.json +++ b/roleplay/info.json @@ -6,8 +6,7 @@ "description": "Create a role play channel where users can contribute in secret, with some customisation options and logging for admins.", "end_user_data_statement": "This cog does not persistently store data or metadata about users. Messages sent in channels designated as role play channels by Administrators are not stored in memory by this cog, but they are re-sent from the bot with no association to the user. As such this cog does not directly store data, but data is posted to a Discord channel.", "install_msg": "Thanks for installing!\n\nTo set the cog up, start with `roleplay channel`. There are some other optional configuration settings available under `roleplay`\n\nThis cog has docs! Check them out at <https://go.vexcodes.com/c/roleplay>", - "max_bot_version": "3.5.99", - "min_bot_version": "3.4.0", + "min_bot_version": "3.5.1", "min_python_version": [ 3, 8, diff --git a/roleplay/roleplay.py b/roleplay/roleplay.py index a7ec026e..ff62778b 100644 --- a/roleplay/roleplay.py +++ b/roleplay/roleplay.py @@ -467,7 +467,7 @@ async def settings(self, ctx: commands.Context): name="Delete After", value=(str(data["delete_after"]) + "mins") or "Disabled" ) embed.add_field(name="Radio Title", value=data["radiotitle"]) - embed.add_field(name="Radio image", value=data["radioimage"] or "Not set") - embed.add_field(name="Radio footer", value=data["radiofooter"] or "Not set") + embed.add_field(name="Radio image", value=data.get("radioimage", "Not set")) + embed.add_field(name="Radio footer", value=data.get("radiofooter", "Not set")) await ctx.send(embed=embed) diff --git a/roleplay/vexutils/meta.py b/roleplay/vexutils/meta.py index cae0ae73..1e963e4c 100644 --- a/roleplay/vexutils/meta.py +++ b/roleplay/vexutils/meta.py @@ -2,11 +2,12 @@ import asyncio import json -from logging import Logger, getLogger from pathlib import Path from typing import Literal, NamedTuple import aiohttp +from red_commons.logging import RedTraceLogger +from red_commons.logging import getLogger as red_get_logger from redbot.core import VersionInfo, commands from redbot.core import version_info as cur_red_version from rich import box as rich_box @@ -16,13 +17,13 @@ from .consts import DOCS_BASE, GREEN_CIRCLE, RED_CIRCLE from .loop import VexLoop -log = getLogger("red.vex-utils") +log = red_get_logger("red.vex-utils") cog_ver_lock = asyncio.Lock() -def get_vex_logger(name: str) -> Logger: +def get_vex_logger(name: str) -> RedTraceLogger: """Get a logger for the given name. Parameters @@ -42,7 +43,7 @@ def get_vex_logger(name: str) -> Logger: else: # otherwise use full path final_name += name - return getLogger(final_name) + return red_get_logger(final_name) def format_help(self: commands.Cog, ctx: commands.Context) -> str: diff --git a/stattrack/__init__.py b/stattrack/__init__.py index 944b5567..19909e56 100644 --- a/stattrack/__init__.py +++ b/stattrack/__init__.py @@ -17,7 +17,4 @@ async def setup(bot: Red) -> None: cog = StatTrack(bot) await out_of_date_check("stattrack", cog.__version__) - await cog.async_init() - r = bot.add_cog(cog) - if r is not None: - await r + await bot.add_cog(cog) diff --git a/stattrack/abc.py b/stattrack/abc.py index cb8ffedb..61f92a5d 100644 --- a/stattrack/abc.py +++ b/stattrack/abc.py @@ -43,7 +43,3 @@ class MixinMeta(ABC): @abstractmethod async def plot(self, df: pandas.DataFrame, ylabel: str, status_colours: bool) -> discord.File: raise NotImplementedError - - @abstractmethod - async def async_init(self) -> None: - raise NotImplementedError diff --git a/stattrack/commands.py b/stattrack/commands.py index 88005051..674c38bc 100644 --- a/stattrack/commands.py +++ b/stattrack/commands.py @@ -2,7 +2,7 @@ import datetime import json -from io import StringIO +from io import BytesIO, StringIO from time import monotonic from typing import Iterable, Optional @@ -13,6 +13,7 @@ from redbot.core.utils.chat_formatting import box, humanize_number, humanize_timedelta from stattrack.abc import MixinMeta +from stattrack.components.view import StatTrackView from stattrack.converters import ( ChannelGraphConverter, StatusGraphConverter, @@ -30,11 +31,13 @@ class StatTrackCommands(MixinMeta): async def all_in_one( self, - ctx: commands.Context, + ctx: commands.Context | discord.Message, + chart: str, delta: datetime.timedelta, label: str | Iterable[str], title: str, ylabel: str | None = None, + author: discord.User | discord.Member | None = None, *, more_options: bool = False, status_colours: bool = False, @@ -47,6 +50,8 @@ async def all_in_one( df = await self.driver.read_partial([label] if isinstance(label, str) else label, delta) db_time = monotonic() - db_start + log.trace("pd df obj: %s", df) + if len(df) < 2: await ctx.send("I need a little longer to collect data. Try again in a minute.") return @@ -87,7 +92,10 @@ async def all_in_one( processing_time = monotonic() - processing_start plot_start = monotonic() - async with ctx.typing(): + if isinstance(ctx, commands.Context): + async with ctx.channel.typing(): + graph = await self.plot(df, ylabel, status_colours) + else: # from select menu so already empherially typing graph = await self.plot(df, ylabel, status_colours) plot_time = monotonic() - plot_start @@ -99,7 +107,7 @@ async def all_in_one( embed = discord.Embed( title=title + str_delta + (" (10 min averages)" if do_average else ""), - colour=await ctx.embed_colour(), + colour=await self.bot.get_embed_color(ctx.channel), ) if len(df.columns) == 1: @@ -109,7 +117,7 @@ async def all_in_one( if show_total is True: embed.add_field(name="Total", value=total_before_avg) # type:ignore - if more_options: + if more_options and isinstance(ctx, commands.Context): embed.description = ( "You can choose to only display certian metrics with " f"`{ctx.clean_prefix}stattrack {ctx.command.name} <metrics>`, see " @@ -119,7 +127,18 @@ async def all_in_one( embed.set_footer(text="Times are in UTC") embed.set_image(url="attachment://plot.png") - msg = await ctx.send(file=graph, embed=embed) + view = StatTrackView( + current_delta=delta, + author=author or ctx.author, + comclass=self, + chart=chart, + current_metrics=[label] if isinstance(label, str) else label, + ) + + if isinstance(ctx, commands.Context): + msg = await ctx.send(file=graph, embed=embed, view=view) + else: + msg = await ctx.edit(attachments=[graph], embed=embed, view=view) send_time = monotonic() - send_start @@ -136,7 +155,7 @@ async def all_in_one( "send_time": send_time, # time taken for sending the message } - log.debug(f"Plot finished, info: {debug_info}") + log.trace(f"Plot finished, info: {debug_info}") self.last_plot_debug = debug_info @commands.cooldown(10, 60.0, BucketType.user) @@ -157,7 +176,9 @@ async def devimport(self, ctx: commands.Context): async with ctx.typing(): self.loop.cancel() await self.driver.write( - pd.read_json(await ctx.message.attachments[0].read(), orient="split", typ="frame") + pd.read_json( + BytesIO(await ctx.message.attachments[0].read()), orient="split", typ="frame" + ) ) await ctx.send("Done.") @@ -266,7 +287,7 @@ async def latency(self, ctx: commands.Context, timespan: TimespanConverter = DEF - `[p]stattrack latency 5d` - `[p]stattrack latency all` """ - await self.all_in_one(ctx, timespan, "ping", "Latency", "Latency (ms)") + await self.all_in_one(ctx, "latency", timespan, "ping", "Latency", "Latency (ms)") @stattrack.command(aliases=["time", "loop"]) async def looptime(self, ctx: commands.Context, timespan: TimespanConverter = DEFAULT_DELTA): @@ -283,7 +304,9 @@ async def looptime(self, ctx: commands.Context, timespan: TimespanConverter = DE - `[p]stattrack looptime 5d` - `[p]stattrack looptime all` """ - await self.all_in_one(ctx, timespan, "loop_time_s", "Loop time", "Loop time (seconds)") + await self.all_in_one( + ctx, "looptime", timespan, "loop_time_s", "Loop time", "Loop time (seconds)" + ) @stattrack.command(name="commands") async def com(self, ctx: commands.Context, timespan: TimespanConverter = DEFAULT_DELTA): @@ -301,7 +324,13 @@ async def com(self, ctx: commands.Context, timespan: TimespanConverter = DEFAULT - `[p]stattrack commands all` """ await self.all_in_one( - ctx, timespan, "command_count", "Commands per minute", do_average=True, show_total=True + ctx, + "commands", + timespan, + "command_count", + "Commands per minute", + do_average=True, + show_total=True, ) @stattrack.command() @@ -320,7 +349,13 @@ async def messages(self, ctx: commands.Context, timespan: TimespanConverter = DE - `[p]stattrack messages all` """ await self.all_in_one( - ctx, timespan, "message_count", "Messages per minute", do_average=True, show_total=True + ctx, + "messages", + timespan, + "message_count", + "Messages per minute", + do_average=True, + show_total=True, ) @stattrack.command(aliases=["guilds"]) @@ -340,6 +375,7 @@ async def servers(self, ctx: commands.Context, timespan: TimespanConverter = DEF """ await self.all_in_one( ctx, + "servers", timespan, "guilds", "Server count", @@ -380,6 +416,7 @@ async def status( await self.all_in_one( ctx, + "status", timespan, ["status_" + g for g in metrics], "User status", @@ -424,7 +461,12 @@ async def users( metrics = ("total", "unique", "humans", "bots") await self.all_in_one( - ctx, timespan, ["users_" + g for g in metrics], "Users", more_options=True + ctx, + "users", + timespan, + ["users_" + g for g in metrics], + "Users", + more_options=True, ) @stattrack.command(usage="[timespan=1d] [metrics]") @@ -471,7 +513,12 @@ async def channels( metrics = ("total", "text", "voice", "cat", "stage") await self.all_in_one( - ctx, timespan, ["channels_" + g for g in metrics], "Channels", more_options=True + ctx, + "channels", + timespan, + ["channels_" + g for g in metrics], + "Channels", + more_options=True, ) @stattrack.group(aliases=["sys"]) @@ -493,7 +540,7 @@ async def cpu(self, ctx: commands.Context, timespan: TimespanConverter = DEFAULT - `[p]stattrack system cpu 5d` - `[p]stattrack system cpu all` """ - await self.all_in_one(ctx, timespan, "sys_cpu", "CPU Usage", "Percentage CPU Usage") + await self.all_in_one(ctx, "cpu", timespan, "sys_cpu", "CPU Usage", "Percentage CPU Usage") @system.command(aliases=["memory", "ram"]) async def mem(self, ctx: commands.Context, timespan: TimespanConverter = DEFAULT_DELTA): @@ -510,4 +557,4 @@ async def mem(self, ctx: commands.Context, timespan: TimespanConverter = DEFAULT - `[p]stattrack system mem 5d` - `[p]stattrack system mem all` """ - await self.all_in_one(ctx, timespan, "sys_mem", "RAM Usage", "Percentage RAM Usage") + await self.all_in_one(ctx, "mem", timespan, "sys_mem", "RAM Usage", "Percentage RAM Usage") diff --git a/stattrack/components/view.py b/stattrack/components/view.py new file mode 100644 index 00000000..f3e0f563 --- /dev/null +++ b/stattrack/components/view.py @@ -0,0 +1,209 @@ +from __future__ import annotations + +from datetime import timedelta +from typing import TYPE_CHECKING, Iterable + +import discord +from redbot.core.utils.chat_formatting import humanize_timedelta + +from ..consts import ALL_CHARTS, TRACE_FRIENDLY_NAMES + +if TYPE_CHECKING: + from stattrack.commands import StatTrackCommands + + +class ChangeChartDropdown(discord.ui.Select): + def __init__(self, charts: list[discord.SelectOption]) -> None: + super().__init__(placeholder="Change chart", options=charts) + + async def callback(self, interaction: discord.Interaction) -> None: + await interaction.response.defer(ephemeral=True, thinking=True) + assert isinstance(self.view, StatTrackView) + + chart = self.values[0] + + self.view.stop() + await self.view.comclass.all_in_one( + ctx=interaction.message, + chart=chart, + delta=self.view.delta, + label=ALL_CHARTS[chart]["valid_metrics"], + title=ALL_CHARTS[chart]["title"], + ylabel=ALL_CHARTS[chart]["ylabel"], + author=self.view.author, + more_options=ALL_CHARTS[chart]["more_options"], + do_average=ALL_CHARTS[chart]["do_average"], + show_total=ALL_CHARTS[chart]["show_total"], + status_colours=ALL_CHARTS[chart]["status_colours"], + ) + + await interaction.followup.send("Edited") + + +class ChangeMetricsDropdown(discord.ui.Select): + def __init__(self, metrics: list[discord.SelectOption]) -> None: + super().__init__(placeholder="Change metrics", options=metrics, max_values=len(metrics)) + + async def callback(self, interaction: discord.Interaction) -> None: + await interaction.response.defer(ephemeral=True, thinking=True) + + assert isinstance(self.view, StatTrackView) + + self.view.stop() + + chart = self.view.chart + + await self.view.comclass.all_in_one( + ctx=interaction.message, + chart=chart, + delta=self.view.delta, + label=self.values, + title=ALL_CHARTS[chart]["title"], + ylabel=ALL_CHARTS[chart]["ylabel"], + author=self.view.author, + more_options=ALL_CHARTS[chart]["more_options"], + do_average=ALL_CHARTS[chart]["do_average"], + show_total=ALL_CHARTS[chart]["show_total"], + status_colours=ALL_CHARTS[chart]["status_colours"], + ) + + await interaction.followup.send("Edited") + + +class ChangeTimespanDropdown(discord.ui.Select): + DEFAULT_TIMES = { + "1 hour": timedelta(hours=1), + "1 day": timedelta(days=1), + "1 week": timedelta(days=7), + "1 month": timedelta(days=30), + "all": timedelta(days=9000), + } + + def __init__(self, metrics: list[discord.SelectOption]) -> None: + super().__init__(placeholder="Change timespan", options=metrics) + + async def callback(self, interaction: discord.Interaction) -> None: + await interaction.response.defer(ephemeral=True, thinking=True) + + assert isinstance(self.view, StatTrackView) + + self.view.stop() + + chart = self.view.chart + + new_delta = timedelta(seconds=float(self.values[0])) + + await self.view.comclass.all_in_one( + ctx=interaction.message, + chart=chart, + delta=new_delta, + label=self.view.current_metrics, + title=ALL_CHARTS[chart]["title"], + ylabel=ALL_CHARTS[chart]["ylabel"], + author=self.view.author, + more_options=ALL_CHARTS[chart]["more_options"], + do_average=ALL_CHARTS[chart]["do_average"], + show_total=ALL_CHARTS[chart]["show_total"], + status_colours=ALL_CHARTS[chart]["status_colours"], + ) + + await interaction.followup.send("Edited") + + +class StatTrackView(discord.ui.View): + def __init__( + self, + *, + comclass: StatTrackCommands, + chart: str, + current_metrics: Iterable[str], + author: discord.User | discord.Member, + current_delta: timedelta, + ) -> None: + super().__init__() + + self.comclass = comclass + self.author = author + self.delta = current_delta + + self.chart = chart + self.current_metrics = current_metrics + + charts = [] + for chart_name, chart_data in ALL_CHARTS.items(): + if chart_name == chart: + charts.append( + discord.SelectOption( + label=chart_data["title"], + value=chart_name, + default=True, + ) + ) + else: + charts.append( + discord.SelectOption( + label=chart_data["title"], + value=chart_name, + ) + ) + + self.add_item(ChangeChartDropdown(charts)) + + metrics = [] + if len(ALL_CHARTS[chart]["valid_metrics"]) > 1: + for metric in ALL_CHARTS[chart]["valid_metrics"]: + if metric in current_metrics: + metrics.append( + discord.SelectOption( + label=TRACE_FRIENDLY_NAMES[metric], + value=metric, + default=True, + ) + ) + else: + metrics.append( + discord.SelectOption( + label=TRACE_FRIENDLY_NAMES[metric], + value=metric, + ) + ) + + self.add_item(ChangeMetricsDropdown(metrics)) + + deltas = [] + got_current_delta = False + for human, delta in ChangeTimespanDropdown.DEFAULT_TIMES.items(): + if delta == current_delta: + deltas.append( + discord.SelectOption( + label=human, + value=str(delta.total_seconds()), + default=True, + ) + ) + got_current_delta = True + else: + deltas.append( + discord.SelectOption( + label=human, + value=str(delta.total_seconds()), + ) + ) + if not got_current_delta: + deltas.append( + discord.SelectOption( + label=humanize_timedelta(timedelta=current_delta), + value=str(current_delta.total_seconds()), + default=True, + ) + ) + + self.add_item(ChangeTimespanDropdown(deltas)) + + async def interaction_check(self, interaction: discord.Interaction) -> bool: + if interaction.user != self.author: + await interaction.response.send_message( + "You are not authorized to interact with this.", ephemeral=True + ) + return False + return True diff --git a/stattrack/consts.py b/stattrack/consts.py new file mode 100644 index 00000000..134503e8 --- /dev/null +++ b/stattrack/consts.py @@ -0,0 +1,137 @@ +from __future__ import annotations + +from typing import TypedDict + + +class Chart(TypedDict): + title: str + ylabel: str + valid_metrics: list[str] + do_average: bool + show_total: bool + more_options: bool + status_colours: bool + + +ALL_CHARTS: dict[str, Chart] = { + "latency": { + "title": "Latency", + "ylabel": "Latency (ms)", + "valid_metrics": ["ping"], + "do_average": False, + "show_total": False, + "more_options": False, + "status_colours": False, + }, + "looptime": { + "title": "Loop Time", + "ylabel": "Loop Time (seconds)", + "valid_metrics": ["loop_time_s"], + "do_average": False, + "show_total": False, + "more_options": False, + "status_colours": False, + }, + "commands": { + "title": "Commands per minute", + "ylabel": "Commands per minute", + "valid_metrics": ["command_count"], + "do_average": True, + "show_total": True, + "more_options": False, + "status_colours": False, + }, + "messages": { + "title": "Messages per minute", + "ylabel": "Messages per minute", + "valid_metrics": ["message_count"], + "do_average": True, + "show_total": True, + "more_options": False, + "status_colours": False, + }, + "servers": { + "title": "Server count", + "ylabel": "Server count", + "valid_metrics": ["guilds"], + "do_average": False, + "show_total": False, + "more_options": False, + "status_colours": False, + }, + "status": { + "title": "User status", + "ylabel": "User count", + "valid_metrics": ["status_online", "status_idle", "status_offline", "status_dnd"], + "do_average": False, + "show_total": False, + "more_options": False, + "status_colours": True, + }, + "users": { + "title": "User count", + "ylabel": "User count", + "valid_metrics": ["users_total", "users_humans", "users_bots", "users_unique"], + "do_average": False, + "show_total": False, + "more_options": False, + "status_colours": False, + }, + "channels": { + "title": "Channels", + "ylabel": "Channel count", + "valid_metrics": [ + "channels_total", + "channels_text", + "channels_voice", + "channels_cat", + "channels_stage", + ], + "do_average": False, + "show_total": False, + "more_options": False, + "status_colours": False, + }, + "cpu": { + "title": "CPU Usage", + "ylabel": "CPU Usage (%)", + "valid_metrics": ["sys_cpu"], + "do_average": False, + "show_total": False, + "more_options": False, + "status_colours": False, + }, + "mem": { + "title": "Memory Usage", + "ylabel": "Memory Usage (%)", + "valid_metrics": ["sys_mem"], + "do_average": False, + "show_total": False, + "more_options": False, + "status_colours": False, + }, +} + + +TRACE_FRIENDLY_NAMES = { + "ping": "Latency", + "loop_time_s": "Loop time", + "users_unique": "Unique", + "users_total": "Total", + "users_humans": "Humans", + "users_bots": "Bots", + "guilds": "Servers", + "channels_total": "Total", + "channels_text": "Text", + "channels_voice": "Voice", + "channels_stage": "Stage", + "channels_cat": "Categories", + "sys_mem": "Memory usage", + "sys_cpu": "CPU Usage", + "command_count": "Commands", + "message_count": "Messages", + "status_online": "Online", + "status_idle": "Idle", + "status_offline": "Offline", + "status_dnd": "DnD", +} diff --git a/stattrack/info.json b/stattrack/info.json index a60f309f..6986ce3c 100644 --- a/stattrack/info.json +++ b/stattrack/info.json @@ -6,8 +6,7 @@ "description": "Track your bot's statistics over time, including ping, members, guild and message/command counts.\n\nThis has a background process which could be intensive.\n\nThis stores data in Red's config which is not especially built for this cog's usage.", "end_user_data_statement": "This cog permanently stores anonymised and aggregated data about users' statuses and counts messages sent, not including their content. This data cannot be traced back to individual users.", "install_msg": "This cog will immediately start a background process which could use some extra resources. See <https://go.vexcodes.com/c/stattrack#resource-usage> for details.\n\n**Once you load the cog, please wait a few seconds then run the `stattrackinfo` command. If the 'loop time' is None then wait a bit and try again. When it appears, if it's over 30 seconds __you should not use this cog on your bot.__**\n\nThis cog has docs! Check them out at <https://go.vexcodes.com/c/stattrack>", - "max_bot_version": "3.6.0.dev0", - "min_bot_version": "3.4.0", + "min_bot_version": "3.5.1", "min_python_version": [ 3, 8, diff --git a/stattrack/plot.py b/stattrack/plot.py index 20c7aefd..6b21a1fd 100644 --- a/stattrack/plot.py +++ b/stattrack/plot.py @@ -7,7 +7,8 @@ import pandas as pd from plotly import express as px -from stattrack.abc import MixinMeta +from .abc import MixinMeta +from .consts import TRACE_FRIENDLY_NAMES if TYPE_CHECKING: from plotly.graph_objs._figure import Figure @@ -19,29 +20,6 @@ ONE_DAY_SECONDS = 86400 -TRACE_FRIENDLY_NAMES = { - "ping": "Latency", - "loop_time_s": "Loop time", - "users_unique": "Unique", - "users_total": "Total", - "users_humans": "Humans", - "users_bots": "Bots", - "guilds": "Servers", - "channels_total": "Total", - "channels_text": "Text", - "channels_voice": "Voice", - "channels_stage": "Stage", - "channels_cat": "Categories", - "sys_mem": "Memory usage", - "sys_cpu": "CPU Usage", - "command_count": "Commands", - "message_count": "Messages", - "status_online": "Online", - "status_idle": "Idle", - "status_offline": "Offline", - "status_dnd": "DnD", -} - class StatPlot(MixinMeta): def __init__(self) -> None: diff --git a/stattrack/stattrack.py b/stattrack/stattrack.py index abc2fea8..cfb943b9 100644 --- a/stattrack/stattrack.py +++ b/stattrack/stattrack.py @@ -21,7 +21,7 @@ from .vexutils.chat import humanize_bytes from .vexutils.loop import VexLoop -_log = get_vex_logger(__name__) +log = get_vex_logger(__name__) def snapped_utcnow(): @@ -37,7 +37,7 @@ class StatTrack(commands.Cog, StatTrackCommands, StatPlot, metaclass=CompositeMe Data can also be exported with `[p]stattrack export` into a few different formats. """ - __version__ = "1.9.1" + __version__ = "1.10.0" __author__ = "Vexed#0714" def __init__(self, bot: Red) -> None: @@ -67,7 +67,7 @@ async def red_delete_data_for_user(self, **kwargs) -> None: """Nothing to delete""" return - def cog_unload(self) -> None: + async def cog_unload(self) -> None: if self.loop: self.loop.cancel() @@ -79,9 +79,9 @@ def cog_unload(self) -> None: except KeyError: pass - async def async_init(self) -> None: + async def cog_load(self) -> None: if await self.config.version() < 2: - _log.info("Migrating StatTrack config from 1 to 2.") + log.info("Migrating StatTrack config from 1 to 2.") df_conf = await self.config.main_df() if df_conf: # needs migration @@ -91,7 +91,7 @@ async def async_init(self) -> None: df = pandas.DataFrame() await self.driver.write(df) await self.config.version.set(2) - _log.info("Done.") + log.info("Done.") self.loop = self.bot.loop.create_task(self.stattrack_loop()) self.loop_meta = VexLoop("StatTrack loop", 60.0) @@ -146,14 +146,14 @@ async def stattrack_loop(self): await self.bot.wait_until_red_ready() while True: - _log.debug("StatTrack loop has started next iteration") + log.verbose("StatTrack loop has started next iteration") try: self.loop_meta.iter_start() await self.update_stats() self.loop_meta.iter_finish() except Exception as e: self.loop_meta.iter_error(e) - _log.exception( + log.exception( "Something went wrong in the StatTrack loop. The loop will try again shortly.", exc_info=e, ) @@ -169,7 +169,7 @@ async def update_stats(self): if ( now == await self.driver.get_last_index() ): # just reloaded and this min's data collected - _log.debug("Skipping this loop - cog was likely recently reloaded") + log.debug("Skipping this loop - cog was likely recently reloaded") return start = time.monotonic() data = {} @@ -227,16 +227,18 @@ async def update_stats(self): df = pandas.DataFrame(data, index=[snapped_utcnow()]) + log.trace("new data pd obj:\n%s", df) + end = time.monotonic() main_time = round((end - start), 3) - _log.debug(f"Loop finished in {main_time} seconds") + log.trace(f"Loop finished in {main_time} seconds") try: start = time.monotonic() await self.driver.append(df) end = time.monotonic() save_time = round(end - start, 3) - _log.debug(f"SQLite append operation took {save_time} seconds") + log.trace(f"SQLite append operation took {save_time} seconds") except Exception: # could be new schema (columns) start = time.monotonic() old_df = await self.driver.read_all() @@ -244,14 +246,14 @@ async def update_stats(self): await self.driver.write(df) end = time.monotonic() save_time = round(end - start, 3) - _log.debug(f"SQLite write operation took {save_time} seconds") + log.trace(f"SQLite write operation took {save_time} seconds") total_time = main_time + save_time self.last_loop_raw = total_time if total_time > 30.0: # TODO: only warn once + send to owners - _log.warning( + log.warning( "StatTrack loop took a while. This means that it's using lots of resources on " "this machine. You might want to consider unloading or removing the cog. There " "is also a high chance of some datapoints on the graphs being skipped." diff --git a/stattrack/vexutils/meta.py b/stattrack/vexutils/meta.py index cae0ae73..1e963e4c 100644 --- a/stattrack/vexutils/meta.py +++ b/stattrack/vexutils/meta.py @@ -2,11 +2,12 @@ import asyncio import json -from logging import Logger, getLogger from pathlib import Path from typing import Literal, NamedTuple import aiohttp +from red_commons.logging import RedTraceLogger +from red_commons.logging import getLogger as red_get_logger from redbot.core import VersionInfo, commands from redbot.core import version_info as cur_red_version from rich import box as rich_box @@ -16,13 +17,13 @@ from .consts import DOCS_BASE, GREEN_CIRCLE, RED_CIRCLE from .loop import VexLoop -log = getLogger("red.vex-utils") +log = red_get_logger("red.vex-utils") cog_ver_lock = asyncio.Lock() -def get_vex_logger(name: str) -> Logger: +def get_vex_logger(name: str) -> RedTraceLogger: """Get a logger for the given name. Parameters @@ -42,7 +43,7 @@ def get_vex_logger(name: str) -> Logger: else: # otherwise use full path final_name += name - return getLogger(final_name) + return red_get_logger(final_name) def format_help(self: commands.Cog, ctx: commands.Context) -> str: diff --git a/status/__init__.py b/status/__init__.py index 7a86b116..267ef7f3 100644 --- a/status/__init__.py +++ b/status/__init__.py @@ -51,7 +51,4 @@ async def setup(bot: Red) -> None: cog = Status(bot) await out_of_date_check("status", cog.__version__) - await cog.async_init() - r = bot.add_cog(cog) - if r is not None: - await r + await bot.add_cog(cog) diff --git a/status/commands/components.py b/status/commands/components.py new file mode 100644 index 00000000..3dc18a40 --- /dev/null +++ b/status/commands/components.py @@ -0,0 +1,74 @@ +from __future__ import annotations + +import discord +from discord import ui + + +class AddServiceView(ui.View): + def __init__(self, author: discord.Member, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.author_id = author.id + + self.mode: str | None = None + self.webhook: bool | None = None + self.restrict: bool | None = None + + async def interaction_check(self, interaction: discord.Interaction) -> bool: + if interaction.user.id == self.author_id: + return True + + await interaction.response.send_message( + "You do not have permission to interact with this." + ) + return False + + @ui.select( + placeholder="Mode", + options=[ + discord.SelectOption(label="Mode: all", value="all"), + discord.SelectOption(label="Mode: latest", value="latest"), + discord.SelectOption(label="Mode: edit", value="edit"), + ], + ) + async def slt_mode(self, interaction: discord.Interaction, select: ui.Select): + self.mode = select.values[0] + await interaction.response.defer() + + @ui.select( + placeholder="Webhook", + options=[ + discord.SelectOption(label="Webhook: yes", value="yes"), + discord.SelectOption(label="Webhook: no", value="no"), + ], + ) + async def slt_webhook(self, interaction: discord.Interaction, select: ui.Select): + self.webhook = select.values[0] == "yes" + await interaction.response.defer() + + @ui.select( + placeholder="Restrict", + options=[ + discord.SelectOption(label="Restrict: yes", value="yes"), + discord.SelectOption(label="Restrict: no", value="no"), + ], + ) + async def slt_restrict(self, interaction: discord.Interaction, select: ui.Select): + self.restrict = select.values[0] == "yes" + await interaction.response.defer() + + @ui.button(label="Submit", style=discord.ButtonStyle.primary) + async def btn_submit(self, interaction: discord.Interaction, button: ui.Button): + if self.mode is None: + await interaction.response.send_message("Please select a mode.") + return + if self.webhook is None: + await interaction.response.send_message("Please select a webhook option.") + return + if self.restrict is None: + await interaction.response.send_message("Please select a restrict option.") + return + + self.stop() + + await interaction.response.defer() diff --git a/status/commands/status_com.py b/status/commands/status_com.py index b43800a3..c0536ca1 100644 --- a/status/commands/status_com.py +++ b/status/commands/status_com.py @@ -5,7 +5,6 @@ from typing import TYPE_CHECKING, NamedTuple import discord -from discord.channel import TextChannel from redbot.core import commands from redbot.core.utils.chat_formatting import humanize_list, humanize_timedelta, pagify @@ -69,7 +68,11 @@ async def status(self, ctx: commands.Context, service: ServiceConverter): if restrictions := self.service_restrictions_cache.get_guild(ctx.guild.id, service.name): channels = [self.bot.get_channel(channel) for channel in restrictions] channel_list = humanize_list( - [channel.mention for channel in channels if isinstance(channel, TextChannel)], + [ + channel.mention + for channel in channels + if isinstance(channel, (discord.TextChannel, discord.Thread)) + ], style="or", ) if channel_list: diff --git a/status/commands/statusset_com.py b/status/commands/statusset_com.py index 3faf5fb3..b941d885 100644 --- a/status/commands/statusset_com.py +++ b/status/commands/statusset_com.py @@ -1,8 +1,7 @@ from __future__ import annotations -import asyncio from time import time -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Optional, Union import discord from discord.abc import GuildChannel @@ -12,6 +11,7 @@ from tabulate import tabulate from ..commands.command import DynamicHelp, DynamicHelpGroup +from ..commands.components import AddServiceView from ..commands.converters import ModeConverter, ServiceConverter from ..core import FEEDS, SPECIAL_INFO from ..core.abc import MixinMeta @@ -20,13 +20,6 @@ from ..updateloop import SendUpdate, process_json from ..vexutils.chat import inline_hum_list -if discord.__version__.startswith("1"): - from redbot.core.utils.predicates import MessagePredicate -else: - from discord.enums import ButtonStyle - - from ..vexutils.button_pred import PredItem, wait_for_press, wait_for_yes_no - class StatusSetCom(MixinMeta): @commands.guild_only() # type:ignore @@ -45,7 +38,7 @@ async def statusset_add( self, ctx: commands.Context, service: ServiceConverter, - chan: Optional[discord.TextChannel], + chan: Optional[Union[discord.TextChannel, discord.Thread]], ): """ Start getting status updates for the chosen service! @@ -63,7 +56,8 @@ async def statusset_add( if TYPE_CHECKING: assert ctx.guild is not None assert isinstance(ctx.me, Member) - assert isinstance(ctx.channel, GuildChannel) + assert isinstance(ctx.author, Member) + assert isinstance(ctx.channel, (discord.TextChannel, discord.Thread)) channel = chan or ctx.channel @@ -79,133 +73,76 @@ async def statusset_add( f"updates. You can edit it with `{ctx.clean_prefix}statusset edit`." ) - modes = ( - "**All**: Every time the service posts an update on an incident, I will send a new " - "message containing the previous updates as well as the new update. Best used in a " - "fast-moving channel with other users.\n\n" - "**Latest**: Every time the service posts an update on an incident, I will send a new " - "message containing only the latest update. Best used in a dedicated status channel.\n" - "\n**Edit**: When a new incident is created, I will sent a new message. When this " - "incident is updated, I will then add the update to the original message. Best used " - "in a dedicated status channel.\n\n" + view = AddServiceView(ctx.author) + embed = discord.Embed(title="Options") + embed.set_footer(text="If you don't see the options bellow, update your client.") + embed.add_field( + name="Mode", + value=( + "**All**: Every time the service posts an update on an incident, I will send a new" + " message containing the previous updates as well as the new update. Best used in" + " a fast-moving channel with other users.\n**Latest**: Every time the service" + " posts an update on an incident, I will send a new message containing only the" + " latest update. Best used in a dedicated status channel.\n**Edit**: When a new" + " incident is created, I will sent a new message. When this incident is updated, I" + " will then add the update to the original message. Best used in a dedicated" + " status channel." + ), + inline=False, ) - - # === MODE === - - msg = ( - "You have 3 minutes to answer each question.\nIf you aren't sure what to choose, take " - f"a look at the **`{ctx.clean_prefix}statusset preview`** command.\n\n**What mode do " - f"you want to use?**\n\n{modes}" + embed.add_field( + name="Webhook", + value=( + "If you choose yes, status updates will be sent by a webhook with" + f" {service.friendly}'s logo and with the name if `{service.friendly} Status" + " Update`, instead of my avatar and name." + ), + inline=False, + ) + embed.add_field( + name="Restrict", + value=( + f"Restrict access to {service.friendly} in the" + f" `{ctx.clean_prefix}status` command. If there's an incident, members will" + f" instead be redirected to {channel.mention} and any other channels that you've" + f" set to receive {service.friendly} status updates which have restrict enabled." + ), + inline=False, ) - try: - if discord.__version__.startswith("1"): - await ctx.send(msg) - - # really shouldn't monkey patch this - mode = await ModeConverter.convert( # type:ignore - None, - ctx, - ( - await self.bot.wait_for( - "message", check=MessagePredicate.same_context(ctx), timeout=120 - ) - ).content, - ) - else: - mode = await wait_for_press( - ctx, - [ - PredItem("all", ButtonStyle.blurple, "All"), - PredItem("latest", ButtonStyle.blurple, "Latest"), - PredItem("edit", ButtonStyle.blurple, "Edit"), - ], - content=msg, - ) - except asyncio.TimeoutError: - return await ctx.send("Timed out. Cancelling.") - except commands.BadArgument as e: - return await ctx.send(e) - - # === WEBHOOK === + await ctx.send(embed=embed, view=view) + timeout = await view.wait() - if service.name == "discord": # webhook names cannot contain "discord" - webhook = False - else: - if channel.permissions_for(ctx.me).manage_webhooks: - msg = ( - "**Would you like to use a webhook?** (yes or no answer)\nUsing a webhook " - "means that the status updates will be sent with the avatar as " - f"{service.friendly}'s logo and the name will be `{service.friendly} Status " - "Update`, instead of my avatar and name." - ) + if timeout: + return - try: - if discord.__version__.startswith("1"): - await ctx.send(msg) - pred = MessagePredicate.yes_or_no(ctx) - await self.bot.wait_for("message", check=pred, timeout=120) - webhook = pred.result - else: - webhook = await wait_for_yes_no(ctx, msg) - except asyncio.TimeoutError: - return await ctx.send("Timed out. Cancelling.") - - if webhook: - # maybe creating a webhook so users feel it worked - existing_webhook = any( - hook.name == ctx.me.name for hook in await channel.webhooks() - ) - if not existing_webhook: - await channel.create_webhook( - name=ctx.me.name, reason="Created for status updates." - ) - else: - await ctx.send( - "I would ask about whether you want me to send updates as a webhook (so they " - "match the service), however I don't have the `manage webhooks` permission." + if view.webhook: + webhook_channel = channel.parent if isinstance(channel, discord.Thread) else channel + if webhook_channel is None: # Thread.parent can be None + return await ctx.send("I can't create a webhook in this thread.") + existing_webhook = any( + hook.name == ctx.me.name for hook in await webhook_channel.webhooks() + ) + if not existing_webhook: + await webhook_channel.create_webhook( + name=ctx.me.name, reason="Created for status updates." ) - webhook = False - - # === RESTRICT === - - msg = ( - f"**Would you like to restrict access to {service.friendly} in the " # type:ignore - f"`{ctx.clean_prefix}status` command?** (yes or no answer)\nThis will reduce spam. If " - f"there's an incident, members will instead be redirected to {channel.mention} and " - f"any other channels that you've set to receive {service.friendly} status updates " - "which have restrict enabled." - ) - - try: - if discord.__version__.startswith("1"): - await ctx.send(msg) - pred = MessagePredicate.yes_or_no(ctx) - await self.bot.wait_for("message", check=pred, timeout=120) - restrict = pred.result - else: - restrict = await wait_for_yes_no(ctx, msg) - except asyncio.TimeoutError: - return await ctx.send("Timed out. Cancelling.") - if restrict is True: + if view.restrict: async with self.config.guild(ctx.guild).service_restrictions() as sr: try: sr[service.name].append(channel.id) except KeyError: sr[service.name] = [channel.id] - self.service_restrictions_cache.add_restriction( - ctx.guild.id, service.name, channel.id - ) - - # === FINISH === + self.service_restrictions_cache.add_restriction(ctx.guild.id, service.name, channel.id) if service.name not in self.used_feeds.get_list(): # need to get it up to date so no mass sending on add - await self.get_initial_data(service.name) + async with ctx.typing(): + await self.get_initial_data(service.name) - settings = {"mode": mode, "webhook": webhook, "edit_id": {}} + settings = {"mode": view.mode, "webhook": view.webhook, "edit_id": {}} await self.config.channel(channel).feeds.set_raw( # type:ignore service.name, value=settings ) @@ -225,7 +162,7 @@ async def statusset_remove( self, ctx: commands.Context, service: ServiceConverter, - chan: Optional[discord.TextChannel], + chan: Optional[Union[discord.TextChannel, discord.Thread]] = None, ): """ Stop status updates for a specific service in this server. @@ -238,7 +175,7 @@ async def statusset_remove( """ # guild check on group if TYPE_CHECKING: - assert isinstance(ctx.channel, GuildChannel) + assert isinstance(ctx.channel, (GuildChannel, discord.Thread)) assert ctx.guild is not None channel = chan or ctx.channel @@ -388,7 +325,7 @@ async def statusset_preview( """ # guild check on group if TYPE_CHECKING: - assert isinstance(ctx.channel, GuildChannel) + assert isinstance(ctx.channel, (discord.TextChannel, discord.Thread)) assert isinstance(ctx.me, Member) if webhook and not ctx.channel.permissions_for(ctx.me).manage_messages: @@ -424,7 +361,9 @@ async def statusset_preview( ).send({ctx.channel.id: {"mode": mode, "webhook": webhook, "edit_id": {}}}) @statusset.command(name="clear", aliases=["erase"], usage="[channel]") - async def statusset_clear(self, ctx: commands.Context, *, chan: Optional[discord.TextChannel]): + async def statusset_clear( + self, ctx: commands.Context, *, chan: Optional[Union[discord.TextChannel, discord.Thread]] + ): """ Remove all feeds from a channel. @@ -436,7 +375,7 @@ async def statusset_clear(self, ctx: commands.Context, *, chan: Optional[discord """ # guild check on group if TYPE_CHECKING: - assert isinstance(ctx.channel, GuildChannel) + assert isinstance(ctx.channel, (discord.TextChannel, discord.Thread)) assert ctx.guild is not None channel = chan or ctx.channel @@ -460,7 +399,7 @@ async def edit(self, ctx: commands.Context): async def edit_mode( self, ctx: commands.Context, - chan: Optional[discord.TextChannel], + chan: Optional[Union[discord.TextChannel, discord.Thread]], service: ServiceConverter, mode: ModeConverter, ): @@ -485,7 +424,7 @@ async def edit_mode( """ # guild check on group if TYPE_CHECKING: - assert isinstance(ctx.channel, GuildChannel) + assert isinstance(ctx.channel, (discord.TextChannel, discord.Thread)) channel = chan or ctx.channel @@ -515,7 +454,7 @@ async def edit_mode( async def edit_webhook( self, ctx: commands.Context, - chan: Optional[discord.TextChannel], + chan: Optional[Union[discord.TextChannel, discord.Thread]], service: ServiceConverter, webhook: bool, ): @@ -532,7 +471,7 @@ async def edit_webhook( """ # guild check on group if TYPE_CHECKING: - assert isinstance(ctx.channel, GuildChannel) + assert isinstance(ctx.channel, (discord.TextChannel, discord.Thread)) assert isinstance(ctx.me, Member) channel = chan or ctx.channel @@ -576,7 +515,7 @@ async def edit_webhook( async def edit_restrict( self, ctx: commands.Context, - chan: Optional[discord.TextChannel], + chan: Optional[Union[discord.TextChannel, discord.Thread]], service: ServiceConverter, restrict: bool, ): @@ -593,7 +532,7 @@ async def edit_restrict( """ # guild check on group if TYPE_CHECKING: - assert isinstance(ctx.channel, GuildChannel) + assert isinstance(ctx.channel, (discord.TextChannel, discord.Thread)) assert ctx.guild is not None channel = chan or ctx.channel diff --git a/status/core/abc.py b/status/core/abc.py index dd9d62d6..666862d0 100644 --- a/status/core/abc.py +++ b/status/core/abc.py @@ -50,7 +50,3 @@ class MixinMeta(ABC): @abstractmethod async def get_initial_data(self, specific_service: Optional[SERVICE_LITERAL] = None) -> None: raise NotImplementedError() - - @abstractmethod - async def async_init(self) -> None: - raise NotImplementedError() diff --git a/status/core/core.py b/status/core/core.py index 62023857..08d6b6cf 100644 --- a/status/core/core.py +++ b/status/core/core.py @@ -42,7 +42,7 @@ class Status( make an issue on the GitHub repo (or even better a PR!). """ - __version__ = "2.5.6" + __version__ = "2.5.7" __author__ = "Vexed#0714" def __init__(self, bot: Red) -> None: @@ -76,9 +76,9 @@ def __init__(self, bot: Red) -> None: self.bot.add_dev_env_value("status", lambda _: self) self.bot.add_dev_env_value("statusapi", lambda _: self.statusapi) self.bot.add_dev_env_value("sendupdate", lambda _: SendUpdate) - log.debug("Added dev env vars.") + log.trace("Added dev env vars.") except Exception: - log.exception("Unable to add dev env vars.", exc_info=True) + log.debug("Unable to add dev env vars.", exc_info=True) def format_help_for_context(self, ctx: commands.Context) -> str: """Thanks Sinbad.""" @@ -88,9 +88,9 @@ async def red_delete_data_for_user(self, **kwargs) -> None: """Nothing to delete""" return - def cog_unload(self) -> None: + async def cog_unload(self) -> None: self.loop.cancel() - self.bot.loop.create_task(self.session.close()) + await self.session.close() try: self.bot.remove_dev_env_value("status") @@ -101,7 +101,7 @@ def cog_unload(self) -> None: log.info("Status unloaded.") - async def async_init(self) -> None: + async def cog_load(self) -> None: if await self.config.version() == 2: await self.migrate_to_v3() await self.config.incidents.clear() @@ -125,11 +125,16 @@ async def async_init(self) -> None: # this will start the loop self.ready.set() + log.trace("status ready") + async def get_initial_data(self, specific_service: Optional[SERVICE_LITERAL] = None) -> None: """Start with initial data from services.""" old_ids = [] - for service, settings in FEEDS.items(): - log.debug(f"Starting {service}.") + services_to_get = ( + {specific_service: FEEDS[specific_service]} if specific_service else FEEDS + ) + for service, settings in services_to_get.items(): + log.trace(f"Starting {service}.") try: incidents, etag, status = await self.statusapi.incidents(settings["id"]) if status != 200: diff --git a/status/info.json b/status/info.json index 5cd22c67..24bee72a 100644 --- a/status/info.json +++ b/status/info.json @@ -6,8 +6,7 @@ "description": "This cog can automatically send updates form various services including Discord, Cloudflare and GitHub. Members can also use a command to check incidents on-demand. Support all Statuspage.io which I have added.", "end_user_data_statement": "This cog does not persistently store data or metadata about users.", "install_msg": "Thanks for installing! The auto commands are all in `statusset`, and anyone can check statuses with the `status` command. Make sure to load the cog first.\n\nThis cog has docs! Check them out at <https://go.vexcodes.com/c/status>", - "max_bot_version": "3.6.0.dev0", - "min_bot_version": "3.4.6", + "min_bot_version": "3.5.1", "min_python_version": [ 3, 8, diff --git a/status/objects/channel.py b/status/objects/channel.py index 04922046..179ae7aa 100644 --- a/status/objects/channel.py +++ b/status/objects/channel.py @@ -2,14 +2,14 @@ from dataclasses import dataclass -from discord import TextChannel +import discord from ..core import MODES_LITERAL @dataclass class ChannelData: - channel: TextChannel + channel: discord.TextChannel | discord.Thread mode: MODES_LITERAL webhook: bool embed: bool diff --git a/status/updateloop/sendupdate.py b/status/updateloop/sendupdate.py index 904ed0da..81fd5b58 100644 --- a/status/updateloop/sendupdate.py +++ b/status/updateloop/sendupdate.py @@ -5,7 +5,7 @@ from time import monotonic import discord -from discord import Embed, Message, TextChannel +from discord import Embed, Message, TextChannel, Thread from redbot.core.bot import Red from ..core import FEEDS, UPDATE_NAME @@ -21,7 +21,7 @@ from ..vexutils import get_vex_logger from .utils import get_channel_data, get_webhook -_log = get_vex_logger(__name__) +log = get_vex_logger(__name__) class SendUpdate: @@ -68,19 +68,19 @@ async def send(self, channels: dict[int, ConfChannelSettings]) -> None: await asyncio.sleep(1) start = monotonic() - _log.info(f"Sending update for {self.service} to {len(channels)} channels...") + log.info(f"Sending update for {self.service} to {len(channels)} channels...") for c_id, settings in channels.items(): try: await self._send_updated_feed(c_id, settings) except Exception: - return _log.warning( + return log.warning( f"Something went wrong sending to {c_id} - skipping.", exc_info=True ) end = monotonic() time = floor(end - start) or "under a" - _log.info(f"Sending update for {self.service} took {time} second(s).") + log.verbose(f"Sending update for {self.service} took {time} second(s).") async def _send_updated_feed(self, c_id: int, settings: ConfChannelSettings) -> None: """Send feed decalred in init to a channel. @@ -123,12 +123,12 @@ async def _send_updated_feed(self, c_id: int, settings: ConfChannelSettings) -> # TODO: maybe try to do some DRY on the next 3 - async def _send_webhook(self, channel: TextChannel, embed: Embed) -> None: + async def _send_webhook(self, channel: TextChannel | Thread, embed: Embed) -> None: """Send a webhook to the specified channel Parameters ---------- - channel : TextChannel + channel : TextChannel | Thread Channel to send to embed : Embed Embed to use @@ -148,6 +148,7 @@ async def _send_webhook(self, channel: TextChannel, embed: Embed) -> None: avatar_url=ICON_BASE.format(self.service), embed=embed, wait=True, + thread=channel if isinstance(channel, Thread) else discord.utils.MISSING, ) await self.config_wrapper.update_edit_id( channel.id, self.service, self.incidentdata.incident_id, sent_webhook.id @@ -158,14 +159,15 @@ async def _send_webhook(self, channel: TextChannel, embed: Embed) -> None: username=UPDATE_NAME.format(FEEDS[self.service]["friendly"]), avatar_url=ICON_BASE.format(self.service), embed=embed, + thread=channel if isinstance(channel, Thread) else discord.utils.MISSING, ) - async def _send_embed(self, channel: TextChannel, embed: Embed) -> None: + async def _send_embed(self, channel: TextChannel | Thread, embed: Embed) -> None: """Send an embed to the specified channel Parameters ---------- - channel : TextChannel + channel : TextChannel | Thread Channel to send to embed : Embed Embed to use @@ -190,12 +192,12 @@ async def _send_embed(self, channel: TextChannel, embed: Embed) -> None: else: await channel.send(embed=embed) - async def _send_plain(self, channel: TextChannel, msg: str) -> None: + async def _send_plain(self, channel: TextChannel | Thread, msg: str) -> None: """Send a plain message to the specified channel Parameters ---------- - channel : TextChannel + channel : TextChannel | Thread Channel to send to msg : str Message to send diff --git a/status/updateloop/updatechecker.py b/status/updateloop/updatechecker.py index 3614a3fd..50a7de98 100644 --- a/status/updateloop/updatechecker.py +++ b/status/updateloop/updatechecker.py @@ -14,7 +14,7 @@ from .processfeed import process_json from .sendupdate import SendUpdate -_log = get_vex_logger(__name__) +log = get_vex_logger(__name__) class StatusLoop(MixinMeta): @@ -35,9 +35,9 @@ async def status_loop(self): while True: self.loop_meta.iter_start() - _log.debug("Update loop started.") + log.verbose("Update loop started.") if not self.used_feeds.get_list(): - return _log.debug("Nothing to do - no channels have registered for auto updates.") + return log.verbose("Nothing to do - no channels have registered for auto updates.") start = monotonic() try: @@ -46,12 +46,13 @@ async def status_loop(self): self.loop_meta.iter_finish() except asyncio.TimeoutError as e: self.loop_meta.iter_error(e) - _log.warning( + log.warning( "Update checking timed out after 4 minutes. If this happens a lot contact " "Vexed." ) except Exception as e: - _log.error( + self.loop_meta.iter_error(e) + log.error( "Unable to check and send updates. Some services were likely missed. The " "might be picked up on the next loop. You may want to report this to Vexed.", exc_info=e, @@ -59,7 +60,7 @@ async def status_loop(self): end = monotonic() total = round(end - start, 1) - _log.debug(f"Update loop finished in {total}s.") + log.trace(f"Update loop finished in {total}s.") self.actually_send = True @@ -73,36 +74,36 @@ async def _check_for_updates(self) -> None: FEEDS[service]["id"], self.etags.get(f"incidents-{service}", "") ) except asyncio.TimeoutError: - _log.warning( + log.warning( f"Timeout checking {service}. Any missed updates will be caught on the next " "loop." ) continue except (aiohttp.ClientError, ClientOSError): - _log.warning( + log.warning( f"Unable to check {service}. Any missed updates will be caught on the next " "loop." ) continue except Exception: # want to catch everything and anything - _log.error(f"Something unexpected went wrong checking {service}.", exc_info=True) + log.error(f"Something unexpected went wrong checking {service}.", exc_info=True) continue if status == 304: - _log.debug(f"Incidents: no update for {service} - 304") + log.trace(f"Incidents: no update for {service} - 304") self.last_checked.update_time(service) elif status == 200: - _log.debug(f"Incidents: update detected for {service} - 200") + log.trace(f"Incidents: update detected for {service} - 200") self.etags[f"incidents-{service}"] = new_etag # dont need to update checked time as above because _maybe_send_update does it await self._maybe_send_update(resp_json, service, "incidents") elif str(status)[0] == "5": - _log.info( + log.debug( f"I was unable to get an update for {service} due to problems on their side. " f"(HTTP error {status})" ) else: - _log.warning( + log.warning( f"Unexpected status code received from {service}: {status}. Please report " "this to Vexed." ) @@ -114,36 +115,36 @@ async def _check_for_updates(self) -> None: FEEDS[service]["id"], self.etags.get(f"scheduled-{service}", "") ) except asyncio.TimeoutError: - _log.warning( + log.warning( f"Timeout checking {service}. Any missed updates will be caught on the next " "loop." ) continue except (aiohttp.ClientError, ClientOSError): - _log.warning( + log.warning( f"Unable to check {service}. Any missed updates will be caught on the next " "loop." ) continue except Exception: # want to catch everything and anything - _log.error(f"Something unexpected went wrong checking {service}.", exc_info=True) + log.error(f"Something unexpected went wrong checking {service}.", exc_info=True) continue if status == 304: - _log.debug(f"Scheduled: no update for {service} - 304") + log.trace(f"Scheduled: no update for {service} - 304") self.last_checked.update_time(service) elif status == 200: - _log.debug(f"Scheduled: update detected for {service} - 200") + log.trace(f"Scheduled: update detected for {service} - 200") self.etags[f"scheduled-{service}"] = new_etag # dont need to update checked time as above because _maybe_send_update does it await self._maybe_send_update(resp_json, service, "scheduled") elif str(status)[0] == "5": - _log.info( + log.debug( f"I was unable to get an update for {service} due to problems on their side. " f"(HTTP error {status})" ) else: - _log.warning( + log.warning( f"Unexpected status code received from {service}: {status}. Please report " "this to Vexed." ) @@ -154,7 +155,7 @@ async def _maybe_send_update( real = await self._check_real_update(process_json(resp_json, type), service) if not real: - return _log.debug(f"Ghost status update for {service} ({type}) detected.") + return log.trace(f"Ghost status update for {service} ({type}) detected.") # skip just after migration if not self.actually_send: @@ -162,7 +163,7 @@ async def _maybe_send_update( if len(real) > 3: real = real[:3] # latest 3 - _log.warning(f"Lots of updates detected for {service}. I will only send the latest 3.") + log.warning(f"Lots of updates detected for {service}. I will only send the latest 3.") for update in real: channels = await self.config_wrapper.get_channels(service) @@ -189,7 +190,7 @@ async def _check_real_update( new_fields = [] for field in incidentdata.fields: if field.update_id not in stored_ids: - _log.debug( + log.trace( f"New field detected with ID {field.update_id} on incident " f"{incidentdata.incident_id}" ) diff --git a/status/updateloop/utils.py b/status/updateloop/utils.py index 9b69f3d0..93d640db 100644 --- a/status/updateloop/utils.py +++ b/status/updateloop/utils.py @@ -1,6 +1,8 @@ +from __future__ import annotations + from typing import TYPE_CHECKING -from discord import TextChannel, Webhook +from discord import TextChannel, Thread, Webhook from redbot.core.bot import Red from ..objects import ChannelData, CogDisabled, ConfChannelSettings, NoPermission, NotFound @@ -9,12 +11,12 @@ _log = get_vex_logger(__name__) -async def get_webhook(channel: TextChannel) -> Webhook: +async def get_webhook(channel: TextChannel | Thread) -> Webhook: """Get, or create, a webhook for the specified channel and return it. Parameters ---------- - channel : TextChannel + channel : TextChannel | Thread Target channel Returns @@ -22,6 +24,11 @@ async def get_webhook(channel: TextChannel) -> Webhook: Webhook Valid webhook """ + if isinstance(channel, Thread): + if channel.parent is None: + raise ValueError("Thread does not have a parent; cannot have webhooks") + channel = channel.parent + for webhook in await channel.webhooks(): if webhook.name == channel.guild.me.name: return webhook @@ -68,7 +75,7 @@ async def get_channel_data(bot: Red, c_id: int, settings: ConfChannelSettings) - raise NotFound if TYPE_CHECKING: - assert isinstance(channel, TextChannel) + assert isinstance(channel, (TextChannel, Thread)) if await bot.cog_disabled_in_guild_raw("Status", channel.guild.id): _log.info( diff --git a/status/vexutils/meta.py b/status/vexutils/meta.py index cae0ae73..1e963e4c 100644 --- a/status/vexutils/meta.py +++ b/status/vexutils/meta.py @@ -2,11 +2,12 @@ import asyncio import json -from logging import Logger, getLogger from pathlib import Path from typing import Literal, NamedTuple import aiohttp +from red_commons.logging import RedTraceLogger +from red_commons.logging import getLogger as red_get_logger from redbot.core import VersionInfo, commands from redbot.core import version_info as cur_red_version from rich import box as rich_box @@ -16,13 +17,13 @@ from .consts import DOCS_BASE, GREEN_CIRCLE, RED_CIRCLE from .loop import VexLoop -log = getLogger("red.vex-utils") +log = red_get_logger("red.vex-utils") cog_ver_lock = asyncio.Lock() -def get_vex_logger(name: str) -> Logger: +def get_vex_logger(name: str) -> RedTraceLogger: """Get a logger for the given name. Parameters @@ -42,7 +43,7 @@ def get_vex_logger(name: str) -> Logger: else: # otherwise use full path final_name += name - return getLogger(final_name) + return red_get_logger(final_name) def format_help(self: commands.Cog, ctx: commands.Context) -> str: diff --git a/system/__init__.py b/system/__init__.py index 700c9668..d3889f8b 100644 --- a/system/__init__.py +++ b/system/__init__.py @@ -17,6 +17,4 @@ async def setup(bot: Red) -> None: cog = System(bot) await out_of_date_check("system", cog.__version__) - r = bot.add_cog(cog) - if r is not None: - await r + await bot.add_cog(cog) diff --git a/system/backend.py b/system/backend.py index 5243b3b3..886ba2b8 100644 --- a/system/backend.py +++ b/system/backend.py @@ -77,7 +77,7 @@ async def get_cpu() -> dict[str, str]: return data -async def get_mem() -> dict[str, str]: +def get_mem() -> dict[str, str]: """Get memory metrics""" physical = psutil.virtual_memory() swap = psutil.swap_memory() @@ -97,7 +97,7 @@ async def get_mem() -> dict[str, str]: return data -async def get_sensors(fahrenheit: bool) -> dict[str, str]: +def get_sensors(fahrenheit: bool) -> dict[str, str]: """Get metrics from sensors""" temp = psutil.sensors_temperatures(fahrenheit) fans = psutil.sensors_fans() @@ -123,20 +123,18 @@ async def get_sensors(fahrenheit: bool) -> dict[str, str]: return data -async def get_users(embed: bool) -> dict[str, str]: +def get_users() -> dict[str, str]: """Get users connected""" users: list[psutil._common.suser] = psutil.users() - e = "`" if embed else "" - data = {} for user in users: - data[f"{e}{user.name}{e}"] = "[Terminal] {}\n".format(user.terminal or "Unknown") + data[f"`{user.name}`"] = "[Terminal] {}\n".format(user.terminal or "Unknown") started = datetime.datetime.fromtimestamp(user.started).strftime("%Y-%m-%d at %H:%M:%S") - data[f"{e}{user.name}{e}"] += f"[Started] {started}\n" + data[f"`{user.name}`"] += f"[Started] {started}\n" if not psutil.WINDOWS: - data[f"{e}{user.name}{e}"] += f"[PID] {user.pid}" + data[f"`{user.name}`"] += f"[PID] {user.pid}" return data @@ -146,7 +144,7 @@ class PartitionData(TypedDict): usage: psutil._common.sdiskusage -async def get_disk(embed: bool) -> dict[str, str]: +def get_disk() -> dict[str, str]: """Get disk info""" partitions = psutil.disk_partitions() partition_data: dict[str, PartitionData] = {} @@ -161,8 +159,6 @@ async def get_disk(embed: bool) -> dict[str, str]: except Exception: continue - e = "`" if embed else "" - data = {} for k, v in partition_data.items(): @@ -171,10 +167,10 @@ async def get_disk(embed: bool) -> dict[str, str]: if v["usage"].total > 1073741824 else f"{humanize_bytes(v['usage'].total)}" ) - data[f"{e}{k}{e}"] = f"[Usage] {v['usage'].percent} %\n" - data[f"{e}{k}{e}"] += f"[Total] {total_avaliable}\n" - data[f"{e}{k}{e}"] += f"[Filesystem] {v['part'].fstype}\n" - data[f"{e}{k}{e}"] += f"[Mount point] {v['part'].mountpoint}\n" + data[f"`{k}`"] = f"[Usage] {v['usage'].percent} %\n" + data[f"`{k}`"] += f"[Total] {total_avaliable}\n" + data[f"`{k}`"] += f"[Filesystem] {v['part'].fstype}\n" + data[f"`{k}`"] += f"[Mount point] {v['part'].mountpoint}\n" return data @@ -211,7 +207,7 @@ async def get_proc() -> dict[str, str]: return data -async def get_net() -> dict[str, str]: +def get_net() -> dict[str, str]: """Get network stats. May have reset from zero at some point.""" net = psutil.net_io_counters() @@ -224,7 +220,7 @@ async def get_net() -> dict[str, str]: return data -async def get_uptime() -> dict[str, str]: +def get_uptime() -> dict[str, str]: """Get uptime info""" boot_time = datetime.datetime.fromtimestamp(psutil.boot_time()) diff --git a/system/components/view.py b/system/components/view.py new file mode 100644 index 00000000..c3d34c54 --- /dev/null +++ b/system/components/view.py @@ -0,0 +1,78 @@ +from __future__ import annotations + +import sys +from typing import TYPE_CHECKING + +import discord + +if TYPE_CHECKING: + from ..system import System + +FRIENDLY_NAMES = { + "cpu": "CPU", + "mem": "Memory", + "sensors": "Sensors", + "users": "Users", + "disk": "Disk", + "proc": "Processes", + "net": "Network", + "uptime": "Uptime", + "red": "Red", + "all": "All", +} + + +class SystemDropdown(discord.ui.Select): + async def callback(self, interaction: discord.Interaction): + assert isinstance(self.view, SystemView) + if self.values[0] in ("all", "proc"): # takes too long + await interaction.response.defer(ephemeral=True, thinking=True) + await interaction.message.edit( + embed=await self.view.cog_methods[self.values[0]](interaction.channel), + ) + await interaction.followup.send("Done!") + else: + await interaction.response.edit_message( + embed=await self.view.cog_methods[self.values[0]](interaction.channel), + ) + + +class SystemView(discord.ui.View): + def __init__(self, author: discord.User | discord.Member, cog: System, initial_metric: str): + super().__init__() + + self.author = author + + self.cog_methods = { + "cpu": cog.prep_cpu_msg, + "mem": cog.prep_mem_msg, + "sensors": cog.prep_sensors_msg, + "users": cog.prep_users_msg, + "disk": cog.prep_disk_msg, + "proc": cog.prep_proc_msg, + "net": cog.prep_net_msg, + "uptime": cog.prep_uptime_msg, + "red": cog.prep_red_msg, + "all": cog.prep_all_msg, + } + + options = [] + for metric, friendly in FRIENDLY_NAMES.items(): + if sys.platform != "linux" and metric == "sensors": + continue + options.append( + discord.SelectOption( + label=friendly, + value=metric, + ) + ) + + self.add_item(SystemDropdown(options=options, placeholder="Change metric")) + + async def interaction_check(self, interaction: discord.Interaction) -> bool: + if interaction.user != self.author: + await interaction.response.send_message( + "You are not authorized to interact with this.", ephemeral=True + ) + return False + return True diff --git a/system/info.json b/system/info.json index 9227e8c3..2c0f2c8b 100644 --- a/system/info.json +++ b/system/info.json @@ -6,8 +6,7 @@ "description": "Check system metrics on the host device, such as CPU or RAM usage", "end_user_data_statement": "This cog does not persistently store data or metadata about users.", "install_msg": "Thanks for installing!\n\nThis cog supports **both embeds and non-embeds**. Embeds are enabled by default, you can disable them with `[p]embedset`. Because of how Red handles this, you will need to do this for each subcommand.\nThis DOES NOT try to be similar to standard console commands.\n\nThis cog has docs! Check them out at <https://go.vexcodes.com/c/system>", - "max_bot_version": "3.6.0.dev0", - "min_bot_version": "3.4.0", + "min_bot_version": "3.5.1", "min_python_version": [ 3, 8, diff --git a/system/system.py b/system/system.py index 984c6f54..d1138865 100644 --- a/system/system.py +++ b/system/system.py @@ -9,6 +9,8 @@ from redbot.core.bot import Red from redbot.core.utils.chat_formatting import humanize_timedelta +from system.components.view import SystemView + from .backend import ( box, get_cpu, @@ -23,13 +25,11 @@ up_for, ) from .command import DynamicHelp -from .vexutils import format_help, format_info, get_vex_logger +from .vexutils import format_help, format_info if TYPE_CHECKING: from discord.types.embed import EmbedField -log = get_vex_logger(__name__) - UNAVAILABLE = "\N{CROSS MARK} This command isn't available on your system." ZERO_WIDTH = "\u200b" @@ -44,7 +44,7 @@ class System(commands.Cog): See the help for individual commands for detailed limitations. """ - __version__ = "1.3.10" + __version__ = "1.4.0" __author__ = "Vexed#0714" def __init__(self, bot: Red) -> None: @@ -102,6 +102,7 @@ def finalise_embed(self, e: discord.Embed) -> discord.Embed: return e + @commands.has_permissions(embed_links=True) @commands.is_owner() @commands.group() async def system(self, ctx: commands.Context): @@ -126,25 +127,20 @@ async def system_cpu(self, ctx: commands.Context): on Linux it's current and per-core. """ async with ctx.typing(): - data = await get_cpu() - percent = data["percent"] - time = data["time"] - freq = data["freq"] - if await ctx.embed_requested(): - embed = discord.Embed(title="CPU Metrics", colour=await ctx.embed_colour()) - embed.add_field(name="CPU Usage", value=box(percent)) - embed.add_field(name="CPU Times", value=box(time)) - extra = data["freq_note"] - embed.add_field(name=f"CPU Frequency{extra}", value=box(freq)) - await ctx.send(embed=self.finalise_embed(embed)) - else: - msg = "**CPU Metrics**\n" - to_box = f"CPU Usage\n{percent}\n" - to_box += f"CPU Times\n{time}\n" - extra = data["freq_note"] - to_box += f"CPU Frequency{extra}\n{freq}\n" - msg += box(to_box) - await ctx.send(msg) + embed = await self.prep_cpu_msg(ctx.channel) + await ctx.send(embed=embed, view=SystemView(ctx.author, self, "cpu")) + + async def prep_cpu_msg(self, channel: discord.abc.Messageable) -> discord.Embed | str: + data = await get_cpu() + percent = data["percent"] + time = data["time"] + freq = data["freq"] + embed = discord.Embed(title="CPU Metrics", colour=await self.bot.get_embed_color(channel)) + embed.add_field(name="CPU Usage", value=box(percent)) + embed.add_field(name="CPU Times", value=box(time)) + extra = data["freq_note"] + embed.add_field(name=f"CPU Frequency{extra}", value=box(freq)) + return self.finalise_embed(embed) @system.command( name="mem", aliases=["memory", "ram"], cls=DynamicHelp, supported_sys=True # all systems @@ -158,20 +154,18 @@ async def system_mem(self, ctx: commands.Context): Platforms: Windows, Linux, Mac OS """ - data = await get_mem() + await ctx.send( + embed=await self.prep_mem_msg(ctx.channel), view=SystemView(ctx.author, self, "mem") + ) + + async def prep_mem_msg(self, channel: discord.abc.Messageable) -> discord.Embed | str: + data = get_mem() physical = data["physical"] swap = data["swap"] - if await ctx.embed_requested(): - embed = discord.Embed(title="Memory", colour=await ctx.embed_colour()) - embed.add_field(name="Physical Memory", value=box(physical)) - embed.add_field(name="SWAP Memory", value=box(swap)) - await ctx.send(embed=self.finalise_embed(embed)) - else: - msg = "**Memory**\n" - to_box = f"Physical Memory\n{physical}\n" - to_box += f"SWAP Memory\n{swap}\n" - msg += box(to_box) - await ctx.send(msg) + embed = discord.Embed(title="Memory", colour=await self.bot.get_embed_color(channel)) + embed.add_field(name="Physical Memory", value=box(physical)) + embed.add_field(name="SWAP Memory", value=box(swap)) + return self.finalise_embed(embed) @system.command( name="sensors", @@ -192,20 +186,21 @@ async def system_sensors(self, ctx: commands.Context, fahrenheit: bool = False): if not psutil.LINUX: return await ctx.send(UNAVAILABLE) - data = await get_sensors(fahrenheit) + await ctx.send( + embed=await self.prep_sensors_msg(ctx.channel, fahrenheit), + view=SystemView(ctx.author, self, "sensors"), + ) + + async def prep_sensors_msg( + self, channel: discord.abc.Messageable, fahrenheit: bool = False + ) -> discord.Embed: + data = get_sensors(fahrenheit) temp = data["temp"] fans = data["fans"] - if await ctx.embed_requested(): - embed = discord.Embed(title="Sensors", colour=await ctx.embed_colour()) - embed.add_field(name="Temperatures", value=box(temp)) - embed.add_field(name="Fans", value=box(fans)) - await ctx.send(embed=self.finalise_embed(embed)) - else: - msg = "**Temperature**\n" - to_box = f"Temperatures\n{temp}\n" - to_box += f"Fans\n{fans}\n" - msg += box(to_box) - await ctx.send(msg) + embed = discord.Embed(title="Sensors", colour=await self.bot.get_embed_colour(channel)) + embed.add_field(name="Temperatures", value=box(temp)) + embed.add_field(name="Fans", value=box(fans)) + return self.finalise_embed(embed) @system.command(name="users", cls=DynamicHelp, supported_sys=True) # all systems async def system_users(self, ctx: commands.Context): @@ -218,35 +213,26 @@ async def system_users(self, ctx: commands.Context): Platforms: Windows, Linux, Mac OS Note: PID is not available on Windows. Terminal is usually `Unknown` """ - embed = await ctx.embed_requested() - data = await get_users(embed) - - if embed: - embed = discord.Embed(title="Users", colour=await ctx.embed_colour()) - if not data: - embed.add_field( - name="No one's logged in", - value=( - "If you're expecting data here, you're probably using WSL or other " - "virtualisation technology" - ), - ) - - for name, userdata in data.items(): - embed.add_field(name=name, value=box(userdata)) - await ctx.send(embed=self.finalise_embed(embed)) - else: - msg = "**Users**\n" - if not data: - data = { - "No one's logged in": ( - "If you're expecting data here, you're probably using WSL or other " - "virtualisation technology" - ) - } - to_box = "".join(f"{name}\n{userdata}" for name, userdata in data.items()) - msg += box(to_box) - await ctx.send(msg) + await ctx.send( + embed=await self.prep_users_msg(ctx.channel), + view=SystemView(ctx.author, self, "users"), + ) + + async def prep_users_msg(self, channel: discord.abc.Messageable) -> discord.Embed: + data = get_users() + embed = discord.Embed(title="Users", colour=await self.bot.get_embed_colour(channel)) + if not data: + embed.add_field( + name="No one's logged in", + value=( + "If you're expecting data here, you're probably using WSL or other " + "virtualisation technology" + ), + ) + + for name, userdata in data.items(): + embed.add_field(name=name, value=box(userdata)) + return self.finalise_embed(embed) @system.command( name="disk", aliases=["df"], cls=DynamicHelp, supported_sys=True # all systems @@ -265,8 +251,15 @@ async def system_disk(self, ctx: commands.Context, ignore_loop: bool = True): Note: Mount point is basically useless on Windows as it's the same as the drive name, though it's still shown. """ - embed = await ctx.embed_requested() - pre_data = await get_disk(embed) + await ctx.send( + embed=await self.prep_disk_msg(ctx.channel, ignore_loop), + view=SystemView(ctx.author, self, "disk"), + ) + + async def prep_disk_msg( + self, channel: discord.abc.Messageable, ignore_loop: bool = True + ) -> discord.Embed: + pre_data = get_disk() data: dict[str, str] = {} if ignore_loop: @@ -277,31 +270,18 @@ async def system_disk(self, ctx: commands.Context, ignore_loop: bool = True): else: data = pre_data - if embed: - embed = discord.Embed(title="Disks", colour=await ctx.embed_colour()) - if not data: - embed.add_field( - name="No disks found", - value=( - "That's not something you see very often! You're probably using WSL or " - "other virtualisation technology" - ), - ) - for name, diskdata in data.items(): - embed.add_field(name=name, value=box(diskdata)) - await ctx.send(embed=self.finalise_embed(embed)) - else: - msg = "**Disks**\n" - if not data: - data = { - "No disks found": ( - "That's not something you see very often! You're probably using WSL or " - "other virtualisation technology" - ) - } - to_box = "".join(f"{name}\n{diskdata}" for name, diskdata in data.items()) - msg += box(to_box) - await ctx.send(msg) + embed = discord.Embed(title="Disks", colour=await self.bot.get_embed_colour(channel)) + if not data: + embed.add_field( + name="No disks found", + value=( + "That's not something you see very often! You're probably using WSL or " + "other virtualisation technology" + ), + ) + for name, diskdata in data.items(): + embed.add_field(name=name, value=box(diskdata)) + return self.finalise_embed(embed) @system.command( name="processes", aliases=["proc"], cls=DynamicHelp, supported_sys=True # all systems @@ -313,16 +293,16 @@ async def system_processes(self, ctx: commands.Context): Platforms: Windows, Linux, Mac OS """ async with ctx.typing(): - proc = (await get_proc())["statuses"] + await ctx.send( + embed=await self.prep_proc_msg(ctx.channel), + view=SystemView(ctx.author, self, "proc"), + ) - if await ctx.embed_requested(): - embed = discord.Embed(title="Processes", colour=await ctx.embed_colour()) - embed.add_field(name="Status", value=box(proc)) - await ctx.send(embed=self.finalise_embed(embed)) - else: - msg = "**Processes**\n" - msg += box(f"CPU\n{proc}\n") - await ctx.send(msg) + async def prep_proc_msg(self, channel: discord.abc.Messageable) -> discord.Embed: + proc = (await get_proc())["statuses"] + embed = discord.Embed(title="Processes", colour=await self.bot.get_embed_colour(channel)) + embed.add_field(name="Status", value=box(proc)) + return self.finalise_embed(embed) @system.command( name="network", aliases=["net"], cls=DynamicHelp, supported_sys=True # all systems @@ -333,16 +313,16 @@ async def system_net(self, ctx: commands.Context): Platforms: Windows, Linux, Mac OS """ - stats = (await get_net())["counters"] + await ctx.send( + embed=await self.prep_net_msg(ctx.channel), view=SystemView(ctx.author, self, "net") + ) - if await ctx.embed_requested(): - embed = discord.Embed(title="Network", colour=await ctx.embed_colour()) - embed.add_field(name="Network Stats", value=box(stats)) - await ctx.send(embed=self.finalise_embed(embed)) - else: - msg = "**Network**\n" - msg += box(f"Network Stats\n{stats}\n") - await ctx.send(msg) + async def prep_net_msg(self, channel: discord.abc.Messageable) -> discord.Embed: + stats = (get_net())["counters"] + + embed = discord.Embed(title="Network", colour=await self.bot.get_embed_colour(channel)) + embed.add_field(name="Network Stats", value=box(stats)) + return self.finalise_embed(embed) @system.command( name="uptime", aliases=["up"], cls=DynamicHelp, supported_sys=True # all systems @@ -353,16 +333,17 @@ async def system_uptime(self, ctx: commands.Context): Platforms: Windows, Linux, Mac OS """ - uptime = (await get_uptime())["uptime"] + await ctx.send( + embed=await self.prep_uptime_msg(ctx.channel), + view=SystemView(ctx.author, self, "uptime"), + ) - if await ctx.embed_requested(): - embed = discord.Embed(title="Uptime", colour=await ctx.embed_colour()) - embed.add_field(name="Uptime", value=box(uptime)) - await ctx.send(embed=self.finalise_embed(embed)) - else: - msg = "**Utime**\n" - msg += box(f"Uptime\n{uptime}\n") - await ctx.send(msg) + async def prep_uptime_msg(self, channel: discord.abc.Messageable) -> discord.Embed: + uptime = (get_uptime())["uptime"] + + embed = discord.Embed(title="Uptime", colour=await self.bot.get_embed_colour(channel)) + embed.add_field(name="Uptime", value=box(uptime)) + return self.finalise_embed(embed) @system.command(name="red", cls=DynamicHelp, supported_sys=True) # all systems async def system_red(self, ctx: commands.Context): @@ -372,25 +353,27 @@ async def system_red(self, ctx: commands.Context): Platforms: Windows, Linux, Mac OS Note: SWAP memory information is only available on Linux. """ + + async with ctx.typing(): + await ctx.send( + embed=await self.prep_red_msg(ctx.channel), + view=SystemView(ctx.author, self, "red"), + ) + + async def prep_red_msg(self, channel: discord.abc.Messageable) -> discord.Embed: # i jolly hope we are logged in... if TYPE_CHECKING: assert self.bot.user is not None - async with ctx.typing(): - red = (await get_red())["red"] + red = (await get_red())["red"] botname = self.bot.user.name - if await ctx.embed_requested(): - embed = discord.Embed( - title=f"{botname}'s resource usage", colour=await ctx.embed_colour() - ) - embed.add_field(name="Resource usage", value=box(red)) - await ctx.send(embed=self.finalise_embed(embed)) - else: - msg = f"**{botname}'s resource usage**\n" - msg += box(f"Resource usage\n{red}\n") - await ctx.send(msg) + embed = discord.Embed( + title=f"{botname}'s resource usage", colour=await self.bot.get_embed_colour(channel) + ) + embed.add_field(name="Resource usage", value=box(red)) + return self.finalise_embed(embed) @system.command( name="all", aliases=["overview", "top"], cls=DynamicHelp, supported_sys=True # all systems @@ -405,15 +388,21 @@ async def system_all(self, ctx: commands.Context): Platforms: Windows, Linux, Mac OS Note: This command appears to be very slow in Windows. """ + async with ctx.typing(): + await ctx.send( + embed=await self.prep_all_msg(ctx.channel), + view=SystemView(ctx.author, self, "all"), + ) + + async def prep_all_msg(self, channel: discord.abc.Messageable) -> discord.Embed: # i jolly hope we are logged in... if TYPE_CHECKING: assert self.bot.user is not None - async with ctx.typing(): - cpu = await get_cpu() - mem = await get_mem() - proc = await get_proc() - red = (await get_red())["red"] + cpu = await get_cpu() + mem = get_mem() + proc = await get_proc() + red = (await get_red())["red"] percent = cpu["percent"] times = cpu["time"] @@ -422,22 +411,11 @@ async def system_all(self, ctx: commands.Context): procs = proc["statuses"] botname = self.bot.user.name - if await ctx.embed_requested(): - embed = discord.Embed(title="Overview", colour=await ctx.embed_colour()) - embed.add_field(name="CPU Usage", value=box(percent)) - embed.add_field(name="CPU Times", value=box(times)) - embed.add_field(name="Physical Memory", value=box(physical)) - embed.add_field(name="SWAP Memory", value=box(swap)) - embed.add_field(name="Processes", value=box(procs)) - embed.add_field(name=f"{botname}'s resource usage", value=box(red)) - await ctx.send(embed=self.finalise_embed(embed)) - else: - msg = "**Overview**\n" - to_box = f"CPU Usage\n{percent}\n" - to_box += f"CPU Times\n{times}\n" - to_box += f"Physical Memory\n{physical}\n" - to_box += f"SWAP Memory\n{swap}\n" - to_box += f"Processes\n{procs}\n" - to_box += f"{botname}'s resource usage\n{red}\n" - msg += box(to_box) - await ctx.send(msg) + embed = discord.Embed(title="Overview", colour=await self.bot.get_embed_colour(channel)) + embed.add_field(name="CPU Usage", value=box(percent)) + embed.add_field(name="CPU Times", value=box(times)) + embed.add_field(name="Physical Memory", value=box(physical)) + embed.add_field(name="SWAP Memory", value=box(swap)) + embed.add_field(name="Processes", value=box(procs)) + embed.add_field(name=f"{botname}'s resource usage", value=box(red)) + return self.finalise_embed(embed) diff --git a/system/vexutils/meta.py b/system/vexutils/meta.py index cae0ae73..1e963e4c 100644 --- a/system/vexutils/meta.py +++ b/system/vexutils/meta.py @@ -2,11 +2,12 @@ import asyncio import json -from logging import Logger, getLogger from pathlib import Path from typing import Literal, NamedTuple import aiohttp +from red_commons.logging import RedTraceLogger +from red_commons.logging import getLogger as red_get_logger from redbot.core import VersionInfo, commands from redbot.core import version_info as cur_red_version from rich import box as rich_box @@ -16,13 +17,13 @@ from .consts import DOCS_BASE, GREEN_CIRCLE, RED_CIRCLE from .loop import VexLoop -log = getLogger("red.vex-utils") +log = red_get_logger("red.vex-utils") cog_ver_lock = asyncio.Lock() -def get_vex_logger(name: str) -> Logger: +def get_vex_logger(name: str) -> RedTraceLogger: """Get a logger for the given name. Parameters @@ -42,7 +43,7 @@ def get_vex_logger(name: str) -> Logger: else: # otherwise use full path final_name += name - return getLogger(final_name) + return red_get_logger(final_name) def format_help(self: commands.Cog, ctx: commands.Context) -> str: diff --git a/timechannel/__init__.py b/timechannel/__init__.py index 1ead6f68..45ad49f2 100644 --- a/timechannel/__init__.py +++ b/timechannel/__init__.py @@ -17,7 +17,4 @@ async def setup(bot: Red) -> None: cog = TimeChannel(bot) await out_of_date_check("timechannel", cog.__version__) - await cog.maybe_migrate() - r = bot.add_cog(cog) - if r is not None: - await r + await bot.add_cog(cog) diff --git a/timechannel/info.json b/timechannel/info.json index de52f207..113f54db 100644 --- a/timechannel/info.json +++ b/timechannel/info.json @@ -6,8 +6,7 @@ "description": "Get the time in different timezones in voice channels.", "end_user_data_statement": "This cog does not persistently store data or metadata about users.", "install_msg": "Thanks for installing! You can get started with the `tcset` command, and anything added as a channel can be seen by users in the server with `[p]timezones`\n\nThis cog has docs! Check them out at <https://go.vexcodes.com/c/timechannel>", - "max_bot_version": "3.6.0.dev0", - "min_bot_version": "3.4.0", + "min_bot_version": "3.5.1", "min_python_version": [ 3, 8, diff --git a/timechannel/loop.py b/timechannel/loop.py index 60355925..f0660ecc 100644 --- a/timechannel/loop.py +++ b/timechannel/loop.py @@ -26,20 +26,20 @@ async def wait_until_iter(self) -> None: next_iter = datetime.datetime.fromtimestamp(time) - now seconds_to_sleep = (next_iter).total_seconds() - _log.debug(f"Sleeping for {seconds_to_sleep} seconds until next iter...") + _log.trace(f"Sleeping for {seconds_to_sleep} seconds until next iter...") await asyncio.sleep(seconds_to_sleep) async def timechannel_loop(self) -> None: await self.bot.wait_until_red_ready() await asyncio.sleep(1) - _log.debug("Timechannel loop has started.") + _log.verbose("Timechannel loop has started.") while True: try: self.loop_meta.iter_start() await self.maybe_update_channels() self.loop_meta.iter_finish() - _log.debug("Timechannel iteration finished") + _log.verbose("Timechannel iteration finished") except Exception as e: _log.exception( "Something went wrong in the timechannel loop. Some channels may have been " @@ -51,7 +51,7 @@ async def timechannel_loop(self) -> None: async def maybe_update_channels(self) -> None: all_guilds: dict[int, dict[str, dict[int, str]]] = await self.config.all_guilds() if not all_guilds: - _log.debug("No time channels registered, nothing to do...") + _log.trace("No time channels registered, nothing to do...") return reps = gen_replacements() @@ -59,14 +59,14 @@ async def maybe_update_channels(self) -> None: for guild_id, guild_data in all_guilds.items(): guild = self.bot.get_guild(guild_id) if guild is None: - _log.debug(f"Can't find guild with ID {guild_id} - skipping") + _log.trace(f"Can't find guild with ID {guild_id} - skipping") continue for c_id, string in guild_data.get("timechannels", {}).items(): channel = self.bot.get_channel(int(c_id)) if channel is None: # yes log *could* be inaccurate but a timezone being removed is unlikely - _log.debug(f"Can't find channel with ID {c_id} - skipping") + _log.trace(f"Can't find channel with ID {c_id} - skipping") continue assert isinstance(channel, VoiceChannel) @@ -78,7 +78,9 @@ async def maybe_update_channels(self) -> None: name=new_name, reason="Edited for timechannel - disable with `tcset remove`", ) - _log.debug(f"Edited channel {c_id} to {new_name}") + _log.trace(f"Edited channel {c_id} to {new_name}") except HTTPException: - _log.debug(f"Unable to edit channel ID {c_id}") + _log.warning( + f"Unable to edit channel ID {c_id} in guild {guild_id} ({guild.name})" + ) continue diff --git a/timechannel/timechannel.py b/timechannel/timechannel.py index 3ce72541..a1090b65 100644 --- a/timechannel/timechannel.py +++ b/timechannel/timechannel.py @@ -54,15 +54,18 @@ async def red_delete_data_for_user(self, **kwargs) -> None: """Nothing to delete""" return - def cog_unload(self) -> None: + async def cog_unload(self) -> None: self.loop.cancel() - log.debug("Loop stopped as cog unloaded.") + log.verbose("Loop stopped as cog unloaded.") + + async def cog_load(self) -> None: + await self.maybe_migrate() async def maybe_migrate(self) -> None: if await self.config.version() == 2: return - log.debug("Migating to config v2") + log.verbose("Migrating to config v2") keys = list(ZONE_KEYS.keys()) values = list(ZONE_KEYS.values()) all_guilds = await self.config.all_guilds() diff --git a/timechannel/vexutils/meta.py b/timechannel/vexutils/meta.py index cae0ae73..1e963e4c 100644 --- a/timechannel/vexutils/meta.py +++ b/timechannel/vexutils/meta.py @@ -2,11 +2,12 @@ import asyncio import json -from logging import Logger, getLogger from pathlib import Path from typing import Literal, NamedTuple import aiohttp +from red_commons.logging import RedTraceLogger +from red_commons.logging import getLogger as red_get_logger from redbot.core import VersionInfo, commands from redbot.core import version_info as cur_red_version from rich import box as rich_box @@ -16,13 +17,13 @@ from .consts import DOCS_BASE, GREEN_CIRCLE, RED_CIRCLE from .loop import VexLoop -log = getLogger("red.vex-utils") +log = red_get_logger("red.vex-utils") cog_ver_lock = asyncio.Lock() -def get_vex_logger(name: str) -> Logger: +def get_vex_logger(name: str) -> RedTraceLogger: """Get a logger for the given name. Parameters @@ -42,7 +43,7 @@ def get_vex_logger(name: str) -> Logger: else: # otherwise use full path final_name += name - return getLogger(final_name) + return red_get_logger(final_name) def format_help(self: commands.Cog, ctx: commands.Context) -> str: diff --git a/tox.ini b/tox.ini index 2265fd6e..54a5fc5d 100644 --- a/tox.ini +++ b/tox.ini @@ -32,7 +32,7 @@ deps = # pytest pytest - red-discordbot==3.4.18 + git+https://github.com/Cog-Creators/Red-DiscordBot # for dpy 2, this is temporary markdownify # type diff --git a/uptimeresponder/__init__.py b/uptimeresponder/__init__.py index 7f9ac25f..6ba4bcd3 100644 --- a/uptimeresponder/__init__.py +++ b/uptimeresponder/__init__.py @@ -13,9 +13,4 @@ async def setup(bot: Red) -> None: cog = UptimeResponder(bot) await out_of_date_check("uptimeresponder", cog.__version__) - - r = bot.add_cog(cog) - if r is not None: - await r - - await cog.start_webserver() + await bot.add_cog(cog) diff --git a/uptimeresponder/info.json b/uptimeresponder/info.json index 45ce428f..754152f9 100644 --- a/uptimeresponder/info.json +++ b/uptimeresponder/info.json @@ -5,8 +5,7 @@ "description": "A cog which starts up a simple webserver whenever the cog is loaded, which can then be used by uptime monitoring services such as UptimeRobot, Pingdom, Uptime.com, or self-hosted ones like UptimeKuma or Upptime.. If you are using an external monitor, you will need to configure port forwarding. Make sure you are aware of the security risks of exposing your machine to the internet. The cog responds with status code 200.", "end_user_data_statement": "This cog does not persistently store data or metadata about users.", "install_msg": "As soon as you start the cog, it will start a simple web server for listening to responses on port 8710. The server is shut down when the cog is unloaded. You can change the port with `[p]uptimeresponderport <new_port>`.\n\nIf you are using an external uptime monitor, you will need to open up port forwarding. Make sure you are aware of the security risk of exposing your machine to the internet. This cog responds with status code 200 at the root only.\n\nThis cog has docs! ", - "max_bot_version": "3.6.0.dev0", - "min_bot_version": "3.4.0", + "min_bot_version": "3.5.1", "min_python_version": [ 3, 8, diff --git a/uptimeresponder/uptimeresponder.py b/uptimeresponder/uptimeresponder.py index 296d5133..3ed470ad 100644 --- a/uptimeresponder/uptimeresponder.py +++ b/uptimeresponder/uptimeresponder.py @@ -36,16 +36,17 @@ def __init__(self, bot: Red) -> None: ) self.config.register_global(port=8710) - def cog_unload(self) -> None: - self.bot.loop.create_task(self.shutdown_webserver()) + async def cog_unload(self) -> None: + await self.shutdown_webserver() def format_help_for_context(self, ctx: commands.Context) -> str: """Thanks Sinbad.""" return format_help(self, ctx) - @commands.command( - hidden=True, - ) + async def cog_load(self) -> None: + await self.start_webserver() + + @commands.command(hidden=True) async def uptimeresponderinfo(self, ctx: commands.Context): await ctx.send(await format_info(ctx, self.qualified_name, self.__version__)) @@ -60,6 +61,7 @@ async def red_delete_data_for_user(self, *args, **kwargs) -> None: async def main_page(self, request: web.Request) -> web.Response: name = self.bot.user.name if self.bot.user else "Unknown" + log.trace("received HTTP GET request from %s", request.remote) return web.Response( text=f"{name} is online and the UptimeResponder cog is loaded.", status=200 ) diff --git a/uptimeresponder/vexutils/meta.py b/uptimeresponder/vexutils/meta.py index cae0ae73..1e963e4c 100644 --- a/uptimeresponder/vexutils/meta.py +++ b/uptimeresponder/vexutils/meta.py @@ -2,11 +2,12 @@ import asyncio import json -from logging import Logger, getLogger from pathlib import Path from typing import Literal, NamedTuple import aiohttp +from red_commons.logging import RedTraceLogger +from red_commons.logging import getLogger as red_get_logger from redbot.core import VersionInfo, commands from redbot.core import version_info as cur_red_version from rich import box as rich_box @@ -16,13 +17,13 @@ from .consts import DOCS_BASE, GREEN_CIRCLE, RED_CIRCLE from .loop import VexLoop -log = getLogger("red.vex-utils") +log = red_get_logger("red.vex-utils") cog_ver_lock = asyncio.Lock() -def get_vex_logger(name: str) -> Logger: +def get_vex_logger(name: str) -> RedTraceLogger: """Get a logger for the given name. Parameters @@ -42,7 +43,7 @@ def get_vex_logger(name: str) -> Logger: else: # otherwise use full path final_name += name - return getLogger(final_name) + return red_get_logger(final_name) def format_help(self: commands.Cog, ctx: commands.Context) -> str: diff --git a/wol/__init__.py b/wol/__init__.py index b7c38fb1..2b2607cf 100644 --- a/wol/__init__.py +++ b/wol/__init__.py @@ -17,6 +17,4 @@ async def setup(bot: Red): cog = WOL(bot) await out_of_date_check("wol", cog.__version__) - r = bot.add_cog(cog) - if r is not None: - await r + await bot.add_cog(cog) diff --git a/wol/info.json b/wol/info.json index 9d686cbb..fda7a008 100644 --- a/wol/info.json +++ b/wol/info.json @@ -6,8 +6,7 @@ "description": "Use Wake on LAN from Discord! This cog sends magic packets on the local network. You'll need to have set up the computer beforehand to listen to these - just search how to set it up for your operating system.", "end_user_data_statement": "This cog does not persistently store data or metadata about users.", "install_msg": "Thanks for installing! Once you've loaded the cog, get started by adding the computer you want to wake's MAC address with `wolset add <friendly_name> <mac>` then you can wake it with just `wol <friendly_name>`. `<friendly_name>` can be whatever you want.\n\nThis cog has docs! Check them out at <https://go.vexcodes.com/c/wol>", - "max_bot_version": "3.6.0.dev0", - "min_bot_version": "3.4.0", + "min_bot_version": "3.5.1", "min_python_version": [ 3, 8, diff --git a/wol/vexutils/meta.py b/wol/vexutils/meta.py index cae0ae73..1e963e4c 100644 --- a/wol/vexutils/meta.py +++ b/wol/vexutils/meta.py @@ -2,11 +2,12 @@ import asyncio import json -from logging import Logger, getLogger from pathlib import Path from typing import Literal, NamedTuple import aiohttp +from red_commons.logging import RedTraceLogger +from red_commons.logging import getLogger as red_get_logger from redbot.core import VersionInfo, commands from redbot.core import version_info as cur_red_version from rich import box as rich_box @@ -16,13 +17,13 @@ from .consts import DOCS_BASE, GREEN_CIRCLE, RED_CIRCLE from .loop import VexLoop -log = getLogger("red.vex-utils") +log = red_get_logger("red.vex-utils") cog_ver_lock = asyncio.Lock() -def get_vex_logger(name: str) -> Logger: +def get_vex_logger(name: str) -> RedTraceLogger: """Get a logger for the given name. Parameters @@ -42,7 +43,7 @@ def get_vex_logger(name: str) -> Logger: else: # otherwise use full path final_name += name - return getLogger(final_name) + return red_get_logger(final_name) def format_help(self: commands.Cog, ctx: commands.Context) -> str: