diff --git a/chatter/README.md b/chatter/README.md index 8ef6734..d92ad2b 100644 --- a/chatter/README.md +++ b/chatter/README.md @@ -95,7 +95,8 @@ pip install --no-deps "chatterbot>=1.1" #### Step 1: Built-in Downloader ``` -[p]cog install Chatter +[p]repo add Fox https://github.com/bobloy/Fox-V3 +[p]cog install Fox chatter ``` #### Step 2: Install Requirements diff --git a/chatter/chat.py b/chatter/chat.py index ef75bb8..4e3400c 100644 --- a/chatter/chat.py +++ b/chatter/chat.py @@ -10,7 +10,7 @@ from chatterbot import ChatBot from chatterbot.comparisons import JaccardSimilarity, LevenshteinDistance, SpacySimilarity from chatterbot.response_selection import get_random_response from chatterbot.trainers import ChatterBotCorpusTrainer, ListTrainer, UbuntuCorpusTrainer -from redbot.core import Config, commands +from redbot.core import Config, checks, commands from redbot.core.commands import Cog from redbot.core.data_manager import cog_data_path from redbot.core.utils.predicates import MessagePredicate @@ -159,7 +159,9 @@ class Chatter(Cog): return out def _train_ubuntu(self): - trainer = UbuntuCorpusTrainer(self.chatbot) + trainer = UbuntuCorpusTrainer( + self.chatbot, ubuntu_corpus_data_directory=cog_data_path(self) / "ubuntu_data" + ) trainer.train() return True @@ -191,6 +193,7 @@ class Chatter(Cog): if ctx.invoked_subcommand is None: pass + @checks.admin() @chatter.command(name="channel") async def chatter_channel( self, ctx: commands.Context, channel: Optional[discord.TextChannel] = None @@ -210,6 +213,7 @@ class Chatter(Cog): await self.config.guild(ctx.guild).chatchannel.set(channel.id) await ctx.maybe_send_embed(f"Chat channel is now {channel.mention}") + @checks.is_owner() @chatter.command(name="cleardata") async def chatter_cleardata(self, ctx: commands.Context, confirm: bool = False): """ @@ -242,6 +246,7 @@ class Chatter(Cog): await ctx.tick() + @checks.is_owner() @chatter.command(name="algorithm", aliases=["algo"]) async def chatter_algorithm( self, ctx: commands.Context, algo_number: int, threshold: float = None @@ -275,6 +280,7 @@ class Chatter(Cog): await ctx.tick() + @checks.is_owner() @chatter.command(name="model") async def chatter_model(self, ctx: commands.Context, model_number: int): """ @@ -312,6 +318,7 @@ class Chatter(Cog): f"Model has been switched to {self.tagger_language.ISO_639_1}" ) + @checks.is_owner() @chatter.command(name="minutes") async def minutes(self, ctx: commands.Context, minutes: int): """ @@ -327,6 +334,7 @@ class Chatter(Cog): await ctx.tick() + @checks.is_owner() @chatter.command(name="age") async def age(self, ctx: commands.Context, days: int): """ @@ -341,6 +349,7 @@ class Chatter(Cog): await self.config.guild(ctx.guild).days.set(days) await ctx.tick() + @checks.is_owner() @chatter.command(name="backup") async def backup(self, ctx, backupname): """ @@ -362,6 +371,7 @@ class Chatter(Cog): else: await ctx.maybe_send_embed("Error occurred :(") + @checks.is_owner() @chatter.command(name="trainubuntu") async def chatter_train_ubuntu(self, ctx: commands.Context, confirmation: bool = False): """ @@ -383,6 +393,7 @@ class Chatter(Cog): else: await ctx.send("Error occurred :(") + @checks.is_owner() @chatter.command(name="trainenglish") async def chatter_train_english(self, ctx: commands.Context): """ @@ -396,6 +407,7 @@ class Chatter(Cog): else: await ctx.maybe_send_embed("Error occurred :(") + @checks.is_owner() @chatter.command() async def train(self, ctx: commands.Context, channel: discord.TextChannel): """ diff --git a/fifo/fifo.py b/fifo/fifo.py index acd01ac..c6479b4 100644 --- a/fifo/fifo.py +++ b/fifo/fifo.py @@ -1,5 +1,6 @@ +import itertools import logging -from datetime import datetime, timedelta, tzinfo +from datetime import datetime, timedelta, tzinfo, MAXYEAR from typing import Optional, Union import discord @@ -10,7 +11,7 @@ from apscheduler.schedulers.base import STATE_PAUSED, STATE_RUNNING from redbot.core import Config, checks, commands from redbot.core.bot import Red from redbot.core.commands import TimedeltaConverter -from redbot.core.utils.chat_formatting import pagify +from redbot.core.utils.chat_formatting import humanize_list, humanize_timedelta, pagify from .datetime_cron_converters import CronConverter, DatetimeConverter, TimezoneConverter from .task import Task @@ -26,6 +27,7 @@ async def _execute_task(task_state): task = Task(**task_state) if await task.load_from_config(): return await task.execute() + log.warning(f"Failed to load data on {task_state=}") return False @@ -37,6 +39,27 @@ def _disassemble_job_id(job_id: str): return job_id.split("_") +def _get_run_times(job: Job, now: datetime = None): + """ + Computes the scheduled run times between ``next_run_time`` and ``now`` (inclusive). + + Modified to be asynchronous and yielding instead of all-or-nothing + + """ + if not job.next_run_time: + raise StopIteration() + + if now is None: + now = datetime(MAXYEAR, 12, 31, 23, 59, 59, 999999, tzinfo=job.next_run_time.tzinfo) + yield from _get_run_times(job, now) + raise StopIteration() + + next_run_time = job.next_run_time + while next_run_time and next_run_time <= now: + yield next_run_time + next_run_time = job.trigger.get_next_fire_time(next_run_time, now) + + class FIFO(commands.Cog): """ Simple Scheduling Cog @@ -173,6 +196,30 @@ class FIFO(commands.Cog): if ctx.invoked_subcommand is None: pass + @fifo.command(name="checktask", aliases=["checkjob", "check"]) + async def fifo_checktask(self, ctx: commands.Context, task_name: str): + """Returns the next 10 scheduled executions of the task""" + task = Task(task_name, ctx.guild.id, self.config, bot=self.bot) + await task.load_from_config() + + if task.data is None: + await ctx.maybe_send_embed( + f"Task by the name of {task_name} is not found in this guild" + ) + return + + job = await self._get_job(task) + if job is None: + await ctx.maybe_send_embed("No job scheduled for this task") + return + now = datetime.now(job.next_run_time.tzinfo) + + times = [ + humanize_timedelta(timedelta=x - now) + for x in itertools.islice(_get_run_times(job), 10) + ] + await ctx.maybe_send_embed("\n\n".join(times)) + @fifo.command(name="set") async def fifo_set( self, @@ -324,7 +371,7 @@ class FIFO(commands.Cog): out = "" all_tasks = await self.config.guild(ctx.guild).tasks() for task_name, task_data in all_tasks.items(): - out += f"{task_name}: {task_data}\n" + out += f"{task_name}: {task_data}\n\n" if out: if len(out) > 2000: @@ -394,6 +441,7 @@ class FIFO(commands.Cog): return await task.clear_triggers() + await self._remove_job(task) await ctx.tick() @fifo.group(name="addtrigger", aliases=["trigger"]) diff --git a/fifo/task.py b/fifo/task.py index f7dc45a..7c51ee4 100644 --- a/fifo/task.py +++ b/fifo/task.py @@ -39,7 +39,7 @@ def parse_triggers(data: Union[Dict, None]): return None if len(data["triggers"]) > 1: # Multiple triggers - return OrTrigger(get_trigger(t_data) for t_data in data["triggers"]) + return OrTrigger([get_trigger(t_data) for t_data in data["triggers"]]) return get_trigger(data["triggers"][0]) @@ -108,20 +108,6 @@ class Task: "tzinfo": getattr(t["tzinfo"], "zone", None), } ) - # triggers.append( - # { - # "type": t["type"], - # "time_data": { - # "year": dt.year, - # "month": dt.month, - # "day": dt.day, - # "hour": dt.hour, - # "minute": dt.minute, - # "second": dt.second, - # "tzinfo": dt.tzinfo, - # }, - # } - # ) continue if t["type"] == "cron": @@ -239,20 +225,26 @@ class Task: async def execute(self): if not self.data or not self.get_command_str(): - log.warning(f"Could not execute task due to data problem: {self.data=}") + log.warning(f"Could not execute Task[{self.name}] due to data problem: {self.data=}") return False guild: discord.Guild = self.bot.get_guild(self.guild_id) # used for get_prefix if guild is None: - log.warning(f"Could not execute task due to missing guild: {self.guild_id}") + log.warning( + f"Could not execute Task[{self.name}] due to missing guild: {self.guild_id}" + ) return False channel: discord.TextChannel = guild.get_channel(self.channel_id) if channel is None: - log.warning(f"Could not execute task due to missing channel: {self.channel_id}") + log.warning( + f"Could not execute Task[{self.name}] due to missing channel: {self.channel_id}" + ) return False author: discord.User = guild.get_member(self.author_id) if author is None: - log.warning(f"Could not execute task due to missing author: {self.author_id}") + log.warning( + f"Could not execute Task[{self.name}] due to missing author: {self.author_id}" + ) return False actual_message: discord.Message = channel.last_message @@ -267,15 +259,15 @@ class Task: actual_message = await author.history(limit=1).flatten() if not actual_message: # Okay, the *author* has never sent a message? log.warning("No message found in channel cache yet, skipping execution") - return + return False actual_message = actual_message[0] message = FakeMessage(actual_message) # message = FakeMessage2 message.author = author - message.guild = guild # Just in case we got desperate + message.guild = guild # Just in case we got desperate, see above message.channel = channel - message.id = time_snowflake(datetime.now()) # Pretend to be now + message.id = time_snowflake(datetime.utcnow(), high=False) # Pretend to be now message = neuter_message(message) # absolutely weird that this takes a message object instead of guild @@ -287,15 +279,21 @@ class Task: message.content = f"{prefix}{self.get_command_str()}" - if not message.guild or not message.author or not message.content: - log.warning(f"Could not execute task due to message problem: {message}") + if ( + not message.guild + or not message.author + or not message.content + or message.content == prefix + ): + log.warning(f"Could not execute Task[{self.name}] due to message problem: {message}") return False new_ctx: commands.Context = await self.bot.get_context(message) new_ctx.assume_yes = True if not new_ctx.valid: log.warning( - f"Could not execute Task[{self.name}] due invalid context: {new_ctx.invoked_with}" + f"Could not execute Task[{self.name}] due invalid context: " + f"{new_ctx.invoked_with=} {new_ctx.prefix=} {new_ctx.command=}" ) return False diff --git a/nudity/nudity.py b/nudity/nudity.py index 4233460..64ec02a 100644 --- a/nudity/nudity.py +++ b/nudity/nudity.py @@ -8,9 +8,7 @@ from redbot.core.data_manager import cog_data_path class Nudity(commands.Cog): - """ - V3 Cog Template - """ + """Monitor images for NSFW content and moves them to a nsfw channel if possible""" def __init__(self, bot: Red): super().__init__() diff --git a/planttycoon/planttycoon.py b/planttycoon/planttycoon.py index 665fc9a..4209b53 100644 --- a/planttycoon/planttycoon.py +++ b/planttycoon/planttycoon.py @@ -793,7 +793,7 @@ class PlantTycoon(commands.Cog): pass await asyncio.sleep(self.defaults["timers"]["notification"] * 60) - def __unload(self): + def cog_unload(self): self.completion_task.cancel() # self.degradation_task.cancel() self.notification_task.cancel() diff --git a/stealemoji/stealemoji.py b/stealemoji/stealemoji.py index 492ef70..a492527 100644 --- a/stealemoji/stealemoji.py +++ b/stealemoji/stealemoji.py @@ -50,6 +50,7 @@ class StealEmoji(Cog): default_global = { "stolemoji": {}, "guildbanks": [], + "autobanked_guilds": [], "on": False, "notify": 0, "autobank": False, @@ -145,11 +146,54 @@ class StealEmoji(Cog): await ctx.maybe_send_embed("AutoBanking is now " + str(not curr_setting)) + @checks.is_owner() + @commands.guild_only() + @stealemoji.command(name="deleteserver", aliases=["deleteguild"]) + async def se_deleteserver(self, ctx: commands.Context, guild_id=None): + """Delete servers the bot is the owner of. + + Useful for auto-generated guildbanks.""" + if guild_id is None: + guild = ctx.guild + else: + guild = await self.bot.get_guild(guild_id) + + if guild is None: + await ctx.maybe_send_embed("Failed to get guild, cancelling") + return + guild: discord.Guild + await ctx.maybe_send_embed( + f"Will attempt to delete {guild.name} ({guild.id})\n" f"Okay to continue? (yes/no)" + ) + + def check(m): + return m.author == ctx.author and m.channel == ctx.channel + + try: + answer = await self.bot.wait_for("message", timeout=120, check=check) + except asyncio.TimeoutError: + await ctx.send("Timed out, canceling") + return + + if answer.content.upper() not in ["Y", "YES"]: + await ctx.maybe_send_embed("Cancelling") + return + try: + await guild.delete() + except discord.Forbidden: + log.exception("No permission to delete. I'm probably not the guild owner") + await ctx.maybe_send_embed("No permission to delete. I'm probably not the guild owner") + except discord.HTTPException: + log.exception("Unexpected error when deleting guild") + await ctx.maybe_send_embed("Unexpected error when deleting guild") + else: + await self.bot.send_to_owners(f"Guild {guild.name} deleted") + @checks.is_owner() @commands.guild_only() @stealemoji.command(name="bank") async def se_bank(self, ctx): - """Add current server as emoji bank""" + """Add or remove current server as emoji bank""" def check(m): return ( @@ -235,6 +279,9 @@ class StealEmoji(Cog): return async with self.config.guildbanks() as guildbanks: guildbanks.append(guildbank.id) + # Track generated guilds for easier deletion + async with self.config.autobanked_guilds() as autobanked_guilds: + autobanked_guilds.append(guildbank.id) await asyncio.sleep(2) diff --git a/timerole/timerole.py b/timerole/timerole.py index 1b56b69..714bcc8 100644 --- a/timerole/timerole.py +++ b/timerole/timerole.py @@ -235,8 +235,8 @@ class Timerole(Cog): has_roles = set(r.id for r in member.roles) # Stop if they currently have or don't have the role, and mark had_role - if (role_id in has_roles and not role_data["remove"]) or ( - role_id not in has_roles and role_data["remove"] + if (int(role_id) in has_roles and not role_data["remove"]) or ( + int(role_id) not in has_roles and role_data["remove"] ): if not mr_dict["had_role"]: await self.config.custom( diff --git a/werewolf/werewolf.py b/werewolf/werewolf.py index bd68a6f..dd711ed 100644 --- a/werewolf/werewolf.py +++ b/werewolf/werewolf.py @@ -56,7 +56,7 @@ class Werewolf(Cog): """Nothing to delete""" return - def __unload(self): + def cog_unload(self): log.debug("Unload called") for game in self.games.values(): del game