Compare commits
	
		
			2 Commits
		
	
	
		
			master
			...
			annd_devel
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 393d7cb350 | ||
|   | 70f4b0b716 | 
							
								
								
									
										2
									
								
								.github/labeler.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/labeler.yml
									
									
									
									
										vendored
									
									
								
							| @ -59,4 +59,4 @@ | ||||
| 'cog: unicode': | ||||
|   - unicode/* | ||||
| 'cog: werewolf': | ||||
|   - werewolf/* | ||||
|   - werewolf | ||||
							
								
								
									
										2
									
								
								.github/workflows/labeler.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/labeler.yml
									
									
									
									
										vendored
									
									
								
							| @ -6,7 +6,7 @@ | ||||
| # https://github.com/actions/labeler | ||||
| 
 | ||||
| name: Labeler | ||||
| on: [pull_request_target] | ||||
| on: [pull_request] | ||||
| 
 | ||||
| jobs: | ||||
|   label: | ||||
|  | ||||
| @ -53,7 +53,7 @@ Check out *Deprecated* my V2 cogs at [Fox-Cogs v2](https://github.com/bobloy/Fox | ||||
| # Contact | ||||
| Get support on the [Third Party Cog Server](https://discord.gg/GET4DVk) | ||||
| 
 | ||||
| Feel free to @ me in the #support_fox-v3 channel | ||||
| Feel free to @ me in the #support_othercogs channel | ||||
| 
 | ||||
| Discord: Bobloy#6513 | ||||
| 
 | ||||
|  | ||||
| @ -6,4 +6,4 @@ from .announcedaily import AnnounceDaily | ||||
| def setup(bot: Red): | ||||
|     daily = AnnounceDaily(bot) | ||||
|     bot.add_cog(daily) | ||||
|     bot.loop.create_task(daily.check_day()) | ||||
|     daily.announce_task = bot.loop.create_task(daily.check_day()) | ||||
|  | ||||
| @ -1,6 +1,8 @@ | ||||
| import asyncio | ||||
| import random | ||||
| from asyncio import Task | ||||
| from datetime import datetime, timedelta | ||||
| from typing import Optional | ||||
| 
 | ||||
| import discord | ||||
| from redbot.core import Config, checks, commands | ||||
| @ -38,10 +40,16 @@ class AnnounceDaily(Cog): | ||||
|         self.config.register_global(**default_global) | ||||
|         self.config.register_guild(**default_guild) | ||||
| 
 | ||||
|         self.announce_task = None | ||||
| 
 | ||||
|     async def red_delete_data_for_user(self, **kwargs): | ||||
|         """Nothing to delete""" | ||||
|         return | ||||
| 
 | ||||
|     def cog_unload(self): | ||||
|         if self.announce_task is not None: | ||||
|             self.announce_task.cancel() | ||||
| 
 | ||||
|     async def _get_msgs(self): | ||||
|         return DEFAULT_MESSAGES + await self.config.messages() | ||||
| 
 | ||||
| @ -54,7 +62,8 @@ class AnnounceDaily(Cog): | ||||
| 
 | ||||
|         Do `[p]help annd <subcommand>` for more details | ||||
|         """ | ||||
|         pass | ||||
|         if ctx.invoked_subcommand is None: | ||||
|             pass | ||||
| 
 | ||||
|     @commands.command() | ||||
|     @checks.guildowner() | ||||
|  | ||||
| @ -168,7 +168,7 @@ class AudioTrivia(Trivia): | ||||
|     @commands.guild_only() | ||||
|     async def audiotrivia_list(self, ctx: commands.Context): | ||||
|         """List available trivia including audio categories.""" | ||||
|         lists = {p.stem for p in self._all_audio_lists()} | ||||
|         lists = set(p.stem for p in self._all_audio_lists()) | ||||
|         if await ctx.embed_requested(): | ||||
|             await ctx.send( | ||||
|                 embed=discord.Embed( | ||||
|  | ||||
| @ -3,7 +3,6 @@ import logging | ||||
| import re | ||||
| 
 | ||||
| import discord | ||||
| from discord.ext.commands import RoleConverter, Greedy, CommandError, ArgumentParsingError | ||||
| from discord.ext.commands.view import StringView | ||||
| from redbot.core import Config, checks, commands | ||||
| from redbot.core.bot import Red | ||||
| @ -14,38 +13,15 @@ log = logging.getLogger("red.fox_v3.ccrole") | ||||
| 
 | ||||
| 
 | ||||
| async def _get_roles_from_content(ctx, content): | ||||
|     # greedy = Greedy[RoleConverter] | ||||
|     view = StringView(content) | ||||
|     rc = RoleConverter() | ||||
| 
 | ||||
|     # "Borrowed" from discord.ext.commands.Command._transform_greedy_pos | ||||
|     result = [] | ||||
|     while not view.eof: | ||||
|         # for use with a manual undo | ||||
|         previous = view.index | ||||
| 
 | ||||
|         view.skip_ws() | ||||
|         try: | ||||
|             argument = view.get_quoted_word() | ||||
|             value = await rc.convert(ctx, argument) | ||||
|         except (CommandError, ArgumentParsingError): | ||||
|             view.index = previous | ||||
|             break | ||||
|         else: | ||||
|             result.append(value) | ||||
| 
 | ||||
|     return [r.id for r in result] | ||||
| 
 | ||||
|     # Old method | ||||
|     # content_list = content.split(",") | ||||
|     # try: | ||||
|     #     role_list = [ | ||||
|     #         discord.utils.get(ctx.guild.roles, name=role.strip(" ")).id for role in content_list | ||||
|     #     ] | ||||
|     # except (discord.HTTPException, AttributeError):  # None.id is attribute error | ||||
|     #     return None | ||||
|     # else: | ||||
|     #     return role_list | ||||
|     content_list = content.split(",") | ||||
|     try: | ||||
|         role_list = [ | ||||
|             discord.utils.get(ctx.guild.roles, name=role.strip(" ")).id for role in content_list | ||||
|         ] | ||||
|     except (discord.HTTPException, AttributeError):  # None.id is attribute error | ||||
|         return None | ||||
|     else: | ||||
|         return role_list | ||||
| 
 | ||||
| 
 | ||||
| class CCRole(commands.Cog): | ||||
| @ -72,7 +48,8 @@ class CCRole(commands.Cog): | ||||
|         """Custom commands management with roles | ||||
| 
 | ||||
|         Highly customizable custom commands with role management.""" | ||||
|         pass | ||||
|         if not ctx.invoked_subcommand: | ||||
|             pass | ||||
| 
 | ||||
|     @ccrole.command(name="add") | ||||
|     @checks.mod_or_permissions(administrator=True) | ||||
| @ -108,7 +85,7 @@ class CCRole(commands.Cog): | ||||
| 
 | ||||
|         # Roles to add | ||||
|         await ctx.send( | ||||
|             "What roles should it add?\n" | ||||
|             "What roles should it add? (Must be **comma separated**)\n" | ||||
|             "Say `None` to skip adding roles" | ||||
|         ) | ||||
| 
 | ||||
| @ -130,7 +107,7 @@ class CCRole(commands.Cog): | ||||
| 
 | ||||
|         # Roles to remove | ||||
|         await ctx.send( | ||||
|             "What roles should it remove?\n" | ||||
|             "What roles should it remove? (Must be comma separated)\n" | ||||
|             "Say `None` to skip removing roles" | ||||
|         ) | ||||
|         try: | ||||
| @ -148,7 +125,7 @@ class CCRole(commands.Cog): | ||||
| 
 | ||||
|         # Roles to use | ||||
|         await ctx.send( | ||||
|             "What roles are allowed to use this command?\n" | ||||
|             "What roles are allowed to use this command? (Must be comma separated)\n" | ||||
|             "Say `None` to allow all roles" | ||||
|         ) | ||||
| 
 | ||||
| @ -251,7 +228,7 @@ class CCRole(commands.Cog): | ||||
|             if not role_list: | ||||
|                 return "None" | ||||
|             return ", ".join( | ||||
|                 discord.utils.get(ctx.guild.roles, id=roleid).name for roleid in role_list | ||||
|                 [discord.utils.get(ctx.guild.roles, id=roleid).name for roleid in role_list] | ||||
|             ) | ||||
| 
 | ||||
|         embed.add_field(name="Text", value="```{}```".format(cmd["text"]), inline=False) | ||||
| @ -275,7 +252,7 @@ class CCRole(commands.Cog): | ||||
|             ) | ||||
|             return | ||||
| 
 | ||||
|         cmd_list = ", ".join(ctx.prefix + c for c in sorted(cmd_list.keys())) | ||||
|         cmd_list = ", ".join([ctx.prefix + c for c in sorted(cmd_list.keys())]) | ||||
|         cmd_list = "Custom commands:\n\n" + cmd_list | ||||
| 
 | ||||
|         if ( | ||||
| @ -315,13 +292,13 @@ class CCRole(commands.Cog): | ||||
|         # Thank you Cog-Creators | ||||
| 
 | ||||
|         cmd = ctx.invoked_with | ||||
|         cmd = cmd.lower()  # Continues the proud case-insensitivity tradition of ccrole | ||||
|         cmd = cmd.lower()  # Continues the proud case_insentivity tradition of ccrole | ||||
|         guild = ctx.guild | ||||
|         # message = ctx.message  # Unneeded since switch to `on_message_without_command` from `on_command_error` | ||||
| 
 | ||||
|         cmd_list = self.config.guild(guild).cmdlist | ||||
|         cmdlist = self.config.guild(guild).cmdlist | ||||
|         # cmd = message.content[len(prefix) :].split()[0].lower() | ||||
|         cmd = await cmd_list.get_raw(cmd, default=None) | ||||
|         cmd = await cmdlist.get_raw(cmd, default=None) | ||||
| 
 | ||||
|         if cmd is not None: | ||||
|             await self.eval_cc(cmd, message, ctx) | ||||
| @ -348,7 +325,9 @@ class CCRole(commands.Cog): | ||||
| 
 | ||||
|     async def eval_cc(self, cmd, message: discord.Message, ctx: commands.Context): | ||||
|         """Does all the work""" | ||||
|         if cmd["proles"] and not {role.id for role in message.author.roles} & set(cmd["proles"]): | ||||
|         if cmd["proles"] and not ( | ||||
|             set(role.id for role in message.author.roles) & set(cmd["proles"]) | ||||
|         ): | ||||
|             log.debug(f"{message.author} missing required role to execute {ctx.invoked_with}") | ||||
|             return  # Not authorized, do nothing | ||||
| 
 | ||||
|  | ||||
| @ -59,50 +59,63 @@ Install these on your windows machine before attempting the installation: | ||||
| [Pandoc - Universal Document Converter](https://pandoc.org/installing.html) | ||||
| 
 | ||||
| ## Methods | ||||
| ### Automatic | ||||
| ### Windows - Manually | ||||
| #### Step 1: Built-in Downloader | ||||
| 
 | ||||
| This method requires some luck to pull off. | ||||
| You need to get a copy of the requirements.txt provided with chatter, I recommend this method. | ||||
| 
 | ||||
| #### Step 1: Add repo and install cog | ||||
| ``` | ||||
| [p]repo add Fox https://github.com/bobloy/Fox-V3 | ||||
| ``` | ||||
| 
 | ||||
| #### Step 2: Install Requirements | ||||
| 
 | ||||
| Make sure you have your virtual environment that you installed Red on activated before starting this step. See the Red Docs for details on how. | ||||
| 
 | ||||
| In a terminal running as an admin, navigate to the directory containing this repo.  | ||||
| 
 | ||||
| I've used my install directory as an example. | ||||
| 
 | ||||
| ``` | ||||
| cd C:\Users\Bobloy\AppData\Local\Red-DiscordBot\Red-DiscordBot\data\bobbot\cogs\RepoManager\repos\Fox\chatter | ||||
| pip install -r requirements.txt | ||||
| pip install --no-deps "chatterbot>=1.1" | ||||
| ``` | ||||
| 
 | ||||
| #### Step 3: Load Chatter | ||||
| 
 | ||||
| ``` | ||||
| [p]repo add Fox https://github.com/bobloy/Fox-V3  # If you didn't already do this in step 1 | ||||
| [p]cog install Fox chatter | ||||
| [p]load chatter | ||||
| ``` | ||||
| 
 | ||||
| ### Linux - Manually | ||||
| 
 | ||||
| #### Step 1: Built-in Downloader | ||||
| 
 | ||||
| ``` | ||||
| [p]repo add Fox https://github.com/bobloy/Fox-V3 | ||||
| [p]cog install Fox chatter | ||||
| ``` | ||||
| 
 | ||||
| If you get an error at this step, stop and skip to one of the manual methods below. | ||||
| #### Step 2: Install Requirements | ||||
| 
 | ||||
| #### Step 2: Install additional dependencies | ||||
| 
 | ||||
| Here you need to decide which training models you want to have available to you. | ||||
| 
 | ||||
| Shutdown the bot and run any number of these in the console: | ||||
| In your console with your virtual environment activated: | ||||
| 
 | ||||
| ``` | ||||
| python -m spacy download en_core_web_sm  # ~15 MB | ||||
| 
 | ||||
| python -m spacy download en_core_web_md  # ~50 MB | ||||
| 
 | ||||
| python -m spacy download en_core_web_lg  # ~750 MB (CPU Optimized) | ||||
| 
 | ||||
| python -m spacy download en_core_web_trf  # ~500 MB (GPU Optimized) | ||||
| pip install --no-deps "chatterbot>=1.1" | ||||
| ``` | ||||
| 
 | ||||
| #### Step 3: Load the cog and get started | ||||
| ### Step 3: Load Chatter | ||||
| 
 | ||||
| ``` | ||||
| [p]load chatter | ||||
| ``` | ||||
| 
 | ||||
| ### Windows - Manually | ||||
| Deprecated | ||||
| 
 | ||||
| ### Linux - Manually | ||||
| Deprecated | ||||
| 
 | ||||
| # Configuration | ||||
| 
 | ||||
| Chatter works out the box without any training by learning as it goes,  | ||||
| Chatter works out the the box without any training by learning as it goes,  | ||||
| but will have very poor and repetitive responses at first. | ||||
| 
 | ||||
| Initial training is recommended to speed up its learning. | ||||
|  | ||||
| @ -1,10 +1,8 @@ | ||||
| from .chat import Chatter | ||||
| 
 | ||||
| 
 | ||||
| async def setup(bot): | ||||
|     cog = Chatter(bot) | ||||
|     await cog.initialize() | ||||
|     bot.add_cog(cog) | ||||
| def setup(bot): | ||||
|     bot.add_cog(Chatter(bot)) | ||||
| 
 | ||||
| 
 | ||||
| # __all__ = ( | ||||
|  | ||||
							
								
								
									
										404
									
								
								chatter/chat.py
									
									
									
									
									
								
							
							
						
						
									
										404
									
								
								chatter/chat.py
									
									
									
									
									
								
							| @ -2,10 +2,8 @@ import asyncio | ||||
| import logging | ||||
| import os | ||||
| import pathlib | ||||
| from collections import defaultdict | ||||
| from datetime import datetime, timedelta | ||||
| from functools import partial | ||||
| from typing import Dict, List, Optional | ||||
| from typing import Optional | ||||
| 
 | ||||
| import discord | ||||
| from chatterbot import ChatBot | ||||
| @ -17,9 +15,6 @@ from redbot.core.commands import Cog | ||||
| from redbot.core.data_manager import cog_data_path | ||||
| from redbot.core.utils.predicates import MessagePredicate | ||||
| 
 | ||||
| from chatter.trainers import MovieTrainer, TwitterCorpusTrainer, UbuntuCorpusTrainer2 | ||||
| 
 | ||||
| chatterbot_log = logging.getLogger("red.fox_v3.chatterbot") | ||||
| log = logging.getLogger("red.fox_v3.chatter") | ||||
| 
 | ||||
| 
 | ||||
| @ -30,12 +25,6 @@ def my_local_get_prefix(prefixes, content): | ||||
|     return None | ||||
| 
 | ||||
| 
 | ||||
| class ENG_TRF: | ||||
|     ISO_639_1 = "en_core_web_trf" | ||||
|     ISO_639 = "eng" | ||||
|     ENGLISH_NAME = "English" | ||||
| 
 | ||||
| 
 | ||||
| class ENG_LG: | ||||
|     ISO_639_1 = "en_core_web_lg" | ||||
|     ISO_639 = "eng" | ||||
| @ -59,77 +48,50 @@ class Chatter(Cog): | ||||
|     This cog trains a chatbot that will talk like members of your Guild | ||||
|     """ | ||||
| 
 | ||||
|     models = [ENG_SM, ENG_MD, ENG_LG, ENG_TRF] | ||||
|     algos = [SpacySimilarity, JaccardSimilarity, LevenshteinDistance] | ||||
| 
 | ||||
|     def __init__(self, bot): | ||||
|         super().__init__() | ||||
|         self.bot = bot | ||||
|         self.config = Config.get_conf(self, identifier=6710497116116101114) | ||||
|         default_global = {"learning": True, "model_number": 0, "algo_number": 0, "threshold": 0.90} | ||||
|         self.default_guild = { | ||||
|             "whitelist": None, | ||||
|             "days": 1, | ||||
|             "convo_delta": 15, | ||||
|             "chatchannel": None, | ||||
|             "reply": True, | ||||
|         } | ||||
|         default_global = {} | ||||
|         default_guild = {"whitelist": None, "days": 1, "convo_delta": 15, "chatchannel": None} | ||||
|         path: pathlib.Path = cog_data_path(self) | ||||
|         self.data_path = path / "database.sqlite3" | ||||
| 
 | ||||
|         # TODO: Move training_model and similarity_algo to config | ||||
|         # TODO: Add an option to see current settings | ||||
| 
 | ||||
|         self.tagger_language = ENG_SM | ||||
|         self.tagger_language = ENG_MD | ||||
|         self.similarity_algo = SpacySimilarity | ||||
|         self.similarity_threshold = 0.90 | ||||
|         self.chatbot = None | ||||
|         self.chatbot = self._create_chatbot() | ||||
|         # self.chatbot.set_trainer(ListTrainer) | ||||
| 
 | ||||
|         # self.trainer = ListTrainer(self.chatbot) | ||||
| 
 | ||||
|         self.config.register_global(**default_global) | ||||
|         self.config.register_guild(**self.default_guild) | ||||
|         self.config.register_guild(**default_guild) | ||||
| 
 | ||||
|         self.loop = asyncio.get_event_loop() | ||||
| 
 | ||||
|         self._guild_cache = defaultdict(dict) | ||||
|         self._global_cache = {} | ||||
| 
 | ||||
|         self._last_message_per_channel: Dict[Optional[discord.Message]] = defaultdict(lambda: None) | ||||
| 
 | ||||
|     async def red_delete_data_for_user(self, **kwargs): | ||||
|         """Nothing to delete""" | ||||
|         return | ||||
| 
 | ||||
|     async def initialize(self): | ||||
|         all_config = dict(self.config.defaults["GLOBAL"]) | ||||
|         all_config.update(await self.config.all()) | ||||
|         model_number = all_config["model_number"] | ||||
|         algo_number = all_config["algo_number"] | ||||
|         threshold = all_config["threshold"] | ||||
| 
 | ||||
|         self.tagger_language = self.models[model_number] | ||||
|         self.similarity_algo = self.algos[algo_number] | ||||
|         self.similarity_threshold = threshold | ||||
|         self.chatbot = self._create_chatbot() | ||||
| 
 | ||||
|     def _create_chatbot(self): | ||||
| 
 | ||||
|         return ChatBot( | ||||
|             "ChatterBot", | ||||
|             # storage_adapter="chatterbot.storage.SQLStorageAdapter", | ||||
|             storage_adapter="chatter.storage_adapters.MyDumbSQLStorageAdapter", | ||||
|             storage_adapter="chatterbot.storage.SQLStorageAdapter", | ||||
|             database_uri="sqlite:///" + str(self.data_path), | ||||
|             statement_comparison_function=self.similarity_algo, | ||||
|             response_selection_method=get_random_response, | ||||
|             logic_adapters=["chatterbot.logic.BestMatch"], | ||||
|             maximum_similarity_threshold=self.similarity_threshold, | ||||
|             tagger_language=self.tagger_language, | ||||
|             logger=chatterbot_log, | ||||
|             logger=log, | ||||
|         ) | ||||
| 
 | ||||
|     async def _get_conversation(self, ctx, in_channels: List[discord.TextChannel]): | ||||
|     async def _get_conversation(self, ctx, in_channel: discord.TextChannel = None): | ||||
|         """ | ||||
|         Compiles all conversation in the Guild this bot can get it's hands on | ||||
|         Currently takes a stupid long time | ||||
| @ -143,12 +105,20 @@ class Chatter(Cog): | ||||
|             return msg.clean_content | ||||
| 
 | ||||
|         def new_conversation(msg, sent, out_in, delta): | ||||
|             # Should always be positive numbers | ||||
|             # if sent is None: | ||||
|             #     return False | ||||
| 
 | ||||
|             # Don't do "too short" processing here. Sometimes people don't respond. | ||||
|             # if len(out_in) < 2: | ||||
|             #     return False | ||||
| 
 | ||||
|             # print(msg.created_at - sent) | ||||
| 
 | ||||
|             return msg.created_at - sent >= delta | ||||
| 
 | ||||
|         for channel in in_channels: | ||||
|             # if in_channel: | ||||
|             #     channel = in_channel | ||||
|         for channel in ctx.guild.text_channels: | ||||
|             if in_channel: | ||||
|                 channel = in_channel | ||||
|             await ctx.maybe_send_embed("Gathering {}".format(channel.mention)) | ||||
|             user = None | ||||
|             i = 0 | ||||
| @ -183,47 +153,16 @@ class Chatter(Cog): | ||||
|             except discord.HTTPException: | ||||
|                 pass | ||||
| 
 | ||||
|             # if in_channel: | ||||
|             #     break | ||||
|             if in_channel: | ||||
|                 break | ||||
| 
 | ||||
|         return out | ||||
| 
 | ||||
|     def _train_twitter(self, *args, **kwargs): | ||||
|         trainer = TwitterCorpusTrainer(self.chatbot) | ||||
|         trainer.train(*args, **kwargs) | ||||
|         return True | ||||
| 
 | ||||
|     def _train_ubuntu(self): | ||||
|         trainer = UbuntuCorpusTrainer( | ||||
|             self.chatbot, ubuntu_corpus_data_directory=cog_data_path(self) / "ubuntu_data" | ||||
|         ) | ||||
|         trainer = UbuntuCorpusTrainer(self.chatbot) | ||||
|         trainer.train() | ||||
|         return True | ||||
| 
 | ||||
|     async def _train_movies(self): | ||||
|         trainer = MovieTrainer(self.chatbot, cog_data_path(self)) | ||||
|         return await trainer.asynctrain() | ||||
| 
 | ||||
|     async def _train_ubuntu2(self, intensity): | ||||
|         train_kwarg = {} | ||||
|         if intensity == 196: | ||||
|             train_kwarg["train_dialogue"] = False | ||||
|             train_kwarg["train_196"] = True | ||||
|         elif intensity == 301: | ||||
|             train_kwarg["train_dialogue"] = False | ||||
|             train_kwarg["train_301"] = True | ||||
|         elif intensity == 497: | ||||
|             train_kwarg["train_dialogue"] = False | ||||
|             train_kwarg["train_196"] = True | ||||
|             train_kwarg["train_301"] = True | ||||
|         elif intensity >= 9000:  # NOT 9000! | ||||
|             train_kwarg["train_dialogue"] = True | ||||
|             train_kwarg["train_196"] = True | ||||
|             train_kwarg["train_301"] = True | ||||
| 
 | ||||
|         trainer = UbuntuCorpusTrainer2(self.chatbot, cog_data_path(self)) | ||||
|         return await trainer.asynctrain(**train_kwarg) | ||||
| 
 | ||||
|     def _train_english(self): | ||||
|         trainer = ChatterBotCorpusTrainer(self.chatbot) | ||||
|         # try: | ||||
| @ -235,10 +174,13 @@ class Chatter(Cog): | ||||
|     def _train(self, data): | ||||
|         trainer = ListTrainer(self.chatbot) | ||||
|         total = len(data) | ||||
|         # try: | ||||
|         for c, convo in enumerate(data, 1): | ||||
|             log.info(f"{c} / {total}") | ||||
|             if len(convo) > 1:  # TODO: Toggleable skipping short conversations | ||||
|                 print(f"{c} / {total}") | ||||
|                 trainer.train(convo) | ||||
|         # except: | ||||
|         #     return False | ||||
|         return True | ||||
| 
 | ||||
|     @commands.group(invoke_without_command=False) | ||||
| @ -246,10 +188,10 @@ class Chatter(Cog): | ||||
|         """ | ||||
|         Base command for this cog. Check help for the commands list. | ||||
|         """ | ||||
|         self._guild_cache[ctx.guild.id] = {}  # Clear cache when modifying values | ||||
|         self._global_cache = {} | ||||
|         if ctx.invoked_subcommand is None: | ||||
|             pass | ||||
| 
 | ||||
|     @commands.admin() | ||||
|     @checks.admin() | ||||
|     @chatter.command(name="channel") | ||||
|     async def chatter_channel( | ||||
|         self, ctx: commands.Context, channel: Optional[discord.TextChannel] = None | ||||
| @ -269,55 +211,13 @@ 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}") | ||||
| 
 | ||||
|     @commands.admin() | ||||
|     @chatter.command(name="reply") | ||||
|     async def chatter_reply(self, ctx: commands.Context, toggle: Optional[bool] = None): | ||||
|         """ | ||||
|         Toggle bot reply to messages if conversation continuity is not present | ||||
| 
 | ||||
|         """ | ||||
|         reply = await self.config.guild(ctx.guild).reply() | ||||
|         if toggle is None: | ||||
|             toggle = not reply | ||||
|         await self.config.guild(ctx.guild).reply.set(toggle) | ||||
| 
 | ||||
|         if toggle: | ||||
|             await ctx.maybe_send_embed( | ||||
|                 "I will now respond to you if conversation continuity is not present" | ||||
|             ) | ||||
|         else: | ||||
|             await ctx.maybe_send_embed( | ||||
|                 "I will not reply to your message if conversation continuity is not present, anymore" | ||||
|             ) | ||||
| 
 | ||||
|     @commands.is_owner() | ||||
|     @chatter.command(name="learning") | ||||
|     async def chatter_learning(self, ctx: commands.Context, toggle: Optional[bool] = None): | ||||
|         """ | ||||
|         Toggle the bot learning from its conversations. | ||||
| 
 | ||||
|         This is a global setting. | ||||
|         This is on by default. | ||||
|         """ | ||||
|         learning = await self.config.learning() | ||||
|         if toggle is None: | ||||
|             toggle = not learning | ||||
|         await self.config.learning.set(toggle) | ||||
| 
 | ||||
|         if toggle: | ||||
|             await ctx.maybe_send_embed("I will now learn from conversations.") | ||||
|         else: | ||||
|             await ctx.maybe_send_embed("I will no longer learn from conversations.") | ||||
| 
 | ||||
|     @commands.is_owner() | ||||
|     @checks.is_owner() | ||||
|     @chatter.command(name="cleardata") | ||||
|     async def chatter_cleardata(self, ctx: commands.Context, confirm: bool = False): | ||||
|         """ | ||||
|         This command will erase all training data and reset your configuration settings. | ||||
|         This command will erase all training data and reset your configuration settings | ||||
| 
 | ||||
|         This applies to all guilds. | ||||
| 
 | ||||
|         Use `[p]chatter cleardata True` to confirm. | ||||
|         Use `[p]chatter cleardata True` | ||||
|         """ | ||||
| 
 | ||||
|         if not confirm: | ||||
| @ -344,18 +244,21 @@ class Chatter(Cog): | ||||
| 
 | ||||
|         await ctx.tick() | ||||
| 
 | ||||
|     @commands.is_owner() | ||||
|     @checks.is_owner() | ||||
|     @chatter.command(name="algorithm", aliases=["algo"]) | ||||
|     async def chatter_algorithm( | ||||
|         self, ctx: commands.Context, algo_number: int, threshold: float = None | ||||
|     ): | ||||
|         """ | ||||
|         Switch the active logic algorithm to one of the three. Default is Spacy | ||||
|         Switch the active logic algorithm to one of the three. Default after reload is Spacy | ||||
| 
 | ||||
|         0: Spacy | ||||
|         1: Jaccard | ||||
|         2: Levenshtein | ||||
|         """ | ||||
| 
 | ||||
|         algos = [SpacySimilarity, JaccardSimilarity, LevenshteinDistance] | ||||
| 
 | ||||
|         if algo_number < 0 or algo_number > 2: | ||||
|             await ctx.send_help() | ||||
|             return | ||||
| @ -367,33 +270,32 @@ class Chatter(Cog): | ||||
|                 ) | ||||
|                 return | ||||
|             else: | ||||
|                 self.similarity_threshold = threshold | ||||
|                 await self.config.threshold.set(self.similarity_threshold) | ||||
| 
 | ||||
|         self.similarity_algo = self.algos[algo_number] | ||||
|         await self.config.algo_number.set(algo_number) | ||||
|                 self.similarity_algo = threshold | ||||
| 
 | ||||
|         self.similarity_algo = algos[algo_number] | ||||
|         async with ctx.typing(): | ||||
|             self.chatbot = self._create_chatbot() | ||||
| 
 | ||||
|             await ctx.tick() | ||||
| 
 | ||||
|     @commands.is_owner() | ||||
|     @checks.is_owner() | ||||
|     @chatter.command(name="model") | ||||
|     async def chatter_model(self, ctx: commands.Context, model_number: int): | ||||
|         """ | ||||
|         Switch the active model to one of the three. Default is Small | ||||
|         Switch the active model to one of the three. Default after reload is Medium | ||||
| 
 | ||||
|         0: Small | ||||
|         1: Medium (Requires additional setup) | ||||
|         1: Medium | ||||
|         2: Large (Requires additional setup) | ||||
|         3. Accurate (Requires additional setup) | ||||
|         """ | ||||
|         if model_number < 0 or model_number > 3: | ||||
| 
 | ||||
|         models = [ENG_SM, ENG_MD, ENG_LG] | ||||
| 
 | ||||
|         if model_number < 0 or model_number > 2: | ||||
|             await ctx.send_help() | ||||
|             return | ||||
| 
 | ||||
|         if model_number >= 0: | ||||
|         if model_number == 2: | ||||
|             await ctx.maybe_send_embed( | ||||
|                 "Additional requirements needed. See guide before continuing.\n" "Continue?" | ||||
|             ) | ||||
| @ -406,8 +308,7 @@ class Chatter(Cog): | ||||
|             if not pred.result: | ||||
|                 return | ||||
| 
 | ||||
|         self.tagger_language = self.models[model_number] | ||||
|         await self.config.model_number.set(model_number) | ||||
|         self.tagger_language = models[model_number] | ||||
|         async with ctx.typing(): | ||||
|             self.chatbot = self._create_chatbot() | ||||
| 
 | ||||
| @ -415,14 +316,8 @@ class Chatter(Cog): | ||||
|                 f"Model has been switched to {self.tagger_language.ISO_639_1}" | ||||
|             ) | ||||
| 
 | ||||
|     @commands.is_owner() | ||||
|     @chatter.group(name="trainset") | ||||
|     async def chatter_trainset(self, ctx: commands.Context): | ||||
|         """Commands for configuring training""" | ||||
|         pass | ||||
| 
 | ||||
|     @commands.is_owner() | ||||
|     @chatter_trainset.command(name="minutes") | ||||
|     @checks.is_owner() | ||||
|     @chatter.command(name="minutes") | ||||
|     async def minutes(self, ctx: commands.Context, minutes: int): | ||||
|         """ | ||||
|         Sets the number of minutes the bot will consider a break in a conversation during training | ||||
| @ -433,12 +328,12 @@ class Chatter(Cog): | ||||
|             await ctx.send_help() | ||||
|             return | ||||
| 
 | ||||
|         await self.config.guild(ctx.guild).convo_delta.set(minutes) | ||||
|         await self.config.guild(ctx.guild).convo_length.set(minutes) | ||||
| 
 | ||||
|         await ctx.tick() | ||||
| 
 | ||||
|     @commands.is_owner() | ||||
|     @chatter_trainset.command(name="age") | ||||
|     @checks.is_owner() | ||||
|     @chatter.command(name="age") | ||||
|     async def age(self, ctx: commands.Context, days: int): | ||||
|         """ | ||||
|         Sets the number of days to look back | ||||
| @ -452,16 +347,7 @@ class Chatter(Cog): | ||||
|         await self.config.guild(ctx.guild).days.set(days) | ||||
|         await ctx.tick() | ||||
| 
 | ||||
|     @commands.is_owner() | ||||
|     @chatter.command(name="kaggle") | ||||
|     async def chatter_kaggle(self, ctx: commands.Context): | ||||
|         """Register with the kaggle API to download additional datasets for training""" | ||||
|         if not await self.check_for_kaggle(): | ||||
|             await ctx.maybe_send_embed( | ||||
|                 "[Click here for instructions to setup the kaggle api](https://github.com/Kaggle/kaggle-api#api-credentials)" | ||||
|             ) | ||||
| 
 | ||||
|     @commands.is_owner() | ||||
|     @checks.is_owner() | ||||
|     @chatter.command(name="backup") | ||||
|     async def backup(self, ctx, backupname): | ||||
|         """ | ||||
| @ -483,71 +369,8 @@ class Chatter(Cog): | ||||
|         else: | ||||
|             await ctx.maybe_send_embed("Error occurred :(") | ||||
| 
 | ||||
|     @commands.is_owner() | ||||
|     @chatter.group(name="train") | ||||
|     async def chatter_train(self, ctx: commands.Context): | ||||
|         """Commands for training the bot""" | ||||
|         pass | ||||
| 
 | ||||
|     @chatter_train.group(name="kaggle") | ||||
|     async def chatter_train_kaggle(self, ctx: commands.Context): | ||||
|         """ | ||||
|         Base command for kaggle training sets. | ||||
| 
 | ||||
|         See `[p]chatter kaggle` for details on how to enable this option | ||||
|         """ | ||||
|         pass | ||||
| 
 | ||||
|     @chatter_train_kaggle.command(name="ubuntu") | ||||
|     async def chatter_train_kaggle_ubuntu( | ||||
|         self, ctx: commands.Context, confirmation: bool = False, intensity=0 | ||||
|     ): | ||||
|         """ | ||||
|         WARNING: Large Download! Trains the bot using *NEW* Ubuntu Dialog Corpus data. | ||||
|         """ | ||||
| 
 | ||||
|         if not confirmation: | ||||
|             await ctx.maybe_send_embed( | ||||
|                 "Warning: This command downloads ~800MB and is CPU intensive during training\n" | ||||
|                 "If you're sure you want to continue, run `[p]chatter train kaggle ubuntu True`" | ||||
|             ) | ||||
|             return | ||||
| 
 | ||||
|         async with ctx.typing(): | ||||
|             future = await self._train_ubuntu2(intensity) | ||||
| 
 | ||||
|         if future: | ||||
|             await ctx.maybe_send_embed("Training successful!") | ||||
|         else: | ||||
|             await ctx.maybe_send_embed("Error occurred :(") | ||||
| 
 | ||||
|     @chatter_train_kaggle.command(name="movies") | ||||
|     async def chatter_train_kaggle_movies(self, ctx: commands.Context, confirmation: bool = False): | ||||
|         """ | ||||
|         WARNING: Language! Trains the bot using Cornell University's "Movie Dialog Corpus". | ||||
| 
 | ||||
|         This training set contains dialog from a spread of movies with different MPAA. | ||||
|         This dialog includes racism, sexism, and any number of sensitive topics. | ||||
| 
 | ||||
|         Use at your own risk. | ||||
|         """ | ||||
| 
 | ||||
|         if not confirmation: | ||||
|             await ctx.maybe_send_embed( | ||||
|                 "Warning: This command downloads ~29MB and is CPU intensive during training\n" | ||||
|                 "If you're sure you want to continue, run `[p]chatter train kaggle movies True`" | ||||
|             ) | ||||
|             return | ||||
| 
 | ||||
|         async with ctx.typing(): | ||||
|             future = await self._train_movies() | ||||
| 
 | ||||
|         if future: | ||||
|             await ctx.maybe_send_embed("Training successful!") | ||||
|         else: | ||||
|             await ctx.maybe_send_embed("Error occurred :(") | ||||
| 
 | ||||
|     @chatter_train.command(name="ubuntu") | ||||
|     @checks.is_owner() | ||||
|     @chatter.command(name="trainubuntu") | ||||
|     async def chatter_train_ubuntu(self, ctx: commands.Context, confirmation: bool = False): | ||||
|         """ | ||||
|         WARNING: Large Download! Trains the bot using Ubuntu Dialog Corpus data. | ||||
| @ -555,8 +378,8 @@ class Chatter(Cog): | ||||
| 
 | ||||
|         if not confirmation: | ||||
|             await ctx.maybe_send_embed( | ||||
|                 "Warning: This command downloads ~500MB and is CPU intensive during training\n" | ||||
|                 "If you're sure you want to continue, run `[p]chatter train ubuntu True`" | ||||
|                 "Warning: This command downloads ~500MB then eats your CPU for training\n" | ||||
|                 "If you're sure you want to continue, run `[p]chatter trainubuntu True`" | ||||
|             ) | ||||
|             return | ||||
| 
 | ||||
| @ -564,11 +387,12 @@ class Chatter(Cog): | ||||
|             future = await self.loop.run_in_executor(None, self._train_ubuntu) | ||||
| 
 | ||||
|         if future: | ||||
|             await ctx.maybe_send_embed("Training successful!") | ||||
|             await ctx.send("Training successful!") | ||||
|         else: | ||||
|             await ctx.maybe_send_embed("Error occurred :(") | ||||
|             await ctx.send("Error occurred :(") | ||||
| 
 | ||||
|     @chatter_train.command(name="english") | ||||
|     @checks.is_owner() | ||||
|     @chatter.command(name="trainenglish") | ||||
|     async def chatter_train_english(self, ctx: commands.Context): | ||||
|         """ | ||||
|         Trains the bot in english | ||||
| @ -581,32 +405,12 @@ class Chatter(Cog): | ||||
|         else: | ||||
|             await ctx.maybe_send_embed("Error occurred :(") | ||||
| 
 | ||||
|     @chatter_train.command(name="list") | ||||
|     async def chatter_train_list(self, ctx: commands.Context): | ||||
|         """Trains the bot based on an uploaded list. | ||||
| 
 | ||||
|         Must be a file in the format of a python list: ['prompt', 'response1', 'response2'] | ||||
|     @checks.is_owner() | ||||
|     @chatter.command() | ||||
|     async def train(self, ctx: commands.Context, channel: discord.TextChannel): | ||||
|         """ | ||||
|         if not ctx.message.attachments: | ||||
|             await ctx.maybe_send_embed("You must upload a file when using this command") | ||||
|             return | ||||
| 
 | ||||
|         attachment: discord.Attachment = ctx.message.attachments[0] | ||||
| 
 | ||||
|         a_bytes = await attachment.read() | ||||
| 
 | ||||
|         await ctx.send("Not yet implemented") | ||||
| 
 | ||||
|     @chatter_train.command(name="channel") | ||||
|     async def chatter_train_channel( | ||||
|         self, ctx: commands.Context, channels: commands.Greedy[discord.TextChannel] | ||||
|     ): | ||||
|         Trains the bot based on language in this guild | ||||
|         """ | ||||
|         Trains the bot based on language in this guild. | ||||
|         """ | ||||
|         if not channels: | ||||
|             await ctx.send_help() | ||||
|             return | ||||
| 
 | ||||
|         await ctx.maybe_send_embed( | ||||
|             "Warning: The cog may use significant RAM or CPU if trained on large data sets.\n" | ||||
| @ -615,7 +419,7 @@ class Chatter(Cog): | ||||
|         ) | ||||
| 
 | ||||
|         async with ctx.typing(): | ||||
|             conversation = await self._get_conversation(ctx, channels) | ||||
|             conversation = await self._get_conversation(ctx, channel) | ||||
| 
 | ||||
|         if not conversation: | ||||
|             await ctx.maybe_send_embed("Failed to gather training data") | ||||
| @ -657,7 +461,7 @@ class Chatter(Cog): | ||||
| 
 | ||||
|         guild: discord.Guild = getattr(message, "guild", None) | ||||
| 
 | ||||
|         if guild is None or await self.bot.cog_disabled_in_guild(self, guild): | ||||
|         if await self.bot.cog_disabled_in_guild(self, guild): | ||||
|             return | ||||
| 
 | ||||
|         ctx: commands.Context = await self.bot.get_context(message) | ||||
| @ -669,18 +473,7 @@ class Chatter(Cog): | ||||
|         # Thank you Cog-Creators | ||||
|         channel: discord.TextChannel = message.channel | ||||
| 
 | ||||
|         if not self._guild_cache[guild.id]: | ||||
|             self._guild_cache[guild.id] = await self.config.guild(guild).all() | ||||
| 
 | ||||
|         is_reply = False  # this is only useful with in_response_to | ||||
|         if ( | ||||
|             message.reference is not None | ||||
|             and isinstance(message.reference.resolved, discord.Message) | ||||
|             and message.reference.resolved.author.id == self.bot.user.id | ||||
|         ): | ||||
|             is_reply = True  # this is only useful with in_response_to | ||||
|             pass  # this is a reply to the bot, good to go | ||||
|         elif guild is not None and channel.id == self._guild_cache[guild.id]["chatchannel"]: | ||||
|         if guild is not None and channel.id == await self.config.guild(guild).chatchannel(): | ||||
|             pass  # good to go | ||||
|         else: | ||||
|             when_mentionables = commands.when_mentioned(self.bot, message) | ||||
| @ -695,57 +488,10 @@ class Chatter(Cog): | ||||
| 
 | ||||
|         text = message.clean_content | ||||
| 
 | ||||
|         async with ctx.typing(): | ||||
| 
 | ||||
|             if is_reply: | ||||
|                 in_response_to = message.reference.resolved.content | ||||
|             elif self._last_message_per_channel[ctx.channel.id] is not None: | ||||
|                 last_m: discord.Message = self._last_message_per_channel[ctx.channel.id] | ||||
|                 minutes = self._guild_cache[ctx.guild.id]["convo_delta"] | ||||
|                 if (datetime.utcnow() - last_m.created_at).seconds > minutes * 60: | ||||
|                     in_response_to = None | ||||
|                 else: | ||||
|                     in_response_to = last_m.content | ||||
|             else: | ||||
|                 in_response_to = None | ||||
| 
 | ||||
|             # Always use generate reponse | ||||
|             # Chatterbot tries to learn based on the result it comes up with, which is dumb | ||||
|             log.debug("Generating response") | ||||
|             Statement = self.chatbot.storage.get_object("statement") | ||||
|             future = await self.loop.run_in_executor( | ||||
|                 None, self.chatbot.generate_response, Statement(text) | ||||
|             ) | ||||
| 
 | ||||
|             if not self._global_cache: | ||||
|                 self._global_cache = await self.config.all() | ||||
| 
 | ||||
|             if in_response_to is not None and self._global_cache["learning"]: | ||||
|                 log.debug("learning response") | ||||
|                 await self.loop.run_in_executor( | ||||
|                     None, | ||||
|                     partial( | ||||
|                         self.chatbot.learn_response, | ||||
|                         Statement(text), | ||||
|                         previous_statement=in_response_to, | ||||
|                     ), | ||||
|                 ) | ||||
| 
 | ||||
|             replying = None | ||||
|             if ( | ||||
|                 "reply" not in self._guild_cache[guild.id] and self.default_guild["reply"] | ||||
|             ) or self._guild_cache[guild.id]["reply"]: | ||||
|                 if message != ctx.channel.last_message: | ||||
|                     replying = message | ||||
|         async with channel.typing(): | ||||
|             future = await self.loop.run_in_executor(None, self.chatbot.get_response, text) | ||||
| 
 | ||||
|             if future and str(future): | ||||
|                 self._last_message_per_channel[ctx.channel.id] = await channel.send( | ||||
|                     str(future), reference=replying | ||||
|                 ) | ||||
|                 await channel.send(str(future)) | ||||
|             else: | ||||
|                 await ctx.send(":thinking:") | ||||
| 
 | ||||
|     async def check_for_kaggle(self): | ||||
|         """Check whether Kaggle is installed and configured properly""" | ||||
|         # TODO: This | ||||
|         return False | ||||
|                 await channel.send(":thinking:") | ||||
|  | ||||
| @ -2,15 +2,22 @@ | ||||
|   "author": [ | ||||
|     "Bobloy" | ||||
|   ], | ||||
|   "min_bot_version": "3.4.6", | ||||
|   "description": "Create an offline chatbot that talks like your average member using Machine Learning. See setup instructions at https://github.com/bobloy/Fox-V3/tree/master/chatter", | ||||
|   "min_bot_version": "3.4.0", | ||||
|   "description": "Create an offline chatbot that talks like your average member using Machine Learning", | ||||
|   "hidden": false, | ||||
|   "install_msg": "Thank you for installing Chatter! Please make sure you check the install instructions at https://github.com/bobloy/Fox-V3/blob/master/chatter/README.md\nAfter that, get started ith `[p]load chatter` and `[p]help Chatter`", | ||||
|   "requirements": [ | ||||
|     "git+git://github.com/bobloy/ChatterBot@fox#egg=ChatterBot>=1.1.0.dev4", | ||||
|     "kaggle", | ||||
|     "https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.1.0/en_core_web_sm-3.1.0.tar.gz#egg=en_core_web_sm", | ||||
|     "https://github.com/explosion/spacy-models/releases/download/en_core_web_md-3.1.0/en_core_web_md-3.1.0.tar.gz#egg=en_core_web_md" | ||||
|     "git+git://github.com/gunthercox/chatterbot-corpus@master#egg=chatterbot_corpus", | ||||
|     "mathparse>=0.1,<0.2", | ||||
|     "nltk>=3.2,<4.0", | ||||
|     "pint>=0.8.1", | ||||
|     "python-dateutil>=2.8,<2.9", | ||||
|     "pyyaml>=5.3,<5.4", | ||||
|     "sqlalchemy>=1.3,<1.4", | ||||
|     "pytz", | ||||
|     "https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-2.3.1/en_core_web_sm-2.3.1.tar.gz#egg=en_core_web_sm", | ||||
|     "https://github.com/explosion/spacy-models/releases/download/en_core_web_md-2.3.1/en_core_web_md-2.3.1.tar.gz#egg=en_core_web_md", | ||||
|     "spacy>=2.3,<2.4" | ||||
|   ], | ||||
|   "short": "Local Chatbot run on machine learning", | ||||
|   "end_user_data_statement": "This cog only stores anonymous conversations data; no End User Data is stored.", | ||||
|  | ||||
							
								
								
									
										12
									
								
								chatter/requirements.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								chatter/requirements.txt
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | ||||
| git+git://github.com/gunthercox/chatterbot-corpus@master#egg=chatterbot_corpus | ||||
| mathparse>=0.1,<0.2 | ||||
| nltk>=3.2,<4.0 | ||||
| pint>=0.8.1 | ||||
| python-dateutil>=2.8,<2.9 | ||||
| pyyaml>=5.3,<5.4 | ||||
| sqlalchemy>=1.3,<1.4 | ||||
| pytz | ||||
| spacy>=2.3,<2.4 | ||||
| https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-2.3.1/en_core_web_sm-2.3.1.tar.gz#egg=en_core_web_sm | ||||
| https://github.com/explosion/spacy-models/releases/download/en_core_web_md-2.3.1/en_core_web_md-2.3.1.tar.gz#egg=en_core_web_md | ||||
| # https://github.com/explosion/spacy-models/releases/download/en_core_web_lg-2.3.1/en_core_web_lg-2.3.1.tar.gz#egg=en_core_web_lg | ||||
| @ -1,71 +0,0 @@ | ||||
| from chatterbot.storage import StorageAdapter, SQLStorageAdapter | ||||
| 
 | ||||
| 
 | ||||
| class MyDumbSQLStorageAdapter(SQLStorageAdapter): | ||||
|     def __init__(self, **kwargs): | ||||
|         super(SQLStorageAdapter, self).__init__(**kwargs) | ||||
| 
 | ||||
|         from sqlalchemy import create_engine, inspect | ||||
|         from sqlalchemy.orm import sessionmaker | ||||
| 
 | ||||
|         self.database_uri = kwargs.get("database_uri", False) | ||||
| 
 | ||||
|         # None results in a sqlite in-memory database as the default | ||||
|         if self.database_uri is None: | ||||
|             self.database_uri = "sqlite://" | ||||
| 
 | ||||
|         # Create a file database if the database is not a connection string | ||||
|         if not self.database_uri: | ||||
|             self.database_uri = "sqlite:///db.sqlite3" | ||||
| 
 | ||||
|         self.engine = create_engine(self.database_uri, connect_args={"check_same_thread": False}) | ||||
| 
 | ||||
|         if self.database_uri.startswith("sqlite://"): | ||||
|             from sqlalchemy.engine import Engine | ||||
|             from sqlalchemy import event | ||||
| 
 | ||||
|             @event.listens_for(Engine, "connect") | ||||
|             def set_sqlite_pragma(dbapi_connection, connection_record): | ||||
|                 dbapi_connection.execute("PRAGMA journal_mode=WAL") | ||||
|                 dbapi_connection.execute("PRAGMA synchronous=NORMAL") | ||||
| 
 | ||||
|         if not inspect(self.engine).has_table("Statement"): | ||||
|             self.create_database() | ||||
| 
 | ||||
|         self.Session = sessionmaker(bind=self.engine, expire_on_commit=True) | ||||
| 
 | ||||
| 
 | ||||
| class AsyncSQLStorageAdapter(SQLStorageAdapter): | ||||
|     def __init__(self, **kwargs): | ||||
|         super(SQLStorageAdapter, self).__init__(**kwargs) | ||||
| 
 | ||||
|         self.database_uri = kwargs.get("database_uri", False) | ||||
| 
 | ||||
|         # None results in a sqlite in-memory database as the default | ||||
|         if self.database_uri is None: | ||||
|             self.database_uri = "sqlite://" | ||||
| 
 | ||||
|         # Create a file database if the database is not a connection string | ||||
|         if not self.database_uri: | ||||
|             self.database_uri = "sqlite:///db.sqlite3" | ||||
| 
 | ||||
|     async def initialize(self): | ||||
|         # from sqlalchemy import create_engine | ||||
|         from aiomysql.sa import create_engine | ||||
|         from sqlalchemy.orm import sessionmaker | ||||
| 
 | ||||
|         self.engine = await create_engine(self.database_uri, convert_unicode=True) | ||||
| 
 | ||||
|         if self.database_uri.startswith("sqlite://"): | ||||
|             from sqlalchemy.engine import Engine | ||||
|             from sqlalchemy import event | ||||
| 
 | ||||
|             @event.listens_for(Engine, "connect") | ||||
|             def set_sqlite_pragma(dbapi_connection, connection_record): | ||||
|                 dbapi_connection.execute("PRAGMA journal_mode=WAL") | ||||
|                 dbapi_connection.execute("PRAGMA synchronous=NORMAL") | ||||
| 
 | ||||
|         if not self.engine.dialect.has_table(self.engine, "Statement"): | ||||
|             self.create_database() | ||||
| 
 | ||||
|         self.Session = sessionmaker(bind=self.engine, expire_on_commit=True) | ||||
| @ -1,351 +0,0 @@ | ||||
| import asyncio | ||||
| import csv | ||||
| import html | ||||
| import logging | ||||
| import os | ||||
| import pathlib | ||||
| import time | ||||
| from functools import partial | ||||
| 
 | ||||
| from chatterbot import utils | ||||
| from chatterbot.conversation import Statement | ||||
| from chatterbot.tagging import PosLemmaTagger | ||||
| from chatterbot.trainers import Trainer | ||||
| from redbot.core.bot import Red | ||||
| from dateutil import parser as date_parser | ||||
| from redbot.core.utils import AsyncIter | ||||
| 
 | ||||
| log = logging.getLogger("red.fox_v3.chatter.trainers") | ||||
| 
 | ||||
| 
 | ||||
| class KaggleTrainer(Trainer): | ||||
|     def __init__(self, chatbot, datapath: pathlib.Path, **kwargs): | ||||
|         super().__init__(chatbot, **kwargs) | ||||
| 
 | ||||
|         self.data_directory = datapath / kwargs.get("downloadpath", "kaggle_download") | ||||
| 
 | ||||
|         self.kaggle_dataset = kwargs.get( | ||||
|             "kaggle_dataset", | ||||
|             "Cornell-University/movie-dialog-corpus", | ||||
|         ) | ||||
| 
 | ||||
|         # Create the data directory if it does not already exist | ||||
|         if not os.path.exists(self.data_directory): | ||||
|             os.makedirs(self.data_directory) | ||||
| 
 | ||||
|     def is_downloaded(self, file_path): | ||||
|         """ | ||||
|         Check if the data file is already downloaded. | ||||
|         """ | ||||
|         if os.path.exists(file_path): | ||||
|             self.chatbot.logger.info("File is already downloaded") | ||||
|             return True | ||||
| 
 | ||||
|         return False | ||||
| 
 | ||||
|     async def download(self, dataset): | ||||
|         import kaggle  # This triggers the API token check | ||||
| 
 | ||||
|         future = await asyncio.get_event_loop().run_in_executor( | ||||
|             None, | ||||
|             partial( | ||||
|                 kaggle.api.dataset_download_files, | ||||
|                 dataset=dataset, | ||||
|                 path=self.data_directory, | ||||
|                 quiet=False, | ||||
|                 unzip=True, | ||||
|             ), | ||||
|         ) | ||||
| 
 | ||||
|     def train(self, *args, **kwargs): | ||||
|         log.error("See asynctrain instead") | ||||
| 
 | ||||
|     def asynctrain(self, *args, **kwargs): | ||||
|         raise self.TrainerInitializationException() | ||||
| 
 | ||||
| 
 | ||||
| class SouthParkTrainer(KaggleTrainer): | ||||
|     def __init__(self, chatbot, datapath: pathlib.Path, **kwargs): | ||||
|         super().__init__( | ||||
|             chatbot, | ||||
|             datapath, | ||||
|             downloadpath="ubuntu_data_v2", | ||||
|             kaggle_dataset="tovarischsukhov/southparklines", | ||||
|             **kwargs, | ||||
|         ) | ||||
| 
 | ||||
| 
 | ||||
| class MovieTrainer(KaggleTrainer): | ||||
|     def __init__(self, chatbot, datapath: pathlib.Path, **kwargs): | ||||
|         super().__init__( | ||||
|             chatbot, | ||||
|             datapath, | ||||
|             downloadpath="kaggle_movies", | ||||
|             kaggle_dataset="Cornell-University/movie-dialog-corpus", | ||||
|             **kwargs, | ||||
|         ) | ||||
| 
 | ||||
|     async def run_movie_training(self): | ||||
|         dialogue_file = "movie_lines.tsv" | ||||
|         conversation_file = "movie_conversations.tsv" | ||||
|         log.info(f"Beginning dialogue training on {dialogue_file}") | ||||
|         start_time = time.time() | ||||
| 
 | ||||
|         tagger = PosLemmaTagger(language=self.chatbot.storage.tagger.language) | ||||
| 
 | ||||
|         # [lineID, characterID, movieID, character name, text of utterance] | ||||
|         # File parsing from https://www.kaggle.com/mushaya/conversation-chatbot | ||||
| 
 | ||||
|         with open(self.data_directory / conversation_file, "r", encoding="utf-8-sig") as conv_tsv: | ||||
|             conv_lines = conv_tsv.readlines() | ||||
|         with open(self.data_directory / dialogue_file, "r", encoding="utf-8-sig") as lines_tsv: | ||||
|             dialog_lines = lines_tsv.readlines() | ||||
| 
 | ||||
|         # trans_dict = str.maketrans({"<u>": "__", "</u>": "__", '""': '"'}) | ||||
| 
 | ||||
|         lines_dict = {} | ||||
|         for line in dialog_lines: | ||||
|             _line = line[:-1].strip('"').split("\t") | ||||
|             if len(_line) >= 5:  # Only good lines | ||||
|                 lines_dict[_line[0]] = ( | ||||
|                     html.unescape(("".join(_line[4:])).strip()) | ||||
|                     .replace("<u>", "__") | ||||
|                     .replace("</u>", "__") | ||||
|                     .replace('""', '"') | ||||
|                 ) | ||||
|             else: | ||||
|                 log.debug(f"Bad line {_line}") | ||||
| 
 | ||||
|         # collecting line ids for each conversation | ||||
|         conv = [] | ||||
|         for line in conv_lines[:-1]: | ||||
|             _line = line[:-1].split("\t")[-1][1:-1].replace("'", "").replace(" ", ",") | ||||
|             conv.append(_line.split(",")) | ||||
| 
 | ||||
|         # conversations = csv.reader(conv_tsv, delimiter="\t") | ||||
|         # | ||||
|         # reader = csv.reader(lines_tsv, delimiter="\t") | ||||
|         # | ||||
|         # | ||||
|         # | ||||
|         # lines_dict = {} | ||||
|         # for row in reader: | ||||
|         #     try: | ||||
|         #         lines_dict[row[0].strip('"')] = row[4] | ||||
|         #     except: | ||||
|         #         log.exception(f"Bad line: {row}") | ||||
|         #         pass | ||||
|         #     else: | ||||
|         #         # log.info(f"Good line: {row}") | ||||
|         #         pass | ||||
|         # | ||||
|         # # lines_dict = {row[0].strip('"'): row[4] for row in reader_list} | ||||
| 
 | ||||
|         statements_from_file = [] | ||||
|         save_every = 300 | ||||
|         count = 0 | ||||
| 
 | ||||
|         # [characterID of first, characterID of second, movieID, list of utterances] | ||||
|         async for lines in AsyncIter(conv): | ||||
|             previous_statement_text = None | ||||
|             previous_statement_search_text = "" | ||||
| 
 | ||||
|             for line in lines: | ||||
|                 text = lines_dict[line] | ||||
|                 statement = Statement( | ||||
|                     text=text, | ||||
|                     in_response_to=previous_statement_text, | ||||
|                     conversation="training", | ||||
|                 ) | ||||
| 
 | ||||
|                 for preprocessor in self.chatbot.preprocessors: | ||||
|                     statement = preprocessor(statement) | ||||
| 
 | ||||
|                 statement.search_text = tagger.get_text_index_string(statement.text) | ||||
|                 statement.search_in_response_to = previous_statement_search_text | ||||
| 
 | ||||
|                 previous_statement_text = statement.text | ||||
|                 previous_statement_search_text = statement.search_text | ||||
| 
 | ||||
|                 statements_from_file.append(statement) | ||||
| 
 | ||||
|             count += 1 | ||||
|             if count >= save_every: | ||||
|                 if statements_from_file: | ||||
|                     self.chatbot.storage.create_many(statements_from_file) | ||||
|                     statements_from_file = [] | ||||
|                 count = 0 | ||||
| 
 | ||||
|         if statements_from_file: | ||||
|             self.chatbot.storage.create_many(statements_from_file) | ||||
| 
 | ||||
|         log.info(f"Training took {time.time() - start_time} seconds.") | ||||
| 
 | ||||
|     async def asynctrain(self, *args, **kwargs): | ||||
|         extracted_lines = self.data_directory / "movie_lines.tsv" | ||||
|         extracted_lines: pathlib.Path | ||||
| 
 | ||||
|         # Download and extract the Ubuntu dialog corpus if needed | ||||
|         if not extracted_lines.exists(): | ||||
|             await self.download(self.kaggle_dataset) | ||||
|         else: | ||||
|             log.info("Movie dialog already downloaded") | ||||
|         if not extracted_lines.exists(): | ||||
|             raise FileNotFoundError(f"{extracted_lines}") | ||||
| 
 | ||||
|         await self.run_movie_training() | ||||
| 
 | ||||
|         return True | ||||
| 
 | ||||
|         # train_dialogue = kwargs.get("train_dialogue", True) | ||||
|         # train_196_dialogue = kwargs.get("train_196", False) | ||||
|         # train_301_dialogue = kwargs.get("train_301", False) | ||||
|         # | ||||
|         # if train_dialogue: | ||||
|         #     await self.run_dialogue_training(extracted_dir, "dialogueText.csv") | ||||
|         # | ||||
|         # if train_196_dialogue: | ||||
|         #     await self.run_dialogue_training(extracted_dir, "dialogueText_196.csv") | ||||
|         # | ||||
|         # if train_301_dialogue: | ||||
|         #     await self.run_dialogue_training(extracted_dir, "dialogueText_301.csv") | ||||
| 
 | ||||
| 
 | ||||
| class UbuntuCorpusTrainer2(KaggleTrainer): | ||||
|     def __init__(self, chatbot, datapath: pathlib.Path, **kwargs): | ||||
|         super().__init__( | ||||
|             chatbot, | ||||
|             datapath, | ||||
|             downloadpath="kaggle_ubuntu", | ||||
|             kaggle_dataset="rtatman/ubuntu-dialogue-corpus", | ||||
|             **kwargs, | ||||
|         ) | ||||
| 
 | ||||
|     async def asynctrain(self, *args, **kwargs): | ||||
|         extracted_dir = self.data_directory / "Ubuntu-dialogue-corpus" | ||||
| 
 | ||||
|         # Download and extract the Ubuntu dialog corpus if needed | ||||
|         if not extracted_dir.exists(): | ||||
|             await self.download(self.kaggle_dataset) | ||||
|         else: | ||||
|             log.info("Ubuntu dialogue already downloaded") | ||||
|         if not extracted_dir.exists(): | ||||
|             raise FileNotFoundError("Did not extract in the expected way") | ||||
| 
 | ||||
|         train_dialogue = kwargs.get("train_dialogue", True) | ||||
|         train_196_dialogue = kwargs.get("train_196", False) | ||||
|         train_301_dialogue = kwargs.get("train_301", False) | ||||
| 
 | ||||
|         if train_dialogue: | ||||
|             await self.run_dialogue_training(extracted_dir, "dialogueText.csv") | ||||
| 
 | ||||
|         if train_196_dialogue: | ||||
|             await self.run_dialogue_training(extracted_dir, "dialogueText_196.csv") | ||||
| 
 | ||||
|         if train_301_dialogue: | ||||
|             await self.run_dialogue_training(extracted_dir, "dialogueText_301.csv") | ||||
| 
 | ||||
|         return True | ||||
| 
 | ||||
|     async def run_dialogue_training(self, extracted_dir, dialogue_file): | ||||
|         log.info(f"Beginning dialogue training on {dialogue_file}") | ||||
|         start_time = time.time() | ||||
| 
 | ||||
|         tagger = PosLemmaTagger(language=self.chatbot.storage.tagger.language) | ||||
| 
 | ||||
|         with open(extracted_dir / dialogue_file, "r", encoding="utf-8") as dg: | ||||
|             reader = csv.DictReader(dg) | ||||
| 
 | ||||
|             next(reader)  # Skip the header | ||||
| 
 | ||||
|             last_dialogue_id = None | ||||
|             previous_statement_text = None | ||||
|             previous_statement_search_text = "" | ||||
|             statements_from_file = [] | ||||
| 
 | ||||
|             save_every = 50 | ||||
|             count = 0 | ||||
| 
 | ||||
|             async for row in AsyncIter(reader): | ||||
|                 dialogue_id = row["dialogueID"] | ||||
|                 if dialogue_id != last_dialogue_id: | ||||
|                     previous_statement_text = None | ||||
|                     previous_statement_search_text = "" | ||||
|                     last_dialogue_id = dialogue_id | ||||
|                     count += 1 | ||||
|                     if count >= save_every: | ||||
|                         if statements_from_file: | ||||
|                             self.chatbot.storage.create_many(statements_from_file) | ||||
|                             statements_from_file = [] | ||||
|                         count = 0 | ||||
| 
 | ||||
|                 if len(row) > 0: | ||||
|                     statement = Statement( | ||||
|                         text=row["text"], | ||||
|                         in_response_to=previous_statement_text, | ||||
|                         conversation="training", | ||||
|                         # created_at=date_parser.parse(row["date"]), | ||||
|                         persona=row["from"], | ||||
|                     ) | ||||
| 
 | ||||
|                     for preprocessor in self.chatbot.preprocessors: | ||||
|                         statement = preprocessor(statement) | ||||
| 
 | ||||
|                     statement.search_text = tagger.get_text_index_string(statement.text) | ||||
|                     statement.search_in_response_to = previous_statement_search_text | ||||
| 
 | ||||
|                     previous_statement_text = statement.text | ||||
|                     previous_statement_search_text = statement.search_text | ||||
| 
 | ||||
|                     statements_from_file.append(statement) | ||||
| 
 | ||||
|             if statements_from_file: | ||||
|                 self.chatbot.storage.create_many(statements_from_file) | ||||
| 
 | ||||
|         log.info(f"Training took {time.time() - start_time} seconds.") | ||||
| 
 | ||||
| 
 | ||||
| class TwitterCorpusTrainer(Trainer): | ||||
|     pass | ||||
|     # def train(self, *args, **kwargs): | ||||
|     #     """ | ||||
|     #     Train the chat bot based on the provided list of | ||||
|     #     statements that represents a single conversation. | ||||
|     #     """ | ||||
|     #     import twint | ||||
|     # | ||||
|     #     c = twint.Config() | ||||
|     #     c.__dict__.update(kwargs) | ||||
|     #     twint.run.Search(c) | ||||
|     # | ||||
|     # | ||||
|     #     previous_statement_text = None | ||||
|     #     previous_statement_search_text = '' | ||||
|     # | ||||
|     #     statements_to_create = [] | ||||
|     # | ||||
|     #     for conversation_count, text in enumerate(conversation): | ||||
|     #         if self.show_training_progress: | ||||
|     #             utils.print_progress_bar( | ||||
|     #                 'List Trainer', | ||||
|     #                 conversation_count + 1, len(conversation) | ||||
|     #             ) | ||||
|     # | ||||
|     #         statement_search_text = self.chatbot.storage.tagger.get_text_index_string(text) | ||||
|     # | ||||
|     #         statement = self.get_preprocessed_statement( | ||||
|     #             Statement( | ||||
|     #                 text=text, | ||||
|     #                 search_text=statement_search_text, | ||||
|     #                 in_response_to=previous_statement_text, | ||||
|     #                 search_in_response_to=previous_statement_search_text, | ||||
|     #                 conversation='training' | ||||
|     #             ) | ||||
|     #         ) | ||||
|     # | ||||
|     #         previous_statement_text = statement.text | ||||
|     #         previous_statement_search_text = statement_search_text | ||||
|     # | ||||
|     #         statements_to_create.append(statement) | ||||
|     # | ||||
|     #     self.chatbot.storage.create_many(statements_to_create) | ||||
| @ -58,7 +58,11 @@ class CogLint(Cog): | ||||
| 
 | ||||
|         future = await self.bot.loop.run_in_executor(None, lint.py_run, path, "return_std=True") | ||||
| 
 | ||||
|         (pylint_stdout, pylint_stderr) = future or (None, None) | ||||
|         if future: | ||||
|             (pylint_stdout, pylint_stderr) = future | ||||
|         else: | ||||
|             (pylint_stdout, pylint_stderr) = None, None | ||||
| 
 | ||||
|         # print(pylint_stderr) | ||||
|         # print(pylint_stdout) | ||||
| 
 | ||||
|  | ||||
| @ -1,6 +1,5 @@ | ||||
| import asyncio | ||||
| import json | ||||
| import logging | ||||
| import os | ||||
| import pathlib | ||||
| from abc import ABC | ||||
| @ -14,8 +13,6 @@ from redbot.core import Config, commands | ||||
| from redbot.core.bot import Red | ||||
| from redbot.core.data_manager import bundled_data_path, cog_data_path | ||||
| 
 | ||||
| log = logging.getLogger("red.fox_v3.conquest") | ||||
| 
 | ||||
| 
 | ||||
| class Conquest(commands.Cog): | ||||
|     """ | ||||
| @ -56,28 +53,23 @@ class Conquest(commands.Cog): | ||||
|         self.current_map = await self.config.current_map() | ||||
| 
 | ||||
|         if self.current_map: | ||||
|             if not await self.current_map_load(): | ||||
|                 await self.config.current_map.clear() | ||||
|             await self.current_map_load() | ||||
| 
 | ||||
|     async def current_map_load(self): | ||||
|         map_data_path = self.asset_path / self.current_map / "data.json" | ||||
|         if not map_data_path.exists(): | ||||
|             log.warning(f"{map_data_path} does not exist. Clearing current map") | ||||
|             return False | ||||
| 
 | ||||
|         with map_data_path.open() as mapdata: | ||||
|             self.map_data: dict = json.load(mapdata) | ||||
|         self.ext = self.map_data["extension"] | ||||
|         self.ext_format = "JPEG" if self.ext.upper() == "JPG" else self.ext.upper() | ||||
|         return True | ||||
| 
 | ||||
|     @commands.group() | ||||
|     async def conquest(self, ctx: commands.Context): | ||||
|         """ | ||||
|         Base command for conquest cog. Start with `[p]conquest set map` to select a map. | ||||
|         """ | ||||
|         if ctx.invoked_subcommand is None and self.current_map is not None: | ||||
|             await self._conquest_current(ctx) | ||||
|         if ctx.invoked_subcommand is None: | ||||
|             if self.current_map is not None: | ||||
|                 await self._conquest_current(ctx) | ||||
| 
 | ||||
|     @conquest.command(name="list") | ||||
|     async def _conquest_list(self, ctx: commands.Context): | ||||
| @ -88,13 +80,14 @@ class Conquest(commands.Cog): | ||||
| 
 | ||||
|         with maps_json.open() as maps: | ||||
|             maps_json = json.load(maps) | ||||
|             map_list = "\n".join(maps_json["maps"]) | ||||
|             map_list = "\n".join(map_name for map_name in maps_json["maps"]) | ||||
|             await ctx.maybe_send_embed(f"Current maps:\n{map_list}") | ||||
| 
 | ||||
|     @conquest.group(name="set") | ||||
|     async def conquest_set(self, ctx: commands.Context): | ||||
|         """Base command for admin actions like selecting a map""" | ||||
|         pass | ||||
|         if ctx.invoked_subcommand is None: | ||||
|             pass | ||||
| 
 | ||||
|     @conquest_set.command(name="resetzoom") | ||||
|     async def _conquest_set_resetzoom(self, ctx: commands.Context): | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| { | ||||
|   "maps": [ | ||||
|     "simple", | ||||
| 	"ck2", | ||||
| 	"HoI" | ||||
|     "simple_blank_map", | ||||
| 	"test", | ||||
| 	"test2" | ||||
|   ] | ||||
| } | ||||
| @ -30,7 +30,8 @@ class MapMaker(commands.Cog): | ||||
|         """ | ||||
|         Base command for managing current maps or creating new ones | ||||
|         """ | ||||
|         pass | ||||
|         if ctx.invoked_subcommand is None: | ||||
|             pass | ||||
| 
 | ||||
|     @mapmaker.command(name="upload") | ||||
|     async def _mapmaker_upload(self, ctx: commands.Context, map_path=""): | ||||
|  | ||||
| @ -65,7 +65,7 @@ def floodfill(image, xy, value, border=None, thresh=0) -> set: | ||||
|                     if border is None: | ||||
|                         fill = _color_diff(p, background) <= thresh | ||||
|                     else: | ||||
|                         fill = p not in [value, border] | ||||
|                         fill = p != value and p != border | ||||
|                     if fill: | ||||
|                         pixel[s, t] = value | ||||
|                         new_edge.add((s, t)) | ||||
|  | ||||
| @ -27,7 +27,8 @@ class ExclusiveRole(Cog): | ||||
|     async def exclusive(self, ctx): | ||||
|         """Base command for managing exclusive roles""" | ||||
| 
 | ||||
|         pass | ||||
|         if not ctx.invoked_subcommand: | ||||
|             pass | ||||
| 
 | ||||
|     @exclusive.command(name="add") | ||||
|     @checks.mod_or_permissions(administrator=True) | ||||
| @ -84,7 +85,7 @@ class ExclusiveRole(Cog): | ||||
|         if role_set is None: | ||||
|             role_set = set(await self.config.guild(member.guild).role_list()) | ||||
| 
 | ||||
|         member_set = {role.id for role in member.roles} | ||||
|         member_set = set([role.id for role in member.roles]) | ||||
|         to_remove = (member_set - role_set) - {member.guild.default_role.id} | ||||
| 
 | ||||
|         if to_remove and member_set & role_set: | ||||
| @ -102,7 +103,7 @@ class ExclusiveRole(Cog): | ||||
|         await asyncio.sleep(1) | ||||
| 
 | ||||
|         role_set = set(await self.config.guild(after.guild).role_list()) | ||||
|         member_set = {role.id for role in after.roles} | ||||
|         member_set = set([role.id for role in after.roles]) | ||||
| 
 | ||||
|         if role_set & member_set: | ||||
|             try: | ||||
|  | ||||
| @ -1,15 +1,5 @@ | ||||
| import sys | ||||
| 
 | ||||
| from .fifo import FIFO | ||||
| 
 | ||||
| # Applying fix from: https://github.com/Azure/azure-functions-python-worker/issues/640 | ||||
| # [Fix] Create a wrapper for importing imgres | ||||
| from .date_trigger import * | ||||
| from . import CustomDateTrigger | ||||
| 
 | ||||
| # [Fix] Register imgres into system modules | ||||
| sys.modules["CustomDateTrigger"] = CustomDateTrigger | ||||
| 
 | ||||
| 
 | ||||
| async def setup(bot): | ||||
|     cog = FIFO(bot) | ||||
|  | ||||
| @ -1,10 +0,0 @@ | ||||
| from apscheduler.triggers.date import DateTrigger | ||||
| 
 | ||||
| 
 | ||||
| class CustomDateTrigger(DateTrigger): | ||||
|     def get_next_fire_time(self, previous_fire_time, now): | ||||
|         next_run = super().get_next_fire_time(previous_fire_time, now) | ||||
|         return next_run if next_run is not None and next_run >= now else None | ||||
| 
 | ||||
|     def __getstate__(self): | ||||
|         return {"version": 1, "run_date": self.run_date} | ||||
							
								
								
									
										213
									
								
								fifo/fifo.py
									
									
									
									
									
								
							
							
						
						
									
										213
									
								
								fifo/fifo.py
									
									
									
									
									
								
							| @ -1,10 +1,8 @@ | ||||
| import itertools | ||||
| import logging | ||||
| from datetime import MAXYEAR, datetime, timedelta, tzinfo | ||||
| from datetime import datetime, timedelta, tzinfo | ||||
| from typing import Optional, Union | ||||
| 
 | ||||
| import discord | ||||
| import pytz | ||||
| from apscheduler.job import Job | ||||
| from apscheduler.jobstores.base import JobLookupError | ||||
| from apscheduler.schedulers.asyncio import AsyncIOScheduler | ||||
| @ -12,7 +10,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 humanize_timedelta, pagify | ||||
| from redbot.core.utils.chat_formatting import pagify | ||||
| 
 | ||||
| from .datetime_cron_converters import CronConverter, DatetimeConverter, TimezoneConverter | ||||
| from .task import Task | ||||
| @ -23,12 +21,11 @@ schedule_log.setLevel(logging.DEBUG) | ||||
| log = logging.getLogger("red.fox_v3.fifo") | ||||
| 
 | ||||
| 
 | ||||
| async def _execute_task(**task_state): | ||||
|     log.info(f"Executing {task_state.get('name')}") | ||||
| async def _execute_task(task_state): | ||||
|     log.info(f"Executing {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 | ||||
| 
 | ||||
| 
 | ||||
| @ -40,37 +37,6 @@ 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)  # Recursion | ||||
|         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 CapturePrint: | ||||
|     """Silly little class to get `print` output""" | ||||
| 
 | ||||
|     def __init__(self): | ||||
|         self.string = None | ||||
| 
 | ||||
|     def write(self, string): | ||||
|         self.string = string if self.string is None else self.string + "\n" + string | ||||
| 
 | ||||
| 
 | ||||
| class FIFO(commands.Cog): | ||||
|     """ | ||||
|     Simple Scheduling Cog | ||||
| @ -89,7 +55,7 @@ class FIFO(commands.Cog): | ||||
|         self.config.register_global(**default_global) | ||||
|         self.config.register_guild(**default_guild) | ||||
| 
 | ||||
|         self.scheduler: Optional[AsyncIOScheduler] = None | ||||
|         self.scheduler = None | ||||
|         self.jobstore = None | ||||
| 
 | ||||
|         self.tz_cog = None | ||||
| @ -105,22 +71,17 @@ class FIFO(commands.Cog): | ||||
| 
 | ||||
|     async def initialize(self): | ||||
| 
 | ||||
|         job_defaults = { | ||||
|             "coalesce": True,  # Multiple missed triggers within the grace time will only fire once | ||||
|             "max_instances": 5,  # This is probably way too high, should likely only be one | ||||
|             "misfire_grace_time": 15,  # 15 seconds ain't much, but it's honest work | ||||
|             "replace_existing": True,  # Very important for persistent data | ||||
|         } | ||||
|         job_defaults = {"coalesce": False, "max_instances": 1} | ||||
| 
 | ||||
|         # executors = {"default": AsyncIOExecutor()} | ||||
| 
 | ||||
|         # Default executor is already AsyncIOExecutor | ||||
|         self.scheduler = AsyncIOScheduler(job_defaults=job_defaults, logger=schedule_log) | ||||
| 
 | ||||
|         from .redconfigjobstore import RedConfigJobStore  # Wait to import to prevent cyclic import | ||||
|         from .redconfigjobstore import RedConfigJobStore | ||||
| 
 | ||||
|         self.jobstore = RedConfigJobStore(self.config, self.bot) | ||||
|         await self.jobstore.load_from_config() | ||||
|         await self.jobstore.load_from_config(self.scheduler, "default") | ||||
|         self.scheduler.add_jobstore(self.jobstore, "default") | ||||
| 
 | ||||
|         self.scheduler.start() | ||||
| @ -143,59 +104,41 @@ class FIFO(commands.Cog): | ||||
|         await task.delete_self() | ||||
| 
 | ||||
|     async def _process_task(self, task: Task): | ||||
|         # None of this is necessar, we have `replace_existing` already | ||||
|         # job: Union[Job, None] = await self._get_job(task) | ||||
|         # if job is not None: | ||||
|         #     combined_trigger_ = await task.get_combined_trigger() | ||||
|         #     if combined_trigger_ is None: | ||||
|         #         job.remove() | ||||
|         #     else: | ||||
|         #         job.reschedule(combined_trigger_) | ||||
|         #     return job | ||||
|         job: Union[Job, None] = await self._get_job(task) | ||||
|         if job is not None: | ||||
|             job.reschedule(await task.get_combined_trigger()) | ||||
|             return job | ||||
|         return await self._add_job(task) | ||||
| 
 | ||||
|     async def _get_job(self, task: Task) -> Job: | ||||
|         return self.scheduler.get_job(_assemble_job_id(task.name, task.guild_id)) | ||||
| 
 | ||||
|     async def _add_job(self, task: Task): | ||||
|         combined_trigger_ = await task.get_combined_trigger() | ||||
|         if combined_trigger_ is None: | ||||
|             return None | ||||
| 
 | ||||
|         return self.scheduler.add_job( | ||||
|             _execute_task, | ||||
|             kwargs=task.__getstate__(), | ||||
|             args=[task.__getstate__()], | ||||
|             id=_assemble_job_id(task.name, task.guild_id), | ||||
|             trigger=combined_trigger_, | ||||
|             name=task.name, | ||||
|             replace_existing=True, | ||||
|             trigger=await task.get_combined_trigger(), | ||||
|         ) | ||||
| 
 | ||||
|     async def _resume_job(self, task: Task): | ||||
|         job: Union[Job, None] = await self._get_job(task) | ||||
|         if job is not None: | ||||
|             job.resume() | ||||
|         else: | ||||
|         try: | ||||
|             job = self.scheduler.resume_job(job_id=_assemble_job_id(task.name, task.guild_id)) | ||||
|         except JobLookupError: | ||||
|             job = await self._process_task(task) | ||||
|         return job | ||||
| 
 | ||||
|     async def _pause_job(self, task: Task): | ||||
|         try: | ||||
|             return self.scheduler.pause_job(job_id=_assemble_job_id(task.name, task.guild_id)) | ||||
|         except JobLookupError: | ||||
|             return False | ||||
|         return self.scheduler.pause_job(job_id=_assemble_job_id(task.name, task.guild_id)) | ||||
| 
 | ||||
|     async def _remove_job(self, task: Task): | ||||
|         try: | ||||
|             self.scheduler.remove_job(job_id=_assemble_job_id(task.name, task.guild_id)) | ||||
|         except JobLookupError: | ||||
|             pass | ||||
|         return self.scheduler.remove_job(job_id=_assemble_job_id(task.name, task.guild_id)) | ||||
| 
 | ||||
|     async def _get_tz(self, user: Union[discord.User, discord.Member]) -> Union[None, tzinfo]: | ||||
|         if self.tz_cog is None: | ||||
|             self.tz_cog = self.bot.get_cog("Timezone") | ||||
|         if self.tz_cog is None: | ||||
|             self.tz_cog = False  # only try once to get the timezone cog | ||||
|             if self.tz_cog is None: | ||||
|                 self.tz_cog = False  # only try once to get the timezone cog | ||||
| 
 | ||||
|         if not self.tz_cog: | ||||
|             return None | ||||
| @ -227,42 +170,8 @@ class FIFO(commands.Cog): | ||||
|         """ | ||||
|         Base command for handling scheduling of tasks | ||||
|         """ | ||||
|         pass | ||||
| 
 | ||||
|     @fifo.command(name="wakeup") | ||||
|     async def fifo_wakeup(self, ctx: commands.Context): | ||||
|         """Debug command to fix missed executions. | ||||
| 
 | ||||
|         If you see a negative "Next run time" when adding a trigger, this may help resolve it. | ||||
|         Check the logs when using this command. | ||||
|         """ | ||||
| 
 | ||||
|         self.scheduler.wakeup() | ||||
|         await ctx.tick() | ||||
| 
 | ||||
|     @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)) | ||||
|         if ctx.invoked_subcommand is None: | ||||
|             pass | ||||
| 
 | ||||
|     @fifo.command(name="set") | ||||
|     async def fifo_set( | ||||
| @ -391,14 +300,10 @@ class FIFO(commands.Cog): | ||||
| 
 | ||||
|         else: | ||||
|             embed.add_field(name="Server", value="Server not found", inline=False) | ||||
|         triggers, expired_triggers = await task.get_triggers() | ||||
| 
 | ||||
|         trigger_str = "\n".join(str(t) for t in triggers) | ||||
|         expired_str = "\n".join(str(t) for t in expired_triggers) | ||||
|         trigger_str = "\n".join(str(t) for t in await task.get_triggers()) | ||||
|         if trigger_str: | ||||
|             embed.add_field(name="Triggers", value=trigger_str, inline=False) | ||||
|         if expired_str: | ||||
|             embed.add_field(name="Expired Triggers", value=expired_str, inline=False) | ||||
| 
 | ||||
|         job = await self._get_job(task) | ||||
|         if job and job.next_run_time: | ||||
| @ -414,12 +319,12 @@ class FIFO(commands.Cog): | ||||
|         Do `[p]fifo list True` to see tasks from all guilds | ||||
|         """ | ||||
|         if all_guilds: | ||||
|             pass  # TODO: All guilds | ||||
|             pass | ||||
|         else: | ||||
|             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\n" | ||||
|                 out += f"{task_name}: {task_data}\n" | ||||
| 
 | ||||
|             if out: | ||||
|                 if len(out) > 2000: | ||||
| @ -430,28 +335,6 @@ class FIFO(commands.Cog): | ||||
|             else: | ||||
|                 await ctx.maybe_send_embed("No tasks to list") | ||||
| 
 | ||||
|     @fifo.command(name="printschedule") | ||||
|     async def fifo_printschedule(self, ctx: commands.Context): | ||||
|         """ | ||||
|         Print the current schedule of execution. | ||||
| 
 | ||||
|         Useful for debugging. | ||||
|         """ | ||||
|         cp = CapturePrint() | ||||
|         self.scheduler.print_jobs(out=cp) | ||||
| 
 | ||||
|         out = cp.string | ||||
|         out=out.replace("*","\*") | ||||
| 
 | ||||
|         if out: | ||||
|             if len(out) > 2000: | ||||
|                 for page in pagify(out): | ||||
|                     await ctx.maybe_send_embed(page) | ||||
|             else: | ||||
|                 await ctx.maybe_send_embed(out) | ||||
|         else: | ||||
|             await ctx.maybe_send_embed("Failed to get schedule from scheduler") | ||||
| 
 | ||||
|     @fifo.command(name="add") | ||||
|     async def fifo_add(self, ctx: commands.Context, task_name: str, *, command_to_execute: str): | ||||
|         """ | ||||
| @ -511,7 +394,6 @@ class FIFO(commands.Cog): | ||||
|             return | ||||
| 
 | ||||
|         await task.clear_triggers() | ||||
|         await self._remove_job(task) | ||||
|         await ctx.tick() | ||||
| 
 | ||||
|     @fifo.group(name="addtrigger", aliases=["trigger"]) | ||||
| @ -519,7 +401,8 @@ class FIFO(commands.Cog): | ||||
|         """ | ||||
|         Add a new trigger for a task from the current guild. | ||||
|         """ | ||||
|         pass | ||||
|         if ctx.invoked_subcommand is None: | ||||
|             pass | ||||
| 
 | ||||
|     @fifo_trigger.command(name="interval") | ||||
|     async def fifo_trigger_interval( | ||||
| @ -530,7 +413,7 @@ class FIFO(commands.Cog): | ||||
|         """ | ||||
| 
 | ||||
|         task = Task(task_name, ctx.guild.id, self.config, bot=self.bot) | ||||
|         await task.load_from_config()  # Will set the channel and author | ||||
|         await task.load_from_config() | ||||
| 
 | ||||
|         if task.data is None: | ||||
|             await ctx.maybe_send_embed( | ||||
| @ -552,40 +435,6 @@ class FIFO(commands.Cog): | ||||
|             f"Next run time: {job.next_run_time} ({delta_from_now.total_seconds()} seconds)" | ||||
|         ) | ||||
| 
 | ||||
|     @fifo_trigger.command(name="relative") | ||||
|     async def fifo_trigger_relative( | ||||
|         self, ctx: commands.Context, task_name: str, *, time_from_now: TimedeltaConverter | ||||
|     ): | ||||
|         """ | ||||
|         Add a "run once" trigger at a time relative from now to the specified 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 | ||||
| 
 | ||||
|         time_to_run = datetime.now(pytz.utc) + time_from_now | ||||
| 
 | ||||
|         result = await task.add_trigger("date", time_to_run, time_to_run.tzinfo) | ||||
|         if not result: | ||||
|             await ctx.maybe_send_embed( | ||||
|                 "Failed to add a date trigger to this task, see console for logs" | ||||
|             ) | ||||
|             return | ||||
| 
 | ||||
|         await task.save_data() | ||||
|         job: Job = await self._process_task(task) | ||||
|         delta_from_now: timedelta = job.next_run_time - datetime.now(job.next_run_time.tzinfo) | ||||
|         await ctx.maybe_send_embed( | ||||
|             f"Task `{task_name}` added {time_to_run} to its scheduled runtimes\n" | ||||
|             f"Next run time: {job.next_run_time} ({delta_from_now.total_seconds()} seconds)" | ||||
|         ) | ||||
| 
 | ||||
|     @fifo_trigger.command(name="date") | ||||
|     async def fifo_trigger_date( | ||||
|         self, ctx: commands.Context, task_name: str, *, datetime_str: DatetimeConverter | ||||
| @ -594,7 +443,7 @@ class FIFO(commands.Cog): | ||||
|         Add a "run once" datetime trigger to the specified task | ||||
|         """ | ||||
| 
 | ||||
|         task = Task(task_name, ctx.guild.id, self.config, bot=self.bot) | ||||
|         task = Task(task_name, ctx.guild.id, self.config) | ||||
|         await task.load_from_config() | ||||
| 
 | ||||
|         if task.data is None: | ||||
| @ -634,7 +483,7 @@ class FIFO(commands.Cog): | ||||
| 
 | ||||
|         See https://crontab.guru/ for help generating the cron_str | ||||
|         """ | ||||
|         task = Task(task_name, ctx.guild.id, self.config, bot=self.bot) | ||||
|         task = Task(task_name, ctx.guild.id, self.config) | ||||
|         await task.load_from_config() | ||||
| 
 | ||||
|         if task.data is None: | ||||
|  | ||||
| @ -10,8 +10,7 @@ | ||||
|   "end_user_data_statement": "This cog does not store any End User Data", | ||||
|   "requirements": [ | ||||
|     "apscheduler", | ||||
|     "pytz", | ||||
|     "python-dateutil" | ||||
|     "pytz" | ||||
|   ], | ||||
|   "tags": [ | ||||
|     "bobloy", | ||||
|  | ||||
| @ -2,14 +2,17 @@ import asyncio | ||||
| import base64 | ||||
| import logging | ||||
| import pickle | ||||
| from datetime import datetime | ||||
| from typing import Tuple, Union | ||||
| 
 | ||||
| from apscheduler.job import Job | ||||
| from apscheduler.jobstores.base import ConflictingIdError, JobLookupError | ||||
| from apscheduler.jobstores.memory import MemoryJobStore | ||||
| from apscheduler.schedulers.asyncio import run_in_event_loop | ||||
| from apscheduler.util import datetime_to_utc_timestamp | ||||
| from redbot.core import Config | ||||
| 
 | ||||
| # TODO: use get_lock on config maybe | ||||
| # TODO: use get_lock on config | ||||
| from redbot.core.bot import Red | ||||
| from redbot.core.utils import AsyncIter | ||||
| 
 | ||||
| @ -25,55 +28,44 @@ class RedConfigJobStore(MemoryJobStore): | ||||
|         self.config = config | ||||
|         self.bot = bot | ||||
|         self.pickle_protocol = pickle.HIGHEST_PROTOCOL | ||||
|         self._eventloop = self.bot.loop  # Used for @run_in_event_loop | ||||
|         self._eventloop = self.bot.loop | ||||
|         # TODO: self.config.jobs_index is never used, | ||||
|         #  fine but maybe a sign of inefficient use of config | ||||
| 
 | ||||
|         # task = asyncio.create_task(self.load_from_config()) | ||||
|         # while not task.done(): | ||||
|         #     sleep(0.1) | ||||
|         # future = asyncio.ensure_future(self.load_from_config(), loop=self.bot.loop) | ||||
| 
 | ||||
|     @run_in_event_loop | ||||
|     def start(self, scheduler, alias): | ||||
|         super().start(scheduler, alias) | ||||
|         for job, timestamp in self._jobs: | ||||
|             job._scheduler = self._scheduler | ||||
|             job._jobstore_alias = self._alias | ||||
| 
 | ||||
|     async def load_from_config(self): | ||||
|     async def load_from_config(self, scheduler, alias): | ||||
|         super().start(scheduler, alias) | ||||
|         _jobs = await self.config.jobs() | ||||
|         # self._jobs = [ | ||||
|         #     (await self._decode_job(job), timestamp) async for (job, timestamp) in AsyncIter(_jobs) | ||||
|         # ] | ||||
|         async for job, timestamp in AsyncIter(_jobs, steps=5): | ||||
|             job = await self._decode_job(job) | ||||
|             index = self._get_job_index(timestamp, job.id) | ||||
|             self._jobs.insert(index, (job, timestamp)) | ||||
|             self._jobs_index[job.id] = (job, timestamp) | ||||
| 
 | ||||
|     async def save_to_config(self): | ||||
|         """Yea that's basically it""" | ||||
|         await self.config.jobs.set( | ||||
|             [(self._encode_job(job), timestamp) for job, timestamp in self._jobs] | ||||
|         ) | ||||
| 
 | ||||
|         self._jobs = [ | ||||
|             (await self._decode_job(job), timestamp) async for (job, timestamp) in AsyncIter(_jobs) | ||||
|         ] | ||||
|         # self._jobs_index = await self.config.jobs_index.all()  # Overwritten by next | ||||
|         # self._jobs_index = {job.id: (job, timestamp) for job, timestamp in self._jobs} | ||||
|         self._jobs_index = {job.id: (job, timestamp) for job, timestamp in self._jobs} | ||||
| 
 | ||||
|     def _encode_job(self, job: Job): | ||||
|         job_state = job.__getstate__() | ||||
|         job_state["kwargs"]["config"] = None | ||||
|         job_state["kwargs"]["bot"] = None | ||||
|         # new_kwargs = job_state["kwargs"] | ||||
|         # new_kwargs["config"] = None | ||||
|         # new_kwargs["bot"] = None | ||||
|         # job_state["kwargs"] = new_kwargs | ||||
|         new_args = list(job_state["args"]) | ||||
|         new_args[0]["config"] = None | ||||
|         new_args[0]["bot"] = None | ||||
|         job_state["args"] = tuple(new_args) | ||||
|         encoded = base64.b64encode(pickle.dumps(job_state, self.pickle_protocol)) | ||||
|         out = { | ||||
|             "_id": job.id, | ||||
|             "next_run_time": datetime_to_utc_timestamp(job.next_run_time), | ||||
|             "job_state": encoded.decode("ascii"), | ||||
|         } | ||||
|         job_state["kwargs"]["config"] = self.config | ||||
|         job_state["kwargs"]["bot"] = self.bot | ||||
|         # new_kwargs = job_state["kwargs"] | ||||
|         # new_kwargs["config"] = self.config | ||||
|         # new_kwargs["bot"] = self.bot | ||||
|         # job_state["kwargs"] = new_kwargs | ||||
|         new_args = list(job_state["args"]) | ||||
|         new_args[0]["config"] = self.config | ||||
|         new_args[0]["bot"] = self.bot | ||||
|         job_state["args"] = tuple(new_args) | ||||
|         # log.debug(f"Encoding job id: {job.id}\n" | ||||
|         #           f"Encoded as: {out}") | ||||
| 
 | ||||
| @ -84,15 +76,10 @@ class RedConfigJobStore(MemoryJobStore): | ||||
|             return None | ||||
|         job_state = in_job["job_state"] | ||||
|         job_state = pickle.loads(base64.b64decode(job_state)) | ||||
|         if job_state["args"]:  # Backwards compatibility on args to kwargs | ||||
|             job_state["kwargs"] = {**job_state["args"][0]} | ||||
|             job_state["args"] = [] | ||||
|         job_state["kwargs"]["config"] = self.config | ||||
|         job_state["kwargs"]["bot"] = self.bot | ||||
|         # new_kwargs = job_state["kwargs"] | ||||
|         # new_kwargs["config"] = self.config | ||||
|         # new_kwargs["bot"] = self.bot | ||||
|         # job_state["kwargs"] = new_kwargs | ||||
|         new_args = list(job_state["args"]) | ||||
|         new_args[0]["config"] = self.config | ||||
|         new_args[0]["bot"] = self.bot | ||||
|         job_state["args"] = tuple(new_args) | ||||
|         job = Job.__new__(Job) | ||||
|         job.__setstate__(job_state) | ||||
|         job._scheduler = self._scheduler | ||||
| @ -109,6 +96,79 @@ class RedConfigJobStore(MemoryJobStore): | ||||
| 
 | ||||
|         return job | ||||
| 
 | ||||
|     @run_in_event_loop | ||||
|     def add_job(self, job: Job): | ||||
|         if job.id in self._jobs_index: | ||||
|             raise ConflictingIdError(job.id) | ||||
|         # log.debug(f"Check job args: {job.args=}") | ||||
|         timestamp = datetime_to_utc_timestamp(job.next_run_time) | ||||
|         index = self._get_job_index(timestamp, job.id)  # This is fine | ||||
|         self._jobs.insert(index, (job, timestamp)) | ||||
|         self._jobs_index[job.id] = (job, timestamp) | ||||
|         asyncio.create_task(self._async_add_job(job, index, timestamp)) | ||||
|         # log.debug(f"Added job: {self._jobs[index][0].args}") | ||||
| 
 | ||||
|     async def _async_add_job(self, job, index, timestamp): | ||||
|         encoded_job = self._encode_job(job) | ||||
|         job_tuple = tuple([encoded_job, timestamp]) | ||||
|         async with self.config.jobs() as jobs: | ||||
|             jobs.insert(index, job_tuple) | ||||
|         # await self.config.jobs_index.set_raw(job.id, value=job_tuple) | ||||
|         return True | ||||
| 
 | ||||
|     @run_in_event_loop | ||||
|     def update_job(self, job): | ||||
|         old_tuple: Tuple[Union[Job, None], Union[datetime, None]] = self._jobs_index.get( | ||||
|             job.id, (None, None) | ||||
|         ) | ||||
|         old_job = old_tuple[0] | ||||
|         old_timestamp = old_tuple[1] | ||||
|         if old_job is None: | ||||
|             raise JobLookupError(job.id) | ||||
| 
 | ||||
|         # If the next run time has not changed, simply replace the job in its present index. | ||||
|         # Otherwise, reinsert the job to the list to preserve the ordering. | ||||
|         old_index = self._get_job_index(old_timestamp, old_job.id) | ||||
|         new_timestamp = datetime_to_utc_timestamp(job.next_run_time) | ||||
|         asyncio.create_task( | ||||
|             self._async_update_job(job, new_timestamp, old_index, old_job, old_timestamp) | ||||
|         ) | ||||
| 
 | ||||
|     async def _async_update_job(self, job, new_timestamp, old_index, old_job, old_timestamp): | ||||
|         encoded_job = self._encode_job(job) | ||||
|         if old_timestamp == new_timestamp: | ||||
|             self._jobs[old_index] = (job, new_timestamp) | ||||
|             async with self.config.jobs() as jobs: | ||||
|                 jobs[old_index] = (encoded_job, new_timestamp) | ||||
|         else: | ||||
|             del self._jobs[old_index] | ||||
|             new_index = self._get_job_index(new_timestamp, job.id)  # This is fine | ||||
|             self._jobs.insert(new_index, (job, new_timestamp)) | ||||
|             async with self.config.jobs() as jobs: | ||||
|                 del jobs[old_index] | ||||
|                 jobs.insert(new_index, (encoded_job, new_timestamp)) | ||||
|         self._jobs_index[old_job.id] = (job, new_timestamp) | ||||
|         # await self.config.jobs_index.set_raw(old_job.id, value=(encoded_job, new_timestamp)) | ||||
| 
 | ||||
|         log.debug(f"Async Updated {job.id=}") | ||||
|         log.debug(f"Check job args: {job.args=}") | ||||
| 
 | ||||
|     @run_in_event_loop | ||||
|     def remove_job(self, job_id): | ||||
|         job, timestamp = self._jobs_index.get(job_id, (None, None)) | ||||
|         if job is None: | ||||
|             raise JobLookupError(job_id) | ||||
| 
 | ||||
|         index = self._get_job_index(timestamp, job_id) | ||||
|         del self._jobs[index] | ||||
|         del self._jobs_index[job.id] | ||||
|         asyncio.create_task(self._async_remove_job(index, job)) | ||||
| 
 | ||||
|     async def _async_remove_job(self, index, job): | ||||
|         async with self.config.jobs() as jobs: | ||||
|             del jobs[index] | ||||
|         # await self.config.jobs_index.clear_raw(job.id) | ||||
| 
 | ||||
|     @run_in_event_loop | ||||
|     def remove_all_jobs(self): | ||||
|         super().remove_all_jobs() | ||||
| @ -120,9 +180,4 @@ class RedConfigJobStore(MemoryJobStore): | ||||
| 
 | ||||
|     def shutdown(self): | ||||
|         """Removes all jobs without clearing config""" | ||||
|         asyncio.create_task(self.async_shutdown()) | ||||
| 
 | ||||
|     async def async_shutdown(self): | ||||
|         await self.save_to_config() | ||||
|         self._jobs = [] | ||||
|         self._jobs_index = {} | ||||
|         super().remove_all_jobs() | ||||
|  | ||||
							
								
								
									
										262
									
								
								fifo/task.py
									
									
									
									
									
								
							
							
						
						
									
										262
									
								
								fifo/task.py
									
									
									
									
									
								
							| @ -1,19 +1,18 @@ | ||||
| import logging | ||||
| from datetime import datetime, timedelta | ||||
| from typing import Dict, List, Optional, Tuple, Union | ||||
| from typing import Dict, List, Union | ||||
| 
 | ||||
| import discord | ||||
| import pytz | ||||
| from apscheduler.triggers.base import BaseTrigger | ||||
| from apscheduler.triggers.combining import OrTrigger | ||||
| from apscheduler.triggers.cron import CronTrigger | ||||
| from apscheduler.triggers.date import DateTrigger | ||||
| from apscheduler.triggers.interval import IntervalTrigger | ||||
| from discord.utils import time_snowflake | ||||
| from pytz import timezone | ||||
| from redbot.core import Config, commands | ||||
| from redbot.core.bot import Red | ||||
| 
 | ||||
| from fifo.date_trigger import CustomDateTrigger | ||||
| 
 | ||||
| log = logging.getLogger("red.fox_v3.fifo.task") | ||||
| 
 | ||||
| 
 | ||||
| @ -27,7 +26,7 @@ def get_trigger(data): | ||||
|         return IntervalTrigger(days=parsed_time.days, seconds=parsed_time.seconds) | ||||
| 
 | ||||
|     if data["type"] == "date": | ||||
|         return CustomDateTrigger(data["time_data"], timezone=data["tzinfo"]) | ||||
|         return DateTrigger(data["time_data"], timezone=data["tzinfo"]) | ||||
| 
 | ||||
|     if data["type"] == "cron": | ||||
|         return CronTrigger.from_crontab(data["time_data"], timezone=data["tzinfo"]) | ||||
| @ -35,127 +34,20 @@ def get_trigger(data): | ||||
|     return False | ||||
| 
 | ||||
| 
 | ||||
| def check_expired_trigger(trigger: BaseTrigger): | ||||
|     return trigger.get_next_fire_time(None, datetime.now(pytz.utc)) is None | ||||
| 
 | ||||
| 
 | ||||
| def parse_triggers(data: Union[Dict, None]): | ||||
|     if data is None or not data.get("triggers", False):  # No triggers | ||||
|         return None | ||||
| 
 | ||||
|     if len(data["triggers"]) > 1:  # Multiple triggers | ||||
|         triggers_list = [get_trigger(t_data) for t_data in data["triggers"]] | ||||
|         triggers_list = [t for t in triggers_list if not check_expired_trigger(t)] | ||||
|         if not triggers_list: | ||||
|             return None | ||||
|         return OrTrigger(triggers_list) | ||||
|     else: | ||||
|         trigger = get_trigger(data["triggers"][0]) | ||||
|         if check_expired_trigger(trigger): | ||||
|             return None | ||||
|         return trigger | ||||
|         return OrTrigger(get_trigger(t_data) for t_data in data["triggers"]) | ||||
| 
 | ||||
|     return get_trigger(data["triggers"][0]) | ||||
| 
 | ||||
| 
 | ||||
| # class FakeMessage: | ||||
| #     def __init__(self, message: discord.Message): | ||||
| #         d = {k: getattr(message, k, None) for k in dir(message)} | ||||
| #         self.__dict__.update(**d) | ||||
| 
 | ||||
| 
 | ||||
| # Potential FakeMessage subclass of Message | ||||
| # class DeleteSlots(type): | ||||
| #     @classmethod | ||||
| #     def __prepare__(metacls, name, bases): | ||||
| #         """Borrowed a bit from https://stackoverflow.com/q/56579348""" | ||||
| #         super_prepared = super().__prepare__(name, bases) | ||||
| #         print(super_prepared) | ||||
| #         return super_prepared | ||||
| 
 | ||||
| things_for_fakemessage_to_steal = [ | ||||
|     "_state", | ||||
|     "id", | ||||
|     "webhook_id", | ||||
|     # "reactions", | ||||
|     # "attachments", | ||||
|     "embeds", | ||||
|     "application", | ||||
|     "activity", | ||||
|     "channel", | ||||
|     "_edited_time", | ||||
|     "type", | ||||
|     "pinned", | ||||
|     "flags", | ||||
|     "mention_everyone", | ||||
|     "tts", | ||||
|     "content", | ||||
|     "nonce", | ||||
|     "reference", | ||||
|     "_edited_timestamp"  # New 7/23/21 | ||||
| ] | ||||
| 
 | ||||
| things_fakemessage_sets_by_default = { | ||||
|     "attachments": [], | ||||
|     "reactions": [], | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| class FakeMessage(discord.Message): | ||||
|     def __init__(self, *args, message: discord.Message, **kwargs): | ||||
|         d = {k: getattr(message, k, None) for k in things_for_fakemessage_to_steal} | ||||
|         d.update(things_fakemessage_sets_by_default) | ||||
|         for k, v in d.items(): | ||||
|             try: | ||||
|                 # log.debug(f"{k=} {v=}") | ||||
|                 setattr(self, k, v) | ||||
|             except TypeError: | ||||
|                 # log.exception("This is fine") | ||||
|                 pass | ||||
|             except AttributeError: | ||||
|                 # log.exception("This is fine") | ||||
|                 pass | ||||
| 
 | ||||
|         self.id = time_snowflake(datetime.utcnow(), high=False)  # Pretend to be now | ||||
|         self.type = discord.MessageType.default | ||||
| 
 | ||||
|     def process_the_rest( | ||||
|         self, | ||||
|         author: discord.Member, | ||||
|         channel: discord.TextChannel, | ||||
|         content, | ||||
|     ): | ||||
|         # self.content = content | ||||
|         # log.debug(self.content) | ||||
| 
 | ||||
|         # for handler in ('author', 'member', 'mentions', 'mention_roles', 'call', 'flags'): | ||||
|         #     try: | ||||
|         #         getattr(self, '_handle_%s' % handler)(data[handler]) | ||||
|         #     except KeyError: | ||||
|         #         continue | ||||
|         self.author = author | ||||
|         # self._handle_author(author._user._to_minimal_user_json()) | ||||
|         # self._handle_member(author) | ||||
|         self._rebind_channel_reference(channel) | ||||
|         self._update( | ||||
|             { | ||||
|                 "content": content, | ||||
|             } | ||||
|         ) | ||||
|         self._update( | ||||
|             { | ||||
|                 "mention_roles": self.raw_role_mentions, | ||||
|                 "mentions": [{"id": _id} for _id in self.raw_mentions], | ||||
|             } | ||||
|         ) | ||||
| 
 | ||||
|         # self._handle_content(content) | ||||
|         # log.debug(self.content) | ||||
| 
 | ||||
|         self.mention_everyone = "@everyone" in self.content or "@here" in self.content | ||||
| 
 | ||||
|         # self._handle_mention_roles(self.raw_role_mentions) | ||||
|         # self._handle_mentions(self.raw_mentions) | ||||
| 
 | ||||
|         # self.__dict__.update(**d) | ||||
| class FakeMessage: | ||||
|     def __init__(self, message: discord.Message): | ||||
|         d = {k: getattr(message, k, None) for k in dir(message)} | ||||
|         self.__dict__.update(**d) | ||||
| 
 | ||||
| 
 | ||||
| def neuter_message(message: FakeMessage): | ||||
| @ -174,11 +66,11 @@ def neuter_message(message: FakeMessage): | ||||
| 
 | ||||
| 
 | ||||
| class Task: | ||||
|     default_task_data = {"triggers": [], "command_str": "", "expired_triggers": []} | ||||
|     default_task_data = {"triggers": [], "command_str": ""} | ||||
| 
 | ||||
|     default_trigger = { | ||||
|         "type": "", | ||||
|         "time_data": None, | ||||
|         "time_data": None,  # Used for Interval and Date Triggers | ||||
|         "tzinfo": None, | ||||
|     } | ||||
| 
 | ||||
| @ -195,10 +87,9 @@ class Task: | ||||
| 
 | ||||
|     async def _encode_time_triggers(self): | ||||
|         if not self.data or not self.data.get("triggers", None): | ||||
|             return [], [] | ||||
|             return [] | ||||
| 
 | ||||
|         triggers = [] | ||||
|         expired_triggers = [] | ||||
|         for t in self.data["triggers"]: | ||||
|             if t["type"] == "interval":  # Convert into timedelta | ||||
|                 td: timedelta = t["time_data"] | ||||
| @ -210,15 +101,27 @@ class Task: | ||||
| 
 | ||||
|             if t["type"] == "date":  # Convert into datetime | ||||
|                 dt: datetime = t["time_data"] | ||||
|                 data_to_append = { | ||||
|                     "type": t["type"], | ||||
|                     "time_data": dt.isoformat(), | ||||
|                     "tzinfo": getattr(t["tzinfo"], "zone", None), | ||||
|                 } | ||||
|                 if dt < datetime.now(pytz.utc): | ||||
|                     expired_triggers.append(data_to_append) | ||||
|                 else: | ||||
|                     triggers.append(data_to_append) | ||||
|                 triggers.append( | ||||
|                     { | ||||
|                         "type": t["type"], | ||||
|                         "time_data": dt.isoformat(), | ||||
|                         "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": | ||||
| @ -236,7 +139,7 @@ class Task: | ||||
| 
 | ||||
|             raise NotImplemented | ||||
| 
 | ||||
|         return triggers, expired_triggers | ||||
|         return triggers | ||||
| 
 | ||||
|     async def _decode_time_triggers(self): | ||||
|         if not self.data or not self.data.get("triggers", None): | ||||
| @ -249,7 +152,7 @@ class Task: | ||||
| 
 | ||||
|             # First decode timezone if there is one | ||||
|             if t["tzinfo"] is not None: | ||||
|                 t["tzinfo"] = pytz.timezone(t["tzinfo"]) | ||||
|                 t["tzinfo"] = timezone(t["tzinfo"]) | ||||
| 
 | ||||
|             if t["type"] == "interval":  # Convert into timedelta | ||||
|                 t["time_data"] = timedelta(**t["time_data"]) | ||||
| @ -277,7 +180,7 @@ class Task: | ||||
|             return | ||||
| 
 | ||||
|         self.author_id = data["author_id"] | ||||
|         self.guild_id = data["guild_id"]  # Weird I'm doing this, since self.guild_id was just used | ||||
|         self.guild_id = data["guild_id"] | ||||
|         self.channel_id = data["channel_id"] | ||||
| 
 | ||||
|         self.data = data["data"] | ||||
| @ -285,23 +188,14 @@ class Task: | ||||
|         await self._decode_time_triggers() | ||||
|         return self.data | ||||
| 
 | ||||
|     async def get_triggers(self) -> Tuple[List[BaseTrigger], List[BaseTrigger]]: | ||||
|     async def get_triggers(self) -> List[Union[IntervalTrigger, DateTrigger]]: | ||||
|         if not self.data: | ||||
|             await self.load_from_config() | ||||
| 
 | ||||
|         if self.data is None or "triggers" not in self.data:  # No triggers | ||||
|             return [], [] | ||||
|             return [] | ||||
| 
 | ||||
|         trigs = [] | ||||
|         expired_trigs = [] | ||||
|         for t in self.data["triggers"]: | ||||
|             trig = get_trigger(t) | ||||
|             if check_expired_trigger(trig): | ||||
|                 expired_trigs.append(t) | ||||
|             else: | ||||
|                 trigs.append(t) | ||||
| 
 | ||||
|         return trigs, expired_trigs | ||||
|         return [get_trigger(t) for t in self.data["triggers"]] | ||||
| 
 | ||||
|     async def get_combined_trigger(self) -> Union[BaseTrigger, None]: | ||||
|         if not self.data: | ||||
| @ -321,10 +215,7 @@ class Task: | ||||
|         data_to_save = self.default_task_data.copy() | ||||
|         if self.data: | ||||
|             data_to_save["command_str"] = self.get_command_str() | ||||
|             ( | ||||
|                 data_to_save["triggers"], | ||||
|                 data_to_save["expired_triggers"], | ||||
|             ) = await self._encode_time_triggers() | ||||
|             data_to_save["triggers"] = await self._encode_time_triggers() | ||||
| 
 | ||||
|         to_save = { | ||||
|             "guild_id": self.guild_id, | ||||
| @ -340,10 +231,7 @@ class Task: | ||||
|             return | ||||
| 
 | ||||
|         data_to_save = self.data.copy() | ||||
|         ( | ||||
|             data_to_save["triggers"], | ||||
|             data_to_save["expired_triggers"], | ||||
|         ) = await self._encode_time_triggers() | ||||
|         data_to_save["triggers"] = await self._encode_time_triggers() | ||||
| 
 | ||||
|         await self.config.guild_from_id(self.guild_id).tasks.set_raw( | ||||
|             self.name, "data", value=data_to_save | ||||
| @ -351,87 +239,63 @@ class Task: | ||||
| 
 | ||||
|     async def execute(self): | ||||
|         if not self.data or not self.get_command_str(): | ||||
|             log.warning(f"Could not execute Task[{self.name}] due to data problem: {self.data=}") | ||||
|             log.warning(f"Could not execute task 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[{self.name}] due to missing guild: {self.guild_id}" | ||||
|             ) | ||||
|             log.warning(f"Could not execute task 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[{self.name}] due to missing channel: {self.channel_id}" | ||||
|             ) | ||||
|             log.warning(f"Could not execute task due to missing channel: {self.channel_id}") | ||||
|             return False | ||||
|         author: discord.Member = guild.get_member(self.author_id) | ||||
|         author: discord.User = guild.get_member(self.author_id) | ||||
|         if author is None: | ||||
|             log.warning( | ||||
|                 f"Could not execute Task[{self.name}] due to missing author: {self.author_id}" | ||||
|             ) | ||||
|             log.warning(f"Could not execute task due to missing author: {self.author_id}") | ||||
|             return False | ||||
| 
 | ||||
|         actual_message: Optional[discord.Message] = channel.last_message | ||||
|         actual_message: discord.Message = channel.last_message | ||||
|         # I'd like to present you my chain of increasingly desperate message fetching attempts | ||||
|         if actual_message is None: | ||||
|             # log.warning("No message found in channel cache yet, skipping execution") | ||||
|             # return | ||||
|             if channel.last_message_id is not None: | ||||
|                 try: | ||||
|                     actual_message = await channel.fetch_message(channel.last_message_id) | ||||
|                 except discord.NotFound: | ||||
|                     actual_message = None | ||||
|             actual_message = await channel.fetch_message(channel.last_message_id) | ||||
|             if actual_message is None:  # last_message_id was an invalid message I guess | ||||
|                 actual_message = await channel.history(limit=1).flatten() | ||||
|                 if not actual_message:  # Basically only happens if the channel has no messages | ||||
|                     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 False | ||||
|                         return | ||||
|                 actual_message = actual_message[0] | ||||
| 
 | ||||
|         # message._handle_author(author)  # Option when message is subclass | ||||
|         # message._state = self.bot._get_state() | ||||
|         # Time to set the relevant attributes | ||||
|         # message.author = author | ||||
|         # Don't need guild with subclass, guild is just channel.guild | ||||
|         # message.guild = guild  # Just in case we got desperate, see above | ||||
|         # message.channel = channel | ||||
|         message = FakeMessage(actual_message) | ||||
|         # message = FakeMessage2 | ||||
|         message.author = author | ||||
|         message.guild = guild  # Just in case we got desperate | ||||
|         message.channel = channel | ||||
|         message.id = time_snowflake(datetime.now())  # Pretend to be now | ||||
|         message = neuter_message(message) | ||||
| 
 | ||||
|         # absolutely weird that this takes a message object instead of guild | ||||
|         prefixes = await self.bot.get_prefix(actual_message) | ||||
|         prefixes = await self.bot.get_prefix(message) | ||||
|         if isinstance(prefixes, str): | ||||
|             prefix = prefixes | ||||
|         else: | ||||
|             prefix = prefixes[0] | ||||
| 
 | ||||
|         new_content = f"{prefix}{self.get_command_str()}" | ||||
|         # log.debug(f"{new_content=}") | ||||
|         message.content = f"{prefix}{self.get_command_str()}" | ||||
| 
 | ||||
|         message = FakeMessage(message=actual_message) | ||||
|         message = neuter_message(message) | ||||
|         message.process_the_rest(author=author, channel=channel, content=new_content) | ||||
| 
 | ||||
|         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: " | ||||
|                 f"{message.guild=}, {message.author=}, {message.content=}" | ||||
|             ) | ||||
|         if not message.guild or not message.author or not message.content: | ||||
|             log.warning(f"Could not execute task 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: " | ||||
|                 f"{new_ctx.invoked_with=} {new_ctx.prefix=} {new_ctx.command=}" | ||||
|                 f"Could not execute Task[{self.name}] due invalid context: {new_ctx.invoked_with}" | ||||
|             ) | ||||
|             return False | ||||
| 
 | ||||
|  | ||||
| @ -5,8 +5,6 @@ All credit to https://github.com/prefrontal/dateutil-parser-timezones | ||||
| """ | ||||
| 
 | ||||
| # from dateutil.tz import gettz | ||||
| from datetime import datetime | ||||
| 
 | ||||
| from pytz import timezone | ||||
| 
 | ||||
| 
 | ||||
| @ -229,6 +227,4 @@ def assemble_timezones(): | ||||
|     timezones["YAKT"] = timezone("Asia/Yakutsk")  # Yakutsk Time (UTC+09) | ||||
|     timezones["YEKT"] = timezone("Asia/Yekaterinburg")  # Yekaterinburg Time (UTC+05) | ||||
| 
 | ||||
|     dt = datetime(2020, 1, 1) | ||||
|     timezones.update((x, y.localize(dt).tzinfo) for x, y in timezones.items()) | ||||
|     return timezones | ||||
|  | ||||
| @ -53,9 +53,12 @@ class Flag(Cog): | ||||
|     @commands.group() | ||||
|     async def flagset(self, ctx: commands.Context): | ||||
|         """ | ||||
|         Commands for managing Flag settings | ||||
|         My custom cog | ||||
| 
 | ||||
|         Extra information goes here | ||||
|         """ | ||||
|         pass | ||||
|         if ctx.invoked_subcommand is None: | ||||
|             pass | ||||
| 
 | ||||
|     @flagset.command(name="expire") | ||||
|     async def flagset_expire(self, ctx: commands.Context, days: int): | ||||
|  | ||||
| @ -147,7 +147,8 @@ class Hangman(Cog): | ||||
|     @checks.mod_or_permissions(administrator=True) | ||||
|     async def hangset(self, ctx): | ||||
|         """Adjust hangman settings""" | ||||
|         pass | ||||
|         if ctx.invoked_subcommand is None: | ||||
|             pass | ||||
| 
 | ||||
|     @hangset.command() | ||||
|     async def face(self, ctx: commands.Context, theface): | ||||
| @ -249,7 +250,7 @@ class Hangman(Cog): | ||||
| 
 | ||||
|         self.winbool[guild] = True | ||||
|         for i in self.the_data[guild]["answer"]: | ||||
|             if i in [" ", "-"]: | ||||
|             if i == " " or i == "-": | ||||
|                 out_str += i * 2 | ||||
|             elif i in self.the_data[guild]["guesses"] or i not in "ABCDEFGHIJKLMNOPQRSTUVWXYZ": | ||||
|                 out_str += "__" + i + "__ " | ||||
| @ -261,7 +262,9 @@ class Hangman(Cog): | ||||
| 
 | ||||
|     def _guesslist(self, guild): | ||||
|         """Returns the current letter list""" | ||||
|         out_str = "".join(str(i) + "," for i in self.the_data[guild]["guesses"]) | ||||
|         out_str = "" | ||||
|         for i in self.the_data[guild]["guesses"]: | ||||
|             out_str += str(i) + "," | ||||
|         out_str = out_str[:-1] | ||||
| 
 | ||||
|         return out_str | ||||
|  | ||||
| @ -1,7 +1,5 @@ | ||||
| from .infochannel import InfoChannel | ||||
| 
 | ||||
| 
 | ||||
| async def setup(bot): | ||||
|     ic_cog = InfoChannel(bot) | ||||
|     bot.add_cog(ic_cog) | ||||
|     await ic_cog.initialize() | ||||
| def setup(bot): | ||||
|     bot.add_cog(InfoChannel(bot)) | ||||
|  | ||||
| @ -1,53 +1,25 @@ | ||||
| import asyncio | ||||
| import logging | ||||
| from collections import defaultdict | ||||
| from typing import Dict, Optional, Union | ||||
| from typing import Union | ||||
| 
 | ||||
| import discord | ||||
| from redbot.core import Config, checks, commands | ||||
| from redbot.core.bot import Red | ||||
| from redbot.core.commands import Cog | ||||
| 
 | ||||
| # 10 minutes. Rate limit is 2 per 10, so 1 per 6 is safe. | ||||
| RATE_LIMIT_DELAY = 60 * 6  # If you're willing to risk rate limiting, you can decrease the delay | ||||
| # Cog: Any = getattr(commands, "Cog", object) | ||||
| # listener = getattr(commands.Cog, "listener", None)  # Trusty + Sinbad | ||||
| # if listener is None: | ||||
| #     def listener(name=None): | ||||
| #         return lambda x: x | ||||
| 
 | ||||
| log = logging.getLogger("red.fox_v3.infochannel") | ||||
| 
 | ||||
| 
 | ||||
| async def get_channel_counts(category, guild): | ||||
|     # Gets count of bots | ||||
|     bot_num = len([m for m in guild.members if m.bot]) | ||||
|     # Gets count of roles in the server | ||||
|     roles_num = len(guild.roles) - 1 | ||||
|     # Gets count of channels in the server | ||||
|     # <number of total channels> - <number of channels in the stats category> - <categories> | ||||
|     channels_num = len(guild.channels) - len(category.voice_channels) - len(guild.categories) | ||||
|     # Gets all counts of members | ||||
|     members = guild.member_count | ||||
|     offline_num = len(list(filter(lambda m: m.status is discord.Status.offline, guild.members))) | ||||
|     online_num = members - offline_num | ||||
|     # Gets count of actual users | ||||
|     human_num = members - bot_num | ||||
|     # count amount of premium subs/nitro subs. | ||||
|     boosters = guild.premium_subscription_count | ||||
|     return { | ||||
|         "members": members, | ||||
|         "humans": human_num, | ||||
|         "boosters": boosters, | ||||
|         "bots": bot_num, | ||||
|         "roles": roles_num, | ||||
|         "channels": channels_num, | ||||
|         "online": online_num, | ||||
|         "offline": offline_num, | ||||
|     } | ||||
| RATE_LIMIT_DELAY = 60 * 10  # If you're willing to risk rate limiting, you can decrease the delay | ||||
| 
 | ||||
| 
 | ||||
| class InfoChannel(Cog): | ||||
|     """ | ||||
|     Create a channel with updating server info | ||||
| 
 | ||||
|     This relies on editing channels, which is a strictly rate-limited activity. | ||||
|     As such, updates will not be frequent. Currently capped at 1 per 5 minutes per server. | ||||
|     Less important information about the cog | ||||
|     """ | ||||
| 
 | ||||
|     def __init__(self, bot: Red): | ||||
| @ -57,56 +29,23 @@ class InfoChannel(Cog): | ||||
|             self, identifier=731101021116710497110110101108, force_registration=True | ||||
|         ) | ||||
| 
 | ||||
|         # self. so I can get the keys from this later | ||||
|         self.default_channel_names = { | ||||
|             "members": "Members: {count}", | ||||
|             "humans": "Humans: {count}", | ||||
|             "boosters": "Boosters: {count}", | ||||
|             "bots": "Bots: {count}", | ||||
|             "roles": "Roles: {count}", | ||||
|             "channels": "Channels: {count}", | ||||
|             "online": "Online: {count}", | ||||
|             "offline": "Offline: {count}", | ||||
|         } | ||||
| 
 | ||||
|         default_channel_ids = {k: None for k in self.default_channel_names} | ||||
|         # Only members is enabled by default | ||||
|         default_enabled_counts = {k: k == "members" for k in self.default_channel_names} | ||||
| 
 | ||||
|         default_guild = { | ||||
|             "category_id": None, | ||||
|             "channel_ids": default_channel_ids, | ||||
|             "enabled_channels": default_enabled_counts, | ||||
|             "channel_names": self.default_channel_names, | ||||
|             "channel_id": None, | ||||
|             "botchannel_id": None, | ||||
|             "onlinechannel_id": None, | ||||
|             "member_count": True, | ||||
|             "bot_count": False, | ||||
|             "online_count": False, | ||||
|         } | ||||
| 
 | ||||
|         self.config.register_guild(**default_guild) | ||||
| 
 | ||||
|         self.default_role = {"enabled": False, "channel_id": None, "name": "{role}: {count}"} | ||||
| 
 | ||||
|         self.config.register_role(**self.default_role) | ||||
| 
 | ||||
|         self._critical_section_wooah_ = 0 | ||||
| 
 | ||||
|         self.channel_data = defaultdict(dict) | ||||
| 
 | ||||
|         self.edit_queue = defaultdict(lambda: defaultdict(lambda: asyncio.Queue(maxsize=2))) | ||||
| 
 | ||||
|         self._rate_limited_edits: Dict[int, Dict[str, Optional[asyncio.Task]]] = defaultdict( | ||||
|             lambda: defaultdict(lambda: None) | ||||
|         ) | ||||
| 
 | ||||
|     async def red_delete_data_for_user(self, **kwargs): | ||||
|         """Nothing to delete""" | ||||
|         return | ||||
| 
 | ||||
|     async def initialize(self): | ||||
|         for guild in self.bot.guilds: | ||||
|             await self.update_infochannel(guild) | ||||
| 
 | ||||
|     def cog_unload(self): | ||||
|         self.stop_all_queues() | ||||
| 
 | ||||
|     @commands.command() | ||||
|     @checks.admin() | ||||
|     async def infochannel(self, ctx: commands.Context): | ||||
| @ -122,466 +61,233 @@ class InfoChannel(Cog): | ||||
|             ) | ||||
| 
 | ||||
|         guild: discord.Guild = ctx.guild | ||||
|         category_id = await self.config.guild(guild).category_id() | ||||
|         category = None | ||||
|         channel_id = await self.config.guild(guild).channel_id() | ||||
|         channel = None | ||||
|         if channel_id is not None: | ||||
|             channel: Union[discord.VoiceChannel, None] = guild.get_channel(channel_id) | ||||
| 
 | ||||
|         if category_id is not None: | ||||
|             category: Union[discord.CategoryChannel, None] = guild.get_channel(category_id) | ||||
| 
 | ||||
|         if category_id is not None and category is None: | ||||
|             await ctx.maybe_send_embed("Info category has been deleted, recreate it?") | ||||
|         elif category_id is None: | ||||
|             await ctx.maybe_send_embed("Enable info channels on this server?") | ||||
|         if channel_id is not None and channel is None: | ||||
|             await ctx.send("Info channel has been deleted, recreate it?") | ||||
|         elif channel_id is None: | ||||
|             await ctx.send("Enable info channel on this server?") | ||||
|         else: | ||||
|             await ctx.maybe_send_embed("Do you wish to delete current info channels?") | ||||
|             await ctx.send("Do you wish to delete current info channels?") | ||||
| 
 | ||||
|         msg = await self.bot.wait_for("message", check=check) | ||||
| 
 | ||||
|         if msg.content.upper() in ["N", "NO"]: | ||||
|             await ctx.maybe_send_embed("Cancelled") | ||||
|             await ctx.send("Cancelled") | ||||
|             return | ||||
| 
 | ||||
|         if category is None: | ||||
|         if channel is None: | ||||
|             try: | ||||
|                 await self.make_infochannel(guild) | ||||
|             except discord.Forbidden: | ||||
|                 await ctx.maybe_send_embed( | ||||
|                     "Failure: Missing permission to create necessary channels" | ||||
|                 ) | ||||
|                 await ctx.send("Failure: Missing permission to create voice channel") | ||||
|                 return | ||||
|         else: | ||||
|             await self.delete_all_infochannels(guild) | ||||
| 
 | ||||
|         ctx.message = msg | ||||
| 
 | ||||
|         if not await ctx.tick(): | ||||
|             await ctx.maybe_send_embed("Done!") | ||||
|             await ctx.send("Done!") | ||||
| 
 | ||||
|     @commands.group(aliases=["icset"]) | ||||
|     @commands.group() | ||||
|     @checks.admin() | ||||
|     async def infochannelset(self, ctx: commands.Context): | ||||
|         """ | ||||
|         Toggle different types of infochannels | ||||
|         """ | ||||
|         pass | ||||
|         if not ctx.invoked_subcommand: | ||||
|             pass | ||||
| 
 | ||||
|     @infochannelset.command(name="togglechannel") | ||||
|     async def _infochannelset_togglechannel( | ||||
|         self, ctx: commands.Context, channel_type: str, enabled: Optional[bool] = None | ||||
|     ): | ||||
|         """Toggles the infochannel for the specified channel type. | ||||
| 
 | ||||
|         Valid Types are: | ||||
|         - `members`: Total members on the server | ||||
|         - `humans`: Total members that aren't bots | ||||
|         - `boosters`: Total amount of boosters | ||||
|         - `bots`: Total bots | ||||
|         - `roles`: Total number of roles | ||||
|         - `channels`: Total number of channels excluding infochannels, | ||||
|         - `online`: Total online members, | ||||
|         - `offline`: Total offline members, | ||||
|     @infochannelset.command(name="botcount") | ||||
|     async def _infochannelset_botcount(self, ctx: commands.Context, enabled: bool = None): | ||||
|         """ | ||||
|         Toggle an infochannel that shows the amount of bots in the server | ||||
|         """ | ||||
|         guild = ctx.guild | ||||
|         if channel_type not in self.default_channel_names.keys(): | ||||
|             await ctx.maybe_send_embed("Invalid channel type provided.") | ||||
|             return | ||||
| 
 | ||||
|         if enabled is None: | ||||
|             enabled = not await self.config.guild(guild).enabled_channels.get_raw(channel_type) | ||||
|             enabled = not await self.config.guild(guild).bot_count() | ||||
| 
 | ||||
|         await self.config.guild(guild).enabled_channels.set_raw(channel_type, value=enabled) | ||||
|         await self.make_infochannel(ctx.guild, channel_type=channel_type) | ||||
|         await self.config.guild(guild).bot_count.set(enabled) | ||||
|         await self.make_infochannel(ctx.guild) | ||||
| 
 | ||||
|         if enabled: | ||||
|             await ctx.maybe_send_embed(f"InfoChannel `{channel_type}` has been enabled.") | ||||
|             await ctx.send("InfoChannel for bot count has been enabled.") | ||||
|         else: | ||||
|             await ctx.maybe_send_embed(f"InfoChannel `{channel_type}` has been disabled.") | ||||
|             await ctx.send("InfoChannel for bot count has been disabled.") | ||||
| 
 | ||||
|     @infochannelset.command(name="togglerole") | ||||
|     async def _infochannelset_rolecount( | ||||
|         self, ctx: commands.Context, role: discord.Role, enabled: bool = None | ||||
|     ): | ||||
|         """Toggle an infochannel that shows the count of users with the specified role""" | ||||
|         if enabled is None: | ||||
|             enabled = not await self.config.role(role).enabled() | ||||
| 
 | ||||
|         await self.config.role(role).enabled.set(enabled) | ||||
| 
 | ||||
|         await self.make_infochannel(ctx.guild, channel_role=role) | ||||
| 
 | ||||
|         if enabled: | ||||
|             await ctx.maybe_send_embed(f"InfoChannel for {role.name} count has been enabled.") | ||||
|         else: | ||||
|             await ctx.maybe_send_embed(f"InfoChannel for {role.name} count has been disabled.") | ||||
| 
 | ||||
|     @infochannelset.command(name="name") | ||||
|     async def _infochannelset_name(self, ctx: commands.Context, channel_type: str, *, text=None): | ||||
|     @infochannelset.command(name="onlinecount") | ||||
|     async def _infochannelset_onlinecount(self, ctx: commands.Context, enabled: bool = None): | ||||
|         """ | ||||
|         Change the name of the infochannel for the specified channel type. | ||||
| 
 | ||||
|         {count} must be used to display number of total members in the server. | ||||
|         Leave blank to set back to default. | ||||
| 
 | ||||
|         Examples: | ||||
|         - `[p]infochannelset name members Cool Cats: {count}` | ||||
|         - `[p]infochannelset name bots {count} Robot Overlords` | ||||
| 
 | ||||
|         Valid Types are: | ||||
|         - `members`: Total members on the server | ||||
|         - `humans`: Total members that aren't bots | ||||
|         - `boosters`: Total amount of boosters | ||||
|         - `bots`: Total bots | ||||
|         - `roles`: Total number of roles | ||||
|         - `channels`: Total number of channels excluding infochannels | ||||
|         - `online`: Total online members | ||||
|         - `offline`: Total offline members | ||||
| 
 | ||||
|         Warning: This command counts against the channel update rate limit and may be queued. | ||||
|         Toggle an infochannel that shows the amount of online users in the server | ||||
|         """ | ||||
|         guild = ctx.guild | ||||
|         if channel_type not in self.default_channel_names.keys(): | ||||
|             await ctx.maybe_send_embed("Invalid channel type provided.") | ||||
|             return | ||||
|         if enabled is None: | ||||
|             enabled = not await self.config.guild(guild).online_count() | ||||
| 
 | ||||
|         if text is None: | ||||
|             text = self.default_channel_names.get(channel_type) | ||||
|         elif "{count}" not in text: | ||||
|             await ctx.maybe_send_embed( | ||||
|                 "Improperly formatted. Make sure to use `{count}` in your channel name" | ||||
|             ) | ||||
|             return | ||||
|         elif len(text) > 93: | ||||
|             await ctx.maybe_send_embed("Name is too long, max length is 93.") | ||||
|             return | ||||
|         await self.config.guild(guild).online_count.set(enabled) | ||||
|         await self.make_infochannel(ctx.guild) | ||||
| 
 | ||||
|         await self.config.guild(guild).channel_names.set_raw(channel_type, value=text) | ||||
|         await self.update_infochannel(guild, channel_type=channel_type) | ||||
|         if not await ctx.tick(): | ||||
|             await ctx.maybe_send_embed("Done!") | ||||
|         if enabled: | ||||
|             await ctx.send("InfoChannel for online user count has been enabled.") | ||||
|         else: | ||||
|             await ctx.send("InfoChannel for online user count has been disabled.") | ||||
| 
 | ||||
|     @infochannelset.command(name="rolename") | ||||
|     async def _infochannelset_rolename( | ||||
|         self, ctx: commands.Context, role: discord.Role, *, text=None | ||||
|     ): | ||||
|         """ | ||||
|         Change the name of the infochannel for specific roles. | ||||
| 
 | ||||
|         {count} must be used to display number members with the given role. | ||||
|         {role} can be used for the roles name. | ||||
|         Leave blank to set back to default. | ||||
| 
 | ||||
|         Default is set to: `{role}: {count}` | ||||
| 
 | ||||
|         Examples: | ||||
|         - `[p]infochannelset rolename @Patrons {role}: {count}` | ||||
|         - `[p]infochannelset rolename Elite {count} members with {role} role` | ||||
|         - `[p]infochannelset rolename "Space Role" Total boosters: {count}` | ||||
| 
 | ||||
|         Warning: This command counts against the channel update rate limit and may be queued. | ||||
|         """ | ||||
|         guild = ctx.message.guild | ||||
|         if text is None: | ||||
|             text = self.default_role["name"] | ||||
|         elif "{count}" not in text: | ||||
|             await ctx.maybe_send_embed( | ||||
|                 "Improperly formatted. Make sure to use `{count}` in your channel name" | ||||
|             ) | ||||
|             return | ||||
| 
 | ||||
|         await self.config.role(role).name.set(text) | ||||
|         await self.update_infochannel(guild, channel_role=role) | ||||
|         if not await ctx.tick(): | ||||
|             await ctx.maybe_send_embed("Done!") | ||||
| 
 | ||||
|     async def create_individual_channel( | ||||
|         self, guild, category: discord.CategoryChannel, overwrites, channel_type, count | ||||
|     ): | ||||
|         # Delete the channel if it exists | ||||
|         channel_id = await self.config.guild(guild).channel_ids.get_raw(channel_type) | ||||
|         if channel_id is not None: | ||||
|             channel: discord.VoiceChannel = guild.get_channel(channel_id) | ||||
|             if channel: | ||||
|                 self.stop_queue(guild.id, channel_type) | ||||
|                 await channel.delete(reason="InfoChannel delete") | ||||
| 
 | ||||
|         # Only make the channel if it's enabled | ||||
|         if await self.config.guild(guild).enabled_channels.get_raw(channel_type): | ||||
|             name = await self.config.guild(guild).channel_names.get_raw(channel_type) | ||||
|             name = name.format(count=count) | ||||
|             channel = await category.create_voice_channel( | ||||
|                 name, reason="InfoChannel make", overwrites=overwrites | ||||
|             ) | ||||
|             await self.config.guild(guild).channel_ids.set_raw(channel_type, value=channel.id) | ||||
|             return channel | ||||
|         return None | ||||
| 
 | ||||
|     async def create_role_channel( | ||||
|         self, guild, category: discord.CategoryChannel, overwrites, role: discord.Role | ||||
|     ): | ||||
|         # Delete the channel if it exists | ||||
|         channel_id = await self.config.role(role).channel_id() | ||||
|         if channel_id is not None: | ||||
|             channel: discord.VoiceChannel = guild.get_channel(channel_id) | ||||
|             if channel: | ||||
|                 self.stop_queue(guild.id, role.id) | ||||
|                 await channel.delete(reason="InfoChannel delete") | ||||
| 
 | ||||
|         # Only make the channel if it's enabled | ||||
|         if await self.config.role(role).enabled(): | ||||
|             count = len(role.members) | ||||
|             name = await self.config.role(role).name() | ||||
|             name = name.format(role=role.name, count=count) | ||||
|             channel = await category.create_voice_channel( | ||||
|                 name, reason="InfoChannel make", overwrites=overwrites | ||||
|             ) | ||||
|             await self.config.role(role).channel_id.set(channel.id) | ||||
|             return channel | ||||
|         return None | ||||
| 
 | ||||
|     async def make_infochannel(self, guild: discord.Guild, channel_type=None, channel_role=None): | ||||
|     async def make_infochannel(self, guild: discord.Guild): | ||||
|         botcount = await self.config.guild(guild).bot_count() | ||||
|         onlinecount = await self.config.guild(guild).online_count() | ||||
|         overwrites = { | ||||
|             guild.default_role: discord.PermissionOverwrite(connect=False), | ||||
|             guild.me: discord.PermissionOverwrite(manage_channels=True, connect=True), | ||||
|         } | ||||
| 
 | ||||
|         # Check for and create the Infochannel category | ||||
|         category_id = await self.config.guild(guild).category_id() | ||||
|         if category_id is not None: | ||||
|             category: discord.CategoryChannel = guild.get_channel(category_id) | ||||
|             if category is None:  # Category id is invalid, probably deleted. | ||||
|                 category_id = None | ||||
|         if category_id is None: | ||||
|             category: discord.CategoryChannel = await guild.create_category( | ||||
|                 "Server Stats", reason="InfoChannel Category make" | ||||
|         #  Remove the old info channel first | ||||
|         channel_id = await self.config.guild(guild).channel_id() | ||||
|         if channel_id is not None: | ||||
|             channel: discord.VoiceChannel = guild.get_channel(channel_id) | ||||
|             if channel: | ||||
|                 await channel.delete(reason="InfoChannel delete") | ||||
| 
 | ||||
|         # Then create the new one | ||||
|         channel = await guild.create_voice_channel( | ||||
|             "Total Humans:", reason="InfoChannel make", overwrites=overwrites | ||||
|         ) | ||||
|         await self.config.guild(guild).channel_id.set(channel.id) | ||||
| 
 | ||||
|         if botcount: | ||||
|             # Remove the old bot channel first | ||||
|             botchannel_id = await self.config.guild(guild).botchannel_id() | ||||
|             if channel_id is not None: | ||||
|                 botchannel: discord.VoiceChannel = guild.get_channel(botchannel_id) | ||||
|                 if botchannel: | ||||
|                     await botchannel.delete(reason="InfoChannel delete") | ||||
| 
 | ||||
|             # Then create the new one | ||||
|             botchannel = await guild.create_voice_channel( | ||||
|                 "Bots:", reason="InfoChannel botcount", overwrites=overwrites | ||||
|             ) | ||||
|             await self.config.guild(guild).category_id.set(category.id) | ||||
|             await category.edit(position=0) | ||||
|             category_id = category.id | ||||
|             await self.config.guild(guild).botchannel_id.set(botchannel.id) | ||||
|         if onlinecount: | ||||
|             # Remove the old online channel first | ||||
|             onlinechannel_id = await self.config.guild(guild).onlinechannel_id() | ||||
|             if channel_id is not None: | ||||
|                 onlinechannel: discord.VoiceChannel = guild.get_channel(onlinechannel_id) | ||||
|                 if onlinechannel: | ||||
|                     await onlinechannel.delete(reason="InfoChannel delete") | ||||
| 
 | ||||
|         category: discord.CategoryChannel = guild.get_channel(category_id) | ||||
| 
 | ||||
|         channel_data = await get_channel_counts(category, guild) | ||||
| 
 | ||||
|         # Only update a single channel | ||||
|         if channel_type is not None: | ||||
|             await self.create_individual_channel( | ||||
|                 guild, category, overwrites, channel_type, channel_data[channel_type] | ||||
|             # Then create the new one | ||||
|             onlinechannel = await guild.create_voice_channel( | ||||
|                 "Online:", reason="InfoChannel onlinecount", overwrites=overwrites | ||||
|             ) | ||||
|             return | ||||
|         if channel_role is not None: | ||||
|             await self.create_role_channel(guild, category, overwrites, channel_role) | ||||
|             return | ||||
|             await self.config.guild(guild).onlinechannel_id.set(onlinechannel.id) | ||||
| 
 | ||||
|         # Update all channels | ||||
|         for channel_type in self.default_channel_names.keys(): | ||||
|             await self.create_individual_channel( | ||||
|                 guild, category, overwrites, channel_type, channel_data[channel_type] | ||||
|             ) | ||||
| 
 | ||||
|         for role in guild.roles: | ||||
|             await self.create_role_channel(guild, category, overwrites, role) | ||||
| 
 | ||||
|         # await self.update_infochannel(guild) | ||||
|         await self.update_infochannel(guild) | ||||
| 
 | ||||
|     async def delete_all_infochannels(self, guild: discord.Guild): | ||||
|         self.stop_guild_queues(guild.id)  # Stop processing edits | ||||
| 
 | ||||
|         # Delete regular channels | ||||
|         for channel_type in self.default_channel_names.keys(): | ||||
|             channel_id = await self.config.guild(guild).channel_ids.get_raw(channel_type) | ||||
|             if channel_id is not None: | ||||
|                 channel = guild.get_channel(channel_id) | ||||
|                 if channel is not None: | ||||
|                     await channel.delete(reason="InfoChannel delete") | ||||
|                 await self.config.guild(guild).channel_ids.clear_raw(channel_type) | ||||
| 
 | ||||
|         # Delete role channels | ||||
|         for role in guild.roles: | ||||
|             channel_id = await self.config.role(role).channel_id() | ||||
|             if channel_id is not None: | ||||
|                 channel = guild.get_channel(channel_id) | ||||
|                 if channel is not None: | ||||
|                     await channel.delete(reason="InfoChannel delete") | ||||
|                 await self.config.role(role).channel_id.clear() | ||||
| 
 | ||||
|         # Delete the category last | ||||
|         category_id = await self.config.guild(guild).category_id() | ||||
|         if category_id is not None: | ||||
|             category = guild.get_channel(category_id) | ||||
|             if category is not None: | ||||
|                 await category.delete(reason="InfoChannel delete") | ||||
| 
 | ||||
|     async def add_to_queue(self, guild, channel, identifier, count, formatted_name): | ||||
|         self.channel_data[guild.id][identifier] = (count, formatted_name, channel.id) | ||||
|         if not self.edit_queue[guild.id][identifier].full(): | ||||
|             try: | ||||
|                 self.edit_queue[guild.id][identifier].put_nowait(identifier) | ||||
|             except asyncio.QueueFull: | ||||
|                 pass  # If queue is full, disregard | ||||
| 
 | ||||
|         if self._rate_limited_edits[guild.id][identifier] is None: | ||||
|             await self.start_queue(guild.id, identifier) | ||||
| 
 | ||||
|     async def update_individual_channel(self, guild, channel_type, count, guild_data): | ||||
|         name = guild_data["channel_names"][channel_type] | ||||
|         name = name.format(count=count) | ||||
|         channel = guild.get_channel(guild_data["channel_ids"][channel_type]) | ||||
|         if channel is None: | ||||
|             return  # abort | ||||
|         await self.add_to_queue(guild, channel, channel_type, count, name) | ||||
| 
 | ||||
|     async def update_role_channel(self, guild, role: discord.Role, role_data): | ||||
|         if not role_data["enabled"]: | ||||
|             return  # Not enabled | ||||
|         count = len(role.members) | ||||
|         name = role_data["name"] | ||||
|         name = name.format(role=role.name, count=count) | ||||
|         channel = guild.get_channel(role_data["channel_id"]) | ||||
|         if channel is None: | ||||
|             return  # abort | ||||
|         await self.add_to_queue(guild, channel, role.id, count, name) | ||||
| 
 | ||||
|     async def update_infochannel(self, guild: discord.Guild, channel_type=None, channel_role=None): | ||||
|         if channel_type is None and channel_role is None: | ||||
|             return await self.trigger_updates_for( | ||||
|                 guild, | ||||
|                 members=True, | ||||
|                 humans=True, | ||||
|                 boosters=True, | ||||
|                 bots=True, | ||||
|                 roles=True, | ||||
|                 channels=True, | ||||
|                 online=True, | ||||
|                 offline=True, | ||||
|                 extra_roles=set(guild.roles), | ||||
|             ) | ||||
| 
 | ||||
|         if channel_type is not None: | ||||
|             return await self.trigger_updates_for(guild, **{channel_type: True}) | ||||
| 
 | ||||
|         return await self.trigger_updates_for(guild, extra_roles={channel_role}) | ||||
| 
 | ||||
|     async def start_queue(self, guild_id, identifier): | ||||
|         self._rate_limited_edits[guild_id][identifier] = asyncio.create_task( | ||||
|             self._process_queue(guild_id, identifier) | ||||
|         ) | ||||
| 
 | ||||
|     def stop_queue(self, guild_id, identifier): | ||||
|         if self._rate_limited_edits[guild_id][identifier] is not None: | ||||
|             self._rate_limited_edits[guild_id][identifier].cancel() | ||||
| 
 | ||||
|     def stop_guild_queues(self, guild_id): | ||||
|         for identifier in self._rate_limited_edits[guild_id].keys(): | ||||
|             self.stop_queue(guild_id, identifier) | ||||
| 
 | ||||
|     def stop_all_queues(self): | ||||
|         for guild_id in self._rate_limited_edits.keys(): | ||||
|             self.stop_guild_queues(guild_id) | ||||
| 
 | ||||
|     async def _process_queue(self, guild_id, identifier): | ||||
|         while True: | ||||
|             identifier = await self.edit_queue[guild_id][identifier].get()  # Waits forever | ||||
| 
 | ||||
|             count, formatted_name, channel_id = self.channel_data[guild_id][identifier] | ||||
|             channel: discord.VoiceChannel = self.bot.get_channel(channel_id) | ||||
| 
 | ||||
|             if channel.name == formatted_name: | ||||
|                 continue  # Nothing to process | ||||
| 
 | ||||
|             log.debug(f"Processing guild_id: {guild_id} - identifier: {identifier}") | ||||
| 
 | ||||
|             try: | ||||
|                 await channel.edit(reason="InfoChannel update", name=formatted_name) | ||||
|             except (discord.Forbidden, discord.HTTPException): | ||||
|                 pass  # Don't bother figuring it out | ||||
|             except discord.InvalidArgument: | ||||
|                 log.exception(f"Invalid formatted infochannel: {formatted_name}") | ||||
|             else: | ||||
|                 await asyncio.sleep(RATE_LIMIT_DELAY)  # Wait a reasonable amount of time | ||||
| 
 | ||||
|     async def trigger_updates_for(self, guild, **kwargs): | ||||
|         extra_roles: Optional[set] = kwargs.pop("extra_roles", False) | ||||
|         guild_data = await self.config.guild(guild).all() | ||||
|         botchannel_id = guild_data["botchannel_id"] | ||||
|         onlinechannel_id = guild_data["onlinechannel_id"] | ||||
|         botchannel: discord.VoiceChannel = guild.get_channel(botchannel_id) | ||||
|         onlinechannel: discord.VoiceChannel = guild.get_channel(onlinechannel_id) | ||||
|         channel_id = guild_data["channel_id"] | ||||
|         channel: discord.VoiceChannel = guild.get_channel(channel_id) | ||||
|         await channel.delete(reason="InfoChannel delete") | ||||
|         if botchannel_id is not None: | ||||
|             await botchannel.delete(reason="InfoChannel delete") | ||||
|         if onlinechannel_id is not None: | ||||
|             await onlinechannel.delete(reason="InfoChannel delete") | ||||
| 
 | ||||
|         to_update = ( | ||||
|             kwargs.keys() & [key for key, value in guild_data["enabled_channels"].items() if value] | ||||
|         )  # Value in kwargs doesn't matter | ||||
|         await self.config.guild(guild).clear() | ||||
| 
 | ||||
|         if to_update or extra_roles: | ||||
|             log.debug(f"{to_update=}\n" | ||||
|                       f"{extra_roles=}") | ||||
|     async def update_infochannel(self, guild: discord.Guild): | ||||
|         guild_data = await self.config.guild(guild).all() | ||||
|         botcount = guild_data["bot_count"] | ||||
|         onlinecount = guild_data["online_count"] | ||||
| 
 | ||||
|             category = guild.get_channel(guild_data["category_id"]) | ||||
|             if category is None: | ||||
|                 log.debug('Channel category is missing, updating must be off') | ||||
|                 return  # Nothing to update, must be off | ||||
|         # Gets count of bots | ||||
|         # bots = lambda x: x.bot | ||||
|         # def bots(x): return x.bot | ||||
| 
 | ||||
|             channel_data = await get_channel_counts(category, guild) | ||||
|             if to_update: | ||||
|                 for channel_type in to_update: | ||||
|                     await self.update_individual_channel( | ||||
|                         guild, channel_type, channel_data[channel_type], guild_data | ||||
|                     ) | ||||
|             if extra_roles: | ||||
|                 role_data = await self.config.all_roles() | ||||
|                 for channel_role in extra_roles: | ||||
|                     if channel_role.id in role_data: | ||||
|                         await self.update_role_channel( | ||||
|                             guild, channel_role, role_data[channel_role.id] | ||||
|                         ) | ||||
|         bot_num = len([m for m in guild.members if m.bot]) | ||||
|         # bot_msg = f"Bots: {num}" | ||||
| 
 | ||||
|     @Cog.listener(name="on_member_join") | ||||
|     @Cog.listener(name="on_member_remove") | ||||
|     async def on_member_join_remove(self, member: discord.Member): | ||||
|         # Gets count of online users | ||||
|         members = guild.member_count | ||||
|         offline = len(list(filter(lambda m: m.status is discord.Status.offline, guild.members))) | ||||
|         online_num = members - offline | ||||
|         # online_msg = f"Online: {num}" | ||||
| 
 | ||||
|         # Gets count of actual users | ||||
|         total = lambda x: not x.bot | ||||
|         human_num = len([m for m in guild.members if total(m)]) | ||||
|         # human_msg = f"Total Humans: {num}" | ||||
| 
 | ||||
|         channel_id = guild_data["channel_id"] | ||||
|         if channel_id is None: | ||||
|             return False | ||||
| 
 | ||||
|         botchannel_id = guild_data["botchannel_id"] | ||||
|         onlinechannel_id = guild_data["onlinechannel_id"] | ||||
|         channel_id = guild_data["channel_id"] | ||||
|         channel: discord.VoiceChannel = guild.get_channel(channel_id) | ||||
|         botchannel: discord.VoiceChannel = guild.get_channel(botchannel_id) | ||||
|         onlinechannel: discord.VoiceChannel = guild.get_channel(onlinechannel_id) | ||||
| 
 | ||||
|         if guild_data["member_count"]: | ||||
|             name = f"{channel.name.split(':')[0]}: {human_num}" | ||||
| 
 | ||||
|             await channel.edit(reason="InfoChannel update", name=name) | ||||
| 
 | ||||
|         if botcount: | ||||
|             name = f"{botchannel.name.split(':')[0]}: {bot_num}" | ||||
|             await botchannel.edit(reason="InfoChannel update", name=name) | ||||
| 
 | ||||
|         if onlinecount: | ||||
|             name = f"{onlinechannel.name.split(':')[0]}: {online_num}" | ||||
|             await onlinechannel.edit(reason="InfoChannel update", name=name) | ||||
| 
 | ||||
|     async def update_infochannel_with_cooldown(self, guild): | ||||
|         """My attempt at preventing rate limits, lets see how it goes""" | ||||
|         if self._critical_section_wooah_: | ||||
|             if self._critical_section_wooah_ == 2: | ||||
|                 # print("Already pending, skipping") | ||||
|                 return  # Another one is already pending, don't queue more than one | ||||
|             # print("Queuing another update") | ||||
|             self._critical_section_wooah_ = 2 | ||||
| 
 | ||||
|             while self._critical_section_wooah_: | ||||
|                 await asyncio.sleep( | ||||
|                     RATE_LIMIT_DELAY // 4 | ||||
|                 )  # Max delay ends up as 1.25 * RATE_LIMIT_DELAY | ||||
| 
 | ||||
|             # print("Issuing queued update") | ||||
|             return await self.update_infochannel_with_cooldown(guild) | ||||
| 
 | ||||
|         # print("Entering critical") | ||||
|         self._critical_section_wooah_ = 1 | ||||
|         await self.update_infochannel(guild) | ||||
|         await asyncio.sleep(RATE_LIMIT_DELAY) | ||||
|         self._critical_section_wooah_ = 0 | ||||
|         # print("Exiting critical") | ||||
| 
 | ||||
|     @Cog.listener() | ||||
|     async def on_member_join(self, member: discord.Member): | ||||
|         if await self.bot.cog_disabled_in_guild(self, member.guild): | ||||
|             return | ||||
|         await self.update_infochannel_with_cooldown(member.guild) | ||||
| 
 | ||||
|         if member.bot: | ||||
|             await self.trigger_updates_for( | ||||
|                 member.guild, members=True, bots=True, online=True, offline=True | ||||
|             ) | ||||
|         else: | ||||
|             await self.trigger_updates_for( | ||||
|                 member.guild, members=True, humans=True, online=True, offline=True | ||||
|             ) | ||||
|     @Cog.listener() | ||||
|     async def on_member_remove(self, member: discord.Member): | ||||
|         if await self.bot.cog_disabled_in_guild(self, member.guild): | ||||
|             return | ||||
|         await self.update_infochannel_with_cooldown(member.guild) | ||||
| 
 | ||||
|     @Cog.listener() | ||||
|     async def on_member_update(self, before: discord.Member, after: discord.Member): | ||||
|         if await self.bot.cog_disabled_in_guild(self, after.guild): | ||||
|             return | ||||
| 
 | ||||
|         if before.status != after.status: | ||||
|             return await self.trigger_updates_for(after.guild, online=True, offline=True) | ||||
| 
 | ||||
|         # XOR | ||||
|         c = set(after.roles) ^ set(before.roles) | ||||
| 
 | ||||
|         if c: | ||||
|             await self.trigger_updates_for(after.guild, extra_roles=c) | ||||
| 
 | ||||
|     @Cog.listener("on_guild_channel_create") | ||||
|     @Cog.listener("on_guild_channel_delete") | ||||
|     async def on_guild_channel_create_delete(self, channel: discord.TextChannel): | ||||
|         if await self.bot.cog_disabled_in_guild(self, channel.guild): | ||||
|             return | ||||
|         await self.trigger_updates_for(channel.guild, channels=True) | ||||
| 
 | ||||
|     @Cog.listener() | ||||
|     async def on_guild_role_create(self, role): | ||||
|         if await self.bot.cog_disabled_in_guild(self, role.guild): | ||||
|             return | ||||
|         await self.trigger_updates_for(role.guild, roles=True) | ||||
| 
 | ||||
|     @Cog.listener() | ||||
|     async def on_guild_role_delete(self, role): | ||||
|         if await self.bot.cog_disabled_in_guild(self, role.guild): | ||||
|             return | ||||
|         await self.trigger_updates_for(role.guild, roles=True) | ||||
| 
 | ||||
|         role_channel_id = await self.config.role(role).channel_id() | ||||
|         if role_channel_id is not None: | ||||
|             rolechannel: discord.VoiceChannel = role.guild.get_channel(role_channel_id) | ||||
|             if rolechannel: | ||||
|                 await rolechannel.delete(reason="InfoChannel delete") | ||||
| 
 | ||||
|         await self.config.role(role).clear() | ||||
|         onlinecount = await self.config.guild(after.guild).online_count() | ||||
|         if onlinecount: | ||||
|             if before.status != after.status: | ||||
|                 await self.update_infochannel_with_cooldown(after.guild) | ||||
|  | ||||
| @ -10,9 +10,9 @@ log = logging.getLogger("red.fox_v3.isitdown") | ||||
| 
 | ||||
| class IsItDown(commands.Cog): | ||||
|     """ | ||||
|     Cog for checking whether a website is down or not. | ||||
|     Cog Description | ||||
| 
 | ||||
|     Uses the `isitdown.site` API | ||||
|     Less important information about the cog | ||||
|     """ | ||||
| 
 | ||||
|     def __init__(self, bot: Red): | ||||
| @ -36,25 +36,23 @@ class IsItDown(commands.Cog): | ||||
|         Alias: iid | ||||
|         """ | ||||
|         try: | ||||
|             resp, url = await self._check_if_down(url_to_check) | ||||
|             resp = await self._check_if_down(url_to_check) | ||||
|         except AssertionError: | ||||
|             await ctx.maybe_send_embed("Invalid URL provided. Make sure not to include `http://`") | ||||
|             return | ||||
| 
 | ||||
|         # log.debug(resp) | ||||
|         if resp["isitdown"]: | ||||
|             await ctx.maybe_send_embed(f"{url} is DOWN!") | ||||
|             await ctx.maybe_send_embed(f"{url_to_check} is DOWN!") | ||||
|         else: | ||||
|             await ctx.maybe_send_embed(f"{url} is UP!") | ||||
|             await ctx.maybe_send_embed(f"{url_to_check} is UP!") | ||||
| 
 | ||||
|     async def _check_if_down(self, url_to_check): | ||||
|         re_compiled = re.compile(r"https?://(www\.)?") | ||||
|         url = re_compiled.sub("", url_to_check).strip().strip("/") | ||||
|         url = re.compile(r"https?://(www\.)?") | ||||
|         url.sub("", url_to_check).strip().strip("/") | ||||
| 
 | ||||
|         url = f"https://isitdown.site/api/v3/{url}" | ||||
|         # log.debug(url) | ||||
|         async with aiohttp.ClientSession() as session: | ||||
|             async with session.get(url) as response: | ||||
|                 assert response.status == 200 | ||||
|                 resp = await response.json() | ||||
|         return resp, url | ||||
|         return resp | ||||
|  | ||||
| @ -8,7 +8,7 @@ | ||||
|   "install_msg": "Thank you for installing LaunchLib. Get started with `[p]load launchlib`, then `[p]help LaunchLib`", | ||||
|   "short": "Access launch data for space flights", | ||||
|   "end_user_data_statement": "This cog does not store any End User Data", | ||||
|   "requirements": ["python-launch-library>=2.0.3"], | ||||
|   "requirements": ["python-launch-library>=1.0.6"], | ||||
|   "tags": [ | ||||
|     "bobloy", | ||||
|     "utils", | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| import asyncio | ||||
| import functools | ||||
| import logging | ||||
| import re | ||||
| 
 | ||||
| import discord | ||||
| import launchlibrary as ll | ||||
| from redbot.core import Config, commands | ||||
| @ -14,7 +14,9 @@ log = logging.getLogger("red.fox_v3.launchlib") | ||||
| 
 | ||||
| class LaunchLib(commands.Cog): | ||||
|     """ | ||||
|     Cog using `thespacedevs` API to get details about rocket launches | ||||
|     Cog Description | ||||
| 
 | ||||
|     Less important information about the cog | ||||
|     """ | ||||
| 
 | ||||
|     def __init__(self, bot: Red): | ||||
| @ -35,30 +37,27 @@ class LaunchLib(commands.Cog): | ||||
|         return | ||||
| 
 | ||||
|     async def _embed_launch_data(self, launch: ll.AsyncLaunch): | ||||
| 
 | ||||
|         # status: ll.AsyncLaunchStatus = await launch.get_status() | ||||
|         status = launch.status | ||||
|         status: ll.AsyncLaunchStatus = await launch.get_status() | ||||
| 
 | ||||
|         rocket: ll.AsyncRocket = launch.rocket | ||||
| 
 | ||||
|         title = launch.name | ||||
|         description = status["name"] | ||||
|         description = status.description | ||||
| 
 | ||||
|         urls = launch.vid_urls + launch.info_urls | ||||
|         if rocket: | ||||
|             urls += [rocket.info_url, rocket.wiki_url] | ||||
|         if launch.pad: | ||||
|             urls += [launch.pad.info_url, launch.pad.wiki_url] | ||||
|         if not urls and rocket: | ||||
|             urls = rocket.info_urls + [rocket.wiki_url] | ||||
|         if urls: | ||||
|             url = urls[0] | ||||
|         else: | ||||
|             url = None | ||||
| 
 | ||||
|         url = next((url for url in urls if urls is not None), None) if urls else None | ||||
|         color = discord.Color.green() if status["id"] in [1, 3] else discord.Color.red() | ||||
|         color = discord.Color.green() if status.id in [1, 3] else discord.Color.red() | ||||
| 
 | ||||
|         em = discord.Embed(title=title, description=description, url=url, color=color) | ||||
| 
 | ||||
|         if rocket and rocket.image_url and rocket.image_url != "Array": | ||||
|             em.set_image(url=rocket.image_url) | ||||
|         elif launch.pad and launch.pad.map_image: | ||||
|             em.set_image(url=launch.pad.map_image) | ||||
| 
 | ||||
|         agency = getattr(launch, "agency", None) | ||||
|         if agency is not None: | ||||
| @ -90,18 +89,6 @@ class LaunchLib(commands.Cog): | ||||
|                     data = mission.get(f[0], None) | ||||
|                     if data is not None and data: | ||||
|                         em.add_field(name=f[1], value=data) | ||||
|         if launch.pad: | ||||
|             location_url = getattr(launch.pad, "map_url", None) | ||||
|             pad_name = getattr(launch.pad, "name", None) | ||||
| 
 | ||||
|             if pad_name is not None: | ||||
|                 if location_url is not None: | ||||
|                     location_url = re.sub( | ||||
|                         "[^a-zA-Z0-9/:.'+\"°?=,-]", "", location_url | ||||
|                     )  # Fix bad URLS | ||||
|                     em.add_field(name="Launch Pad Name", value=f"[{pad_name}]({location_url})") | ||||
|                 else: | ||||
|                     em.add_field(name="Launch Pad Name", value=pad_name) | ||||
| 
 | ||||
|         if rocket and rocket.family: | ||||
|             em.add_field(name="Rocket Family", value=rocket.family) | ||||
| @ -114,16 +101,11 @@ class LaunchLib(commands.Cog): | ||||
| 
 | ||||
|     @commands.group() | ||||
|     async def launchlib(self, ctx: commands.Context): | ||||
|         """Base command for getting launches""" | ||||
|         pass | ||||
|         if ctx.invoked_subcommand is None: | ||||
|             pass | ||||
| 
 | ||||
|     @launchlib.command() | ||||
|     async def next(self, ctx: commands.Context, num_launches: int = 1): | ||||
|         """ | ||||
|         Show the next launches | ||||
| 
 | ||||
|         Use `num_launches` to get more than one. | ||||
|         """ | ||||
|         # launches = await api.async_next_launches(num_launches) | ||||
|         # loop = asyncio.get_running_loop() | ||||
|         # | ||||
| @ -133,8 +115,6 @@ class LaunchLib(commands.Cog): | ||||
|         # | ||||
|         launches = await self.api.async_fetch_launch(num=num_launches) | ||||
| 
 | ||||
|         # log.debug(str(launches)) | ||||
| 
 | ||||
|         async with ctx.typing(): | ||||
|             for x, launch in enumerate(launches): | ||||
|                 if x >= num_launches: | ||||
|  | ||||
| @ -25,7 +25,8 @@ class Leaver(Cog): | ||||
|     @checks.mod_or_permissions(administrator=True) | ||||
|     async def leaverset(self, ctx): | ||||
|         """Adjust leaver settings""" | ||||
|         pass | ||||
|         if ctx.invoked_subcommand is None: | ||||
|             pass | ||||
| 
 | ||||
|     @leaverset.command() | ||||
|     async def channel(self, ctx: Context): | ||||
| @ -56,3 +57,5 @@ class Leaver(Cog): | ||||
|                 ) | ||||
|             else: | ||||
|                 await channel.send(out) | ||||
|         else: | ||||
|             pass | ||||
|  | ||||
| @ -40,20 +40,16 @@ class LoveCalculator(Cog): | ||||
|         log.debug(f"{resp=}") | ||||
|         soup_object = BeautifulSoup(resp, "html.parser") | ||||
| 
 | ||||
|         description = soup_object.find("div", class_="result__score") | ||||
|         description = soup_object.find("div", class_="result__score").get_text() | ||||
| 
 | ||||
|         if description is None: | ||||
|             description = "Dr. Love is busy right now" | ||||
|         else: | ||||
|             description = description.get_text().strip() | ||||
|             description = description.strip() | ||||
| 
 | ||||
|         result_image = soup_object.find("img", class_="result__image").get("src") | ||||
| 
 | ||||
|         result_text = soup_object.find("div", class_="result-text") | ||||
|         if result_text is None: | ||||
|             result_text = f"{x} and {y} aren't compatible 😔" | ||||
|         else: | ||||
|             result_text = result_text.get_text() | ||||
|         result_text = soup_object.find("div", class_="result-text").get_text() | ||||
|         result_text = " ".join(result_text.split()) | ||||
| 
 | ||||
|         try: | ||||
|  | ||||
| @ -45,12 +45,14 @@ class LastSeen(Cog): | ||||
| 
 | ||||
|     @staticmethod | ||||
|     def get_date_time(s): | ||||
|         return dateutil.parser.parse(s) | ||||
|         d = dateutil.parser.parse(s) | ||||
|         return d | ||||
| 
 | ||||
|     @commands.group(aliases=["setlseen"], name="lseenset") | ||||
|     async def lset(self, ctx: commands.Context): | ||||
|         """Change settings for lseen""" | ||||
|         pass | ||||
|         if ctx.invoked_subcommand is None: | ||||
|             pass | ||||
| 
 | ||||
|     @lset.command(name="toggle") | ||||
|     async def lset_toggle(self, ctx: commands.Context): | ||||
| @ -77,13 +79,11 @@ class LastSeen(Cog): | ||||
|                 return | ||||
|             last_seen = self.get_date_time(last_seen) | ||||
| 
 | ||||
|         embed = discord.Embed( | ||||
|             description="{} was last seen at this date and time".format(member.display_name), | ||||
|             timestamp=last_seen, | ||||
|             color=await self.bot.get_embed_color(ctx), | ||||
|         ) | ||||
|         # embed = discord.Embed( | ||||
|         #     description="{} was last seen at this date and time".format(member.display_name), | ||||
|         #     timestamp=last_seen) | ||||
| 
 | ||||
|         # embed = discord.Embed(timestamp=last_seen, color=await self.bot.get_embed_color(ctx)) | ||||
|         embed = discord.Embed(timestamp=last_seen, color=await self.bot.get_embed_color(ctx)) | ||||
|         await ctx.send(embed=embed) | ||||
| 
 | ||||
|     @commands.Cog.listener() | ||||
|  | ||||
| @ -8,7 +8,9 @@ from redbot.core.data_manager import cog_data_path | ||||
| 
 | ||||
| 
 | ||||
| class Nudity(commands.Cog): | ||||
|     """Monitor images for NSFW content and moves them to a nsfw channel if possible""" | ||||
|     """ | ||||
|     V3 Cog Template | ||||
|     """ | ||||
| 
 | ||||
|     def __init__(self, bot: Red): | ||||
|         super().__init__() | ||||
|  | ||||
| @ -111,8 +111,9 @@ async def _withdraw_points(gardener: Gardener, amount): | ||||
| 
 | ||||
|     if (gardener.points - amount) < 0: | ||||
|         return False | ||||
|     gardener.points -= amount | ||||
|     return True | ||||
|     else: | ||||
|         gardener.points -= amount | ||||
|         return True | ||||
| 
 | ||||
| 
 | ||||
| class PlantTycoon(commands.Cog): | ||||
| @ -244,9 +245,11 @@ class PlantTycoon(commands.Cog): | ||||
|             await self._load_plants_products() | ||||
| 
 | ||||
|         modifiers = sum( | ||||
|             self.products[product]["modifier"] | ||||
|             for product in gardener.products | ||||
|             if gardener.products[product] > 0 | ||||
|             [ | ||||
|                 self.products[product]["modifier"] | ||||
|                 for product in gardener.products | ||||
|                 if gardener.products[product] > 0 | ||||
|             ] | ||||
|         ) | ||||
| 
 | ||||
|         degradation = ( | ||||
| @ -287,31 +290,38 @@ class PlantTycoon(commands.Cog): | ||||
|         product = product.lower() | ||||
|         product_category = product_category.lower() | ||||
|         if product in self.products and self.products[product]["category"] == product_category: | ||||
|             if product in gardener.products and gardener.products[product] > 0: | ||||
|                 gardener.current["health"] += self.products[product]["health"] | ||||
|                 gardener.products[product] -= 1 | ||||
|                 if gardener.products[product] == 0: | ||||
|                     del gardener.products[product.lower()] | ||||
|                 if product_category == "fertilizer": | ||||
|                     emoji = ":poop:" | ||||
|                 elif product_category == "water": | ||||
|                     emoji = ":sweat_drops:" | ||||
|                 else: | ||||
|                     emoji = ":scissors:" | ||||
|                 message = "Your plant got some health back! {}".format(emoji) | ||||
|                 if gardener.current["health"] > gardener.current["threshold"]: | ||||
|                     gardener.current["health"] -= self.products[product]["damage"] | ||||
|                     if product_category == "tool": | ||||
|                         damage_msg = "You used {} too many times!".format(product) | ||||
|             if product in gardener.products: | ||||
|                 if gardener.products[product] > 0: | ||||
|                     gardener.current["health"] += self.products[product]["health"] | ||||
|                     gardener.products[product] -= 1 | ||||
|                     if gardener.products[product] == 0: | ||||
|                         del gardener.products[product.lower()] | ||||
|                     if product_category == "water": | ||||
|                         emoji = ":sweat_drops:" | ||||
|                     elif product_category == "fertilizer": | ||||
|                         emoji = ":poop:" | ||||
|                     # elif product_category == "tool": | ||||
|                     else: | ||||
|                         damage_msg = "You gave too much of {}.".format(product) | ||||
|                     message = "{} Your plant lost some health. :wilted_rose:".format(damage_msg) | ||||
|                 gardener.points += self.defaults["points"]["add_health"] | ||||
|                 await gardener.save_gardener() | ||||
|             elif product in gardener.products or product_category != "tool": | ||||
|                 message = "You have no {}. Go buy some!".format(product) | ||||
|                         emoji = ":scissors:" | ||||
|                     message = "Your plant got some health back! {}".format(emoji) | ||||
|                     if gardener.current["health"] > gardener.current["threshold"]: | ||||
|                         gardener.current["health"] -= self.products[product]["damage"] | ||||
|                         if product_category == "tool": | ||||
|                             damage_msg = "You used {} too many times!".format(product) | ||||
|                         else: | ||||
|                             damage_msg = "You gave too much of {}.".format(product) | ||||
|                         message = "{} Your plant lost some health. :wilted_rose:".format( | ||||
|                             damage_msg | ||||
|                         ) | ||||
|                     gardener.points += self.defaults["points"]["add_health"] | ||||
|                     await gardener.save_gardener() | ||||
|                 else: | ||||
|                     message = "You have no {}. Go buy some!".format(product) | ||||
|             else: | ||||
|                 message = "You don't have a {}. Go buy one!".format(product) | ||||
|                 if product_category == "tool": | ||||
|                     message = "You don't have a {}. Go buy one!".format(product) | ||||
|                 else: | ||||
|                     message = "You have no {}. Go buy some!".format(product) | ||||
|         else: | ||||
|             message = "Are you sure you are using {}?".format(product_category) | ||||
| 
 | ||||
| @ -402,18 +412,24 @@ class PlantTycoon(commands.Cog): | ||||
|             gardener.current = plant | ||||
|             await gardener.save_gardener() | ||||
| 
 | ||||
|             em = discord.Embed(description=message, color=discord.Color.green()) | ||||
|         else: | ||||
|             plant = gardener.current | ||||
|             message = "You're already growing {} **{}**, silly.".format( | ||||
|                 plant["article"], plant["name"] | ||||
|             ) | ||||
|         em = discord.Embed(description=message, color=discord.Color.green()) | ||||
|             em = discord.Embed(description=message, color=discord.Color.green()) | ||||
| 
 | ||||
|         await ctx.send(embed=em) | ||||
| 
 | ||||
|     @_gardening.command(name="profile") | ||||
|     async def _profile(self, ctx: commands.Context, *, member: discord.Member = None): | ||||
|         """Check your gardening profile.""" | ||||
|         author = member if member is not None else ctx.author | ||||
|         if member is not None: | ||||
|             author = member | ||||
|         else: | ||||
|             author = ctx.author | ||||
| 
 | ||||
|         gardener = await self._gardener(author) | ||||
|         try: | ||||
|             await self._apply_degradation(gardener) | ||||
| @ -424,7 +440,9 @@ class PlantTycoon(commands.Cog): | ||||
|         avatar = author.avatar_url if author.avatar else author.default_avatar_url | ||||
|         em.set_author(name="Gardening profile of {}".format(author.name), icon_url=avatar) | ||||
|         em.add_field(name="**Thneeds**", value=str(gardener.points)) | ||||
|         if gardener.current: | ||||
|         if not gardener.current: | ||||
|             em.add_field(name="**Currently growing**", value="None") | ||||
|         else: | ||||
|             em.set_thumbnail(url=gardener.current["image"]) | ||||
|             em.add_field( | ||||
|                 name="**Currently growing**", | ||||
| @ -432,15 +450,16 @@ class PlantTycoon(commands.Cog): | ||||
|                     gardener.current["name"], gardener.current["health"] | ||||
|                 ), | ||||
|             ) | ||||
|         else: | ||||
|             em.add_field(name="**Currently growing**", value="None") | ||||
|         if not gardener.badges: | ||||
|             em.add_field(name="**Badges**", value="None") | ||||
|         else: | ||||
|             badges = "".join("{}\n".format(badge.capitalize()) for badge in gardener.badges) | ||||
| 
 | ||||
|             badges = "" | ||||
|             for badge in gardener.badges: | ||||
|                 badges += "{}\n".format(badge.capitalize()) | ||||
|             em.add_field(name="**Badges**", value=badges) | ||||
|         if gardener.products: | ||||
|         if not gardener.products: | ||||
|             em.add_field(name="**Products**", value="None") | ||||
|         else: | ||||
|             products = "" | ||||
|             for product_name, product_data in gardener.products.items(): | ||||
|                 if self.products[product_name] is None: | ||||
| @ -451,8 +470,6 @@ class PlantTycoon(commands.Cog): | ||||
|                     self.products[product_name]["modifier"], | ||||
|                 ) | ||||
|             em.add_field(name="**Products**", value=products) | ||||
|         else: | ||||
|             em.add_field(name="**Products**", value="None") | ||||
|         if gardener.current: | ||||
|             degradation = await self._degradation(gardener) | ||||
|             die_in = await _die_in(gardener, degradation) | ||||
| @ -583,6 +600,7 @@ class PlantTycoon(commands.Cog): | ||||
|                         self.products[pd]["category"], | ||||
|                     ), | ||||
|                 ) | ||||
|             await ctx.send(embed=em) | ||||
|         else: | ||||
|             if amount <= 0: | ||||
|                 message = "Invalid amount! Must be greater than 1" | ||||
| @ -611,8 +629,7 @@ class PlantTycoon(commands.Cog): | ||||
|                 else: | ||||
|                     message = "I don't have this product." | ||||
|             em = discord.Embed(description=message, color=discord.Color.green()) | ||||
| 
 | ||||
|         await ctx.send(embed=em) | ||||
|             await ctx.send(embed=em) | ||||
| 
 | ||||
|     @_gardening.command(name="convert") | ||||
|     async def _convert(self, ctx: commands.Context, amount: int): | ||||
| @ -646,7 +663,8 @@ class PlantTycoon(commands.Cog): | ||||
|         else: | ||||
|             gardener.current = {} | ||||
|             message = "You successfully shovelled your plant out." | ||||
|             gardener.points = max(gardener.points, 0) | ||||
|             if gardener.points < 0: | ||||
|                 gardener.points = 0 | ||||
|             await gardener.save_gardener() | ||||
| 
 | ||||
|         em = discord.Embed(description=message, color=discord.Color.dark_grey()) | ||||
| @ -663,12 +681,12 @@ class PlantTycoon(commands.Cog): | ||||
|         except discord.Forbidden: | ||||
|             # Couldn't DM the degradation | ||||
|             await ctx.send("ERROR\nYou blocked me, didn't you?") | ||||
|         product = "water" | ||||
|         product_category = "water" | ||||
|         if not gardener.current: | ||||
|             message = "You're currently not growing a plant." | ||||
|             await _send_message(channel, message) | ||||
|         else: | ||||
|             product = "water" | ||||
|             product_category = "water" | ||||
|             await self._add_health(channel, gardener, product, product_category) | ||||
| 
 | ||||
|     @commands.command(name="fertilize") | ||||
| @ -682,11 +700,11 @@ class PlantTycoon(commands.Cog): | ||||
|             await ctx.send("ERROR\nYou blocked me, didn't you?") | ||||
|         channel = ctx.channel | ||||
|         product = fertilizer | ||||
|         product_category = "fertilizer" | ||||
|         if not gardener.current: | ||||
|             message = "You're currently not growing a plant." | ||||
|             await _send_message(channel, message) | ||||
|         else: | ||||
|             product_category = "fertilizer" | ||||
|             await self._add_health(channel, gardener, product, product_category) | ||||
| 
 | ||||
|     @commands.command(name="prune") | ||||
| @ -699,12 +717,12 @@ class PlantTycoon(commands.Cog): | ||||
|             # Couldn't DM the degradation | ||||
|             await ctx.send("ERROR\nYou blocked me, didn't you?") | ||||
|         channel = ctx.channel | ||||
|         product = "pruner" | ||||
|         product_category = "tool" | ||||
|         if not gardener.current: | ||||
|             message = "You're currently not growing a plant." | ||||
|             await _send_message(channel, message) | ||||
|         else: | ||||
|             product = "pruner" | ||||
|             product_category = "tool" | ||||
|             await self._add_health(channel, gardener, product, product_category) | ||||
| 
 | ||||
|     # async def check_degradation(self): | ||||
| @ -775,7 +793,7 @@ class PlantTycoon(commands.Cog): | ||||
|                             pass | ||||
|             await asyncio.sleep(self.defaults["timers"]["notification"] * 60) | ||||
| 
 | ||||
|     def cog_unload(self): | ||||
|     def __unload(self): | ||||
|         self.completion_task.cancel() | ||||
|         # self.degradation_task.cancel() | ||||
|         self.notification_task.cancel() | ||||
|  | ||||
| @ -67,10 +67,8 @@ class QRInvite(Cog): | ||||
| 
 | ||||
|         extension = pathlib.Path(image_url).parts[-1].replace(".", "?").split("?")[1] | ||||
| 
 | ||||
|         save_as_name = f"{ctx.guild.id}-{ctx.author.id}" | ||||
| 
 | ||||
|         path: pathlib.Path = cog_data_path(self) | ||||
|         image_path = path / f"{save_as_name}.{extension}" | ||||
|         image_path = path / (ctx.guild.icon + "." + extension) | ||||
|         async with aiohttp.ClientSession() as session: | ||||
|             async with session.get(image_url) as response: | ||||
|                 image = await response.read() | ||||
| @ -79,29 +77,27 @@ class QRInvite(Cog): | ||||
|             file.write(image) | ||||
| 
 | ||||
|         if extension == "webp": | ||||
|             new_image_path = convert_webp_to_png(str(image_path)) | ||||
|             new_path = convert_webp_to_png(str(image_path)) | ||||
|         elif extension == "gif": | ||||
|             await ctx.maybe_send_embed("gif is not supported yet, stay tuned") | ||||
|             return | ||||
|         elif extension == "png": | ||||
|             new_image_path = str(image_path) | ||||
|         elif extension == "jpg": | ||||
|             new_image_path = convert_jpg_to_png(str(image_path)) | ||||
|             new_path = str(image_path) | ||||
|         else: | ||||
|             await ctx.maybe_send_embed(f"{extension} is not supported yet, stay tuned") | ||||
|             return | ||||
| 
 | ||||
|         myqr.run( | ||||
|             invite, | ||||
|             picture=new_image_path, | ||||
|             save_name=f"{save_as_name}_qrcode.png", | ||||
|             picture=new_path, | ||||
|             save_name=ctx.guild.icon + "_qrcode.png", | ||||
|             save_dir=str(cog_data_path(self)), | ||||
|             colorized=colorized, | ||||
|         ) | ||||
| 
 | ||||
|         png_path: pathlib.Path = path / f"{save_as_name}_qrcode.png" | ||||
|         # with png_path.open("rb") as png_fp: | ||||
|         await ctx.send(file=discord.File(png_path, "qrcode.png")) | ||||
|         png_path: pathlib.Path = path / (ctx.guild.icon + "_qrcode.png") | ||||
|         with png_path.open("rb") as png_fp: | ||||
|             await ctx.send(file=discord.File(png_fp.read(), "qrcode.png")) | ||||
| 
 | ||||
| 
 | ||||
| def convert_webp_to_png(path): | ||||
| @ -114,10 +110,3 @@ def convert_webp_to_png(path): | ||||
|     new_path = path.replace(".webp", ".png") | ||||
|     im.save(new_path, transparency=255) | ||||
|     return new_path | ||||
| 
 | ||||
| 
 | ||||
| def convert_jpg_to_png(path): | ||||
|     im = Image.open(path) | ||||
|     new_path = path.replace(".jpg", ".png") | ||||
|     im.save(new_path) | ||||
|     return new_path | ||||
|  | ||||
| @ -97,7 +97,9 @@ class ReactRestrict(Cog): | ||||
|         """ | ||||
|         current_combos = await self.combo_list() | ||||
| 
 | ||||
|         to_keep = [c for c in current_combos if c.message_id != message_id or c.role_id != role.id] | ||||
|         to_keep = [ | ||||
|             c for c in current_combos if not (c.message_id == message_id and c.role_id == role.id) | ||||
|         ] | ||||
| 
 | ||||
|         if to_keep != current_combos: | ||||
|             await self.set_combo_list(to_keep) | ||||
| @ -208,7 +210,8 @@ class ReactRestrict(Cog): | ||||
|         """ | ||||
|         Base command for this cog. Check help for the commands list. | ||||
|         """ | ||||
|         pass | ||||
|         if ctx.invoked_subcommand is None: | ||||
|             pass | ||||
| 
 | ||||
|     @reactrestrict.command() | ||||
|     async def add(self, ctx: commands.Context, message_id: int, *, role: discord.Role): | ||||
|  | ||||
| @ -32,7 +32,6 @@ class RecyclingPlant(Cog): | ||||
| 
 | ||||
|         x = 0 | ||||
|         reward = 0 | ||||
|         timeoutcount = 0 | ||||
|         await ctx.send( | ||||
|             "{0} has signed up for a shift at the Recycling Plant! Type ``exit`` to terminate it early.".format( | ||||
|                 ctx.author.display_name | ||||
| @ -54,25 +53,14 @@ class RecyclingPlant(Cog): | ||||
|                 return m.author == ctx.author and m.channel == ctx.channel | ||||
| 
 | ||||
|             try: | ||||
|                 answer = await self.bot.wait_for("message", timeout=20, check=check) | ||||
|                 answer = await self.bot.wait_for("message", timeout=120, check=check) | ||||
|             except asyncio.TimeoutError: | ||||
|                 answer = None | ||||
| 
 | ||||
|             if answer is None: | ||||
|                 if timeoutcount == 2: | ||||
|                     await ctx.send( | ||||
|                         "{} slacked off at work, so they were sacked with no pay.".format( | ||||
|                             ctx.author.display_name | ||||
|                         ) | ||||
|                     ) | ||||
|                     break | ||||
|                 else: | ||||
|                     await ctx.send( | ||||
|                         "{} is slacking, and if they carry on not working, they'll be fired.".format( | ||||
|                             ctx.author.display_name | ||||
|                         ) | ||||
|                     ) | ||||
|                     timeoutcount += 1 | ||||
|                 await ctx.send( | ||||
|                     "``{}`` fell down the conveyor belt to be sorted again!".format(used["object"]) | ||||
|                 ) | ||||
|             elif answer.content.lower().strip() == used["action"]: | ||||
|                 await ctx.send( | ||||
|                     "Congratulations! You put ``{}`` down the correct chute! (**+50**)".format( | ||||
|  | ||||
| @ -69,12 +69,13 @@ class RPSLS(Cog): | ||||
| 
 | ||||
|     def get_emote(self, choice): | ||||
|         if choice == "rock": | ||||
|             return ":moyai:" | ||||
|             emote = ":moyai:" | ||||
|         elif choice == "spock": | ||||
|             return ":vulcan:" | ||||
|             emote = ":vulcan:" | ||||
|         elif choice == "paper": | ||||
|             return ":page_facing_up:" | ||||
|             emote = ":page_facing_up:" | ||||
|         elif choice in ["scissors", "lizard"]: | ||||
|             return ":{}:".format(choice) | ||||
|             emote = ":{}:".format(choice) | ||||
|         else: | ||||
|             return None | ||||
|             emote = None | ||||
|         return emote | ||||
|  | ||||
| @ -177,3 +177,7 @@ class SCP(Cog): | ||||
| 
 | ||||
|         msg = "http://www.scp-wiki.net/log-of-unexplained-locations" | ||||
|         await ctx.maybe_send_embed(msg) | ||||
| 
 | ||||
| 
 | ||||
| def setup(bot): | ||||
|     bot.add_cog(SCP(bot)) | ||||
|  | ||||
| @ -6,7 +6,6 @@ import discord | ||||
| from redbot.core import Config, checks, commands | ||||
| from redbot.core.bot import Red | ||||
| from redbot.core.commands import Cog | ||||
| from redbot.core.utils.chat_formatting import pagify | ||||
| 
 | ||||
| log = logging.getLogger("red.fox_v3.stealemoji") | ||||
| # Replaced with discord.Asset.read() | ||||
| @ -17,16 +16,16 @@ log = logging.getLogger("red.fox_v3.stealemoji") | ||||
| 
 | ||||
| 
 | ||||
| async def check_guild(guild, emoji): | ||||
|     if len(guild.emojis) >= 2 * guild.emoji_limit: | ||||
|     if len(guild.emojis) >= 100: | ||||
|         return False | ||||
| 
 | ||||
|     if len(guild.emojis) < guild.emoji_limit: | ||||
|     if len(guild.emojis) < 50: | ||||
|         return True | ||||
| 
 | ||||
|     if emoji.animated: | ||||
|         return sum(e.animated for e in guild.emojis) < guild.emoji_limit | ||||
|         return sum(e.animated for e in guild.emojis) < 50 | ||||
|     else: | ||||
|         return sum(not e.animated for e in guild.emojis) < guild.emoji_limit | ||||
|         return sum(not e.animated for e in guild.emojis) < 50 | ||||
| 
 | ||||
| 
 | ||||
| class StealEmoji(Cog): | ||||
| @ -70,7 +69,8 @@ class StealEmoji(Cog): | ||||
|         """ | ||||
|         Base command for this cog. Check help for the commands list. | ||||
|         """ | ||||
|         pass | ||||
|         if ctx.invoked_subcommand is None: | ||||
|             pass | ||||
| 
 | ||||
|     @checks.is_owner() | ||||
|     @stealemoji.command(name="clearemojis") | ||||
| @ -100,8 +100,7 @@ class StealEmoji(Cog): | ||||
|             await ctx.maybe_send_embed("No stolen emojis yet") | ||||
|             return | ||||
| 
 | ||||
|         for page in pagify(emoj, delims=[" "]): | ||||
|             await ctx.maybe_send_embed(page) | ||||
|         await ctx.maybe_send_embed(emoj) | ||||
| 
 | ||||
|     @checks.is_owner() | ||||
|     @stealemoji.command(name="notify") | ||||
| @ -269,36 +268,37 @@ class StealEmoji(Cog): | ||||
|                 break | ||||
| 
 | ||||
|         if guildbank is None: | ||||
|             if not await self.config.autobank(): | ||||
|                 return | ||||
|             if await self.config.autobank(): | ||||
|                 try: | ||||
|                     guildbank: discord.Guild = await self.bot.create_guild( | ||||
|                         "StealEmoji Guildbank", code="S93bqTqKQ9rM" | ||||
|                     ) | ||||
|                 except discord.HTTPException: | ||||
|                     await self.config.autobank.set(False) | ||||
|                     log.exception("Unable to create guilds, disabling autobank") | ||||
|                     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) | ||||
| 
 | ||||
|             try: | ||||
|                 guildbank: discord.Guild = await self.bot.create_guild( | ||||
|                     "StealEmoji Guildbank", code="S93bqTqKQ9rM" | ||||
|                 ) | ||||
|             except discord.HTTPException: | ||||
|                 await self.config.autobank.set(False) | ||||
|                 log.exception("Unable to create guilds, disabling autobank") | ||||
|                 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) | ||||
| 
 | ||||
|             await asyncio.sleep(2) | ||||
|                 if guildbank.text_channels: | ||||
|                     channel = guildbank.text_channels[0] | ||||
|                 else: | ||||
|                     # Always hits the else. | ||||
|                     # Maybe create_guild doesn't return guild object with | ||||
|                     #    the template channel? | ||||
|                     channel = await guildbank.create_text_channel("invite-channel") | ||||
|                 invite = await channel.create_invite() | ||||
| 
 | ||||
|             if guildbank.text_channels: | ||||
|                 channel = guildbank.text_channels[0] | ||||
|                 await self.bot.send_to_owners(invite) | ||||
|                 log.info(f"Guild created id {guildbank.id}. Invite: {invite}") | ||||
|             else: | ||||
|                 # Always hits the else. | ||||
|                 # Maybe create_guild doesn't return guild object with | ||||
|                 #    the template channel? | ||||
|                 channel = await guildbank.create_text_channel("invite-channel") | ||||
|             invite = await channel.create_invite() | ||||
|                 return | ||||
| 
 | ||||
|             await self.bot.send_to_owners(invite) | ||||
|             log.info(f"Guild created id {guildbank.id}. Invite: {invite}") | ||||
|         # Next, have I saved this emoji before (because uploaded emoji != orignal emoji) | ||||
| 
 | ||||
|         if str(emoji.id) in await self.config.stolemoji(): | ||||
|  | ||||
| @ -37,7 +37,7 @@ class Timerole(Cog): | ||||
|         self.bot = bot | ||||
|         self.config = Config.get_conf(self, identifier=9811198108111121, force_registration=True) | ||||
|         default_global = {} | ||||
|         default_guild = {"announce": None, "reapply": True, "roles": {}, "skipbots": True} | ||||
|         default_guild = {"announce": None, "reapply": True, "roles": {}} | ||||
|         default_rolemember = {"had_role": False, "check_again_time": None} | ||||
| 
 | ||||
|         self.config.register_global(**default_global) | ||||
| @ -77,7 +77,8 @@ class Timerole(Cog): | ||||
|     @commands.guild_only() | ||||
|     async def timerole(self, ctx): | ||||
|         """Adjust timerole settings""" | ||||
|         pass | ||||
|         if ctx.invoked_subcommand is None: | ||||
|             pass | ||||
| 
 | ||||
|     @timerole.command() | ||||
|     async def addrole( | ||||
| @ -92,9 +93,6 @@ class Timerole(Cog): | ||||
|             await ctx.maybe_send_embed("Error: Invalid time string.") | ||||
|             return | ||||
| 
 | ||||
|         if parsed_time is None: | ||||
|             return await ctx.maybe_send_embed("Error: Invalid time string.") | ||||
| 
 | ||||
|         days = parsed_time.days | ||||
|         hours = parsed_time.seconds // 60 // 60 | ||||
| 
 | ||||
| @ -154,14 +152,6 @@ class Timerole(Cog): | ||||
|         await self.config.guild(guild).reapply.set(not current_setting) | ||||
|         await ctx.maybe_send_embed(f"Reapplying roles is now set to: {not current_setting}") | ||||
| 
 | ||||
|     @timerole.command() | ||||
|     async def skipbots(self, ctx: commands.Context): | ||||
|         """Toggle skipping bots when adding/removing roles. Defaults to True""" | ||||
|         guild = ctx.guild | ||||
|         current_setting = await self.config.guild(guild).skipbots() | ||||
|         await self.config.guild(guild).skipbots.set(not current_setting) | ||||
|         await ctx.maybe_send_embed(f"Skipping bots is now set to: {not current_setting}") | ||||
| 
 | ||||
|     @timerole.command() | ||||
|     async def delrole(self, ctx: commands.Context, role: discord.Role): | ||||
|         """Deletes a role from being added/removed after specified time""" | ||||
| @ -210,9 +200,8 @@ class Timerole(Cog): | ||||
|             remove_results = "" | ||||
|             reapply = all_guilds[guild_id]["reapply"] | ||||
|             role_dict = all_guilds[guild_id]["roles"] | ||||
|             skipbots = all_guilds[guild_id]["skipbots"] | ||||
| 
 | ||||
|             if not any(role_dict.values()):  # No roles | ||||
|             if not any(role_data for role_data in role_dict.values()):  # No roles | ||||
|                 log.debug(f"No roles are configured for guild: {guild}") | ||||
|                 continue | ||||
| 
 | ||||
| @ -220,10 +209,6 @@ class Timerole(Cog): | ||||
|             # log.debug(f"{all_mr=}") | ||||
| 
 | ||||
|             async for member in AsyncIter(guild.members, steps=10): | ||||
| 
 | ||||
|                 if member.bot and skipbots: | ||||
|                     continue | ||||
| 
 | ||||
|                 addlist = [] | ||||
|                 removelist = [] | ||||
| 
 | ||||
| @ -247,7 +232,7 @@ class Timerole(Cog): | ||||
|                         log.debug(f"{member.display_name} - Not time to check again yet") | ||||
|                         continue | ||||
|                     member: discord.Member | ||||
|                     has_roles = {r.id for r in member.roles} | ||||
|                     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 (int(role_id) in has_roles and not role_data["remove"]) or ( | ||||
| @ -311,11 +296,8 @@ class Timerole(Cog): | ||||
|                         log.exception("Failed Adding Roles") | ||||
|                         add_results += f"{member.display_name} : **(Failed Adding Roles)**\n" | ||||
|                     else: | ||||
|                         add_results += ( | ||||
|                             " \n".join( | ||||
|                                 f"{member.display_name} : {role.name}" for role in add_roles | ||||
|                             ) | ||||
|                             + "\n" | ||||
|                         add_results += " \n".join( | ||||
|                             f"{member.display_name} : {role.name}" for role in add_roles | ||||
|                         ) | ||||
|                         for role_id in addlist: | ||||
|                             await self.config.custom( | ||||
| @ -329,11 +311,8 @@ class Timerole(Cog): | ||||
|                         log.exception("Failed Removing Roles") | ||||
|                         remove_results += f"{member.display_name} : **(Failed Removing Roles)**\n" | ||||
|                     else: | ||||
|                         remove_results += ( | ||||
|                             " \n".join( | ||||
|                                 f"{member.display_name} : {role.name}" for role in remove_roles | ||||
|                             ) | ||||
|                             + "\n" | ||||
|                         remove_results += " \n".join( | ||||
|                             f"{member.display_name} : {role.name}" for role in remove_roles | ||||
|                         ) | ||||
|                         for role_id in removelist: | ||||
|                             await self.config.custom( | ||||
|  | ||||
							
								
								
									
										46
									
								
								tts/tts.py
									
									
									
									
									
								
							
							
						
						
									
										46
									
								
								tts/tts.py
									
									
									
									
									
								
							| @ -1,35 +1,11 @@ | ||||
| import io | ||||
| import logging | ||||
| from typing import Optional, TYPE_CHECKING | ||||
| 
 | ||||
| import discord | ||||
| from discord.ext.commands import BadArgument, Converter | ||||
| from gtts import gTTS | ||||
| from gtts.lang import _fallback_deprecated_lang, tts_langs | ||||
| from redbot.core import Config, commands | ||||
| from redbot.core.bot import Red | ||||
| from redbot.core.commands import Cog | ||||
| 
 | ||||
| log = logging.getLogger("red.fox_v3.tts") | ||||
| 
 | ||||
| if TYPE_CHECKING: | ||||
|     ISO639Converter = str | ||||
| else: | ||||
| 
 | ||||
|     class ISO639Converter(Converter): | ||||
|         async def convert(self, ctx, argument) -> str: | ||||
|             lang = _fallback_deprecated_lang(argument) | ||||
| 
 | ||||
|             try: | ||||
|                 langs = tts_langs() | ||||
|                 if lang not in langs: | ||||
|                     raise BadArgument("Language not supported: %s" % lang) | ||||
|             except RuntimeError as e: | ||||
|                 log.debug(str(e), exc_info=True) | ||||
|                 log.warning(str(e)) | ||||
| 
 | ||||
|             return lang | ||||
| 
 | ||||
| 
 | ||||
| class TTS(Cog): | ||||
|     """ | ||||
| @ -42,7 +18,7 @@ class TTS(Cog): | ||||
| 
 | ||||
|         self.config = Config.get_conf(self, identifier=9811198108111121, force_registration=True) | ||||
|         default_global = {} | ||||
|         default_guild = {"language": "en"} | ||||
|         default_guild = {} | ||||
| 
 | ||||
|         self.config.register_global(**default_global) | ||||
|         self.config.register_guild(**default_guild) | ||||
| @ -51,29 +27,13 @@ class TTS(Cog): | ||||
|         """Nothing to delete""" | ||||
|         return | ||||
| 
 | ||||
|     @commands.mod() | ||||
|     @commands.command() | ||||
|     async def ttslang(self, ctx: commands.Context, lang: ISO639Converter): | ||||
|         """ | ||||
|         Sets the default language for TTS in this guild. | ||||
| 
 | ||||
|         Default is `en` for English | ||||
|         """ | ||||
|         await self.config.guild(ctx.guild).language.set(lang) | ||||
|         await ctx.send(f"Default tts language set to {lang}") | ||||
| 
 | ||||
|     @commands.command(aliases=["t2s", "text2"]) | ||||
|     async def tts( | ||||
|         self, ctx: commands.Context, lang: Optional[ISO639Converter] = None, *, text: str | ||||
|     ): | ||||
|     async def tts(self, ctx: commands.Context, *, text: str): | ||||
|         """ | ||||
|         Send Text to speech messages as an mp3 | ||||
|         """ | ||||
|         if lang is None: | ||||
|             lang = await self.config.guild(ctx.guild).language() | ||||
| 
 | ||||
|         mp3_fp = io.BytesIO() | ||||
|         tts = gTTS(text, lang=lang) | ||||
|         tts = gTTS(text, lang="en") | ||||
|         tts.write_to_fp(mp3_fp) | ||||
|         mp3_fp.seek(0) | ||||
|         await ctx.send(file=discord.File(mp3_fp, "text.mp3")) | ||||
|  | ||||
| @ -19,7 +19,8 @@ class Unicode(Cog): | ||||
|     @commands.group(name="unicode", pass_context=True) | ||||
|     async def unicode(self, ctx): | ||||
|         """Encode/Decode a Unicode character.""" | ||||
|         pass | ||||
|         if ctx.invoked_subcommand is None: | ||||
|             pass | ||||
| 
 | ||||
|     @unicode.command() | ||||
|     async def decode(self, ctx: commands.Context, character): | ||||
|  | ||||
| @ -71,7 +71,6 @@ W1, W2, W5, W6 = Random Werewolf | ||||
| N1 = Benign Neutral | ||||
| 
 | ||||
| 0001-1112T11W112N2 | ||||
| which translates to | ||||
| 0,0,0,1,11,12,E1,R1,R1,R1,R2,P2 | ||||
| 
 | ||||
| pre-letter = exact role position | ||||
| @ -90,7 +89,7 @@ async def parse_code(code, game): | ||||
|         if len(built) < digits: | ||||
|             built += c | ||||
| 
 | ||||
|         if built in ["T", "W", "N"]: | ||||
|         if built == "T" or built == "W" or built == "N": | ||||
|             # Random Towns | ||||
|             category = built | ||||
|             built = "" | ||||
| @ -116,6 +115,8 @@ async def parse_code(code, game): | ||||
|                 options = [role for role in ROLE_LIST if 10 + idx in role.category] | ||||
|             elif category == "N": | ||||
|                 options = [role for role in ROLE_LIST if 20 + idx in role.category] | ||||
|                 pass | ||||
| 
 | ||||
|             if not options: | ||||
|                 raise IndexError("No Match Found") | ||||
| 
 | ||||
| @ -128,8 +129,11 @@ async def parse_code(code, game): | ||||
| 
 | ||||
| async def encode(role_list, rand_roles): | ||||
|     """Convert role list to code""" | ||||
|     out_code = "" | ||||
| 
 | ||||
|     digit_sort = sorted(role for role in role_list if role < 10) | ||||
|     out_code = "".join(str(role) for role in digit_sort) | ||||
|     for role in digit_sort: | ||||
|         out_code += str(role) | ||||
| 
 | ||||
|     digit_sort = sorted(role for role in role_list if 10 <= role < 100) | ||||
|     if digit_sort: | ||||
|  | ||||
| @ -526,10 +526,9 @@ class Game: | ||||
| 
 | ||||
|     async def _notify(self, event_name, **kwargs): | ||||
|         for i in range(1, 7):  # action guide 1-6 (0 is no action) | ||||
|             tasks = [ | ||||
|                 asyncio.create_task(event(**kwargs)) | ||||
|                 for event in self.listeners.get(event_name, {}).get(i, []) | ||||
|             ] | ||||
|             tasks = [] | ||||
|             for event in self.listeners.get(event_name, {}).get(i, []): | ||||
|                 tasks.append(asyncio.create_task(event(**kwargs))) | ||||
| 
 | ||||
|             # Run same-priority task simultaneously | ||||
|             await asyncio.gather(*tasks) | ||||
| @ -556,7 +555,10 @@ class Game: | ||||
|     async def generate_targets(self, channel, with_roles=False): | ||||
|         embed = discord.Embed(title="Remaining Players", description="[ID] - [Name]") | ||||
|         for i, player in enumerate(self.players): | ||||
|             status = "" if player.alive else "*[Dead]*-" | ||||
|             if player.alive: | ||||
|                 status = "" | ||||
|             else: | ||||
|                 status = "*[Dead]*-" | ||||
|             if with_roles or not player.alive: | ||||
|                 embed.add_field( | ||||
|                     name=f"{i} - {status}{player.member.display_name}", | ||||
| @ -577,7 +579,7 @@ class Game: | ||||
|         if channel_id not in self.p_channels: | ||||
|             self.p_channels[channel_id] = self.default_secret_channel.copy() | ||||
| 
 | ||||
|         for _ in range(10):  # Retry 10 times | ||||
|         for x in range(10):  # Retry 10 times | ||||
|             try: | ||||
|                 await asyncio.sleep(1)  # This will have multiple calls | ||||
|                 self.p_channels[channel_id]["players"].append(role.player) | ||||
| @ -704,7 +706,9 @@ class Game: | ||||
|             if not self.any_votes_remaining: | ||||
|                 await channel.send("Voting is not allowed right now") | ||||
|                 return | ||||
|         elif channel.name not in self.p_channels: | ||||
|         elif channel.name in self.p_channels: | ||||
|             pass | ||||
|         else: | ||||
|             # Not part of the game | ||||
|             await channel.send("Cannot vote in this channel") | ||||
|             return | ||||
| @ -753,14 +757,14 @@ class Game: | ||||
|             await self._at_voted(target) | ||||
| 
 | ||||
|     async def eval_results(self, target, source=None, method=None): | ||||
|         if method is None: | ||||
|         if method is not None: | ||||
|             out = "**{ID}** - " + method | ||||
|             return out.format(ID=target.id, target=target.member.display_name) | ||||
|         else: | ||||
|             return "**{ID}** - {target} the {role} was found dead".format( | ||||
|                 ID=target.id, target=target.member.display_name, role=await target.role.get_role() | ||||
|             ) | ||||
| 
 | ||||
|         out = "**{ID}** - " + method | ||||
|         return out.format(ID=target.id, target=target.member.display_name) | ||||
| 
 | ||||
|     async def _quit(self, player): | ||||
|         """ | ||||
|         Have player quit the game | ||||
|  | ||||
| @ -72,9 +72,6 @@ class Role(WolfListener): | ||||
|         self.blocked = False | ||||
|         self.properties = {}  # Extra data for other roles (i.e. arsonist) | ||||
| 
 | ||||
|     def __str__(self): | ||||
|         return self.__repr__() | ||||
| 
 | ||||
|     def __repr__(self): | ||||
|         return f"{self.__class__.__name__}({self.player.__repr__()})" | ||||
| 
 | ||||
| @ -89,7 +86,7 @@ class Role(WolfListener): | ||||
| 
 | ||||
|         log.debug(f"Assigned {self} to {player}") | ||||
| 
 | ||||
|     async def get_alignment(self, source=None):  # TODO: Rework to be "strength" tiers | ||||
|     async def get_alignment(self, source=None): | ||||
|         """ | ||||
|         Interaction for powerful access of alignment | ||||
|         (Village, Werewolf, Other) | ||||
|  | ||||
| @ -1,10 +1,11 @@ | ||||
| import logging | ||||
| from typing import Optional | ||||
| from typing import List, Union | ||||
| 
 | ||||
| import discord | ||||
| from redbot.core import Config, checks, commands | ||||
| from redbot.core.bot import Red | ||||
| from redbot.core.commands import Cog | ||||
| from redbot.core.utils import AsyncIter | ||||
| from redbot.core.utils.menus import DEFAULT_CONTROLS, menu | ||||
| 
 | ||||
| from werewolf.builder import ( | ||||
| @ -14,11 +15,19 @@ from werewolf.builder import ( | ||||
|     role_from_id, | ||||
|     role_from_name, | ||||
| ) | ||||
| from werewolf.game import Game, anyone_has_role | ||||
| from werewolf.game import Game | ||||
| 
 | ||||
| log = logging.getLogger("red.fox_v3.werewolf") | ||||
| 
 | ||||
| 
 | ||||
| async def anyone_has_role( | ||||
|     member_list: List[discord.Member], role: discord.Role | ||||
| ) -> Union[None, discord.Member]: | ||||
|     return await AsyncIter(member_list).find( | ||||
|         lambda m: AsyncIter(m.roles).find(lambda r: r.id == role.id) | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| class Werewolf(Cog): | ||||
|     """ | ||||
|     Base to host werewolf on a guild | ||||
| @ -47,19 +56,17 @@ class Werewolf(Cog): | ||||
|         """Nothing to delete""" | ||||
|         return | ||||
| 
 | ||||
|     def cog_unload(self): | ||||
|     def __unload(self): | ||||
|         log.debug("Unload called") | ||||
|         for key in self.games.keys(): | ||||
|             del self.games[key] | ||||
|         for game in self.games.values(): | ||||
|             del game | ||||
| 
 | ||||
|     @commands.command() | ||||
|     async def buildgame(self, ctx: commands.Context): | ||||
|         """ | ||||
|         Create game codes to run custom games. | ||||
| 
 | ||||
|         Pick the roles or randomized roles you want to include in a game. | ||||
| 
 | ||||
|         Note: The same role can be picked more than once. | ||||
|         Pick the roles or randomized roles you want to include in a game | ||||
|         """ | ||||
|         gb = GameBuilder() | ||||
|         code = await gb.build_game(ctx) | ||||
| @ -75,7 +82,8 @@ class Werewolf(Cog): | ||||
|         """ | ||||
|         Base command to adjust settings. Check help for command list. | ||||
|         """ | ||||
|         pass | ||||
|         if ctx.invoked_subcommand is None: | ||||
|             pass | ||||
| 
 | ||||
|     @commands.guild_only() | ||||
|     @wwset.command(name="list") | ||||
| @ -84,6 +92,9 @@ class Werewolf(Cog): | ||||
|         Lists current guild settings | ||||
|         """ | ||||
|         valid, role, category, channel, log_channel = await self._get_settings(ctx) | ||||
|         # if not valid: | ||||
|         #     await ctx.send("Failed to get settings") | ||||
|         #     return None | ||||
| 
 | ||||
|         embed = discord.Embed( | ||||
|             title="Current Guild Settings", | ||||
| @ -165,7 +176,8 @@ class Werewolf(Cog): | ||||
|         """ | ||||
|         Base command for this cog. Check help for the commands list. | ||||
|         """ | ||||
|         pass | ||||
|         if ctx.invoked_subcommand is None: | ||||
|             pass | ||||
| 
 | ||||
|     @commands.guild_only() | ||||
|     @ww.command(name="new") | ||||
| @ -251,7 +263,6 @@ class Werewolf(Cog): | ||||
|         game = await self._get_game(ctx) | ||||
|         if not game: | ||||
|             await ctx.maybe_send_embed("No game running, cannot start") | ||||
|             return | ||||
| 
 | ||||
|         if not await game.setup(ctx): | ||||
|             pass  # ToDo something? | ||||
| @ -274,8 +285,7 @@ class Werewolf(Cog): | ||||
| 
 | ||||
|         game = await self._get_game(ctx) | ||||
|         game.game_over = True | ||||
|         if game.current_action: | ||||
|             game.current_action.cancel() | ||||
|         game.current_action.cancel() | ||||
|         await ctx.maybe_send_embed("Game has been stopped") | ||||
| 
 | ||||
|     @commands.guild_only() | ||||
| @ -346,7 +356,8 @@ class Werewolf(Cog): | ||||
|         """ | ||||
|         Find custom roles by name, alignment, category, or ID | ||||
|         """ | ||||
|         pass | ||||
|         if ctx.invoked_subcommand is None or ctx.invoked_subcommand == self.ww_search: | ||||
|             pass | ||||
| 
 | ||||
|     @ww_search.command(name="name") | ||||
|     async def ww_search_name(self, ctx: commands.Context, *, name): | ||||
| @ -388,7 +399,7 @@ class Werewolf(Cog): | ||||
|             else: | ||||
|                 await ctx.maybe_send_embed("Role ID not found") | ||||
| 
 | ||||
|     async def _get_game(self, ctx: commands.Context, game_code=None) -> Optional[Game]: | ||||
|     async def _get_game(self, ctx: commands.Context, game_code=None) -> Union[Game, None]: | ||||
|         guild: discord.Guild = getattr(ctx, "guild", None) | ||||
| 
 | ||||
|         if guild is None: | ||||
| @ -415,7 +426,7 @@ class Werewolf(Cog): | ||||
| 
 | ||||
|         return self.games[guild.id] | ||||
| 
 | ||||
|     async def _game_start(self, game: Game): | ||||
|     async def _game_start(self, game): | ||||
|         await game.start() | ||||
| 
 | ||||
|     async def _get_settings(self, ctx): | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user