Compare commits
	
		
			2 Commits
		
	
	
		
			master
			...
			sayurl-dev
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | c113899bc1 | ||
|   | 7e5dff0c64 | 
							
								
								
									
										26
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										26
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
								
							| @ -1,26 +0,0 @@ | ||||
| --- | ||||
| name: Bug report | ||||
| about: Create an issue to report a bug | ||||
| title: '' | ||||
| labels: bug | ||||
| assignees: bobloy | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| **Describe the bug** | ||||
| <!--A clear and concise description of what the bug is.--> | ||||
| 
 | ||||
| **To Reproduce** | ||||
| <!--Steps to reproduce the behavior:--> | ||||
| 1. Load cog '...' | ||||
| 2. Run command '....' | ||||
| 3. See error | ||||
| 
 | ||||
| **Expected behavior** | ||||
| <!--A clear and concise description of what you expected to happen.--> | ||||
| 
 | ||||
| **Screenshots or Error Messages** | ||||
| <!--If applicable, add screenshots to help explain your problem.--> | ||||
| 
 | ||||
| **Additional context** | ||||
| <!--Add any other context about the problem here.--> | ||||
							
								
								
									
										14
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										14
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
								
							| @ -1,14 +0,0 @@ | ||||
| --- | ||||
| name: Feature request | ||||
| about: Suggest an idea for this project | ||||
| title: "[Feature Request]" | ||||
| labels: enhancement | ||||
| assignees: '' | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| **Is your feature request related to a problem? Please describe.** | ||||
| <!--A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]--> | ||||
| 
 | ||||
| **Describe the solution you'd like** | ||||
| <!--A clear and concise description of what you want to happen. Include which cog or cogs this would interact with--> | ||||
							
								
								
									
										26
									
								
								.github/ISSUE_TEMPLATE/new-audiotrivia-list.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										26
									
								
								.github/ISSUE_TEMPLATE/new-audiotrivia-list.md
									
									
									
									
										vendored
									
									
								
							| @ -1,26 +0,0 @@ | ||||
| --- | ||||
| name: New AudioTrivia List | ||||
| about: Submit a new AudioTrivia list to be added | ||||
| title: "[AudioTrivia Submission]" | ||||
| labels: 'cog: audiotrivia' | ||||
| assignees: bobloy | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| **What is this trivia list?** | ||||
| <!--What's in the list? What kind of category is?--> | ||||
| 
 | ||||
| **Number of Questions** | ||||
| <!--Rough estimate at the number of question in this list--> | ||||
| 
 | ||||
| **Original Content?** | ||||
| <!--Did you come up with this list yourself or did you get it from some else's work?--> | ||||
| <!--If no, be sure to include the source--> | ||||
| - [ ] Yes | ||||
| - [ ] No | ||||
| 
 | ||||
| 
 | ||||
| **Did I test the list?** | ||||
| <!--Did you already try out the list and find no bugs?--> | ||||
| - [ ] Yes | ||||
| - [ ] No | ||||
							
								
								
									
										62
									
								
								.github/labeler.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										62
									
								
								.github/labeler.yml
									
									
									
									
										vendored
									
									
								
							| @ -1,62 +0,0 @@ | ||||
| 'cog: announcedaily': | ||||
|   - announcedaily/* | ||||
| 'cog: audiotrivia': | ||||
|   - audiotrivia/* | ||||
| 'cog: ccrole': | ||||
|   - ccrole/* | ||||
| 'cog: chatter': | ||||
|   - chatter/* | ||||
| 'cog: conquest': | ||||
|   - conquest/* | ||||
| 'cog: dad': | ||||
|   - dad/* | ||||
| 'cog: exclusiverole': | ||||
|   - exclusiverole/* | ||||
| 'cog: fifo': | ||||
|   - fifo/* | ||||
| 'cog: firstmessage': | ||||
|   - firstmessage/* | ||||
| 'cog: flag': | ||||
|   - flag/* | ||||
| 'cog: forcemention': | ||||
|   - forcemention/* | ||||
| 'cog: hangman': | ||||
|   - hangman | ||||
| 'cog: infochannel': | ||||
|   - infochannel/* | ||||
| 'cog: isitdown': | ||||
|   - isitdown/* | ||||
| 'cog: launchlib': | ||||
|   - launchlib/* | ||||
| 'cog: leaver': | ||||
|   - leaver/* | ||||
| 'cog: lovecalculator': | ||||
|   - lovecalculator/* | ||||
| 'cog: lseen': | ||||
|   - lseen/* | ||||
| 'cog: nudity': | ||||
|   - nudity/* | ||||
| 'cog: planttycoon': | ||||
|   - planttycoon/* | ||||
| 'cog: qrinvite': | ||||
|   - qrinvite/* | ||||
| 'cog: reactrestrict': | ||||
|   - reactrestrict/* | ||||
| 'cog: recyclingplant': | ||||
|   - recyclingplant/* | ||||
| 'cog: rpsls': | ||||
|   - rpsls/* | ||||
| 'cog: sayurl': | ||||
|   - sayurl/* | ||||
| 'cog: scp': | ||||
|   - scp/* | ||||
| 'cog: stealemoji': | ||||
|   - stealemoji/* | ||||
| 'cog: timerole': | ||||
|   - timerole/* | ||||
| 'cog: tts': | ||||
|   - tts/* | ||||
| 'cog: unicode': | ||||
|   - unicode/* | ||||
| 'cog: werewolf': | ||||
|   - werewolf/* | ||||
							
								
								
									
										20
									
								
								.github/workflows/black_check.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										20
									
								
								.github/workflows/black_check.yml
									
									
									
									
										vendored
									
									
								
							| @ -1,20 +0,0 @@ | ||||
| # GitHub Action that uses Black to reformat the Python code in an incoming pull request. | ||||
| # If all Python code in the pull request is compliant with Black then this Action does nothing. | ||||
| # Othewrwise, Black is run and its changes are committed back to the incoming pull request. | ||||
| # https://github.com/cclauss/autoblack | ||||
| 
 | ||||
| name: black | ||||
| on: [pull_request] | ||||
| jobs: | ||||
|   build: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/checkout@v2 | ||||
|       - name: Set up Python 3.8 | ||||
|         uses: actions/setup-python@v2 | ||||
|         with: | ||||
|           python-version: '3.8' | ||||
|       - name: Install Black | ||||
|         run: pip install --upgrade --no-cache-dir black | ||||
|       - name: Run black --check . | ||||
|         run: black --check --diff -l 99 . | ||||
							
								
								
									
										19
									
								
								.github/workflows/labeler.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										19
									
								
								.github/workflows/labeler.yml
									
									
									
									
										vendored
									
									
								
							| @ -1,19 +0,0 @@ | ||||
| # This workflow will triage pull requests and apply a label based on the | ||||
| # paths that are modified in the pull request. | ||||
| # | ||||
| # To use this workflow, you will need to set up a .github/labeler.yml | ||||
| # file with configuration.  For more information, see: | ||||
| # https://github.com/actions/labeler | ||||
| 
 | ||||
| name: Labeler | ||||
| on: [pull_request_target] | ||||
| 
 | ||||
| jobs: | ||||
|   label: | ||||
| 
 | ||||
|     runs-on: ubuntu-latest | ||||
| 
 | ||||
|     steps: | ||||
|     - uses: actions/labeler@2.2.0 | ||||
|       with: | ||||
|         repo-token: "${{ secrets.GITHUB_TOKEN }}" | ||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -4,4 +4,3 @@ venv/ | ||||
| v-data/ | ||||
| database.sqlite3 | ||||
| /venv3.4/ | ||||
| /.venv/ | ||||
|  | ||||
| @ -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 | ||||
| 
 | ||||
|  | ||||
| @ -54,7 +54,8 @@ class AnnounceDaily(Cog): | ||||
| 
 | ||||
|         Do `[p]help annd <subcommand>` for more details | ||||
|         """ | ||||
|         pass | ||||
|         if ctx.invoked_subcommand is None: | ||||
|             pass | ||||
| 
 | ||||
|     @commands.command() | ||||
|     @checks.guildowner() | ||||
|  | ||||
| @ -1,25 +1,21 @@ | ||||
| """Module to manage audio trivia sessions.""" | ||||
| import asyncio | ||||
| import logging | ||||
| 
 | ||||
| import lavalink | ||||
| from redbot.cogs.trivia import TriviaSession | ||||
| from redbot.cogs.trivia.session import _parse_answers | ||||
| from redbot.core.utils.chat_formatting import bold | ||||
| 
 | ||||
| log = logging.getLogger("red.fox_v3.audiotrivia.audiosession") | ||||
| 
 | ||||
| 
 | ||||
| class AudioSession(TriviaSession): | ||||
|     """Class to run a session of audio trivia""" | ||||
| 
 | ||||
|     def __init__(self, ctx, question_list: dict, settings: dict, audio=None): | ||||
|     def __init__(self, ctx, question_list: dict, settings: dict, player: lavalink.Player): | ||||
|         super().__init__(ctx, question_list, settings) | ||||
| 
 | ||||
|         self.audio = audio | ||||
|         self.player = player | ||||
| 
 | ||||
|     @classmethod | ||||
|     def start(cls, ctx, question_list, settings, audio=None): | ||||
|         session = cls(ctx, question_list, settings, audio) | ||||
|     def start(cls, ctx, question_list, settings, player: lavalink.Player = None): | ||||
|         session = cls(ctx, question_list, settings, player) | ||||
|         loop = ctx.bot.loop | ||||
|         session._task = loop.create_task(session.run()) | ||||
|         return session | ||||
| @ -27,95 +23,52 @@ class AudioSession(TriviaSession): | ||||
|     async def run(self): | ||||
|         """Run the audio trivia session. | ||||
| 
 | ||||
|         In order for the trivia session to be stopped correctly, this should | ||||
|         only be called internally by `TriviaSession.start`. | ||||
|         """ | ||||
|                 In order for the trivia session to be stopped correctly, this should | ||||
|                 only be called internally by `TriviaSession.start`. | ||||
|                 """ | ||||
|         await self._send_startup_msg() | ||||
|         max_score = self.settings["max_score"] | ||||
|         delay = self.settings["delay"] | ||||
|         audio_delay = self.settings["audio_delay"] | ||||
|         timeout = self.settings["timeout"] | ||||
|         if self.audio is not None: | ||||
|             import lavalink | ||||
| 
 | ||||
|             player = lavalink.get_player(self.ctx.guild.id) | ||||
|             player.store("channel", self.ctx.channel.id)  # What's this for? I dunno | ||||
|             await self.audio.set_player_settings(self.ctx) | ||||
|         else: | ||||
|             lavalink = None | ||||
|             player = False | ||||
| 
 | ||||
|         for question, answers, audio_url in self._iter_questions(): | ||||
|         for question, answers in self._iter_questions(): | ||||
|             async with self.ctx.typing(): | ||||
|                 await asyncio.sleep(3) | ||||
|             self.count += 1 | ||||
|             msg = bold(f"Question number {self.count}!") + f"\n\n{question}" | ||||
|             if player: | ||||
|                 await player.stop() | ||||
|             if audio_url: | ||||
|                 if not player: | ||||
|                     log.debug("Got an audio question in a non-audio trivia session") | ||||
|                     continue | ||||
|             await self.player.stop() | ||||
| 
 | ||||
|                 load_result = await player.load_tracks(audio_url) | ||||
|                 if ( | ||||
|                     load_result.has_error | ||||
|                     or load_result.load_type != lavalink.enums.LoadType.TRACK_LOADED | ||||
|                 ): | ||||
|                     await self.ctx.maybe_send_embed( | ||||
|                         "Audio Track has an error, skipping. See logs for details" | ||||
|                     ) | ||||
|                     log.info(f"Track has error: {load_result.exception_message}") | ||||
|                     continue | ||||
|                 tracks = load_result.tracks | ||||
|                 track = tracks[0] | ||||
|                 seconds = track.length / 1000 | ||||
|                 track.uri = ""  # Hide the info from `now` | ||||
|                 if self.settings["repeat"] and seconds < audio_delay: | ||||
|                     # Append it until it's longer than the delay | ||||
|                     tot_length = seconds + 0 | ||||
|                     while tot_length < audio_delay: | ||||
|                         player.add(self.ctx.author, track) | ||||
|                         tot_length += seconds | ||||
|                 else: | ||||
|                     player.add(self.ctx.author, track) | ||||
|             msg = "**Question number {}!**\n\nName this audio!".format(self.count) | ||||
|             await self.ctx.send(msg) | ||||
|             # print("Audio question: {}".format(question)) | ||||
| 
 | ||||
|                 if not player.current: | ||||
|                     await player.play() | ||||
|             await self.ctx.maybe_send_embed(msg) | ||||
|             log.debug(f"Audio question: {question}") | ||||
|             # await self.ctx.invoke(self.audio.play(ctx=self.ctx, query=question)) | ||||
|             # ctx_copy = copy(self.ctx) | ||||
| 
 | ||||
|             continue_ = await self.wait_for_answer( | ||||
|                 answers, audio_delay if audio_url else delay, timeout | ||||
|             ) | ||||
|             # await self.ctx.invoke(self.player.play, query=question) | ||||
|             query = question.strip("<>") | ||||
|             tracks = await self.player.get_tracks(query) | ||||
|             seconds = tracks[0].length / 1000 | ||||
| 
 | ||||
|             if self.settings["repeat"] and seconds < delay: | ||||
|                 tot_length = seconds + 0 | ||||
|                 while tot_length < delay: | ||||
|                     self.player.add(self.ctx.author, tracks[0]) | ||||
|                     tot_length += seconds | ||||
|             else: | ||||
|                 self.player.add(self.ctx.author, tracks[0]) | ||||
| 
 | ||||
|             if not self.player.current: | ||||
|                 await self.player.play() | ||||
| 
 | ||||
|             continue_ = await self.wait_for_answer(answers, delay, timeout) | ||||
|             if continue_ is False: | ||||
|                 break | ||||
|             if any(score >= max_score for score in self.scores.values()): | ||||
|                 await self.end_game() | ||||
|                 break | ||||
|         else: | ||||
|             await self.ctx.maybe_send_embed("There are no more questions!") | ||||
|             await self.ctx.send("There are no more questions!") | ||||
|             await self.end_game() | ||||
| 
 | ||||
|     async def end_game(self): | ||||
|         await super().end_game() | ||||
|         if self.audio is not None: | ||||
|             await self.ctx.invoke(self.audio.command_disconnect) | ||||
| 
 | ||||
|     def _iter_questions(self): | ||||
|         """Iterate over questions and answers for this session. | ||||
| 
 | ||||
|         Yields | ||||
|         ------ | ||||
|         `tuple` | ||||
|             A tuple containing the question (`str`) and the answers (`tuple` of | ||||
|             `str`). | ||||
| 
 | ||||
|         """ | ||||
|         for question, q_data in self.question_list: | ||||
|             answers = _parse_answers(q_data["answers"]) | ||||
|             _audio = q_data["audio"] | ||||
|             if _audio: | ||||
|                 yield _audio, answers, question.strip("<>") | ||||
|             else: | ||||
|                 yield question, answers, _audio | ||||
|         await self.player.disconnect() | ||||
|  | ||||
| @ -1,22 +1,21 @@ | ||||
| import datetime | ||||
| import logging | ||||
| import pathlib | ||||
| from typing import List, Optional | ||||
| from typing import List | ||||
| 
 | ||||
| import discord | ||||
| import lavalink | ||||
| import yaml | ||||
| from redbot.cogs.audio import Audio | ||||
| from redbot.cogs.trivia.trivia import InvalidListError, Trivia, get_core_lists | ||||
| from redbot.cogs.trivia import LOG | ||||
| from redbot.cogs.trivia.trivia import InvalidListError, Trivia | ||||
| from redbot.core import Config, checks, commands | ||||
| from redbot.core.bot import Red | ||||
| from redbot.core.data_manager import cog_data_path | ||||
| from redbot.core.utils.chat_formatting import bold, box | ||||
| from redbot.core.utils.chat_formatting import box | ||||
| 
 | ||||
| from .audiosession import AudioSession | ||||
| 
 | ||||
| 
 | ||||
| log = logging.getLogger("red.fox_v3.audiotrivia") | ||||
| # from redbot.cogs.audio.utils import userlimit | ||||
| 
 | ||||
| 
 | ||||
| class AudioTrivia(Trivia): | ||||
| @ -28,11 +27,12 @@ class AudioTrivia(Trivia): | ||||
|     def __init__(self, bot: Red): | ||||
|         super().__init__() | ||||
|         self.bot = bot | ||||
|         self.audio = None | ||||
|         self.audioconf = Config.get_conf( | ||||
|             self, identifier=651171001051118411410511810597, force_registration=True | ||||
|         ) | ||||
| 
 | ||||
|         self.audioconf.register_guild(audio_delay=30.0, repeat=True) | ||||
|         self.audioconf.register_guild(delay=30.0, repeat=True) | ||||
| 
 | ||||
|     @commands.group() | ||||
|     @commands.guild_only() | ||||
| @ -43,112 +43,122 @@ class AudioTrivia(Trivia): | ||||
|         settings_dict = await audioset.all() | ||||
|         msg = box( | ||||
|             "**Audio settings**\n" | ||||
|             "Answer time limit: {audio_delay} seconds\n" | ||||
|             "Answer time limit: {delay} seconds\n" | ||||
|             "Repeat Short Audio: {repeat}" | ||||
|             "".format(**settings_dict), | ||||
|             lang="py", | ||||
|         ) | ||||
|         await ctx.send(msg) | ||||
| 
 | ||||
|     @atriviaset.command(name="timelimit") | ||||
|     async def atriviaset_timelimit(self, ctx: commands.Context, seconds: float): | ||||
|     @atriviaset.command(name="delay") | ||||
|     async def atriviaset_delay(self, ctx: commands.Context, seconds: float): | ||||
|         """Set the maximum seconds permitted to answer a question.""" | ||||
|         if seconds < 4.0: | ||||
|             await ctx.send("Must be at least 4 seconds.") | ||||
|             return | ||||
|         settings = self.audioconf.guild(ctx.guild) | ||||
|         await settings.audo_delay.set(seconds) | ||||
|         await ctx.maybe_send_embed(f"Done. Maximum seconds to answer set to {seconds}.") | ||||
|         await settings.delay.set(seconds) | ||||
|         await ctx.send("Done. Maximum seconds to answer set to {}.".format(seconds)) | ||||
| 
 | ||||
|     @atriviaset.command(name="repeat") | ||||
|     async def atriviaset_repeat(self, ctx: commands.Context, true_or_false: bool): | ||||
|         """Set whether or not short audio will be repeated""" | ||||
|         settings = self.audioconf.guild(ctx.guild) | ||||
|         await settings.repeat.set(true_or_false) | ||||
|         await ctx.maybe_send_embed(f"Done. Repeating short audio is now set to {true_or_false}.") | ||||
|         await ctx.send("Done. Repeating short audio is now set to {}.".format(true_or_false)) | ||||
| 
 | ||||
|     @commands.group(invoke_without_command=True) | ||||
|     @commands.guild_only() | ||||
|     async def audiotrivia(self, ctx: commands.Context, *categories: str): | ||||
|         """Start trivia session on the specified category or categories. | ||||
|         """Start trivia session on the specified category. | ||||
| 
 | ||||
|         Includes Audio categories. | ||||
|         You may list multiple categories, in which case the trivia will involve | ||||
|         questions from all of them. | ||||
|         """ | ||||
|         if not categories and ctx.invoked_subcommand is None: | ||||
|             await ctx.send_help() | ||||
|             return | ||||
| 
 | ||||
|         if self.audio is None: | ||||
|             self.audio: Audio = self.bot.get_cog("Audio") | ||||
| 
 | ||||
|         if self.audio is None: | ||||
|             await ctx.send("Audio is not loaded. Load it and try again") | ||||
|             return | ||||
| 
 | ||||
|         categories = [c.lower() for c in categories] | ||||
|         session = self._get_trivia_session(ctx.channel) | ||||
|         if session is not None: | ||||
|             await ctx.maybe_send_embed( | ||||
|                 "There is already an ongoing trivia session in this channel." | ||||
|             ) | ||||
|             await ctx.send("There is already an ongoing trivia session in this channel.") | ||||
|             return | ||||
|         status = await self.audio.config.status() | ||||
|         notify = await self.audio.config.guild(ctx.guild).notify() | ||||
| 
 | ||||
|         if status: | ||||
|             await ctx.send( | ||||
|                 f"It is recommended to disable audio status with `{ctx.prefix}audioset status`" | ||||
|             ) | ||||
| 
 | ||||
|         if notify: | ||||
|             await ctx.send( | ||||
|                 f"It is recommended to disable audio notify with `{ctx.prefix}audioset notify`" | ||||
|             ) | ||||
| 
 | ||||
|         if not self.audio._player_check(ctx): | ||||
|             try: | ||||
|                 if not ctx.author.voice.channel.permissions_for( | ||||
|                     ctx.me | ||||
|                 ).connect or self.audio.is_vc_full(ctx.author.voice.channel): | ||||
|                     return await ctx.send("I don't have permission to connect to your channel.") | ||||
|                 await lavalink.connect(ctx.author.voice.channel) | ||||
|                 lavaplayer = lavalink.get_player(ctx.guild.id) | ||||
|                 lavaplayer.store("connect", datetime.datetime.utcnow()) | ||||
|             except AttributeError: | ||||
|                 return await ctx.send("Connect to a voice channel first.") | ||||
| 
 | ||||
|         lavaplayer = lavalink.get_player(ctx.guild.id) | ||||
|         lavaplayer.store("channel", ctx.channel.id)  # What's this for? I dunno | ||||
| 
 | ||||
|         await self.audio.set_player_settings(ctx) | ||||
| 
 | ||||
|         if not ctx.author.voice or ctx.author.voice.channel != lavaplayer.channel: | ||||
|             return await ctx.send( | ||||
|                 "You must be in the voice channel to use the audiotrivia command." | ||||
|             ) | ||||
| 
 | ||||
|         trivia_dict = {} | ||||
|         authors = [] | ||||
|         any_audio = False | ||||
|         for category in reversed(categories): | ||||
|             # We reverse the categories so that the first list's config takes | ||||
|             # priority over the others. | ||||
|             try: | ||||
|                 dict_ = self.get_audio_list(category) | ||||
|             except FileNotFoundError: | ||||
|                 await ctx.maybe_send_embed( | ||||
|                     f"Invalid category `{category}`. See `{ctx.prefix}audiotrivia list`" | ||||
|                 await ctx.send( | ||||
|                     "Invalid category `{0}`. See `{1}audiotrivia list`" | ||||
|                     " for a list of trivia categories." | ||||
|                     "".format(category, ctx.prefix) | ||||
|                 ) | ||||
|             except InvalidListError: | ||||
|                 await ctx.maybe_send_embed( | ||||
|                 await ctx.send( | ||||
|                     "There was an error parsing the trivia list for" | ||||
|                     f" the `{category}` category. It may be formatted" | ||||
|                     " incorrectly." | ||||
|                     " the `{}` category. It may be formatted" | ||||
|                     " incorrectly.".format(category) | ||||
|                 ) | ||||
|             else: | ||||
|                 is_audio = dict_.pop("AUDIO", False) | ||||
|                 authors.append(dict_.pop("AUTHOR", None)) | ||||
|                 trivia_dict.update( | ||||
|                     {_q: {"audio": is_audio, "answers": _a} for _q, _a in dict_.items()} | ||||
|                 ) | ||||
|                 any_audio = any_audio or is_audio | ||||
|                 trivia_dict.update(dict_) | ||||
|                 authors.append(trivia_dict.pop("AUTHOR", None)) | ||||
|                 continue | ||||
|             return | ||||
|         if not trivia_dict: | ||||
|             await ctx.maybe_send_embed( | ||||
|             await ctx.send( | ||||
|                 "The trivia list was parsed successfully, however it appears to be empty!" | ||||
|             ) | ||||
|             return | ||||
| 
 | ||||
|         if not any_audio: | ||||
|             audio = None | ||||
|         else: | ||||
|             audio: Optional["Audio"] = self.bot.get_cog("Audio") | ||||
|             if audio is None: | ||||
|                 await ctx.send("Audio lists were parsed but Audio is not loaded!") | ||||
|                 return | ||||
|             status = await audio.config.status() | ||||
|             notify = await audio.config.guild(ctx.guild).notify() | ||||
| 
 | ||||
|             if status: | ||||
|                 await ctx.maybe_send_embed( | ||||
|                     f"It is recommended to disable audio status with `{ctx.prefix}audioset status`" | ||||
|                 ) | ||||
| 
 | ||||
|             if notify: | ||||
|                 await ctx.maybe_send_embed( | ||||
|                     f"It is recommended to disable audio notify with `{ctx.prefix}audioset notify`" | ||||
|                 ) | ||||
| 
 | ||||
|             failed = await ctx.invoke(audio.command_summon) | ||||
|             if failed: | ||||
|                 return | ||||
|             lavaplayer = lavalink.get_player(ctx.guild.id) | ||||
|             lavaplayer.store("channel", ctx.channel.id)  # What's this for? I dunno | ||||
| 
 | ||||
|         settings = await self.config.guild(ctx.guild).all() | ||||
|         audiosettings = await self.audioconf.guild(ctx.guild).all() | ||||
|         config = trivia_dict.pop("CONFIG", {"answer": None})["answer"] | ||||
|         config = trivia_dict.pop("CONFIG", None) | ||||
|         if config and settings["allow_override"]: | ||||
|             settings.update(config) | ||||
|         settings["lists"] = dict(zip(categories, reversed(authors))) | ||||
| @ -156,33 +166,22 @@ class AudioTrivia(Trivia): | ||||
|         # Delay in audiosettings overwrites delay in settings | ||||
|         combined_settings = {**settings, **audiosettings} | ||||
|         session = AudioSession.start( | ||||
|             ctx, | ||||
|             trivia_dict, | ||||
|             combined_settings, | ||||
|             audio, | ||||
|             ctx=ctx, question_list=trivia_dict, settings=combined_settings, player=lavaplayer, | ||||
|         ) | ||||
|         self.trivia_sessions.append(session) | ||||
|         log.debug("New audio trivia session; #%s in %d", ctx.channel, ctx.guild.id) | ||||
|         LOG.debug("New audio trivia session; #%s in %d", ctx.channel, ctx.guild.id) | ||||
| 
 | ||||
|     @audiotrivia.command(name="list") | ||||
|     @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()} | ||||
|         if await ctx.embed_requested(): | ||||
|             await ctx.send( | ||||
|                 embed=discord.Embed( | ||||
|                     title="Available trivia lists", | ||||
|                     colour=await ctx.embed_colour(), | ||||
|                     description=", ".join(sorted(lists)), | ||||
|                 ) | ||||
|             ) | ||||
|         else: | ||||
|             msg = box(bold("Available trivia lists") + "\n\n" + ", ".join(sorted(lists))) | ||||
|             if len(msg) > 1000: | ||||
|                 await ctx.author.send(msg) | ||||
|             else: | ||||
|                 await ctx.send(msg) | ||||
|         """List available trivia categories.""" | ||||
|         lists = set(p.stem for p in self._audio_lists()) | ||||
| 
 | ||||
|         msg = box("**Available trivia lists**\n\n{}".format(", ".join(sorted(lists)))) | ||||
|         if len(msg) > 1000: | ||||
|             await ctx.author.send(msg) | ||||
|             return | ||||
|         await ctx.send(msg) | ||||
| 
 | ||||
|     def get_audio_list(self, category: str) -> dict: | ||||
|         """Get the audiotrivia list corresponding to the given category. | ||||
| @ -199,7 +198,7 @@ class AudioTrivia(Trivia): | ||||
| 
 | ||||
|         """ | ||||
|         try: | ||||
|             path = next(p for p in self._all_audio_lists() if p.stem == category) | ||||
|             path = next(p for p in self._audio_lists() if p.stem == category) | ||||
|         except StopIteration: | ||||
|             raise FileNotFoundError("Could not find the `{}` category.".format(category)) | ||||
| 
 | ||||
| @ -211,15 +210,13 @@ class AudioTrivia(Trivia): | ||||
|             else: | ||||
|                 return dict_ | ||||
| 
 | ||||
|     def _all_audio_lists(self) -> List[pathlib.Path]: | ||||
|         # Custom trivia lists uploaded with audiotrivia. Not necessarily audio lists | ||||
|     def _audio_lists(self) -> List[pathlib.Path]: | ||||
|         personal_lists = [p.resolve() for p in cog_data_path(self).glob("*.yaml")] | ||||
| 
 | ||||
|         # Add to that custom lists uploaded with trivia and core lists | ||||
|         return personal_lists + get_core_audio_lists() + self._all_lists() | ||||
|         return personal_lists + get_core_lists() | ||||
| 
 | ||||
| 
 | ||||
| def get_core_audio_lists() -> List[pathlib.Path]: | ||||
| def get_core_lists() -> List[pathlib.Path]: | ||||
|     """Return a list of paths for all trivia lists packaged with the bot.""" | ||||
|     core_lists_path = pathlib.Path(__file__).parent.resolve() / "data/lists" | ||||
|     return list(core_lists_path.glob("*.yaml")) | ||||
|  | ||||
| @ -1,5 +1,4 @@ | ||||
| AUTHOR: Plab | ||||
| AUDIO: "[Audio] Identify this Anime!" | ||||
| https://www.youtube.com/watch?v=2uq34TeWEdQ: | ||||
| - 'Hagane no Renkinjutsushi (2009)' | ||||
| - '(2009) الخيميائي المعدني الكامل' | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -1,14 +1,13 @@ | ||||
| AUTHOR: Plab | ||||
| NEEDS: New links for all songs. | ||||
| https://www.youtube.com/watch?v=f9O2Rjn1azc: | ||||
| https://www.youtube.com/watch?v=--bWm9hhoZo: | ||||
| - Transistor | ||||
| https://www.youtube.com/watch?v=PgUhYFkVdSY: | ||||
| https://www.youtube.com/watch?v=-4nCbgayZNE: | ||||
| - Dark Cloud 2 | ||||
| - Dark Cloud II | ||||
| https://www.youtube.com/watch?v=1T1RZttyMwU: | ||||
| https://www.youtube.com/watch?v=-64NlME4lJU: | ||||
| - Mega Man 7 | ||||
| - Mega Man VII | ||||
| https://www.youtube.com/watch?v=AdDbbzuq1vY: | ||||
| https://www.youtube.com/watch?v=-AesqnudNuw: | ||||
| - Mega Man 9 | ||||
| - Mega Man IX | ||||
| https://www.youtube.com/watch?v=-BmGDtP2t7M: | ||||
| @ -1,5 +1,4 @@ | ||||
| AUTHOR: Lazar | ||||
| AUDIO: "[Audio] Identify this NHL Team by their goal horn" | ||||
| https://youtu.be/6OejNXrGkK0: | ||||
| - Anaheim Ducks | ||||
| - Anaheim | ||||
| @ -3,49 +3,24 @@ 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 | ||||
| from redbot.core.utils.chat_formatting import box, pagify | ||||
| from redbot.core.utils.mod import get_audit_reason | ||||
| 
 | ||||
| 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 +47,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 +84,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 +106,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 +124,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" | ||||
|         ) | ||||
| 
 | ||||
| @ -166,9 +142,8 @@ class CCRole(commands.Cog): | ||||
|                 return | ||||
| 
 | ||||
|         # Selfrole | ||||
|         await ctx.send( | ||||
|             "Is this a targeted command?(yes/no)\n" "No will make this a selfrole command" | ||||
|         ) | ||||
|         await ctx.send("Is this a targeted command?(yes/no)\n" | ||||
|                        "No will make this a selfrole command") | ||||
| 
 | ||||
|         try: | ||||
|             answer = await self.bot.wait_for("message", timeout=120, check=check) | ||||
| @ -251,7 +226,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 +250,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 +290,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 +323,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 | ||||
| 
 | ||||
| @ -381,14 +358,12 @@ class CCRole(commands.Cog): | ||||
|         else: | ||||
|             target = message.author | ||||
| 
 | ||||
|         reason = get_audit_reason(message.author) | ||||
| 
 | ||||
|         if cmd["aroles"]: | ||||
|             arole_list = [ | ||||
|                 discord.utils.get(message.guild.roles, id=roleid) for roleid in cmd["aroles"] | ||||
|             ] | ||||
|             try: | ||||
|                 await target.add_roles(*arole_list, reason=reason) | ||||
|                 await target.add_roles(*arole_list) | ||||
|             except discord.Forbidden: | ||||
|                 log.exception(f"Permission error: Unable to add roles") | ||||
|                 await ctx.send("Permission error: Unable to add roles") | ||||
| @ -398,7 +373,7 @@ class CCRole(commands.Cog): | ||||
|                 discord.utils.get(message.guild.roles, id=roleid) for roleid in cmd["rroles"] | ||||
|             ] | ||||
|             try: | ||||
|                 await target.remove_roles(*rrole_list, reason=reason) | ||||
|                 await target.remove_roles(*rrole_list) | ||||
|             except discord.Forbidden: | ||||
|                 log.exception(f"Permission error: Unable to remove roles") | ||||
|                 await ctx.send("Permission error: Unable to remove roles") | ||||
|  | ||||
| @ -59,50 +59,62 @@ 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. | ||||
| 
 | ||||
| #### Step 1: Add repo and install cog | ||||
| You need to get a copy of the requirements.txt provided with chatter, I recommend this method. | ||||
| 
 | ||||
| ``` | ||||
| [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 | ||||
| ``` | ||||
| 
 | ||||
| If you get an error at this step, stop and skip to one of the manual methods below. | ||||
| ### Linux - Manually | ||||
| 
 | ||||
| #### 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: | ||||
| #### Step 1: Built-in Downloader | ||||
| 
 | ||||
| ``` | ||||
| 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) | ||||
| [p]cog install <Fox> Chatter | ||||
| ``` | ||||
| 
 | ||||
| #### Step 3: Load the cog and get started | ||||
| #### Step 2: Install Requirements | ||||
| 
 | ||||
| In your console with your virtual environment activated: | ||||
| 
 | ||||
| ``` | ||||
| pip install --no-deps "chatterbot>=1.1" | ||||
| ``` | ||||
| 
 | ||||
| ### 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__ = ( | ||||
|  | ||||
							
								
								
									
										398
									
								
								chatter/chat.py
									
									
									
									
									
								
							
							
						
						
									
										398
									
								
								chatter/chat.py
									
									
									
									
									
								
							| @ -2,24 +2,19 @@ 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 | ||||
| from chatterbot.comparisons import JaccardSimilarity, LevenshteinDistance, SpacySimilarity | ||||
| from chatterbot.response_selection import get_random_response | ||||
| from chatterbot.trainers import ChatterBotCorpusTrainer, ListTrainer, UbuntuCorpusTrainer | ||||
| from redbot.core import Config, checks, commands | ||||
| from redbot.core import Config, commands | ||||
| 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,9 @@ 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() | ||||
|     @chatter.command(name="channel") | ||||
|     async def chatter_channel( | ||||
|         self, ctx: commands.Context, channel: Optional[discord.TextChannel] = None | ||||
| @ -269,55 +210,12 @@ 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() | ||||
|     @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 +242,20 @@ class Chatter(Cog): | ||||
| 
 | ||||
|         await ctx.tick() | ||||
| 
 | ||||
|     @commands.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 +267,31 @@ 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() | ||||
|     @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 +304,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 +312,7 @@ 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") | ||||
|     @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 +323,11 @@ 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") | ||||
|     @chatter.command(name="age") | ||||
|     async def age(self, ctx: commands.Context, days: int): | ||||
|         """ | ||||
|         Sets the number of days to look back | ||||
| @ -452,16 +341,6 @@ 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() | ||||
|     @chatter.command(name="backup") | ||||
|     async def backup(self, ctx, backupname): | ||||
|         """ | ||||
| @ -483,71 +362,7 @@ 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") | ||||
|     @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 +370,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 +379,11 @@ 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") | ||||
|     @chatter.command(name="trainenglish") | ||||
|     async def chatter_train_english(self, ctx: commands.Context): | ||||
|         """ | ||||
|         Trains the bot in english | ||||
| @ -581,32 +396,11 @@ 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'] | ||||
|     @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 +409,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") | ||||
| @ -640,7 +434,7 @@ class Chatter(Cog): | ||||
|         else: | ||||
|             await ctx.maybe_send_embed("Error occurred :(") | ||||
| 
 | ||||
|     @Cog.listener() | ||||
|     @commands.Cog.listener() | ||||
|     async def on_message_without_command(self, message: discord.Message): | ||||
|         """ | ||||
|         Credit to https://github.com/Twentysix26/26-Cogs/blob/master/cleverbot/cleverbot.py | ||||
| @ -657,7 +451,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 +463,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 +478,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): | ||||
| @ -166,12 +159,7 @@ class Conquest(commands.Cog): | ||||
|             self.data_path / self.current_map / f"current.{self.ext}", x, y, zoom | ||||
|         ) | ||||
| 
 | ||||
|         await ctx.send( | ||||
|             file=discord.File( | ||||
|                 fp=zoomed_path, | ||||
|                 filename=f"current_zoomed.{self.ext}", | ||||
|             ) | ||||
|         ) | ||||
|         await ctx.send(file=discord.File(fp=zoomed_path, filename=f"current_zoomed.{self.ext}",)) | ||||
| 
 | ||||
|     async def _create_zoomed_map(self, map_path, x, y, zoom, **kwargs): | ||||
|         current_map = Image.open(map_path) | ||||
|  | ||||
| @ -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)) | ||||
|  | ||||
| @ -85,8 +85,6 @@ class Dad(Cog): | ||||
| 
 | ||||
|     @commands.Cog.listener() | ||||
|     async def on_message_without_command(self, message: discord.Message): | ||||
|         if message.author.bot: | ||||
|             return | ||||
|         guild: discord.Guild = getattr(message, "guild", None) | ||||
|         if guild is None: | ||||
|             return | ||||
|  | ||||
| @ -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} | ||||
| @ -1,10 +1,9 @@ | ||||
| from datetime import datetime, tzinfo | ||||
| from datetime import datetime | ||||
| from typing import TYPE_CHECKING | ||||
| 
 | ||||
| from apscheduler.triggers.cron import CronTrigger | ||||
| from dateutil import parser | ||||
| from discord.ext.commands import BadArgument, Converter | ||||
| from pytz import timezone | ||||
| 
 | ||||
| from fifo.timezones import assemble_timezones | ||||
| 
 | ||||
| @ -13,18 +12,6 @@ if TYPE_CHECKING: | ||||
|     CronConverter = str | ||||
| else: | ||||
| 
 | ||||
|     class TimezoneConverter(Converter): | ||||
|         async def convert(self, ctx, argument) -> tzinfo: | ||||
|             tzinfos = assemble_timezones() | ||||
|             if argument.upper() in tzinfos: | ||||
|                 return tzinfos[argument.upper()] | ||||
| 
 | ||||
|             timez = timezone(argument) | ||||
| 
 | ||||
|             if timez is not None: | ||||
|                 return timez | ||||
|             raise BadArgument() | ||||
| 
 | ||||
|     class DatetimeConverter(Converter): | ||||
|         async def convert(self, ctx, argument) -> datetime: | ||||
|             dt = parser.parse(argument, fuzzy=True, tzinfos=assemble_timezones()) | ||||
|  | ||||
							
								
								
									
										256
									
								
								fifo/fifo.py
									
									
									
									
									
								
							
							
						
						
									
										256
									
								
								fifo/fifo.py
									
									
									
									
									
								
							| @ -1,10 +1,8 @@ | ||||
| import itertools | ||||
| import logging | ||||
| from datetime import MAXYEAR, datetime, timedelta, tzinfo | ||||
| from datetime import datetime, timedelta | ||||
| 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,9 +10,8 @@ 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 .datetime_cron_converters import CronConverter, DatetimeConverter, TimezoneConverter | ||||
| from .datetime_cron_converters import CronConverter, DatetimeConverter | ||||
| from .task import Task | ||||
| 
 | ||||
| schedule_log = logging.getLogger("red.fox_v3.fifo.scheduler") | ||||
| @ -23,12 +20,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 +36,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,11 +54,9 @@ 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 | ||||
| 
 | ||||
|     async def red_delete_data_for_user(self, **kwargs): | ||||
|         """Nothing to delete""" | ||||
|         return | ||||
| @ -105,22 +68,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,71 +101,35 @@ 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 | ||||
| 
 | ||||
|     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 not self.tz_cog: | ||||
|             return None | ||||
|         try: | ||||
|             usertime = await self.tz_cog.config.user(user).usertime() | ||||
|         except AttributeError: | ||||
|             return None | ||||
| 
 | ||||
|         if usertime: | ||||
|             return await TimezoneConverter().convert(None, usertime) | ||||
|         else: | ||||
|             return None | ||||
|         return self.scheduler.remove_job(job_id=_assemble_job_id(task.name, task.guild_id)) | ||||
| 
 | ||||
|     @checks.is_owner() | ||||
|     @commands.guild_only() | ||||
| @ -217,7 +139,7 @@ class FIFO(commands.Cog): | ||||
|         self.scheduler.remove_all_jobs() | ||||
|         await self.config.guild(ctx.guild).tasks.clear() | ||||
|         await self.config.jobs.clear() | ||||
|         # await self.config.jobs_index.clear() | ||||
|         await self.config.jobs_index.clear() | ||||
|         await ctx.tick() | ||||
| 
 | ||||
|     @checks.is_owner()  # Will be reduced when I figure out permissions later | ||||
| @ -227,42 +149,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 +279,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,44 +298,18 @@ 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: | ||||
|                     for page in pagify(out): | ||||
|                         await ctx.maybe_send_embed(page) | ||||
|                 else: | ||||
|                     await ctx.maybe_send_embed(out) | ||||
|                 await ctx.maybe_send_embed(out) | ||||
|             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 +369,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 +376,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 +388,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( | ||||
| @ -548,41 +406,7 @@ class FIFO(commands.Cog): | ||||
|         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 interval of {interval_str} to its scheduled runtimes\n\n" | ||||
|             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"Task `{task_name}` added interval of {interval_str} to its scheduled runtimes\n" | ||||
|             f"Next run time: {job.next_run_time} ({delta_from_now.total_seconds()} seconds)" | ||||
|         ) | ||||
| 
 | ||||
| @ -594,7 +418,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: | ||||
| @ -603,9 +427,7 @@ class FIFO(commands.Cog): | ||||
|             ) | ||||
|             return | ||||
| 
 | ||||
|         maybe_tz = await self._get_tz(ctx.author) | ||||
| 
 | ||||
|         result = await task.add_trigger("date", datetime_str, maybe_tz) | ||||
|         result = await task.add_trigger("date", datetime_str) | ||||
|         if not result: | ||||
|             await ctx.maybe_send_embed( | ||||
|                 "Failed to add a date trigger to this task, see console for logs" | ||||
| @ -622,19 +444,14 @@ class FIFO(commands.Cog): | ||||
| 
 | ||||
|     @fifo_trigger.command(name="cron") | ||||
|     async def fifo_trigger_cron( | ||||
|         self, | ||||
|         ctx: commands.Context, | ||||
|         task_name: str, | ||||
|         optional_tz: Optional[TimezoneConverter] = None, | ||||
|         *, | ||||
|         cron_str: CronConverter, | ||||
|         self, ctx: commands.Context, task_name: str, *, cron_str: CronConverter | ||||
|     ): | ||||
|         """ | ||||
|         Add a cron "time of day" trigger to the specified task | ||||
| 
 | ||||
|         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: | ||||
| @ -643,10 +460,7 @@ class FIFO(commands.Cog): | ||||
|             ) | ||||
|             return | ||||
| 
 | ||||
|         if optional_tz is None: | ||||
|             optional_tz = await self._get_tz(ctx.author)  # might still be None | ||||
| 
 | ||||
|         result = await task.add_trigger("cron", cron_str, optional_tz) | ||||
|         result = await task.add_trigger("cron", cron_str) | ||||
|         if not result: | ||||
|             await ctx.maybe_send_embed( | ||||
|                 "Failed to add a cron trigger to this task, see console for logs" | ||||
|  | ||||
| @ -3,14 +3,13 @@ | ||||
|     "Bobloy" | ||||
|   ], | ||||
|   "min_bot_version": "3.4.0", | ||||
|   "description": "[BETA] Schedule commands to be run at certain times or intervals", | ||||
|   "description": "[ALPHA] Schedule commands to be run at certain times or intervals", | ||||
|   "hidden": false, | ||||
|   "install_msg": "Thank you for installing FIFO.\nGet started with `[p]load fifo`, then `[p]help FIFO`", | ||||
|   "short": "[BETA] Schedule commands to be run at certain times or intervals", | ||||
|   "short": "[ALPHA] Schedule commands to be run at certain times or intervals", | ||||
|   "end_user_data_statement": "This cog does not store any End User Data", | ||||
|   "requirements": [ | ||||
|     "apscheduler", | ||||
|     "pytz", | ||||
|     "python-dateutil" | ||||
|   ], | ||||
|   "tags": [ | ||||
| @ -25,7 +24,6 @@ | ||||
|     "date", | ||||
|     "datetime", | ||||
|     "time", | ||||
|     "calendar", | ||||
|     "timezone" | ||||
|     "calendar" | ||||
|   ] | ||||
| } | ||||
| @ -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() | ||||
|  | ||||
							
								
								
									
										296
									
								
								fifo/task.py
									
									
									
									
									
								
							
							
						
						
									
										296
									
								
								fifo/task.py
									
									
									
									
									
								
							| @ -1,19 +1,17 @@ | ||||
| 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 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,135 +25,28 @@ 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"]) | ||||
| 
 | ||||
|     if data["type"] == "cron": | ||||
|         return CronTrigger.from_crontab(data["time_data"], timezone=data["tzinfo"]) | ||||
|         return CronTrigger.from_crontab(data["time_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,12 +65,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, | ||||
|         "tzinfo": None, | ||||
|         "time_data": None,  # Used for Interval and Date Triggers | ||||
|     } | ||||
| 
 | ||||
|     def __init__( | ||||
| @ -195,10 +85,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,59 +99,47 @@ 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()}) | ||||
|                 # 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": | ||||
|                 if t["tzinfo"] is None: | ||||
|                     triggers.append(t)  # already a string, nothing to do | ||||
|                 else: | ||||
|                     triggers.append( | ||||
|                         { | ||||
|                             "type": t["type"], | ||||
|                             "time_data": t["time_data"], | ||||
|                             "tzinfo": getattr(t["tzinfo"], "zone", None), | ||||
|                         } | ||||
|                     ) | ||||
|                 continue | ||||
|                 triggers.append(t)  # already a string, nothing to do | ||||
| 
 | ||||
|                 continue | ||||
|             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): | ||||
|             return | ||||
| 
 | ||||
|         for t in self.data["triggers"]: | ||||
|             # Backwards compatibility | ||||
|             if "tzinfo" not in t: | ||||
|                 t["tzinfo"] = None | ||||
| 
 | ||||
|             # First decode timezone if there is one | ||||
|             if t["tzinfo"] is not None: | ||||
|                 t["tzinfo"] = pytz.timezone(t["tzinfo"]) | ||||
| 
 | ||||
|         for n, t in enumerate(self.data["triggers"]): | ||||
|             if t["type"] == "interval":  # Convert into timedelta | ||||
|                 t["time_data"] = timedelta(**t["time_data"]) | ||||
|                 self.data["triggers"][n]["time_data"] = timedelta(**t["time_data"]) | ||||
|                 continue | ||||
| 
 | ||||
|             if t["type"] == "date":  # Convert into datetime | ||||
|                 # self.data["triggers"][n]["time_data"] = datetime(**t["time_data"]) | ||||
|                 t["time_data"] = datetime.fromisoformat(t["time_data"]) | ||||
|                 self.data["triggers"][n]["time_data"] = datetime.fromisoformat(t["time_data"]) | ||||
|                 continue | ||||
| 
 | ||||
|             if t["type"] == "cron": | ||||
|                 continue  # already a string | ||||
| 
 | ||||
|             raise NotImplemented | ||||
| 
 | ||||
|     # async def load_from_data(self, data: Dict): | ||||
| @ -277,7 +154,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 +162,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 +189,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 +205,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 +213,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 | ||||
| 
 | ||||
| @ -462,16 +300,8 @@ class Task: | ||||
|         self.data["command_str"] = command_str | ||||
|         return True | ||||
| 
 | ||||
|     async def add_trigger( | ||||
|         self, param, parsed_time: Union[timedelta, datetime, str], timezone=None | ||||
|     ): | ||||
|         # TODO: Save timezone separately for cron and date triggers | ||||
|         trigger_data = self.default_trigger.copy() | ||||
|         trigger_data["type"] = param | ||||
|         trigger_data["time_data"] = parsed_time | ||||
|         if timezone is not None: | ||||
|             trigger_data["tzinfo"] = timezone | ||||
| 
 | ||||
|     async def add_trigger(self, param, parsed_time: Union[timedelta, datetime, str]): | ||||
|         trigger_data = {"type": param, "time_data": parsed_time} | ||||
|         if not get_trigger(trigger_data): | ||||
|             return False | ||||
| 
 | ||||
|  | ||||
| @ -4,10 +4,7 @@ Timezone information for the dateutil parser | ||||
| All credit to https://github.com/prefrontal/dateutil-parser-timezones | ||||
| """ | ||||
| 
 | ||||
| # from dateutil.tz import gettz | ||||
| from datetime import datetime | ||||
| 
 | ||||
| from pytz import timezone | ||||
| from dateutil.tz import gettz | ||||
| 
 | ||||
| 
 | ||||
| def assemble_timezones(): | ||||
| @ -17,218 +14,182 @@ def assemble_timezones(): | ||||
|     """ | ||||
|     timezones = {} | ||||
| 
 | ||||
|     timezones["ACDT"] = timezone( | ||||
|         "Australia/Darwin" | ||||
|     )  # Australian Central Daylight Savings Time (UTC+10:30) | ||||
|     timezones["ACST"] = timezone( | ||||
|         "Australia/Darwin" | ||||
|     )  # Australian Central Standard Time (UTC+09:30) | ||||
|     timezones["ACT"] = timezone("Brazil/Acre")  # Acre Time (UTC−05) | ||||
|     timezones["ADT"] = timezone("America/Halifax")  # Atlantic Daylight Time (UTC−03) | ||||
|     timezones["AEDT"] = timezone( | ||||
|         "Australia/Sydney" | ||||
|     )  # Australian Eastern Daylight Savings Time (UTC+11) | ||||
|     timezones["AEST"] = timezone("Australia/Sydney")  # Australian Eastern Standard Time (UTC+10) | ||||
|     timezones["AFT"] = timezone("Asia/Kabul")  # Afghanistan Time (UTC+04:30) | ||||
|     timezones["AKDT"] = timezone("America/Juneau")  # Alaska Daylight Time (UTC−08) | ||||
|     timezones["AKST"] = timezone("America/Juneau")  # Alaska Standard Time (UTC−09) | ||||
|     timezones["AMST"] = timezone("America/Manaus")  # Amazon Summer Time (Brazil)[1] (UTC−03) | ||||
|     timezones["AMT"] = timezone("America/Manaus")  # Amazon Time (Brazil)[2] (UTC−04) | ||||
|     timezones["ART"] = timezone("America/Cordoba")  # Argentina Time (UTC−03) | ||||
|     timezones["AST"] = timezone("Asia/Riyadh")  # Arabia Standard Time (UTC+03) | ||||
|     timezones["AWST"] = timezone("Australia/Perth")  # Australian Western Standard Time (UTC+08) | ||||
|     timezones["AZOST"] = timezone("Atlantic/Azores")  # Azores Summer Time (UTC±00) | ||||
|     timezones["AZOT"] = timezone("Atlantic/Azores")  # Azores Standard Time (UTC−01) | ||||
|     timezones["AZT"] = timezone("Asia/Baku")  # Azerbaijan Time (UTC+04) | ||||
|     timezones["BDT"] = timezone("Asia/Brunei")  # Brunei Time (UTC+08) | ||||
|     timezones["BIOT"] = timezone("Etc/GMT+6")  # British Indian Ocean Time (UTC+06) | ||||
|     timezones["BIT"] = timezone("Pacific/Funafuti")  # Baker Island Time (UTC−12) | ||||
|     timezones["BOT"] = timezone("America/La_Paz")  # Bolivia Time (UTC−04) | ||||
|     timezones["BRST"] = timezone("America/Sao_Paulo")  # Brasilia Summer Time (UTC−02) | ||||
|     timezones["BRT"] = timezone("America/Sao_Paulo")  # Brasilia Time (UTC−03) | ||||
|     timezones["BST"] = timezone("Asia/Dhaka")  # Bangladesh Standard Time (UTC+06) | ||||
|     timezones["BTT"] = timezone("Asia/Thimphu")  # Bhutan Time (UTC+06) | ||||
|     timezones["CAT"] = timezone("Africa/Harare")  # Central Africa Time (UTC+02) | ||||
|     timezones["CCT"] = timezone("Indian/Cocos")  # Cocos Islands Time (UTC+06:30) | ||||
|     timezones["CDT"] = timezone( | ||||
|         "America/Chicago" | ||||
|     )  # Central Daylight Time (North America) (UTC−05) | ||||
|     timezones["CEST"] = timezone( | ||||
|         "Europe/Berlin" | ||||
|     )  # Central European Summer Time (Cf. HAEC) (UTC+02) | ||||
|     timezones["CET"] = timezone("Europe/Berlin")  # Central European Time (UTC+01) | ||||
|     timezones["CHADT"] = timezone("Pacific/Chatham")  # Chatham Daylight Time (UTC+13:45) | ||||
|     timezones["CHAST"] = timezone("Pacific/Chatham")  # Chatham Standard Time (UTC+12:45) | ||||
|     timezones["CHOST"] = timezone("Asia/Choibalsan")  # Choibalsan Summer Time (UTC+09) | ||||
|     timezones["CHOT"] = timezone("Asia/Choibalsan")  # Choibalsan Standard Time (UTC+08) | ||||
|     timezones["CHST"] = timezone("Pacific/Guam")  # Chamorro Standard Time (UTC+10) | ||||
|     timezones["CHUT"] = timezone("Pacific/Chuuk")  # Chuuk Time (UTC+10) | ||||
|     timezones["CIST"] = timezone("Etc/GMT-8")  # Clipperton Island Standard Time (UTC−08) | ||||
|     timezones["CIT"] = timezone("Asia/Makassar")  # Central Indonesia Time (UTC+08) | ||||
|     timezones["CKT"] = timezone("Pacific/Rarotonga")  # Cook Island Time (UTC−10) | ||||
|     timezones["CLST"] = timezone("America/Santiago")  # Chile Summer Time (UTC−03) | ||||
|     timezones["CLT"] = timezone("America/Santiago")  # Chile Standard Time (UTC−04) | ||||
|     timezones["COST"] = timezone("America/Bogota")  # Colombia Summer Time (UTC−04) | ||||
|     timezones["COT"] = timezone("America/Bogota")  # Colombia Time (UTC−05) | ||||
|     timezones["CST"] = timezone( | ||||
|         "America/Chicago" | ||||
|     )  # Central Standard Time (North America) (UTC−06) | ||||
|     timezones["CT"] = timezone("Asia/Chongqing")  # China time (UTC+08) | ||||
|     timezones["CVT"] = timezone("Atlantic/Cape_Verde")  # Cape Verde Time (UTC−01) | ||||
|     timezones["CXT"] = timezone("Indian/Christmas")  # Christmas Island Time (UTC+07) | ||||
|     timezones["DAVT"] = timezone("Antarctica/Davis")  # Davis Time (UTC+07) | ||||
|     timezones["DDUT"] = timezone("Antarctica/DumontDUrville")  # Dumont d'Urville Time (UTC+10) | ||||
|     timezones["DFT"] = timezone( | ||||
|         "Europe/Berlin" | ||||
|     )  # AIX equivalent of Central European Time (UTC+01) | ||||
|     timezones["EASST"] = timezone("Chile/EasterIsland")  # Easter Island Summer Time (UTC−05) | ||||
|     timezones["EAST"] = timezone("Chile/EasterIsland")  # Easter Island Standard Time (UTC−06) | ||||
|     timezones["EAT"] = timezone("Africa/Mogadishu")  # East Africa Time (UTC+03) | ||||
|     timezones["ECT"] = timezone("America/Guayaquil")  # Ecuador Time (UTC−05) | ||||
|     timezones["EDT"] = timezone( | ||||
|         "America/New_York" | ||||
|     )  # Eastern Daylight Time (North America) (UTC−04) | ||||
|     timezones["EEST"] = timezone("Europe/Bucharest")  # Eastern European Summer Time (UTC+03) | ||||
|     timezones["EET"] = timezone("Europe/Bucharest")  # Eastern European Time (UTC+02) | ||||
|     timezones["EGST"] = timezone("America/Scoresbysund")  # Eastern Greenland Summer Time (UTC±00) | ||||
|     timezones["EGT"] = timezone("America/Scoresbysund")  # Eastern Greenland Time (UTC−01) | ||||
|     timezones["EIT"] = timezone("Asia/Jayapura")  # Eastern Indonesian Time (UTC+09) | ||||
|     timezones["EST"] = timezone( | ||||
|         "America/New_York" | ||||
|     )  # Eastern Standard Time (North America) (UTC−05) | ||||
|     timezones["FET"] = timezone("Europe/Minsk")  # Further-eastern European Time (UTC+03) | ||||
|     timezones["FJT"] = timezone("Pacific/Fiji")  # Fiji Time (UTC+12) | ||||
|     timezones["FKST"] = timezone("Atlantic/Stanley")  # Falkland Islands Summer Time (UTC−03) | ||||
|     timezones["FKT"] = timezone("Atlantic/Stanley")  # Falkland Islands Time (UTC−04) | ||||
|     timezones["FNT"] = timezone("Brazil/DeNoronha")  # Fernando de Noronha Time (UTC−02) | ||||
|     timezones["GALT"] = timezone("Pacific/Galapagos")  # Galapagos Time (UTC−06) | ||||
|     timezones["GAMT"] = timezone("Pacific/Gambier")  # Gambier Islands (UTC−09) | ||||
|     timezones["GET"] = timezone("Asia/Tbilisi")  # Georgia Standard Time (UTC+04) | ||||
|     timezones["GFT"] = timezone("America/Cayenne")  # French Guiana Time (UTC−03) | ||||
|     timezones["GILT"] = timezone("Pacific/Tarawa")  # Gilbert Island Time (UTC+12) | ||||
|     timezones["GIT"] = timezone("Pacific/Gambier")  # Gambier Island Time (UTC−09) | ||||
|     timezones["GMT"] = timezone("GMT")  # Greenwich Mean Time (UTC±00) | ||||
|     timezones["GST"] = timezone("Asia/Muscat")  # Gulf Standard Time (UTC+04) | ||||
|     timezones["GYT"] = timezone("America/Guyana")  # Guyana Time (UTC−04) | ||||
|     timezones["HADT"] = timezone("Pacific/Honolulu")  # Hawaii-Aleutian Daylight Time (UTC−09) | ||||
|     timezones["HAEC"] = timezone("Europe/Paris")  # Heure Avancée d'Europe Centrale (CEST) (UTC+02) | ||||
|     timezones["HAST"] = timezone("Pacific/Honolulu")  # Hawaii-Aleutian Standard Time (UTC−10) | ||||
|     timezones["HKT"] = timezone("Asia/Hong_Kong")  # Hong Kong Time (UTC+08) | ||||
|     timezones["HMT"] = timezone("Indian/Kerguelen")  # Heard and McDonald Islands Time (UTC+05) | ||||
|     timezones["HOVST"] = timezone("Asia/Hovd")  # Khovd Summer Time (UTC+08) | ||||
|     timezones["HOVT"] = timezone("Asia/Hovd")  # Khovd Standard Time (UTC+07) | ||||
|     timezones["ICT"] = timezone("Asia/Ho_Chi_Minh")  # Indochina Time (UTC+07) | ||||
|     timezones["IDT"] = timezone("Asia/Jerusalem")  # Israel Daylight Time (UTC+03) | ||||
|     timezones["IOT"] = timezone("Etc/GMT+3")  # Indian Ocean Time (UTC+03) | ||||
|     timezones["IRDT"] = timezone("Asia/Tehran")  # Iran Daylight Time (UTC+04:30) | ||||
|     timezones["IRKT"] = timezone("Asia/Irkutsk")  # Irkutsk Time (UTC+08) | ||||
|     timezones["IRST"] = timezone("Asia/Tehran")  # Iran Standard Time (UTC+03:30) | ||||
|     timezones["IST"] = timezone("Asia/Kolkata")  # Indian Standard Time (UTC+05:30) | ||||
|     timezones["JST"] = timezone("Asia/Tokyo")  # Japan Standard Time (UTC+09) | ||||
|     timezones["KGT"] = timezone("Asia/Bishkek")  # Kyrgyzstan time (UTC+06) | ||||
|     timezones["KOST"] = timezone("Pacific/Kosrae")  # Kosrae Time (UTC+11) | ||||
|     timezones["KRAT"] = timezone("Asia/Krasnoyarsk")  # Krasnoyarsk Time (UTC+07) | ||||
|     timezones["KST"] = timezone("Asia/Seoul")  # Korea Standard Time (UTC+09) | ||||
|     timezones["LHST"] = timezone("Australia/Lord_Howe")  # Lord Howe Standard Time (UTC+10:30) | ||||
|     timezones["LINT"] = timezone("Pacific/Kiritimati")  # Line Islands Time (UTC+14) | ||||
|     timezones["MAGT"] = timezone("Asia/Magadan")  # Magadan Time (UTC+12) | ||||
|     timezones["MART"] = timezone("Pacific/Marquesas")  # Marquesas Islands Time (UTC−09:30) | ||||
|     timezones["MAWT"] = timezone("Antarctica/Mawson")  # Mawson Station Time (UTC+05) | ||||
|     timezones["MDT"] = timezone( | ||||
|         "America/Denver" | ||||
|     )  # Mountain Daylight Time (North America) (UTC−06) | ||||
|     timezones["MEST"] = timezone( | ||||
|         "Europe/Paris" | ||||
|     )  # Middle European Summer Time Same zone as CEST (UTC+02) | ||||
|     timezones["MET"] = timezone("Europe/Berlin")  # Middle European Time Same zone as CET (UTC+01) | ||||
|     timezones["MHT"] = timezone("Pacific/Kwajalein")  # Marshall Islands (UTC+12) | ||||
|     timezones["MIST"] = timezone("Antarctica/Macquarie")  # Macquarie Island Station Time (UTC+11) | ||||
|     timezones["MIT"] = timezone("Pacific/Marquesas")  # Marquesas Islands Time (UTC−09:30) | ||||
|     timezones["MMT"] = timezone("Asia/Rangoon")  # Myanmar Standard Time (UTC+06:30) | ||||
|     timezones["MSK"] = timezone("Europe/Moscow")  # Moscow Time (UTC+03) | ||||
|     timezones["MST"] = timezone( | ||||
|         "America/Denver" | ||||
|     )  # Mountain Standard Time (North America) (UTC−07) | ||||
|     timezones["MUT"] = timezone("Indian/Mauritius")  # Mauritius Time (UTC+04) | ||||
|     timezones["MVT"] = timezone("Indian/Maldives")  # Maldives Time (UTC+05) | ||||
|     timezones["MYT"] = timezone("Asia/Kuching")  # Malaysia Time (UTC+08) | ||||
|     timezones["NCT"] = timezone("Pacific/Noumea")  # New Caledonia Time (UTC+11) | ||||
|     timezones["NDT"] = timezone("Canada/Newfoundland")  # Newfoundland Daylight Time (UTC−02:30) | ||||
|     timezones["NFT"] = timezone("Pacific/Norfolk")  # Norfolk Time (UTC+11) | ||||
|     timezones["NPT"] = timezone("Asia/Kathmandu")  # Nepal Time (UTC+05:45) | ||||
|     timezones["NST"] = timezone("Canada/Newfoundland")  # Newfoundland Standard Time (UTC−03:30) | ||||
|     timezones["NT"] = timezone("Canada/Newfoundland")  # Newfoundland Time (UTC−03:30) | ||||
|     timezones["NUT"] = timezone("Pacific/Niue")  # Niue Time (UTC−11) | ||||
|     timezones["NZDT"] = timezone("Pacific/Auckland")  # New Zealand Daylight Time (UTC+13) | ||||
|     timezones["NZST"] = timezone("Pacific/Auckland")  # New Zealand Standard Time (UTC+12) | ||||
|     timezones["OMST"] = timezone("Asia/Omsk")  # Omsk Time (UTC+06) | ||||
|     timezones["ORAT"] = timezone("Asia/Oral")  # Oral Time (UTC+05) | ||||
|     timezones["PDT"] = timezone( | ||||
|         "America/Los_Angeles" | ||||
|     )  # Pacific Daylight Time (North America) (UTC−07) | ||||
|     timezones["PET"] = timezone("America/Lima")  # Peru Time (UTC−05) | ||||
|     timezones["PETT"] = timezone("Asia/Kamchatka")  # Kamchatka Time (UTC+12) | ||||
|     timezones["PGT"] = timezone("Pacific/Port_Moresby")  # Papua New Guinea Time (UTC+10) | ||||
|     timezones["PHOT"] = timezone("Pacific/Enderbury")  # Phoenix Island Time (UTC+13) | ||||
|     timezones["PKT"] = timezone("Asia/Karachi")  # Pakistan Standard Time (UTC+05) | ||||
|     timezones["PMDT"] = timezone( | ||||
|         "America/Miquelon" | ||||
|     )  # Saint Pierre and Miquelon Daylight time (UTC−02) | ||||
|     timezones["PMST"] = timezone( | ||||
|         "America/Miquelon" | ||||
|     )  # Saint Pierre and Miquelon Standard Time (UTC−03) | ||||
|     timezones["PONT"] = timezone("Pacific/Pohnpei")  # Pohnpei Standard Time (UTC+11) | ||||
|     timezones["PST"] = timezone( | ||||
|         "America/Los_Angeles" | ||||
|     )  # Pacific Standard Time (North America) (UTC−08) | ||||
|     timezones["PYST"] = timezone( | ||||
|         "America/Asuncion" | ||||
|     )  # Paraguay Summer Time (South America)[7] (UTC−03) | ||||
|     timezones["PYT"] = timezone("America/Asuncion")  # Paraguay Time (South America)[8] (UTC−04) | ||||
|     timezones["RET"] = timezone("Indian/Reunion")  # Réunion Time (UTC+04) | ||||
|     timezones["ROTT"] = timezone("Antarctica/Rothera")  # Rothera Research Station Time (UTC−03) | ||||
|     timezones["SAKT"] = timezone("Asia/Vladivostok")  # Sakhalin Island time (UTC+11) | ||||
|     timezones["SAMT"] = timezone("Europe/Samara")  # Samara Time (UTC+04) | ||||
|     timezones["SAST"] = timezone("Africa/Johannesburg")  # South African Standard Time (UTC+02) | ||||
|     timezones["SBT"] = timezone("Pacific/Guadalcanal")  # Solomon Islands Time (UTC+11) | ||||
|     timezones["SCT"] = timezone("Indian/Mahe")  # Seychelles Time (UTC+04) | ||||
|     timezones["SGT"] = timezone("Asia/Singapore")  # Singapore Time (UTC+08) | ||||
|     timezones["SLST"] = timezone("Asia/Colombo")  # Sri Lanka Standard Time (UTC+05:30) | ||||
|     timezones["SRET"] = timezone("Asia/Srednekolymsk")  # Srednekolymsk Time (UTC+11) | ||||
|     timezones["SRT"] = timezone("America/Paramaribo")  # Suriname Time (UTC−03) | ||||
|     timezones["SST"] = timezone("Asia/Singapore")  # Singapore Standard Time (UTC+08) | ||||
|     timezones["SYOT"] = timezone("Antarctica/Syowa")  # Showa Station Time (UTC+03) | ||||
|     timezones["TAHT"] = timezone("Pacific/Tahiti")  # Tahiti Time (UTC−10) | ||||
|     timezones["TFT"] = timezone("Indian/Kerguelen")  # Indian/Kerguelen (UTC+05) | ||||
|     timezones["THA"] = timezone("Asia/Bangkok")  # Thailand Standard Time (UTC+07) | ||||
|     timezones["TJT"] = timezone("Asia/Dushanbe")  # Tajikistan Time (UTC+05) | ||||
|     timezones["TKT"] = timezone("Pacific/Fakaofo")  # Tokelau Time (UTC+13) | ||||
|     timezones["TLT"] = timezone("Asia/Dili")  # Timor Leste Time (UTC+09) | ||||
|     timezones["TMT"] = timezone("Asia/Ashgabat")  # Turkmenistan Time (UTC+05) | ||||
|     timezones["TOT"] = timezone("Pacific/Tongatapu")  # Tonga Time (UTC+13) | ||||
|     timezones["TVT"] = timezone("Pacific/Funafuti")  # Tuvalu Time (UTC+12) | ||||
|     timezones["ULAST"] = timezone("Asia/Ulan_Bator")  # Ulaanbaatar Summer Time (UTC+09) | ||||
|     timezones["ULAT"] = timezone("Asia/Ulan_Bator")  # Ulaanbaatar Standard Time (UTC+08) | ||||
|     timezones["USZ1"] = timezone("Europe/Kaliningrad")  # Kaliningrad Time (UTC+02) | ||||
|     timezones["UTC"] = timezone("UTC")  # Coordinated Universal Time (UTC±00) | ||||
|     timezones["UYST"] = timezone("America/Montevideo")  # Uruguay Summer Time (UTC−02) | ||||
|     timezones["UYT"] = timezone("America/Montevideo")  # Uruguay Standard Time (UTC−03) | ||||
|     timezones["UZT"] = timezone("Asia/Tashkent")  # Uzbekistan Time (UTC+05) | ||||
|     timezones["VET"] = timezone("America/Caracas")  # Venezuelan Standard Time (UTC−04) | ||||
|     timezones["VLAT"] = timezone("Asia/Vladivostok")  # Vladivostok Time (UTC+10) | ||||
|     timezones["VOLT"] = timezone("Europe/Volgograd")  # Volgograd Time (UTC+04) | ||||
|     timezones["VOST"] = timezone("Antarctica/Vostok")  # Vostok Station Time (UTC+06) | ||||
|     timezones["VUT"] = timezone("Pacific/Efate")  # Vanuatu Time (UTC+11) | ||||
|     timezones["WAKT"] = timezone("Pacific/Wake")  # Wake Island Time (UTC+12) | ||||
|     timezones["WAST"] = timezone("Africa/Lagos")  # West Africa Summer Time (UTC+02) | ||||
|     timezones["WAT"] = timezone("Africa/Lagos")  # West Africa Time (UTC+01) | ||||
|     timezones["WEST"] = timezone("Europe/London")  # Western European Summer Time (UTC+01) | ||||
|     timezones["WET"] = timezone("Europe/London")  # Western European Time (UTC±00) | ||||
|     timezones["WIT"] = timezone("Asia/Jakarta")  # Western Indonesian Time (UTC+07) | ||||
|     timezones["WST"] = timezone("Australia/Perth")  # Western Standard Time (UTC+08) | ||||
|     timezones["YAKT"] = timezone("Asia/Yakutsk")  # Yakutsk Time (UTC+09) | ||||
|     timezones["YEKT"] = timezone("Asia/Yekaterinburg")  # Yekaterinburg Time (UTC+05) | ||||
|     timezones['ACDT'] = gettz('Australia/Darwin')             # Australian Central Daylight Savings Time (UTC+10:30) | ||||
|     timezones['ACST'] = gettz('Australia/Darwin')             # Australian Central Standard Time (UTC+09:30) | ||||
|     timezones['ACT'] = gettz('Brazil/Acre')                   # Acre Time (UTC−05) | ||||
|     timezones['ADT'] = gettz('America/Halifax')               # Atlantic Daylight Time (UTC−03) | ||||
|     timezones['AEDT'] = gettz('Australia/Sydney')             # Australian Eastern Daylight Savings Time (UTC+11) | ||||
|     timezones['AEST'] = gettz('Australia/Sydney')             # Australian Eastern Standard Time (UTC+10) | ||||
|     timezones['AFT'] = gettz('Asia/Kabul')                    # Afghanistan Time (UTC+04:30) | ||||
|     timezones['AKDT'] = gettz('America/Juneau')               # Alaska Daylight Time (UTC−08) | ||||
|     timezones['AKST'] = gettz('America/Juneau')               # Alaska Standard Time (UTC−09) | ||||
|     timezones['AMST'] = gettz('America/Manaus')               # Amazon Summer Time (Brazil)[1] (UTC−03) | ||||
|     timezones['AMT'] = gettz('America/Manaus')                # Amazon Time (Brazil)[2] (UTC−04) | ||||
|     timezones['ART'] = gettz('America/Cordoba')               # Argentina Time (UTC−03) | ||||
|     timezones['AST'] = gettz('Asia/Riyadh')                   # Arabia Standard Time (UTC+03) | ||||
|     timezones['AWST'] = gettz('Australia/Perth')              # Australian Western Standard Time (UTC+08) | ||||
|     timezones['AZOST'] = gettz('Atlantic/Azores')             # Azores Summer Time (UTC±00) | ||||
|     timezones['AZOT'] = gettz('Atlantic/Azores')              # Azores Standard Time (UTC−01) | ||||
|     timezones['AZT'] = gettz('Asia/Baku')                     # Azerbaijan Time (UTC+04) | ||||
|     timezones['BDT'] = gettz('Asia/Brunei')                   # Brunei Time (UTC+08) | ||||
|     timezones['BIOT'] = gettz('Etc/GMT+6')                    # British Indian Ocean Time (UTC+06) | ||||
|     timezones['BIT'] = gettz('Pacific/Funafuti')              # Baker Island Time (UTC−12) | ||||
|     timezones['BOT'] = gettz('America/La_Paz')                # Bolivia Time (UTC−04) | ||||
|     timezones['BRST'] = gettz('America/Sao_Paulo')            # Brasilia Summer Time (UTC−02) | ||||
|     timezones['BRT'] = gettz('America/Sao_Paulo')             # Brasilia Time (UTC−03) | ||||
|     timezones['BST'] = gettz('Asia/Dhaka')                    # Bangladesh Standard Time (UTC+06) | ||||
|     timezones['BTT'] = gettz('Asia/Thimphu')                  # Bhutan Time (UTC+06) | ||||
|     timezones['CAT'] = gettz('Africa/Harare')                 # Central Africa Time (UTC+02) | ||||
|     timezones['CCT'] = gettz('Indian/Cocos')                  # Cocos Islands Time (UTC+06:30) | ||||
|     timezones['CDT'] = gettz('America/Chicago')               # Central Daylight Time (North America) (UTC−05) | ||||
|     timezones['CEST'] = gettz('Europe/Berlin')                # Central European Summer Time (Cf. HAEC) (UTC+02) | ||||
|     timezones['CET'] = gettz('Europe/Berlin')                 # Central European Time (UTC+01) | ||||
|     timezones['CHADT'] = gettz('Pacific/Chatham')             # Chatham Daylight Time (UTC+13:45) | ||||
|     timezones['CHAST'] = gettz('Pacific/Chatham')             # Chatham Standard Time (UTC+12:45) | ||||
|     timezones['CHOST'] = gettz('Asia/Choibalsan')             # Choibalsan Summer Time (UTC+09) | ||||
|     timezones['CHOT'] = gettz('Asia/Choibalsan')              # Choibalsan Standard Time (UTC+08) | ||||
|     timezones['CHST'] = gettz('Pacific/Guam')                 # Chamorro Standard Time (UTC+10) | ||||
|     timezones['CHUT'] = gettz('Pacific/Chuuk')                # Chuuk Time (UTC+10) | ||||
|     timezones['CIST'] = gettz('Etc/GMT-8')                    # Clipperton Island Standard Time (UTC−08) | ||||
|     timezones['CIT'] = gettz('Asia/Makassar')                 # Central Indonesia Time (UTC+08) | ||||
|     timezones['CKT'] = gettz('Pacific/Rarotonga')             # Cook Island Time (UTC−10) | ||||
|     timezones['CLST'] = gettz('America/Santiago')             # Chile Summer Time (UTC−03) | ||||
|     timezones['CLT'] = gettz('America/Santiago')              # Chile Standard Time (UTC−04) | ||||
|     timezones['COST'] = gettz('America/Bogota')               # Colombia Summer Time (UTC−04) | ||||
|     timezones['COT'] = gettz('America/Bogota')                # Colombia Time (UTC−05) | ||||
|     timezones['CST'] = gettz('America/Chicago')               # Central Standard Time (North America) (UTC−06) | ||||
|     timezones['CT'] = gettz('Asia/Chongqing')                 # China time (UTC+08) | ||||
|     timezones['CVT'] = gettz('Atlantic/Cape_Verde')           # Cape Verde Time (UTC−01) | ||||
|     timezones['CXT'] = gettz('Indian/Christmas')              # Christmas Island Time (UTC+07) | ||||
|     timezones['DAVT'] = gettz('Antarctica/Davis')             # Davis Time (UTC+07) | ||||
|     timezones['DDUT'] = gettz('Antarctica/DumontDUrville')    # Dumont d'Urville Time (UTC+10) | ||||
|     timezones['DFT'] = gettz('Europe/Berlin')                 # AIX equivalent of Central European Time (UTC+01) | ||||
|     timezones['EASST'] = gettz('Chile/EasterIsland')          # Easter Island Summer Time (UTC−05) | ||||
|     timezones['EAST'] = gettz('Chile/EasterIsland')           # Easter Island Standard Time (UTC−06) | ||||
|     timezones['EAT'] = gettz('Africa/Mogadishu')              # East Africa Time (UTC+03) | ||||
|     timezones['ECT'] = gettz('America/Guayaquil')             # Ecuador Time (UTC−05) | ||||
|     timezones['EDT'] = gettz('America/New_York')              # Eastern Daylight Time (North America) (UTC−04) | ||||
|     timezones['EEST'] = gettz('Europe/Bucharest')             # Eastern European Summer Time (UTC+03) | ||||
|     timezones['EET'] = gettz('Europe/Bucharest')              # Eastern European Time (UTC+02) | ||||
|     timezones['EGST'] = gettz('America/Scoresbysund')         # Eastern Greenland Summer Time (UTC±00) | ||||
|     timezones['EGT'] = gettz('America/Scoresbysund')          # Eastern Greenland Time (UTC−01) | ||||
|     timezones['EIT'] = gettz('Asia/Jayapura')                 # Eastern Indonesian Time (UTC+09) | ||||
|     timezones['EST'] = gettz('America/New_York')              # Eastern Standard Time (North America) (UTC−05) | ||||
|     timezones['FET'] = gettz('Europe/Minsk')                  # Further-eastern European Time (UTC+03) | ||||
|     timezones['FJT'] = gettz('Pacific/Fiji')                  # Fiji Time (UTC+12) | ||||
|     timezones['FKST'] = gettz('Atlantic/Stanley')             # Falkland Islands Summer Time (UTC−03) | ||||
|     timezones['FKT'] = gettz('Atlantic/Stanley')              # Falkland Islands Time (UTC−04) | ||||
|     timezones['FNT'] = gettz('Brazil/DeNoronha')              # Fernando de Noronha Time (UTC−02) | ||||
|     timezones['GALT'] = gettz('Pacific/Galapagos')            # Galapagos Time (UTC−06) | ||||
|     timezones['GAMT'] = gettz('Pacific/Gambier')              # Gambier Islands (UTC−09) | ||||
|     timezones['GET'] = gettz('Asia/Tbilisi')                  # Georgia Standard Time (UTC+04) | ||||
|     timezones['GFT'] = gettz('America/Cayenne')               # French Guiana Time (UTC−03) | ||||
|     timezones['GILT'] = gettz('Pacific/Tarawa')               # Gilbert Island Time (UTC+12) | ||||
|     timezones['GIT'] = gettz('Pacific/Gambier')               # Gambier Island Time (UTC−09) | ||||
|     timezones['GMT'] = gettz('GMT')                           # Greenwich Mean Time (UTC±00) | ||||
|     timezones['GST'] = gettz('Asia/Muscat')                   # Gulf Standard Time (UTC+04) | ||||
|     timezones['GYT'] = gettz('America/Guyana')                # Guyana Time (UTC−04) | ||||
|     timezones['HADT'] = gettz('Pacific/Honolulu')             # Hawaii-Aleutian Daylight Time (UTC−09) | ||||
|     timezones['HAEC'] = gettz('Europe/Paris')                 # Heure Avancée d'Europe Centrale (CEST) (UTC+02) | ||||
|     timezones['HAST'] = gettz('Pacific/Honolulu')             # Hawaii-Aleutian Standard Time (UTC−10) | ||||
|     timezones['HKT'] = gettz('Asia/Hong_Kong')                # Hong Kong Time (UTC+08) | ||||
|     timezones['HMT'] = gettz('Indian/Kerguelen')              # Heard and McDonald Islands Time (UTC+05) | ||||
|     timezones['HOVST'] = gettz('Asia/Hovd')                   # Khovd Summer Time (UTC+08) | ||||
|     timezones['HOVT'] = gettz('Asia/Hovd')                    # Khovd Standard Time (UTC+07) | ||||
|     timezones['ICT'] = gettz('Asia/Ho_Chi_Minh')              # Indochina Time (UTC+07) | ||||
|     timezones['IDT'] = gettz('Asia/Jerusalem')                # Israel Daylight Time (UTC+03) | ||||
|     timezones['IOT'] = gettz('Etc/GMT+3')                     # Indian Ocean Time (UTC+03) | ||||
|     timezones['IRDT'] = gettz('Asia/Tehran')                  # Iran Daylight Time (UTC+04:30) | ||||
|     timezones['IRKT'] = gettz('Asia/Irkutsk')                 # Irkutsk Time (UTC+08) | ||||
|     timezones['IRST'] = gettz('Asia/Tehran')                  # Iran Standard Time (UTC+03:30) | ||||
|     timezones['IST'] = gettz('Asia/Kolkata')                  # Indian Standard Time (UTC+05:30) | ||||
|     timezones['JST'] = gettz('Asia/Tokyo')                    # Japan Standard Time (UTC+09) | ||||
|     timezones['KGT'] = gettz('Asia/Bishkek')                  # Kyrgyzstan time (UTC+06) | ||||
|     timezones['KOST'] = gettz('Pacific/Kosrae')               # Kosrae Time (UTC+11) | ||||
|     timezones['KRAT'] = gettz('Asia/Krasnoyarsk')             # Krasnoyarsk Time (UTC+07) | ||||
|     timezones['KST'] = gettz('Asia/Seoul')                    # Korea Standard Time (UTC+09) | ||||
|     timezones['LHST'] = gettz('Australia/Lord_Howe')          # Lord Howe Standard Time (UTC+10:30) | ||||
|     timezones['LINT'] = gettz('Pacific/Kiritimati')           # Line Islands Time (UTC+14) | ||||
|     timezones['MAGT'] = gettz('Asia/Magadan')                 # Magadan Time (UTC+12) | ||||
|     timezones['MART'] = gettz('Pacific/Marquesas')            # Marquesas Islands Time (UTC−09:30) | ||||
|     timezones['MAWT'] = gettz('Antarctica/Mawson')            # Mawson Station Time (UTC+05) | ||||
|     timezones['MDT'] = gettz('America/Denver')                # Mountain Daylight Time (North America) (UTC−06) | ||||
|     timezones['MEST'] = gettz('Europe/Paris')                 # Middle European Summer Time Same zone as CEST (UTC+02) | ||||
|     timezones['MET'] = gettz('Europe/Berlin')                 # Middle European Time Same zone as CET (UTC+01) | ||||
|     timezones['MHT'] = gettz('Pacific/Kwajalein')             # Marshall Islands (UTC+12) | ||||
|     timezones['MIST'] = gettz('Antarctica/Macquarie')         # Macquarie Island Station Time (UTC+11) | ||||
|     timezones['MIT'] = gettz('Pacific/Marquesas')             # Marquesas Islands Time (UTC−09:30) | ||||
|     timezones['MMT'] = gettz('Asia/Rangoon')                  # Myanmar Standard Time (UTC+06:30) | ||||
|     timezones['MSK'] = gettz('Europe/Moscow')                 # Moscow Time (UTC+03) | ||||
|     timezones['MST'] = gettz('America/Denver')                # Mountain Standard Time (North America) (UTC−07) | ||||
|     timezones['MUT'] = gettz('Indian/Mauritius')              # Mauritius Time (UTC+04) | ||||
|     timezones['MVT'] = gettz('Indian/Maldives')               # Maldives Time (UTC+05) | ||||
|     timezones['MYT'] = gettz('Asia/Kuching')                  # Malaysia Time (UTC+08) | ||||
|     timezones['NCT'] = gettz('Pacific/Noumea')                # New Caledonia Time (UTC+11) | ||||
|     timezones['NDT'] = gettz('Canada/Newfoundland')           # Newfoundland Daylight Time (UTC−02:30) | ||||
|     timezones['NFT'] = gettz('Pacific/Norfolk')               # Norfolk Time (UTC+11) | ||||
|     timezones['NPT'] = gettz('Asia/Kathmandu')                # Nepal Time (UTC+05:45) | ||||
|     timezones['NST'] = gettz('Canada/Newfoundland')           # Newfoundland Standard Time (UTC−03:30) | ||||
|     timezones['NT'] = gettz('Canada/Newfoundland')            # Newfoundland Time (UTC−03:30) | ||||
|     timezones['NUT'] = gettz('Pacific/Niue')                  # Niue Time (UTC−11) | ||||
|     timezones['NZDT'] = gettz('Pacific/Auckland')             # New Zealand Daylight Time (UTC+13) | ||||
|     timezones['NZST'] = gettz('Pacific/Auckland')             # New Zealand Standard Time (UTC+12) | ||||
|     timezones['OMST'] = gettz('Asia/Omsk')                    # Omsk Time (UTC+06) | ||||
|     timezones['ORAT'] = gettz('Asia/Oral')                    # Oral Time (UTC+05) | ||||
|     timezones['PDT'] = gettz('America/Los_Angeles')           # Pacific Daylight Time (North America) (UTC−07) | ||||
|     timezones['PET'] = gettz('America/Lima')                  # Peru Time (UTC−05) | ||||
|     timezones['PETT'] = gettz('Asia/Kamchatka')               # Kamchatka Time (UTC+12) | ||||
|     timezones['PGT'] = gettz('Pacific/Port_Moresby')          # Papua New Guinea Time (UTC+10) | ||||
|     timezones['PHOT'] = gettz('Pacific/Enderbury')            # Phoenix Island Time (UTC+13) | ||||
|     timezones['PKT'] = gettz('Asia/Karachi')                  # Pakistan Standard Time (UTC+05) | ||||
|     timezones['PMDT'] = gettz('America/Miquelon')             # Saint Pierre and Miquelon Daylight time (UTC−02) | ||||
|     timezones['PMST'] = gettz('America/Miquelon')             # Saint Pierre and Miquelon Standard Time (UTC−03) | ||||
|     timezones['PONT'] = gettz('Pacific/Pohnpei')              # Pohnpei Standard Time (UTC+11) | ||||
|     timezones['PST'] = gettz('America/Los_Angeles')           # Pacific Standard Time (North America) (UTC−08) | ||||
|     timezones['PYST'] = gettz('America/Asuncion')             # Paraguay Summer Time (South America)[7] (UTC−03) | ||||
|     timezones['PYT'] = gettz('America/Asuncion')              # Paraguay Time (South America)[8] (UTC−04) | ||||
|     timezones['RET'] = gettz('Indian/Reunion')                # Réunion Time (UTC+04) | ||||
|     timezones['ROTT'] = gettz('Antarctica/Rothera')           # Rothera Research Station Time (UTC−03) | ||||
|     timezones['SAKT'] = gettz('Asia/Vladivostok')             # Sakhalin Island time (UTC+11) | ||||
|     timezones['SAMT'] = gettz('Europe/Samara')                # Samara Time (UTC+04) | ||||
|     timezones['SAST'] = gettz('Africa/Johannesburg')          # South African Standard Time (UTC+02) | ||||
|     timezones['SBT'] = gettz('Pacific/Guadalcanal')           # Solomon Islands Time (UTC+11) | ||||
|     timezones['SCT'] = gettz('Indian/Mahe')                   # Seychelles Time (UTC+04) | ||||
|     timezones['SGT'] = gettz('Asia/Singapore')                # Singapore Time (UTC+08) | ||||
|     timezones['SLST'] = gettz('Asia/Colombo')                 # Sri Lanka Standard Time (UTC+05:30) | ||||
|     timezones['SRET'] = gettz('Asia/Srednekolymsk')           # Srednekolymsk Time (UTC+11) | ||||
|     timezones['SRT'] = gettz('America/Paramaribo')            # Suriname Time (UTC−03) | ||||
|     timezones['SST'] = gettz('Asia/Singapore')                # Singapore Standard Time (UTC+08) | ||||
|     timezones['SYOT'] = gettz('Antarctica/Syowa')             # Showa Station Time (UTC+03) | ||||
|     timezones['TAHT'] = gettz('Pacific/Tahiti')               # Tahiti Time (UTC−10) | ||||
|     timezones['TFT'] = gettz('Indian/Kerguelen')              # Indian/Kerguelen (UTC+05) | ||||
|     timezones['THA'] = gettz('Asia/Bangkok')                  # Thailand Standard Time (UTC+07) | ||||
|     timezones['TJT'] = gettz('Asia/Dushanbe')                 # Tajikistan Time (UTC+05) | ||||
|     timezones['TKT'] = gettz('Pacific/Fakaofo')               # Tokelau Time (UTC+13) | ||||
|     timezones['TLT'] = gettz('Asia/Dili')                     # Timor Leste Time (UTC+09) | ||||
|     timezones['TMT'] = gettz('Asia/Ashgabat')                 # Turkmenistan Time (UTC+05) | ||||
|     timezones['TOT'] = gettz('Pacific/Tongatapu')             # Tonga Time (UTC+13) | ||||
|     timezones['TVT'] = gettz('Pacific/Funafuti')              # Tuvalu Time (UTC+12) | ||||
|     timezones['ULAST'] = gettz('Asia/Ulan_Bator')             # Ulaanbaatar Summer Time (UTC+09) | ||||
|     timezones['ULAT'] = gettz('Asia/Ulan_Bator')              # Ulaanbaatar Standard Time (UTC+08) | ||||
|     timezones['USZ1'] = gettz('Europe/Kaliningrad')           # Kaliningrad Time (UTC+02) | ||||
|     timezones['UTC'] = gettz('UTC')                           # Coordinated Universal Time (UTC±00) | ||||
|     timezones['UYST'] = gettz('America/Montevideo')           # Uruguay Summer Time (UTC−02) | ||||
|     timezones['UYT'] = gettz('America/Montevideo')            # Uruguay Standard Time (UTC−03) | ||||
|     timezones['UZT'] = gettz('Asia/Tashkent')                 # Uzbekistan Time (UTC+05) | ||||
|     timezones['VET'] = gettz('America/Caracas')               # Venezuelan Standard Time (UTC−04) | ||||
|     timezones['VLAT'] = gettz('Asia/Vladivostok')             # Vladivostok Time (UTC+10) | ||||
|     timezones['VOLT'] = gettz('Europe/Volgograd')             # Volgograd Time (UTC+04) | ||||
|     timezones['VOST'] = gettz('Antarctica/Vostok')            # Vostok Station Time (UTC+06) | ||||
|     timezones['VUT'] = gettz('Pacific/Efate')                 # Vanuatu Time (UTC+11) | ||||
|     timezones['WAKT'] = gettz('Pacific/Wake')                 # Wake Island Time (UTC+12) | ||||
|     timezones['WAST'] = gettz('Africa/Lagos')                 # West Africa Summer Time (UTC+02) | ||||
|     timezones['WAT'] = gettz('Africa/Lagos')                  # West Africa Time (UTC+01) | ||||
|     timezones['WEST'] = gettz('Europe/London')                # Western European Summer Time (UTC+01) | ||||
|     timezones['WET'] = gettz('Europe/London')                 # Western European Time (UTC±00) | ||||
|     timezones['WIT'] = gettz('Asia/Jakarta')                  # Western Indonesian Time (UTC+07) | ||||
|     timezones['WST'] = gettz('Australia/Perth')               # Western Standard Time (UTC+08) | ||||
|     timezones['YAKT'] = gettz('Asia/Yakutsk')                 # Yakutsk Time (UTC+09) | ||||
|     timezones['YEKT'] = gettz('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 | ||||
|     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): | ||||
|  | ||||
| @ -30,8 +30,8 @@ class ForceMention(Cog): | ||||
|     @commands.command() | ||||
|     async def forcemention(self, ctx: commands.Context, role: str, *, message=""): | ||||
|         """ | ||||
|         Mentions that role, regardless if it's unmentionable | ||||
|         """ | ||||
|        Mentions that role, regardless if it's unmentionable | ||||
|        """ | ||||
|         role_obj = get(ctx.guild.roles, name=role) | ||||
|         if role_obj is None: | ||||
|             await ctx.maybe_send_embed("Couldn't find role named {}".format(role)) | ||||
|  | ||||
| @ -6,3 +6,4 @@ def setup(bot): | ||||
|     n = Hangman(bot) | ||||
|     data_manager.bundled_data_path(n) | ||||
|     bot.add_cog(n) | ||||
|     bot.add_listener(n.on_react, "on_reaction_add") | ||||
|  | ||||
| @ -50,27 +50,27 @@ class Hangman(Cog): | ||||
|             theface = await self.config.guild(guild).theface() | ||||
|             self.hanglist[guild] = ( | ||||
|                 """> | ||||
|                    \\_________ | ||||
|                    \_________ | ||||
|                     |/         | ||||
|                     |               | ||||
|                     |                 | ||||
|                     |                  | ||||
|                     |                | ||||
|                     |                    | ||||
|                     |\\___                  | ||||
|                     |\___                  | ||||
|                     """, | ||||
|                 """> | ||||
|                    \\_________ | ||||
|                    \_________ | ||||
|                     |/   |       | ||||
|                     |               | ||||
|                     |                 | ||||
|                     |                  | ||||
|                     |                | ||||
|                     |                    | ||||
|                     |\\___                  | ||||
|                     |\___                  | ||||
|                     H""", | ||||
|                 """> | ||||
|                    \\_________        | ||||
|                    \_________        | ||||
|                     |/   |               | ||||
|                     |   """ | ||||
|                 + theface | ||||
| @ -79,10 +79,10 @@ class Hangman(Cog): | ||||
|                     |                        | ||||
|                     |                          | ||||
|                     |                           | ||||
|                     |\\___                        | ||||
|                     |\___                        | ||||
|                     HA""", | ||||
|                 """> | ||||
|                    \\________                | ||||
|                    \________                | ||||
|                     |/   |                    | ||||
|                     |   """ | ||||
|                 + theface | ||||
| @ -91,10 +91,10 @@ class Hangman(Cog): | ||||
|                     |    |                     | ||||
|                     |                            | ||||
|                     |                             | ||||
|                     |\\___                     | ||||
|                     |\___                     | ||||
|                     HAN""", | ||||
|                 """> | ||||
|                    \\_________              | ||||
|                    \_________              | ||||
|                     |/   |                | ||||
|                     |   """ | ||||
|                 + theface | ||||
| @ -103,43 +103,43 @@ class Hangman(Cog): | ||||
|                     |     |                     | ||||
|                     |                         | ||||
|                     |                           | ||||
|                     |\\___                           | ||||
|                     |\___                           | ||||
|                     HANG""", | ||||
|                 """> | ||||
|                    \\_________               | ||||
|                    \_________               | ||||
|                     |/   |                      | ||||
|                     |   """ | ||||
|                 + theface | ||||
|                 + """                       | ||||
|                     |   /|\\                     | ||||
|                     |   /|\                     | ||||
|                     |     |                        | ||||
|                     |                              | ||||
|                     |                             | ||||
|                     |\\___                           | ||||
|                     |\___                           | ||||
|                     HANGM""", | ||||
|                 """> | ||||
|                    \\________                    | ||||
|                    \________                    | ||||
|                     |/   |                          | ||||
|                     |   """ | ||||
|                 + theface | ||||
|                 + """                        | ||||
|                     |   /|\\                              | ||||
|                     |   /|\                              | ||||
|                     |     |                           | ||||
|                     |   /                             | ||||
|                     |                                   | ||||
|                     |\\___                               | ||||
|                     |\___                               | ||||
|                     HANGMA""", | ||||
|                 """> | ||||
|                    \\________ | ||||
|                    \________ | ||||
|                     |/   |      | ||||
|                     |   """ | ||||
|                 + theface | ||||
|                 + """      | ||||
|                     |   /|\\            | ||||
|                     |   /|\            | ||||
|                     |     |         | ||||
|                     |   / \\         | ||||
|                     |   / \         | ||||
|                     |                | ||||
|                     |\\___            | ||||
|                     |\___            | ||||
|                     HANGMAN""", | ||||
|             ) | ||||
| 
 | ||||
| @ -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,19 +250,21 @@ 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 + "__ " | ||||
|             else: | ||||
|                 out_str += "**\\_** " | ||||
|                 out_str += "**\_** " | ||||
|                 self.winbool[guild] = False | ||||
| 
 | ||||
|         return out_str | ||||
| 
 | ||||
|     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 | ||||
| @ -283,10 +286,10 @@ class Hangman(Cog): | ||||
| 
 | ||||
|         await self._reprintgame(message) | ||||
| 
 | ||||
|     @commands.Cog.listener("on_reaction_add") | ||||
|     @commands.Cog.listener() | ||||
|     async def on_react(self, reaction, user: Union[discord.User, discord.Member]): | ||||
|         """Thanks to flapjack reactpoll for guidelines | ||||
|         https://github.com/flapjax/FlapJack-Cogs/blob/master/reactpoll/reactpoll.py""" | ||||
|         """ Thanks to flapjack reactpoll for guidelines | ||||
|             https://github.com/flapjax/FlapJack-Cogs/blob/master/reactpoll/reactpoll.py""" | ||||
|         guild: discord.Guild = getattr(user, "guild", None) | ||||
|         if guild is None: | ||||
|             return | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -33,27 +33,23 @@ class LoveCalculator(Cog): | ||||
|             x.replace(" ", "+"), y.replace(" ", "+") | ||||
|         ) | ||||
|         async with aiohttp.ClientSession(headers={"Connection": "keep-alive"}) as session: | ||||
|             async with session.get(url, ssl=False) as response: | ||||
|             async with session.get(url) as response: | ||||
|                 assert response.status == 200 | ||||
|                 resp = await response.text() | ||||
| 
 | ||||
|         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: | ||||
| @ -64,11 +60,14 @@ class LoveCalculator(Cog): | ||||
|             else: | ||||
|                 emoji = "💔" | ||||
|             title = f"Dr. Love says that the love percentage for {x} and {y} is: {emoji} {description} {emoji}" | ||||
|         except (TypeError, ValueError): | ||||
|         except: | ||||
|             title = "Dr. Love has left a note for you." | ||||
| 
 | ||||
|         em = discord.Embed( | ||||
|             title=title, description=result_text, color=discord.Color.red(), url=url | ||||
|             title=title, | ||||
|             description=result_text, | ||||
|             color=discord.Color.red(), | ||||
|             url=f"https://www.lovecalculator.com/{result_image}", | ||||
|         ) | ||||
|         em.set_image(url=f"https://www.lovecalculator.com/{result_image}") | ||||
| 
 | ||||
|         await ctx.send(embed=em) | ||||
|  | ||||
| @ -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): | ||||
| @ -73,17 +75,17 @@ class LastSeen(Cog): | ||||
|         else: | ||||
|             last_seen = await self.config.member(member).seen() | ||||
|             if last_seen is None: | ||||
|                 await ctx.maybe_send_embed("I've never seen this user") | ||||
|                 await ctx.maybe_send_embed( | ||||
|                     embed=discord.Embed(description="I've never seen this user") | ||||
|                 ) | ||||
|                 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) | ||||
|         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__() | ||||
| @ -83,9 +85,7 @@ class Nudity(commands.Cog): | ||||
|                 if r["unsafe"] > 0.7: | ||||
|                     await nsfw_channel.send( | ||||
|                         "NSFW Image from {}".format(message.channel.mention), | ||||
|                         file=discord.File( | ||||
|                             image, | ||||
|                         ), | ||||
|                         file=discord.File(image,), | ||||
|                     ) | ||||
| 
 | ||||
|     @commands.Cog.listener() | ||||
|  | ||||
| @ -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) | ||||
| 
 | ||||
| @ -350,9 +360,7 @@ class PlantTycoon(commands.Cog): | ||||
|             ``{0}prune``: Prune your plant.\n""" | ||||
| 
 | ||||
|             em = discord.Embed( | ||||
|                 title=title, | ||||
|                 description=description.format(prefix), | ||||
|                 color=discord.Color.green(), | ||||
|                 title=title, description=description.format(prefix), color=discord.Color.green(), | ||||
|             ) | ||||
|             em.set_thumbnail(url="https://image.prntscr.com/image/AW7GuFIBSeyEgkR2W3SeiQ.png") | ||||
|             em.set_footer( | ||||
| @ -402,18 +410,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 +438,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 +448,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 +468,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) | ||||
| @ -510,8 +525,7 @@ class PlantTycoon(commands.Cog): | ||||
| 
 | ||||
|         if t: | ||||
|             em = discord.Embed( | ||||
|                 title="Plant statistics of {}".format(plant["name"]), | ||||
|                 color=discord.Color.green(), | ||||
|                 title="Plant statistics of {}".format(plant["name"]), color=discord.Color.green(), | ||||
|             ) | ||||
|             em.set_thumbnail(url=plant["image"]) | ||||
|             em.add_field(name="**Name**", value=plant["name"]) | ||||
| @ -569,8 +583,7 @@ class PlantTycoon(commands.Cog): | ||||
|         author = ctx.author | ||||
|         if product is None: | ||||
|             em = discord.Embed( | ||||
|                 title="All gardening supplies that you can buy:", | ||||
|                 color=discord.Color.green(), | ||||
|                 title="All gardening supplies that you can buy:", color=discord.Color.green(), | ||||
|             ) | ||||
|             for pd in self.products: | ||||
|                 em.add_field( | ||||
| @ -583,6 +596,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" | ||||
| @ -602,17 +616,13 @@ class PlantTycoon(commands.Cog): | ||||
|                         await gardener.save_gardener() | ||||
|                         message = "You bought {}.".format(product.lower()) | ||||
|                     else: | ||||
|                         message = ( | ||||
|                             "You don't have enough Thneeds. You have {}, but need {}.".format( | ||||
|                                 gardener.points, | ||||
|                                 self.products[product.lower()]["cost"] * amount, | ||||
|                             ) | ||||
|                         message = "You don't have enough Thneeds. You have {}, but need {}.".format( | ||||
|                             gardener.points, self.products[product.lower()]["cost"] * amount, | ||||
|                         ) | ||||
|                 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 +656,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 +674,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 +693,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 +710,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 +786,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 | ||||
|  | ||||
| @ -1,4 +1,3 @@ | ||||
| import logging | ||||
| from typing import List, Union | ||||
| 
 | ||||
| import discord | ||||
| @ -6,8 +5,6 @@ from redbot.core import Config, commands | ||||
| from redbot.core.bot import Red | ||||
| from redbot.core.commands import Cog | ||||
| 
 | ||||
| log = logging.getLogger("red.fox_v3.reactrestrict") | ||||
| 
 | ||||
| 
 | ||||
| class ReactRestrictCombo: | ||||
|     def __init__(self, message_id, role_id): | ||||
| @ -97,7 +94,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) | ||||
| @ -132,12 +131,10 @@ class ReactRestrict(Cog): | ||||
|             If no such channel or member can be found. | ||||
|         """ | ||||
|         channel = self.bot.get_channel(channel_id) | ||||
|         if channel is None: | ||||
|             raise LookupError("no channel found.") | ||||
|         try: | ||||
|             member = channel.guild.get_member(user_id) | ||||
|         except AttributeError as e: | ||||
|             raise LookupError("No member found.") from e | ||||
|             raise LookupError("No channel found.") from e | ||||
| 
 | ||||
|         if member is None: | ||||
|             raise LookupError("No member found.") | ||||
| @ -171,7 +168,7 @@ class ReactRestrict(Cog): | ||||
|         """ | ||||
|         channel = self.bot.get_channel(channel_id) | ||||
|         try: | ||||
|             return await channel.fetch_message(message_id) | ||||
|             return await channel.get_message(message_id) | ||||
|         except discord.NotFound: | ||||
|             pass | ||||
|         except AttributeError:  # VoiceChannel object has no attribute 'get_message' | ||||
| @ -189,11 +186,9 @@ class ReactRestrict(Cog): | ||||
|         :param message_id: | ||||
|         :return: | ||||
|         """ | ||||
| 
 | ||||
|         guild: discord.Guild = ctx.guild | ||||
|         for channel in guild.text_channels: | ||||
|         for channel in ctx.guild.channels: | ||||
|             try: | ||||
|                 return await channel.fetch_message(message_id) | ||||
|                 return await channel.get_message(message_id) | ||||
|             except discord.NotFound: | ||||
|                 pass | ||||
|             except AttributeError:  # VoiceChannel object has no attribute 'get_message' | ||||
| @ -208,7 +203,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): | ||||
| @ -236,7 +232,7 @@ class ReactRestrict(Cog): | ||||
|         # noinspection PyTypeChecker | ||||
|         await self.add_reactrestrict(message_id, role) | ||||
| 
 | ||||
|         await ctx.maybe_send_embed("Message|Role restriction added.") | ||||
|         await ctx.maybe_send_embed("Message|Role combo added.") | ||||
| 
 | ||||
|     @reactrestrict.command() | ||||
|     async def remove(self, ctx: commands.Context, message_id: int, role: discord.Role): | ||||
| @ -252,38 +248,37 @@ class ReactRestrict(Cog): | ||||
|         # noinspection PyTypeChecker | ||||
|         await self.remove_react(message_id, role) | ||||
| 
 | ||||
|         await ctx.send("React restriction removed.") | ||||
|         await ctx.send("Reaction removed.") | ||||
| 
 | ||||
|     @commands.Cog.listener() | ||||
|     async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent): | ||||
|     async def on_raw_reaction_add( | ||||
|         self, emoji: discord.PartialEmoji, message_id: int, channel_id: int, user_id: int | ||||
|     ): | ||||
|         """ | ||||
|         Event handler for long term reaction watching. | ||||
| 
 | ||||
|         :param discord.PartialReactionEmoji emoji: | ||||
|         :param int message_id: | ||||
|         :param int channel_id: | ||||
|         :param int user_id: | ||||
|         :return: | ||||
|         """ | ||||
| 
 | ||||
|         emoji = payload.emoji | ||||
|         message_id = payload.message_id | ||||
|         channel_id = payload.channel_id | ||||
|         user_id = payload.user_id | ||||
| 
 | ||||
|         # if emoji.is_custom_emoji(): | ||||
|         #     emoji_id = emoji.id | ||||
|         # else: | ||||
|         #     emoji_id = emoji.name | ||||
|         if emoji.is_custom_emoji(): | ||||
|             emoji_id = emoji.id | ||||
|         else: | ||||
|             emoji_id = emoji.name | ||||
| 
 | ||||
|         has_reactrestrict, combos = await self.has_reactrestrict_combo(message_id) | ||||
| 
 | ||||
|         if not has_reactrestrict: | ||||
|             log.debug("Message not react restricted") | ||||
|             return | ||||
| 
 | ||||
|         try: | ||||
|             member = self._get_member(channel_id, user_id) | ||||
|         except LookupError: | ||||
|             log.exception("Unable to get member from guild") | ||||
|             return | ||||
| 
 | ||||
|         if member.bot: | ||||
|             log.debug("Won't remove reactions added by bots") | ||||
|             return | ||||
| 
 | ||||
|         if await self.bot.cog_disabled_in_guild(self, member.guild): | ||||
| @ -292,19 +287,14 @@ class ReactRestrict(Cog): | ||||
|         try: | ||||
|             roles = [self._get_role(member.guild, c.role_id) for c in combos] | ||||
|         except LookupError: | ||||
|             log.exception("Couldn't get approved roles from combos") | ||||
|             return | ||||
| 
 | ||||
|         for apprrole in roles: | ||||
|             if apprrole in member.roles: | ||||
|                 log.debug("Has approved role") | ||||
|                 return | ||||
| 
 | ||||
|         message = await self._get_message_from_channel(channel_id, message_id) | ||||
|         try: | ||||
|             await message.remove_reaction(emoji, member) | ||||
|         except (discord.Forbidden, discord.NotFound, discord.HTTPException): | ||||
|             log.exception("Unable to remove reaction") | ||||
|         await message.remove_reaction(emoji, member) | ||||
| 
 | ||||
|     #     try: | ||||
|     #         await member.add_roles(*roles) | ||||
|  | ||||
| @ -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( | ||||
|  | ||||
| @ -28,8 +28,8 @@ class RPSLS(Cog): | ||||
|     @commands.command() | ||||
|     async def rpsls(self, ctx: commands.Context, choice: str): | ||||
|         """ | ||||
|         Play Rock Paper Scissors Lizard Spock by Sam Kass in Discord! | ||||
| 
 | ||||
|         Play Rock Paper Scissors Lizard Spock by Sam Kass in Discord!  | ||||
|          | ||||
|         Rules: | ||||
|         Scissors cuts Paper | ||||
|         Paper covers Rock | ||||
| @ -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 | ||||
|  | ||||
| @ -1,5 +1,7 @@ | ||||
| import aiohttp | ||||
| import html2text | ||||
| import justext | ||||
| 
 | ||||
| from redbot.core import Config, commands | ||||
| from redbot.core.bot import Red | ||||
| from redbot.core.commands import Cog | ||||
| @ -57,3 +59,15 @@ class SayUrl(Cog): | ||||
| 
 | ||||
|         for page in pagify(h.handle(site)): | ||||
|             await ctx.send(page) | ||||
| 
 | ||||
|         # for paragraph in justext.justext(site, justext.get_stoplist("English")): | ||||
|         #     if not paragraph.is_boilerplate: | ||||
|         #         await ctx.send(paragraph.text) | ||||
| 
 | ||||
|         await ctx.tick() | ||||
| 
 | ||||
|         # paragraphs = justext.justext(response.content, justext.get_stoplist("English")) | ||||
|         # for paragraph in paragraphs: | ||||
|         #     if not paragraph.is_boilerplate: | ||||
|         #         print | ||||
|         #         paragraph.text | ||||
| @ -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): | ||||
| @ -51,7 +50,6 @@ class StealEmoji(Cog): | ||||
|         default_global = { | ||||
|             "stolemoji": {}, | ||||
|             "guildbanks": [], | ||||
|             "autobanked_guilds": [], | ||||
|             "on": False, | ||||
|             "notify": 0, | ||||
|             "autobank": False, | ||||
| @ -70,7 +68,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 +99,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") | ||||
| @ -147,54 +145,11 @@ class StealEmoji(Cog): | ||||
| 
 | ||||
|         await ctx.maybe_send_embed("AutoBanking is now " + str(not curr_setting)) | ||||
| 
 | ||||
|     @checks.is_owner() | ||||
|     @commands.guild_only() | ||||
|     @stealemoji.command(name="deleteserver", aliases=["deleteguild"]) | ||||
|     async def se_deleteserver(self, ctx: commands.Context, guild_id=None): | ||||
|         """Delete servers the bot is the owner of. | ||||
| 
 | ||||
|         Useful for auto-generated guildbanks.""" | ||||
|         if guild_id is None: | ||||
|             guild = ctx.guild | ||||
|         else: | ||||
|             guild = await self.bot.get_guild(guild_id) | ||||
| 
 | ||||
|         if guild is None: | ||||
|             await ctx.maybe_send_embed("Failed to get guild, cancelling") | ||||
|             return | ||||
|         guild: discord.Guild | ||||
|         await ctx.maybe_send_embed( | ||||
|             f"Will attempt to delete {guild.name} ({guild.id})\n" f"Okay to continue? (yes/no)" | ||||
|         ) | ||||
| 
 | ||||
|         def check(m): | ||||
|             return m.author == ctx.author and m.channel == ctx.channel | ||||
| 
 | ||||
|         try: | ||||
|             answer = await self.bot.wait_for("message", timeout=120, check=check) | ||||
|         except asyncio.TimeoutError: | ||||
|             await ctx.send("Timed out, canceling") | ||||
|             return | ||||
| 
 | ||||
|         if answer.content.upper() not in ["Y", "YES"]: | ||||
|             await ctx.maybe_send_embed("Cancelling") | ||||
|             return | ||||
|         try: | ||||
|             await guild.delete() | ||||
|         except discord.Forbidden: | ||||
|             log.exception("No permission to delete. I'm probably not the guild owner") | ||||
|             await ctx.maybe_send_embed("No permission to delete. I'm probably not the guild owner") | ||||
|         except discord.HTTPException: | ||||
|             log.exception("Unexpected error when deleting guild") | ||||
|             await ctx.maybe_send_embed("Unexpected error when deleting guild") | ||||
|         else: | ||||
|             await self.bot.send_to_owners(f"Guild {guild.name} deleted") | ||||
| 
 | ||||
|     @checks.is_owner() | ||||
|     @commands.guild_only() | ||||
|     @stealemoji.command(name="bank") | ||||
|     async def se_bank(self, ctx): | ||||
|         """Add or remove current server as emoji bank""" | ||||
|         """Add current server as emoji bank""" | ||||
| 
 | ||||
|         def check(m): | ||||
|             return ( | ||||
| @ -269,36 +224,34 @@ 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) | ||||
| 
 | ||||
|             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(): | ||||
|  | ||||
| @ -1,7 +1,6 @@ | ||||
| import asyncio | ||||
| import logging | ||||
| from datetime import datetime, timedelta | ||||
| from typing import Optional | ||||
| 
 | ||||
| import discord | ||||
| from redbot.core import Config, checks, commands | ||||
| @ -20,15 +19,6 @@ async def sleep_till_next_hour(): | ||||
|     await asyncio.sleep((next_hour - datetime.utcnow()).seconds) | ||||
| 
 | ||||
| 
 | ||||
| async def announce_to_channel(channel, results, title): | ||||
|     if channel is not None and results: | ||||
|         await channel.send(title) | ||||
|         for page in pagify(results, shorten_by=50): | ||||
|             await channel.send(page) | ||||
|     elif results:  # Channel is None, log the results | ||||
|         log.info(results) | ||||
| 
 | ||||
| 
 | ||||
| class Timerole(Cog): | ||||
|     """Add roles to users based on time on server""" | ||||
| 
 | ||||
| @ -37,15 +27,10 @@ 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_rolemember = {"had_role": False, "check_again_time": None} | ||||
|         default_guild = {"announce": None, "roles": {}} | ||||
| 
 | ||||
|         self.config.register_global(**default_global) | ||||
|         self.config.register_guild(**default_guild) | ||||
| 
 | ||||
|         self.config.init_custom("RoleMember", 2) | ||||
|         self.config.register_custom("RoleMember", **default_rolemember) | ||||
| 
 | ||||
|         self.updating = asyncio.create_task(self.check_hour()) | ||||
| 
 | ||||
|     async def red_delete_data_for_user(self, **kwargs): | ||||
| @ -64,20 +49,18 @@ class Timerole(Cog): | ||||
| 
 | ||||
|         Useful for troubleshooting the initial setup | ||||
|         """ | ||||
|         async with ctx.typing(): | ||||
|             pre_run = datetime.utcnow() | ||||
|             await self.timerole_update() | ||||
|             after_run = datetime.utcnow() | ||||
|             await ctx.tick() | ||||
| 
 | ||||
|         await ctx.maybe_send_embed(f"Took {after_run-pre_run} seconds") | ||||
|         async with ctx.typing(): | ||||
|             await self.timerole_update() | ||||
|             await ctx.tick() | ||||
| 
 | ||||
|     @commands.group() | ||||
|     @checks.mod_or_permissions(administrator=True) | ||||
|     @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 +75,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 | ||||
| 
 | ||||
| @ -104,7 +84,9 @@ class Timerole(Cog): | ||||
| 
 | ||||
|         await self.config.guild(guild).roles.set_raw(role.id, value=to_set) | ||||
|         await ctx.maybe_send_embed( | ||||
|             f"Time Role for {role.name} set to {days} days  and {hours} hours until added" | ||||
|             "Time Role for {0} set to {1} days  and {2} hours until added".format( | ||||
|                 role.name, days, hours | ||||
|             ) | ||||
|         ) | ||||
| 
 | ||||
|     @timerole.command() | ||||
| @ -132,35 +114,18 @@ class Timerole(Cog): | ||||
| 
 | ||||
|         await self.config.guild(guild).roles.set_raw(role.id, value=to_set) | ||||
|         await ctx.maybe_send_embed( | ||||
|             f"Time Role for {role.name} set to {days} days  and {hours} hours until removed" | ||||
|             "Time Role for {0} set to {1} days and {2} hours until removed".format( | ||||
|                 role.name, days, hours | ||||
|             ) | ||||
|         ) | ||||
| 
 | ||||
|     @timerole.command() | ||||
|     async def channel(self, ctx: commands.Context, channel: Optional[discord.TextChannel] = None): | ||||
|     async def channel(self, ctx: commands.Context, channel: discord.TextChannel): | ||||
|         """Sets the announce channel for role adds""" | ||||
|         guild = ctx.guild | ||||
|         if channel is None: | ||||
|             await self.config.guild(guild).announce.clear() | ||||
|             await ctx.maybe_send_embed(f"Announce channel has been cleared") | ||||
|         else: | ||||
|             await self.config.guild(guild).announce.set(channel.id) | ||||
|             await ctx.send(f"Announce channel set to {channel.mention}") | ||||
| 
 | ||||
|     @timerole.command() | ||||
|     async def reapply(self, ctx: commands.Context): | ||||
|         """Toggle reapplying roles if the member loses it somehow. Defaults to True""" | ||||
|         guild = ctx.guild | ||||
|         current_setting = await self.config.guild(guild).reapply() | ||||
|         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}") | ||||
|         await self.config.guild(guild).announce.set(channel.id) | ||||
|         await ctx.send("Announce channel set to {0}".format(channel.mention)) | ||||
| 
 | ||||
|     @timerole.command() | ||||
|     async def delrole(self, ctx: commands.Context, role: discord.Role): | ||||
| @ -168,8 +133,7 @@ class Timerole(Cog): | ||||
|         guild = ctx.guild | ||||
| 
 | ||||
|         await self.config.guild(guild).roles.set_raw(role.id, value=None) | ||||
|         await self.config.custom("RoleMember", role.id).clear() | ||||
|         await ctx.maybe_send_embed(f"{role.name} will no longer be applied") | ||||
|         await ctx.send("{0} will no longer be applied".format(role.name)) | ||||
| 
 | ||||
|     @timerole.command() | ||||
|     async def list(self, ctx: commands.Context): | ||||
| @ -189,211 +153,95 @@ class Timerole(Cog): | ||||
|                         str(discord.utils.get(guild.roles, id=int(new_id))) | ||||
|                         for new_id in r_data["required"] | ||||
|                     ] | ||||
|                 out += f"{role} | {r_data['days']} days | requires: {r_roles}\n" | ||||
|                 out += "{} | {} days | requires: {}\n".format(str(role), r_data["days"], r_roles) | ||||
|         await ctx.maybe_send_embed(out) | ||||
| 
 | ||||
|     async def timerole_update(self): | ||||
|         utcnow = datetime.utcnow() | ||||
|         all_guilds = await self.config.all_guilds() | ||||
| 
 | ||||
|         # all_mrs = await self.config.custom("RoleMember").all() | ||||
| 
 | ||||
|         # log.debug(f"Begin timerole update") | ||||
| 
 | ||||
|         for guild in self.bot.guilds: | ||||
|             guild_id = guild.id | ||||
|             if guild_id not in all_guilds: | ||||
|                 log.debug(f"Guild has no configured settings: {guild}") | ||||
|             addlist = [] | ||||
|             removelist = [] | ||||
| 
 | ||||
|             role_dict = await self.config.guild(guild).roles() | ||||
|             if not any(role_data for role_data in role_dict.values()):  # No roles | ||||
|                 continue | ||||
| 
 | ||||
|             add_results = "" | ||||
|             remove_results = "" | ||||
|             reapply = all_guilds[guild_id]["reapply"] | ||||
|             role_dict = all_guilds[guild_id]["roles"] | ||||
|             skipbots = all_guilds[guild_id]["skipbots"] | ||||
|             async for member in AsyncIter(guild.members): | ||||
|                 has_roles = [r.id for r in member.roles] | ||||
| 
 | ||||
|             if not any(role_dict.values()):  # No roles | ||||
|                 log.debug(f"No roles are configured for guild: {guild}") | ||||
|                 continue | ||||
| 
 | ||||
|             # all_mr = await self.config.all_custom("RoleMember") | ||||
|             # log.debug(f"{all_mr=}") | ||||
| 
 | ||||
|             async for member in AsyncIter(guild.members, steps=10): | ||||
| 
 | ||||
|                 if member.bot and skipbots: | ||||
|                     continue | ||||
| 
 | ||||
|                 addlist = [] | ||||
|                 removelist = [] | ||||
| 
 | ||||
|                 for role_id, role_data in role_dict.items(): | ||||
|                     # Skip non-configured roles | ||||
|                     if not role_data: | ||||
|                         continue | ||||
| 
 | ||||
|                     mr_dict = await self.config.custom("RoleMember", role_id, member.id).all() | ||||
| 
 | ||||
|                     # Stop if they've had the role and reapplying is disabled | ||||
|                     if not reapply and mr_dict["had_role"]: | ||||
|                         log.debug(f"{member.display_name} - Not reapplying") | ||||
|                         continue | ||||
| 
 | ||||
|                     # Stop if the check_again_time hasn't passed yet | ||||
|                     if ( | ||||
|                         mr_dict["check_again_time"] is not None | ||||
|                         and datetime.fromisoformat(mr_dict["check_again_time"]) >= utcnow | ||||
|                     ): | ||||
|                         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} | ||||
| 
 | ||||
|                     # 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 ( | ||||
|                         int(role_id) not in has_roles and role_data["remove"] | ||||
|                     ): | ||||
|                         if not mr_dict["had_role"]: | ||||
|                             await self.config.custom( | ||||
|                                 "RoleMember", role_id, member.id | ||||
|                             ).had_role.set(True) | ||||
|                         log.debug(f"{member.display_name} - applying had_role") | ||||
|                         continue | ||||
| 
 | ||||
|                     # Stop if they don't have all the required roles | ||||
|                     if role_data is None or ( | ||||
|                         "required" in role_data and not set(role_data["required"]) & has_roles | ||||
|                     ): | ||||
|                         continue | ||||
| 
 | ||||
|                     check_time = member.joined_at + timedelta( | ||||
|                         days=role_data["days"], | ||||
|                         hours=role_data.get("hours", 0), | ||||
|                     ) | ||||
| 
 | ||||
|                     # Check if enough time has passed to get the role and save the check_again_time | ||||
|                     if check_time >= utcnow: | ||||
|                         await self.config.custom( | ||||
|                             "RoleMember", role_id, member.id | ||||
|                         ).check_again_time.set(check_time.isoformat()) | ||||
|                         log.debug( | ||||
|                             f"{member.display_name} - Not enough time has passed to qualify for the role\n" | ||||
|                             f"Waiting until {check_time}" | ||||
|                         ) | ||||
|                         continue | ||||
| 
 | ||||
|                     if role_data["remove"]: | ||||
|                         removelist.append(role_id) | ||||
|                     else: | ||||
|                         addlist.append(role_id) | ||||
| 
 | ||||
|                 # Done iterating through roles, now add or remove the roles | ||||
|                 if not addlist and not removelist: | ||||
|                     continue | ||||
| 
 | ||||
|                 # log.debug(f"{addlist=}\n{removelist=}") | ||||
|                 add_roles = [ | ||||
|                     discord.utils.get(guild.roles, id=int(role_id)) for role_id in addlist | ||||
|                     int(rID) | ||||
|                     for rID, r_data in role_dict.items() | ||||
|                     if r_data is not None and not r_data["remove"] | ||||
|                 ] | ||||
|                 remove_roles = [ | ||||
|                     discord.utils.get(guild.roles, id=int(role_id)) for role_id in removelist | ||||
|                     int(rID) | ||||
|                     for rID, r_data in role_dict.items() | ||||
|                     if r_data is not None and r_data["remove"] | ||||
|                 ] | ||||
| 
 | ||||
|                 if None in add_roles or None in remove_roles: | ||||
|                     log.info( | ||||
|                         f"Timerole ran into an error with the roles in: {add_roles + remove_roles}" | ||||
|                     ) | ||||
|                 check_add_roles = set(add_roles) - set(has_roles) | ||||
|                 check_remove_roles = set(remove_roles) & set(has_roles) | ||||
| 
 | ||||
|                 if addlist: | ||||
|                     try: | ||||
|                         await member.add_roles(*add_roles, reason="Timerole", atomic=False) | ||||
|                     except (discord.Forbidden, discord.NotFound) as e: | ||||
|                         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" | ||||
|                         ) | ||||
|                         for role_id in addlist: | ||||
|                             await self.config.custom( | ||||
|                                 "RoleMember", role_id, member.id | ||||
|                             ).had_role.set(True) | ||||
|                 await self.check_required_and_date( | ||||
|                     addlist, check_add_roles, has_roles, member, role_dict | ||||
|                 ) | ||||
|                 await self.check_required_and_date( | ||||
|                     removelist, check_remove_roles, has_roles, member, role_dict | ||||
|                 ) | ||||
| 
 | ||||
|                 if removelist: | ||||
|                     try: | ||||
|                         await member.remove_roles(*remove_roles, reason="Timerole", atomic=False) | ||||
|                     except (discord.Forbidden, discord.NotFound) as e: | ||||
|                         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" | ||||
|                         ) | ||||
|                         for role_id in removelist: | ||||
|                             await self.config.custom( | ||||
|                                 "RoleMember", role_id, member.id | ||||
|                             ).had_role.set(True) | ||||
| 
 | ||||
|             # Done iterating through members, now maybe announce to the guild | ||||
|             channel = await self.config.guild(guild).announce() | ||||
|             if channel is not None: | ||||
|                 channel = guild.get_channel(channel) | ||||
| 
 | ||||
|             if add_results: | ||||
|                 title = "**These members have received the following roles**\n" | ||||
|                 await announce_to_channel(channel, add_results, title) | ||||
|             if remove_results: | ||||
|                 title = "**These members have lost the following roles**\n" | ||||
|                 await announce_to_channel(channel, remove_results, title) | ||||
|         # End | ||||
|             title = "**These members have received the following roles**\n" | ||||
|             await self.announce_roles(title, addlist, channel, guild, to_add=True) | ||||
|             title = "**These members have lost the following roles**\n" | ||||
|             await self.announce_roles(title, removelist, channel, guild, to_add=False) | ||||
| 
 | ||||
|     # async def announce_roles(self, title, role_list, channel, guild, to_add: True): | ||||
|     #     results = "" | ||||
|     #     async for member, role_id in AsyncIter(role_list): | ||||
|     #         role = discord.utils.get(guild.roles, id=role_id) | ||||
|     #         try: | ||||
|     #             if to_add: | ||||
|     #                 await member.add_roles(role, reason="Timerole") | ||||
|     #             else: | ||||
|     #                 await member.remove_roles(role, reason="Timerole") | ||||
|     #         except (discord.Forbidden, discord.NotFound) as e: | ||||
|     #             results += f"{member.display_name} : {role.name} **(Failed)**\n" | ||||
|     #         else: | ||||
|     #             results += f"{member.display_name} : {role.name}\n" | ||||
|     #     if channel is not None and results: | ||||
|     #         await channel.send(title) | ||||
|     #         for page in pagify(results, shorten_by=50): | ||||
|     #             await channel.send(page) | ||||
|     #     elif results:  # Channel is None, log the results | ||||
|     #         log.info(results) | ||||
|     async def announce_roles(self, title, role_list, channel, guild, to_add: True): | ||||
|         results = "" | ||||
|         for member, role_id in role_list: | ||||
|             role = discord.utils.get(guild.roles, id=role_id) | ||||
|             try: | ||||
|                 if to_add: | ||||
|                     await member.add_roles(role, reason="Timerole") | ||||
|                 else: | ||||
|                     await member.remove_roles(role, reason="Timerole") | ||||
|             except (discord.Forbidden, discord.NotFound) as e: | ||||
|                 results += "{} : {} **(Failed)**\n".format(member.display_name, role.name) | ||||
|             else: | ||||
|                 results += "{} : {}\n".format(member.display_name, role.name) | ||||
|         if channel is not None and results: | ||||
|             await channel.send(title) | ||||
|             for page in pagify(results, shorten_by=50): | ||||
|                 await channel.send(page) | ||||
|         elif results:  # Channel is None, log the results | ||||
|             log.info(results) | ||||
| 
 | ||||
|     # async def check_required_and_date(self, role_list, check_roles, has_roles, member, role_dict): | ||||
|     #     async for role_id in AsyncIter(check_roles): | ||||
|     #         # Check for required role | ||||
|     #         if "required" in role_dict[str(role_id)]: | ||||
|     #             if not set(role_dict[str(role_id)]["required"]) & set(has_roles): | ||||
|     #                 # Doesn't have required role | ||||
|     #                 continue | ||||
|     # | ||||
|     #         if ( | ||||
|     #             member.joined_at | ||||
|     #             + timedelta( | ||||
|     #                 days=role_dict[str(role_id)]["days"], | ||||
|     #                 hours=role_dict[str(role_id)].get("hours", 0), | ||||
|     #             ) | ||||
|     #             <= datetime.utcnow() | ||||
|     #         ): | ||||
|     #             # Qualifies | ||||
|     #             role_list.append((member, role_id)) | ||||
|     async def check_required_and_date(self, role_list, check_roles, has_roles, member, role_dict): | ||||
|         for role_id in check_roles: | ||||
|             # Check for required role | ||||
|             if "required" in role_dict[str(role_id)]: | ||||
|                 if not set(role_dict[str(role_id)]["required"]) & set(has_roles): | ||||
|                     # Doesn't have required role | ||||
|                     continue | ||||
| 
 | ||||
|             if ( | ||||
|                 member.joined_at | ||||
|                 + timedelta( | ||||
|                     days=role_dict[str(role_id)]["days"], | ||||
|                     hours=role_dict[str(role_id)].get("hours", 0), | ||||
|                 ) | ||||
|                 <= datetime.today() | ||||
|             ): | ||||
|                 # Qualifies | ||||
|                 role_list.append((member, role_id)) | ||||
| 
 | ||||
|     async def check_hour(self): | ||||
|         await sleep_till_next_hour() | ||||
|         while self is self.bot.get_cog("Timerole"): | ||||
|             await self.timerole_update() | ||||
|             await sleep_till_next_hour() | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										50
									
								
								tts/tts.py
									
									
									
									
									
								
							
							
						
						
									
										50
									
								
								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() | ||||
| 
 | ||||
|        Send Text to speech messages as an mp3 | ||||
|        """ | ||||
|         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): | ||||
|  | ||||
| @ -1,7 +1,5 @@ | ||||
| import bisect | ||||
| import logging | ||||
| from collections import defaultdict | ||||
| from operator import attrgetter | ||||
| from random import choice | ||||
| 
 | ||||
| import discord | ||||
| @ -10,57 +8,79 @@ import discord | ||||
| # Import all roles here | ||||
| from redbot.core import commands | ||||
| 
 | ||||
| # from .roles.seer import Seer | ||||
| # from .roles.vanillawerewolf import VanillaWerewolf | ||||
| # from .roles.villager import Villager | ||||
| 
 | ||||
| from werewolf import roles | ||||
| from .roles.seer import Seer | ||||
| from .roles.vanillawerewolf import VanillaWerewolf | ||||
| from .roles.villager import Villager | ||||
| from redbot.core.utils.menus import menu, prev_page, next_page, close_menu | ||||
| 
 | ||||
| from werewolf.constants import ROLE_CATEGORY_DESCRIPTIONS | ||||
| from werewolf.role import Role | ||||
| 
 | ||||
| log = logging.getLogger("red.fox_v3.werewolf.builder") | ||||
| 
 | ||||
| # All roles in this list for iterating | ||||
| 
 | ||||
| ROLE_DICT = {name: cls for name, cls in roles.__dict__.items() if isinstance(cls, type)} | ||||
| ROLE_LIST = sorted( | ||||
|     [cls for cls in ROLE_DICT.values()], | ||||
|     key=attrgetter("alignment"), | ||||
| ) | ||||
| ROLE_LIST = sorted([Villager, Seer, VanillaWerewolf], key=lambda x: x.alignment) | ||||
| 
 | ||||
| log.debug(f"{ROLE_DICT=}") | ||||
| 
 | ||||
| # Town, Werewolf, Neutral | ||||
| ALIGNMENT_COLORS = [0x008000, 0xFF0000, 0xC0C0C0] | ||||
| ALIGNMENT_COLORS = [0x008000, 0xff0000, 0xc0c0c0] | ||||
| TOWN_ROLES = [(idx, role) for idx, role in enumerate(ROLE_LIST) if role.alignment == 1] | ||||
| WW_ROLES = [(idx, role) for idx, role in enumerate(ROLE_LIST) if role.alignment == 2] | ||||
| OTHER_ROLES = [(idx, role) for idx, role in enumerate(ROLE_LIST) if role.alignment not in [0, 1]] | ||||
| 
 | ||||
| ROLE_PAGES = [] | ||||
| PAGE_GROUPS = [0] | ||||
| 
 | ||||
| ROLE_CATEGORIES = { | ||||
|     1: "Random", 2: "Investigative", 3: "Protective", 4: "Government", | ||||
|     5: "Killing", 6: "Power (Special night action)", | ||||
|     11: "Random", 12: "Deception", 15: "Killing", 16: "Support", | ||||
|     21: "Benign", 22: "Evil", 23: "Killing"} | ||||
| 
 | ||||
| CATEGORY_COUNT = [] | ||||
| 
 | ||||
| 
 | ||||
| def role_embed(idx, role: Role, color): | ||||
|     embed = discord.Embed( | ||||
|         title=f"**{idx}** - {role.__name__}", | ||||
|         description=role.game_start_message, | ||||
|         color=color, | ||||
|     ) | ||||
|     if role.icon_url is not None: | ||||
|         embed.set_thumbnail(url=role.icon_url) | ||||
| 
 | ||||
|     embed.add_field( | ||||
|         name="Alignment", value=["Town", "Werewolf", "Neutral"][role.alignment - 1], inline=False | ||||
|     ) | ||||
|     embed.add_field(name="Multiples Allowed", value=str(not role.unique), inline=False) | ||||
|     embed.add_field( | ||||
|         name="Role Types", | ||||
|         value=", ".join(ROLE_CATEGORY_DESCRIPTIONS[x] for x in role.category), | ||||
|         inline=False, | ||||
|     ) | ||||
|     embed.add_field(name="Random Option", value=str(role.rand_choice), inline=False) | ||||
| def role_embed(idx, role, color): | ||||
|     embed = discord.Embed(title="**{}** - {}".format(idx, str(role.__name__)), description=role.game_start_message, | ||||
|                           color=color) | ||||
|     embed.add_field(name='Alignment', value=['Town', 'Werewolf', 'Neutral'][role.alignment - 1], inline=True) | ||||
|     embed.add_field(name='Multiples Allowed', value=str(not role.unique), inline=True) | ||||
|     embed.add_field(name='Role Type', value=", ".join(ROLE_CATEGORIES[x] for x in role.category), inline=True) | ||||
|     embed.add_field(name='Random Option', value=str(role.rand_choice), inline=True) | ||||
| 
 | ||||
|     return embed | ||||
| 
 | ||||
| 
 | ||||
| def setup(): | ||||
|     # Roles | ||||
|     last_alignment = ROLE_LIST[0].alignment | ||||
|     for idx, role in enumerate(ROLE_LIST): | ||||
|         if role.alignment != last_alignment and len(ROLE_PAGES) - 1 not in PAGE_GROUPS: | ||||
|             PAGE_GROUPS.append(len(ROLE_PAGES) - 1) | ||||
|             last_alignment = role.alignment | ||||
| 
 | ||||
|         ROLE_PAGES.append(role_embed(idx, role, ALIGNMENT_COLORS[role.alignment - 1])) | ||||
| 
 | ||||
|     # Random Town Roles | ||||
|     if len(ROLE_PAGES) - 1 not in PAGE_GROUPS: | ||||
|         PAGE_GROUPS.append(len(ROLE_PAGES) - 1) | ||||
|     for k, v in ROLE_CATEGORIES.items(): | ||||
|         if 0 < k <= 6: | ||||
|             ROLE_PAGES.append(discord.Embed(title="RANDOM:Town Role", description="Town {}".format(v), color=0x008000)) | ||||
|             CATEGORY_COUNT.append(k) | ||||
| 
 | ||||
|     # Random WW Roles | ||||
|     if len(ROLE_PAGES) - 1 not in PAGE_GROUPS: | ||||
|         PAGE_GROUPS.append(len(ROLE_PAGES) - 1) | ||||
|     for k, v in ROLE_CATEGORIES.items(): | ||||
|         if 10 < k <= 16: | ||||
|             ROLE_PAGES.append( | ||||
|                 discord.Embed(title="RANDOM:Werewolf Role", description="Werewolf {}".format(v), color=0xff0000)) | ||||
|             CATEGORY_COUNT.append(k) | ||||
|     # Random Neutral Roles | ||||
|     if len(ROLE_PAGES) - 1 not in PAGE_GROUPS: | ||||
|         PAGE_GROUPS.append(len(ROLE_PAGES) - 1) | ||||
|     for k, v in ROLE_CATEGORIES.items(): | ||||
|         if 20 < k <= 26: | ||||
|             ROLE_PAGES.append( | ||||
|                 discord.Embed(title="RANDOM:Neutral Role", description="Neutral {}".format(v), color=0xc0c0c0)) | ||||
|             CATEGORY_COUNT.append(k) | ||||
| 
 | ||||
| 
 | ||||
| """ | ||||
| Example code: | ||||
| 0 = Villager | ||||
| @ -71,7 +91,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 +109,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 +135,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") | ||||
| 
 | ||||
| @ -126,12 +147,15 @@ async def parse_code(code, game): | ||||
|     return decode | ||||
| 
 | ||||
| 
 | ||||
| async def encode(role_list, rand_roles): | ||||
| async def encode(roles, rand_roles): | ||||
|     """Convert role list to code""" | ||||
|     digit_sort = sorted(role for role in role_list if role < 10) | ||||
|     out_code = "".join(str(role) for role in digit_sort) | ||||
|     out_code = "" | ||||
| 
 | ||||
|     digit_sort = sorted(role for role in role_list if 10 <= role < 100) | ||||
|     digit_sort = sorted(role for role in roles if role < 10) | ||||
|     for role in digit_sort: | ||||
|         out_code += str(role) | ||||
| 
 | ||||
|     digit_sort = sorted(role for role in roles if 10 <= role < 100) | ||||
|     if digit_sort: | ||||
|         out_code += "-" | ||||
|         for role in digit_sort: | ||||
| @ -163,20 +187,49 @@ async def encode(role_list, rand_roles): | ||||
|     return out_code | ||||
| 
 | ||||
| 
 | ||||
| async def next_group(ctx: commands.Context, pages: list, | ||||
|                      controls: dict, message: discord.Message, page: int, | ||||
|                      timeout: float, emoji: str): | ||||
|     perms = message.channel.permissions_for(ctx.guild.me) | ||||
|     if perms.manage_messages:  # Can manage messages, so remove react | ||||
|         try: | ||||
|             await message.remove_reaction(emoji, ctx.author) | ||||
|         except discord.NotFound: | ||||
|             pass | ||||
|     page = bisect.bisect_right(PAGE_GROUPS, page) | ||||
| 
 | ||||
|     if page == len(PAGE_GROUPS): | ||||
|         page = PAGE_GROUPS[0] | ||||
|     else: | ||||
|         page = PAGE_GROUPS[page] | ||||
| 
 | ||||
|     return await menu(ctx, pages, controls, message=message, | ||||
|                       page=page, timeout=timeout) | ||||
| 
 | ||||
| 
 | ||||
| async def prev_group(ctx: commands.Context, pages: list, | ||||
|                      controls: dict, message: discord.Message, page: int, | ||||
|                      timeout: float, emoji: str): | ||||
|     perms = message.channel.permissions_for(ctx.guild.me) | ||||
|     if perms.manage_messages:  # Can manage messages, so remove react | ||||
|         try: | ||||
|             await message.remove_reaction(emoji, ctx.author) | ||||
|         except discord.NotFound: | ||||
|             pass | ||||
|     page = PAGE_GROUPS[bisect.bisect_left(PAGE_GROUPS, page) - 1] | ||||
| 
 | ||||
|     return await menu(ctx, pages, controls, message=message, | ||||
|                       page=page, timeout=timeout) | ||||
| 
 | ||||
| 
 | ||||
| def role_from_alignment(alignment): | ||||
|     return [ | ||||
|         role_embed(idx, role, ALIGNMENT_COLORS[role.alignment - 1]) | ||||
|         for idx, role in enumerate(ROLE_LIST) | ||||
|         if alignment == role.alignment | ||||
|     ] | ||||
|     return [role_embed(idx, role, ALIGNMENT_COLORS[role.alignment - 1]) | ||||
|             for idx, role in enumerate(ROLE_LIST) if alignment == role.alignment] | ||||
| 
 | ||||
| 
 | ||||
| def role_from_category(category): | ||||
|     return [ | ||||
|         role_embed(idx, role, ALIGNMENT_COLORS[role.alignment - 1]) | ||||
|         for idx, role in enumerate(ROLE_LIST) | ||||
|         if category in role.category | ||||
|     ] | ||||
|     return [role_embed(idx, role, ALIGNMENT_COLORS[role.alignment - 1]) | ||||
|             for idx, role in enumerate(ROLE_LIST) if category in role.category] | ||||
| 
 | ||||
| 
 | ||||
| def role_from_id(idx): | ||||
| @ -189,11 +242,8 @@ def role_from_id(idx): | ||||
| 
 | ||||
| 
 | ||||
| def role_from_name(name: str): | ||||
|     return [ | ||||
|         role_embed(idx, role, ALIGNMENT_COLORS[role.alignment - 1]) | ||||
|         for idx, role in enumerate(ROLE_LIST) | ||||
|         if name in role.__name__ | ||||
|     ] | ||||
|     return [role_embed(idx, role, ALIGNMENT_COLORS[role.alignment - 1]) | ||||
|             for idx, role in enumerate(ROLE_LIST) if name in role.__name__] | ||||
| 
 | ||||
| 
 | ||||
| def say_role_list(code_list, rand_roles): | ||||
| @ -205,87 +255,34 @@ def say_role_list(code_list, rand_roles): | ||||
| 
 | ||||
|     for role in rand_roles: | ||||
|         if 0 < role <= 6: | ||||
|             role_dict[f"Town {ROLE_CATEGORY_DESCRIPTIONS[role]}"] += 1 | ||||
|             role_dict["Town {}".format(ROLE_CATEGORIES[role])] += 1 | ||||
|         if 10 < role <= 16: | ||||
|             role_dict[f"Werewolf {ROLE_CATEGORY_DESCRIPTIONS[role]}"] += 1 | ||||
|             role_dict["Werewolf {}".format(ROLE_CATEGORIES[role])] += 1 | ||||
|         if 20 < role <= 26: | ||||
|             role_dict[f"Neutral {ROLE_CATEGORY_DESCRIPTIONS[role]}"] += 1 | ||||
|             role_dict["Neutral {}".format(ROLE_CATEGORIES[role])] += 1 | ||||
| 
 | ||||
|     for k, v in role_dict.items(): | ||||
|         embed.add_field(name=k, value=f"Count: {v}", inline=True) | ||||
|         embed.add_field(name=k, value="Count: {}".format(v), inline=True) | ||||
| 
 | ||||
|     return embed | ||||
| 
 | ||||
| 
 | ||||
| class GameBuilder: | ||||
| 
 | ||||
|     def __init__(self): | ||||
|         self.code = [] | ||||
|         self.rand_roles = [] | ||||
|         self.page_groups = [0] | ||||
|         self.category_count = [] | ||||
| 
 | ||||
|         self.setup() | ||||
| 
 | ||||
|     def setup(self): | ||||
|         # Roles | ||||
|         last_alignment = ROLE_LIST[0].alignment | ||||
|         for idx, role in enumerate(ROLE_LIST): | ||||
|             if role.alignment != last_alignment and len(ROLE_PAGES) - 1 not in self.page_groups: | ||||
|                 self.page_groups.append(len(ROLE_PAGES) - 1) | ||||
|                 last_alignment = role.alignment | ||||
| 
 | ||||
|             ROLE_PAGES.append(role_embed(idx, role, ALIGNMENT_COLORS[role.alignment - 1])) | ||||
| 
 | ||||
|         # Random Town Roles | ||||
|         if len(ROLE_PAGES) - 1 not in self.page_groups: | ||||
|             self.page_groups.append(len(ROLE_PAGES) - 1) | ||||
|         for k, v in ROLE_CATEGORY_DESCRIPTIONS.items(): | ||||
|             if 0 < k <= 9: | ||||
|                 ROLE_PAGES.append( | ||||
|                     discord.Embed( | ||||
|                         title="RANDOM:Town Role", | ||||
|                         description=f"Town {v}", | ||||
|                         color=ALIGNMENT_COLORS[0], | ||||
|                     ) | ||||
|                 ) | ||||
|                 self.category_count.append(k) | ||||
| 
 | ||||
|         # Random WW Roles | ||||
|         if len(ROLE_PAGES) - 1 not in self.page_groups: | ||||
|             self.page_groups.append(len(ROLE_PAGES) - 1) | ||||
|         for k, v in ROLE_CATEGORY_DESCRIPTIONS.items(): | ||||
|             if 10 < k <= 19: | ||||
|                 ROLE_PAGES.append( | ||||
|                     discord.Embed( | ||||
|                         title="RANDOM:Werewolf Role", | ||||
|                         description=f"Werewolf {v}", | ||||
|                         color=ALIGNMENT_COLORS[1], | ||||
|                     ) | ||||
|                 ) | ||||
|                 self.category_count.append(k) | ||||
|         # Random Neutral Roles | ||||
|         if len(ROLE_PAGES) - 1 not in self.page_groups: | ||||
|             self.page_groups.append(len(ROLE_PAGES) - 1) | ||||
|         for k, v in ROLE_CATEGORY_DESCRIPTIONS.items(): | ||||
|             if 20 < k <= 29: | ||||
|                 ROLE_PAGES.append( | ||||
|                     discord.Embed( | ||||
|                         title=f"RANDOM:Neutral Role", | ||||
|                         description=f"Neutral {v}", | ||||
|                         color=ALIGNMENT_COLORS[2], | ||||
|                     ) | ||||
|                 ) | ||||
|                 self.category_count.append(k) | ||||
|         setup() | ||||
| 
 | ||||
|     async def build_game(self, ctx: commands.Context): | ||||
|         new_controls = { | ||||
|             "⏪": self.prev_group, | ||||
|             '⏪': prev_group, | ||||
|             "⬅": prev_page, | ||||
|             "☑": self.select_page, | ||||
|             '☑': self.select_page, | ||||
|             "➡": next_page, | ||||
|             "⏩": self.next_group, | ||||
|             "📇": self.list_roles, | ||||
|             "❌": close_menu, | ||||
|             '⏩': next_group, | ||||
|             '📇': self.list_roles, | ||||
|             "❌": close_menu | ||||
|         } | ||||
| 
 | ||||
|         await ctx.send("Browse through roles and add the ones you want using the check mark") | ||||
| @ -295,17 +292,10 @@ class GameBuilder: | ||||
|         out = await encode(self.code, self.rand_roles) | ||||
|         return out | ||||
| 
 | ||||
|     async def list_roles( | ||||
|         self, | ||||
|         ctx: commands.Context, | ||||
|         pages: list, | ||||
|         controls: dict, | ||||
|         message: discord.Message, | ||||
|         page: int, | ||||
|         timeout: float, | ||||
|         emoji: str, | ||||
|     ): | ||||
|         perms = message.channel.permissions_for(ctx.me) | ||||
|     async def list_roles(self, ctx: commands.Context, pages: list, | ||||
|                          controls: dict, message: discord.Message, page: int, | ||||
|                          timeout: float, emoji: str): | ||||
|         perms = message.channel.permissions_for(ctx.guild.me) | ||||
|         if perms.manage_messages:  # Can manage messages, so remove react | ||||
|             try: | ||||
|                 await message.remove_reaction(emoji, ctx.author) | ||||
| @ -314,19 +304,13 @@ class GameBuilder: | ||||
| 
 | ||||
|         await ctx.send(embed=say_role_list(self.code, self.rand_roles)) | ||||
| 
 | ||||
|         return await menu(ctx, pages, controls, message=message, page=page, timeout=timeout) | ||||
|         return await menu(ctx, pages, controls, message=message, | ||||
|                           page=page, timeout=timeout) | ||||
| 
 | ||||
|     async def select_page( | ||||
|         self, | ||||
|         ctx: commands.Context, | ||||
|         pages: list, | ||||
|         controls: dict, | ||||
|         message: discord.Message, | ||||
|         page: int, | ||||
|         timeout: float, | ||||
|         emoji: str, | ||||
|     ): | ||||
|         perms = message.channel.permissions_for(ctx.me) | ||||
|     async def select_page(self, ctx: commands.Context, pages: list, | ||||
|                           controls: dict, message: discord.Message, page: int, | ||||
|                           timeout: float, emoji: str): | ||||
|         perms = message.channel.permissions_for(ctx.guild.me) | ||||
|         if perms.manage_messages:  # Can manage messages, so remove react | ||||
|             try: | ||||
|                 await message.remove_reaction(emoji, ctx.author) | ||||
| @ -334,53 +318,9 @@ class GameBuilder: | ||||
|                 pass | ||||
| 
 | ||||
|         if page >= len(ROLE_LIST): | ||||
|             self.rand_roles.append(self.category_count[page - len(ROLE_LIST)]) | ||||
|             self.rand_roles.append(CATEGORY_COUNT[page - len(ROLE_LIST)]) | ||||
|         else: | ||||
|             self.code.append(page) | ||||
| 
 | ||||
|         return await menu(ctx, pages, controls, message=message, page=page, timeout=timeout) | ||||
| 
 | ||||
|     async def next_group( | ||||
|         self, | ||||
|         ctx: commands.Context, | ||||
|         pages: list, | ||||
|         controls: dict, | ||||
|         message: discord.Message, | ||||
|         page: int, | ||||
|         timeout: float, | ||||
|         emoji: str, | ||||
|     ): | ||||
|         perms = message.channel.permissions_for(ctx.me) | ||||
|         if perms.manage_messages:  # Can manage messages, so remove react | ||||
|             try: | ||||
|                 await message.remove_reaction(emoji, ctx.author) | ||||
|             except discord.NotFound: | ||||
|                 pass | ||||
|         page = bisect.bisect_right(self.page_groups, page) | ||||
| 
 | ||||
|         if page == len(self.page_groups): | ||||
|             page = self.page_groups[0] | ||||
|         else: | ||||
|             page = self.page_groups[page] | ||||
| 
 | ||||
|         return await menu(ctx, pages, controls, message=message, page=page, timeout=timeout) | ||||
| 
 | ||||
|     async def prev_group( | ||||
|         self, | ||||
|         ctx: commands.Context, | ||||
|         pages: list, | ||||
|         controls: dict, | ||||
|         message: discord.Message, | ||||
|         page: int, | ||||
|         timeout: float, | ||||
|         emoji: str, | ||||
|     ): | ||||
|         perms = message.channel.permissions_for(ctx.me) | ||||
|         if perms.manage_messages:  # Can manage messages, so remove react | ||||
|             try: | ||||
|                 await message.remove_reaction(emoji, ctx.author) | ||||
|             except discord.NotFound: | ||||
|                 pass | ||||
|         page = self.page_groups[bisect.bisect_left(self.page_groups, page) - 1] | ||||
| 
 | ||||
|         return await menu(ctx, pages, controls, message=message, page=page, timeout=timeout) | ||||
|         return await menu(ctx, pages, controls, message=message, | ||||
|                           page=page, timeout=timeout) | ||||
|  | ||||
| @ -1,91 +0,0 @@ | ||||
| """ | ||||
| Role Constants | ||||
| 
 | ||||
|     Role Alignment guide as follows: | ||||
|         Town: 1 | ||||
|         Werewolf: 2 | ||||
|         Neutral: 3 | ||||
| 
 | ||||
|         Additional alignments may be added when warring factions are added | ||||
|         (Rival werewolves, cultists, vampires) | ||||
| 
 | ||||
|     Role Category enrollment guide as follows (See Role.category): | ||||
|         Town: | ||||
|         1: Random, 2: Investigative, 3: Protective, 4: Government, | ||||
|         5: Killing, 6: Power (Special night action) | ||||
| 
 | ||||
|         Werewolf: | ||||
|         11: Random, 12: Deception, 15: Killing, 16: Support | ||||
| 
 | ||||
|         Neutral: | ||||
|         21: Benign, 22: Evil, 23: Killing | ||||
| 
 | ||||
| 
 | ||||
|         Example category: | ||||
|         category = [1, 5, 6] Could be Veteran | ||||
|         category = [1, 5] Could be Bodyguard | ||||
|         category = [11, 16] Could be Werewolf Silencer | ||||
|         category = [22] Could be Blob (non-killing) | ||||
|         category = [22, 23] Could be Serial-Killer | ||||
| """ | ||||
| 
 | ||||
| 
 | ||||
| ALIGNMENT_TOWN = 1 | ||||
| ALIGNMENT_WEREWOLF = 2 | ||||
| ALIGNMENT_NEUTRAL = 3 | ||||
| ALIGNMENT_MAP = {"Town": 1, "Werewolf": 2, "Neutral": 3} | ||||
| 
 | ||||
| # 0-9: Town Role Categories | ||||
| # 10-19: Werewolf Role Categories | ||||
| # 20-29: Neutral Role Categories | ||||
| CATEGORY_TOWN_RANDOM = 1 | ||||
| CATEGORY_TOWN_INVESTIGATIVE = 2 | ||||
| CATEGORY_TOWN_PROTECTIVE = 3 | ||||
| CATEGORY_TOWN_GOVERNMENT = 4 | ||||
| CATEGORY_TOWN_KILLING = 5 | ||||
| CATEGORY_TOWN_POWER = 6 | ||||
| 
 | ||||
| CATEGORY_WW_RANDOM = 11 | ||||
| CATEGORY_WW_DECEPTION = 12 | ||||
| CATEGORY_WW_KILLING = 15 | ||||
| CATEGORY_WW_SUPPORT = 16 | ||||
| 
 | ||||
| CATEGORY_NEUTRAL_BENIGN = 21 | ||||
| CATEGORY_NEUTRAL_EVIL = 22 | ||||
| CATEGORY_NEUTRAL_KILLING = 23 | ||||
| 
 | ||||
| ROLE_CATEGORY_DESCRIPTIONS = { | ||||
|     CATEGORY_TOWN_RANDOM: "Random", | ||||
|     CATEGORY_TOWN_INVESTIGATIVE: "Investigative", | ||||
|     CATEGORY_TOWN_PROTECTIVE: "Protective", | ||||
|     CATEGORY_TOWN_GOVERNMENT: "Government", | ||||
|     CATEGORY_TOWN_KILLING: "Killing", | ||||
|     CATEGORY_TOWN_POWER: "Power (Special night action)", | ||||
|     CATEGORY_WW_RANDOM: "Random", | ||||
|     CATEGORY_WW_DECEPTION: "Deception", | ||||
|     CATEGORY_WW_KILLING: "Killing", | ||||
|     CATEGORY_WW_SUPPORT: "Support", | ||||
|     CATEGORY_NEUTRAL_BENIGN: "Benign", | ||||
|     CATEGORY_NEUTRAL_EVIL: "Evil", | ||||
|     CATEGORY_NEUTRAL_KILLING: "Killing", | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| """ | ||||
| Listener Actions Priority Guide | ||||
| 
 | ||||
|     Action priority guide as follows (see listeners.py for wolflistener): | ||||
|         _at_night_start | ||||
|         0. No Action | ||||
|         1. Detain actions (Jailer/Kidnapper) | ||||
|         2. Group discussions and choose targets | ||||
| 
 | ||||
|         _at_night_end | ||||
|         0. No Action | ||||
|         1. Self actions (Veteran) | ||||
|         2. Target switching and role blocks (bus driver, witch, escort) | ||||
|         3. Protection / Preempt actions (bodyguard/framer) | ||||
|         4. Non-disruptive actions (seer/silencer) | ||||
|         5. Disruptive actions (Killing) | ||||
|         6. Role altering actions (Cult / Mason / Shifter) | ||||
| """ | ||||
| @ -1,28 +0,0 @@ | ||||
| from typing import TYPE_CHECKING, Union | ||||
| 
 | ||||
| import discord | ||||
| from discord.ext.commands import BadArgument, Converter | ||||
| from redbot.core import commands | ||||
| 
 | ||||
| from werewolf.player import Player | ||||
| 
 | ||||
| if TYPE_CHECKING: | ||||
|     PlayerConverter = Union[int, discord.Member] | ||||
|     CronConverter = str | ||||
| else: | ||||
| 
 | ||||
|     class PlayerConverter(Converter): | ||||
|         async def convert(self, ctx, argument) -> Player: | ||||
| 
 | ||||
|             try: | ||||
|                 target = await commands.MemberConverter().convert(ctx, argument) | ||||
|             except BadArgument: | ||||
|                 try: | ||||
|                     target = int(argument) | ||||
|                     assert target >= 0 | ||||
|                 except (ValueError, AssertionError): | ||||
|                     raise BadArgument | ||||
| 
 | ||||
|             # TODO: Get the game for context without making a new one | ||||
|             # TODO: Get player from game based on either ID or member object | ||||
|             return target | ||||
							
								
								
									
										673
									
								
								werewolf/game.py
									
									
									
									
									
								
							
							
						
						
									
										673
									
								
								werewolf/game.py
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -4,10 +4,10 @@ | ||||
|   ], | ||||
|   "min_bot_version": "3.3.0", | ||||
|   "description": "Customizable Werewolf Game", | ||||
|   "hidden": false, | ||||
|   "hidden": true, | ||||
|   "install_msg": "Thank you for installing Werewolf! Get started with `[p]load werewolf`\n Use `[p]wwset` to run inital setup", | ||||
|   "requirements": [], | ||||
|   "short": "[ALPHA] Play Werewolf (Mafia) Game in discord", | ||||
|   "short": "Werewolf Game", | ||||
|   "end_user_data_statement": "This stores user IDs in memory while they're actively using the cog, and stores no persistent End User Data.", | ||||
|   "tags": [ | ||||
|     "mafia", | ||||
|  | ||||
| @ -1,106 +0,0 @@ | ||||
| import inspect | ||||
| 
 | ||||
| 
 | ||||
| def wolflistener(name=None, priority=0): | ||||
|     """A decorator that marks a function as a listener. | ||||
| 
 | ||||
|     This is the werewolf.Game equivalent of :meth:`.Cog.listener`. | ||||
| 
 | ||||
|     Parameters | ||||
|     ------------ | ||||
|     name: :class:`str` | ||||
|         The name of the event being listened to. If not provided, it | ||||
|         defaults to the function's name. | ||||
|     priority: :class:`int` | ||||
|         The priority of the listener. | ||||
|         Priority guide as follows: | ||||
|         _at_night_start | ||||
|         0. No Action | ||||
|         1. Detain actions (Jailer/Kidnapper) | ||||
|         2. Group discussions and choose targets | ||||
| 
 | ||||
|         _at_night_end | ||||
|         0. No Action | ||||
|         1. Self actions (Veteran) | ||||
|         2. Target switching and role blocks (bus driver, witch, escort) | ||||
|         3. Protection / Preempt actions (bodyguard/framer) | ||||
|         4. Non-disruptive actions (seer/silencer) | ||||
|         5. Disruptive actions (Killing) | ||||
|         6. Role altering actions (Cult / Mason / Shifter) | ||||
| 
 | ||||
|     Raises | ||||
|     -------- | ||||
|     TypeError | ||||
|         The function is not a coroutine function or a string was not passed as | ||||
|         the name. | ||||
|     """ | ||||
| 
 | ||||
|     if name is not None and not isinstance(name, str): | ||||
|         raise TypeError( | ||||
|             "Game.listener expected str but received {0.__class__.__name__!r} instead.".format( | ||||
|                 name | ||||
|             ) | ||||
|         ) | ||||
| 
 | ||||
|     def decorator(func): | ||||
|         actual = func | ||||
|         if isinstance(actual, staticmethod): | ||||
|             actual = actual.__func__ | ||||
|         if not inspect.iscoroutinefunction(actual): | ||||
|             raise TypeError("Listener function must be a coroutine function.") | ||||
|         actual.__wolf_listener__ = priority | ||||
|         to_assign = name or actual.__name__ | ||||
|         try: | ||||
|             actual.__wolf_listener_names__.append((priority, to_assign)) | ||||
|         except AttributeError: | ||||
|             actual.__wolf_listener_names__ = [(priority, to_assign)] | ||||
|         # we have to return `func` instead of `actual` because | ||||
|         # we need the type to be `staticmethod` for the metaclass | ||||
|         # to pick it up but the metaclass unfurls the function and | ||||
|         # thus the assignments need to be on the actual function | ||||
|         return func | ||||
| 
 | ||||
|     return decorator | ||||
| 
 | ||||
| 
 | ||||
| class WolfListenerMeta(type): | ||||
|     def __new__(mcs, *args, **kwargs): | ||||
|         name, bases, attrs = args | ||||
| 
 | ||||
|         listeners = {} | ||||
|         need_at_msg = "Listeners must start with at_ (in method {0.__name__}.{1})" | ||||
| 
 | ||||
|         new_cls = super().__new__(mcs, name, bases, attrs, **kwargs) | ||||
|         for base in reversed(new_cls.__mro__): | ||||
|             for elem, value in base.__dict__.items(): | ||||
|                 if elem in listeners: | ||||
|                     del listeners[elem] | ||||
| 
 | ||||
|                 is_static_method = isinstance(value, staticmethod) | ||||
|                 if is_static_method: | ||||
|                     value = value.__func__ | ||||
|                 if inspect.iscoroutinefunction(value): | ||||
|                     try: | ||||
|                         is_listener = getattr(value, "__wolf_listener__") | ||||
|                     except AttributeError: | ||||
|                         continue | ||||
|                     else: | ||||
|                         # if not elem.startswith("at_"): | ||||
|                         #     raise TypeError(need_at_msg.format(base, elem)) | ||||
|                         listeners[elem] = value | ||||
| 
 | ||||
|         listeners_as_list = [] | ||||
|         for listener in listeners.values(): | ||||
|             for priority, listener_name in listener.__wolf_listener_names__: | ||||
|                 # I use __name__ instead of just storing the value so I can inject | ||||
|                 # the self attribute when the time comes to add them to the bot | ||||
|                 listeners_as_list.append((priority, listener_name, listener.__name__)) | ||||
| 
 | ||||
|         new_cls.__wolf_listeners__ = listeners_as_list | ||||
|         return new_cls | ||||
| 
 | ||||
| 
 | ||||
| class WolfListener(metaclass=WolfListenerMeta): | ||||
|     def __init__(self, game): | ||||
|         for priority, name, method_name in self.__wolf_listeners__: | ||||
|             game.add_ww_listener(getattr(self, method_name), priority, name) | ||||
| @ -1,8 +1,4 @@ | ||||
| import logging | ||||
| 
 | ||||
| from werewolf.role import Role | ||||
| 
 | ||||
| log = logging.getLogger("red.fox_v3.werewolf.night_powers") | ||||
| from .role import Role | ||||
| 
 | ||||
| 
 | ||||
| def night_immune(role: Role): | ||||
|  | ||||
| @ -1,9 +1,5 @@ | ||||
| import logging | ||||
| 
 | ||||
| import discord | ||||
| 
 | ||||
| log = logging.getLogger("red.fox_v3.werewolf.player") | ||||
| 
 | ||||
| 
 | ||||
| class Player: | ||||
|     """ | ||||
| @ -20,9 +16,6 @@ class Player: | ||||
|         self.muted = False | ||||
|         self.protected = False | ||||
| 
 | ||||
|     def __repr__(self): | ||||
|         return f"{self.__class__.__name__}({self.member})" | ||||
| 
 | ||||
|     async def assign_role(self, role): | ||||
|         """ | ||||
|         Give this player a role | ||||
| @ -35,15 +28,6 @@ class Player: | ||||
| 
 | ||||
|     async def send_dm(self, message): | ||||
|         try: | ||||
|             await self.member.send(message)  # Lets ToDo embeds later | ||||
|             await self.member.send(message)  # Lets do embeds later | ||||
|         except discord.Forbidden: | ||||
|             log.info(f"Unable to mention {self.member.__repr__()}") | ||||
|             await self.role.game.village_channel.send( | ||||
|                 f"Couldn't DM {self.mention}, uh oh", | ||||
|                 allowed_mentions=discord.AllowedMentions(users=[self.member]), | ||||
|             ) | ||||
|         except AttributeError: | ||||
|             log.exception("Someone messed up and added a bot to the game (I think)") | ||||
|             await self.role.game.village_channel.send( | ||||
|                 "Someone messed up and added a bot to the game :eyes:" | ||||
|             ) | ||||
|             await self.role.game.village_channel.send("Couldn't DM {}, uh oh".format(self.mention)) | ||||
|  | ||||
							
								
								
									
										104
									
								
								werewolf/role.py
									
									
									
									
									
								
							
							
						
						
									
										104
									
								
								werewolf/role.py
									
									
									
									
									
								
							| @ -1,41 +1,31 @@ | ||||
| import inspect | ||||
| import logging | ||||
| 
 | ||||
| from werewolf.listener import WolfListener, wolflistener | ||||
| 
 | ||||
| log = logging.getLogger("red.fox_v3.werewolf.role") | ||||
| 
 | ||||
| 
 | ||||
| class Role(WolfListener): | ||||
| class Role: | ||||
|     """ | ||||
|     Base Role class for werewolf game | ||||
| 
 | ||||
|      | ||||
|     Category enrollment guide as follows (category property): | ||||
|         Town: | ||||
|         1: Random, 2: Investigative, 3: Protective, 4: Government, | ||||
|         5: Killing, 6: Power (Special night action) | ||||
| 
 | ||||
|          | ||||
|         Werewolf: | ||||
|         11: Random, 12: Deception, 15: Killing, 16: Support | ||||
| 
 | ||||
|          | ||||
|         Neutral: | ||||
|         21: Benign, 22: Evil, 23: Killing | ||||
| 
 | ||||
| 
 | ||||
|          | ||||
|          | ||||
|         Example category: | ||||
|         category = [1, 5, 6] Could be Veteran | ||||
|         category = [1, 5] Could be Bodyguard | ||||
|         category = [11, 16] Could be Werewolf Silencer | ||||
|         category = [22] Could be Blob (non-killing) | ||||
|         category = [22, 23] Could be Serial-Killer | ||||
| 
 | ||||
| 
 | ||||
|     Action priority guide as follows (on_event function): | ||||
|          | ||||
|      | ||||
|     Action guide as follows (on_event function): | ||||
|         _at_night_start | ||||
|         0. No Action | ||||
|         1. Detain actions (Jailer/Kidnapper) | ||||
|         2. Group discussions and choose targets | ||||
| 
 | ||||
|          | ||||
|         _at_night_end | ||||
|         0. No Action | ||||
|         1. Self actions (Veteran) | ||||
| @ -43,15 +33,13 @@ class Role(WolfListener): | ||||
|         3. Protection / Preempt actions (bodyguard/framer) | ||||
|         4. Non-disruptive actions (seer/silencer) | ||||
|         5. Disruptive actions (Killing) | ||||
|         6. Role altering actions (Cult / Mason / Shifter) | ||||
|         6. Role altering actions (Cult / Mason) | ||||
|     """ | ||||
| 
 | ||||
|     # Determines if it can be picked as a random role (False for unusually disruptive roles) | ||||
|     rand_choice = False  # TODO: Rework random with categories | ||||
|     town_balance = 0  # Guess at power level and it's balance on the town | ||||
|     rand_choice = False  # Determines if it can be picked as a random role (False for unusually disruptive roles) | ||||
|     category = [0]  # List of enrolled categories (listed above) | ||||
|     alignment = 0  # 1: Town, 2: Werewolf, 3: Neutral | ||||
|     channel_name = ""  # Empty for no private channel | ||||
|     channel_id = ""  # Empty for no private channel | ||||
|     unique = False  # Only one of this role per game | ||||
|     game_start_message = ( | ||||
|         "Your role is **Default**\n" | ||||
| @ -66,17 +54,32 @@ class Role(WolfListener): | ||||
|     icon_url = None  # Adding a URL here will enable a thumbnail of the role | ||||
| 
 | ||||
|     def __init__(self, game): | ||||
|         super().__init__(game) | ||||
|         self.game = game | ||||
|         self.player = None | ||||
|         self.blocked = False | ||||
|         self.properties = {}  # Extra data for other roles (i.e. arsonist) | ||||
| 
 | ||||
|     def __str__(self): | ||||
|         return self.__repr__() | ||||
|         self.action_list = [ | ||||
|             (self._at_game_start, 1),  # (Action, Priority) | ||||
|             (self._at_day_start, 0), | ||||
|             (self._at_voted, 0), | ||||
|             (self._at_kill, 0), | ||||
|             (self._at_hang, 0), | ||||
|             (self._at_day_end, 0), | ||||
|             (self._at_night_start, 0), | ||||
|             (self._at_night_end, 0), | ||||
|             (self._at_visit, 0) | ||||
|         ] | ||||
| 
 | ||||
|     def __repr__(self): | ||||
|         return f"{self.__class__.__name__}({self.player.__repr__()})" | ||||
|         return self.__class__.__name__ | ||||
| 
 | ||||
|     async def on_event(self, event, data): | ||||
|         """ | ||||
|         See Game class for event guide | ||||
|         """ | ||||
| 
 | ||||
|         await self.action_list[event][0](data) | ||||
| 
 | ||||
|     async def assign_player(self, player): | ||||
|         """ | ||||
| @ -87,9 +90,7 @@ class Role(WolfListener): | ||||
|         player.role = self | ||||
|         self.player = player | ||||
| 
 | ||||
|         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) | ||||
| @ -100,7 +101,7 @@ class Role(WolfListener): | ||||
|     async def see_alignment(self, source=None): | ||||
|         """ | ||||
|         Interaction for investigative roles attempting | ||||
|         to see alignment (Village, Werewolf, Other) | ||||
|         to see alignment (Village, Werewolf Other) | ||||
|         """ | ||||
|         return "Other" | ||||
| 
 | ||||
| @ -118,16 +119,35 @@ class Role(WolfListener): | ||||
|         """ | ||||
|         return "Default" | ||||
| 
 | ||||
|     @wolflistener("at_game_start", priority=2) | ||||
|     async def _at_game_start(self): | ||||
|         if self.channel_name: | ||||
|             await self.game.register_channel(self.channel_name, self) | ||||
|     async def _at_game_start(self, data=None): | ||||
|         if self.channel_id: | ||||
|             await self.game.register_channel(self.channel_id, self) | ||||
| 
 | ||||
|         try: | ||||
|             await self.player.send_dm(self.game_start_message)  # Maybe embeds eventually | ||||
|         except AttributeError as e: | ||||
|             log.exception(self.__repr__()) | ||||
|             raise e | ||||
|         await self.player.send_dm(self.game_start_message)  # Maybe embeds eventually | ||||
| 
 | ||||
|     async def _at_day_start(self, data=None): | ||||
|         pass | ||||
| 
 | ||||
|     async def _at_voted(self, data=None): | ||||
|         pass | ||||
| 
 | ||||
|     async def _at_kill(self, data=None): | ||||
|         pass | ||||
| 
 | ||||
|     async def _at_hang(self, data=None): | ||||
|         pass | ||||
| 
 | ||||
|     async def _at_day_end(self, data=None): | ||||
|         pass | ||||
| 
 | ||||
|     async def _at_night_start(self, data=None): | ||||
|         pass | ||||
| 
 | ||||
|     async def _at_night_end(self, data=None): | ||||
|         pass | ||||
| 
 | ||||
|     async def _at_visit(self, data=None): | ||||
|         pass | ||||
| 
 | ||||
|     async def kill(self, source): | ||||
|         """ | ||||
|  | ||||
| @ -1,11 +0,0 @@ | ||||
| from .villager import Villager | ||||
| from .seer import Seer | ||||
| 
 | ||||
| from .vanillawerewolf import VanillaWerewolf | ||||
| 
 | ||||
| from .shifter import Shifter | ||||
| 
 | ||||
| # Don't sort these imports. They are unstably in order | ||||
| # TODO: Replace with unique IDs for roles in the future | ||||
| 
 | ||||
| __all__ = ["Seer", "Shifter", "VanillaWerewolf", "Villager"] | ||||
| @ -1,101 +0,0 @@ | ||||
| import logging | ||||
| import random | ||||
| 
 | ||||
| from werewolf.constants import ALIGNMENT_NEUTRAL, CATEGORY_NEUTRAL_EVIL | ||||
| from werewolf.listener import wolflistener | ||||
| from werewolf.player import Player | ||||
| from werewolf.role import Role | ||||
| 
 | ||||
| log = logging.getLogger("red.fox_v3.werewolf.role.blob") | ||||
| 
 | ||||
| 
 | ||||
| class TheBlob(Role): | ||||
|     rand_choice = True | ||||
|     category = [CATEGORY_NEUTRAL_EVIL]  # List of enrolled categories | ||||
|     alignment = ALIGNMENT_NEUTRAL  # 1: Town, 2: Werewolf, 3: Neutral | ||||
|     channel_id = ""  # Empty for no private channel | ||||
|     unique = True  # Only one of this role per game | ||||
|     game_start_message = ( | ||||
|         "Your role is **The Blob**\n" | ||||
|         "You win by absorbing everyone town\n" | ||||
|         "Lynch players during the day with `[p]ww vote <ID>`\n" | ||||
|         "Each night you will absorb an adjacent player" | ||||
|     ) | ||||
|     description = ( | ||||
|         "A mysterious green blob of jelly, slowly growing in size.\n" | ||||
|         "The Blob fears no evil, must be dealt with in town" | ||||
|     ) | ||||
| 
 | ||||
|     def __init__(self, game): | ||||
|         super().__init__(game) | ||||
| 
 | ||||
|         self.blob_target = None | ||||
| 
 | ||||
|     async def see_alignment(self, source=None): | ||||
|         """ | ||||
|         Interaction for investigative roles attempting | ||||
|         to see team (Village, Werewolf, Other) | ||||
|         """ | ||||
|         return ALIGNMENT_NEUTRAL | ||||
| 
 | ||||
|     async def get_role(self, source=None): | ||||
|         """ | ||||
|         Interaction for powerful access of role | ||||
|         Unlikely to be able to deceive this | ||||
|         """ | ||||
|         return "The Blob" | ||||
| 
 | ||||
|     async def see_role(self, source=None): | ||||
|         """ | ||||
|         Interaction for investigative roles. | ||||
|         More common to be able to deceive these roles | ||||
|         """ | ||||
|         return "The Blob" | ||||
| 
 | ||||
|     async def kill(self, source): | ||||
|         """ | ||||
|         Called when someone is trying to kill you! | ||||
|         Can you do anything about it? | ||||
|         self.player.alive is now set to False, set to True to stay alive | ||||
|         """ | ||||
| 
 | ||||
|         # Blob cannot simply be killed | ||||
|         self.player.alive = True | ||||
| 
 | ||||
|     @wolflistener("at_night_start", priority=2) | ||||
|     async def _at_night_start(self): | ||||
|         if not self.player.alive: | ||||
|             return | ||||
| 
 | ||||
|         self.blob_target = None | ||||
|         idx = self.player.id | ||||
|         left_or_right = random.choice((-1, 1)) | ||||
|         while self.blob_target is None: | ||||
|             idx += left_or_right | ||||
|             if idx >= len(self.game.players): | ||||
|                 idx = 0 | ||||
| 
 | ||||
|             player = self.game.players[idx] | ||||
| 
 | ||||
|             # you went full circle, everyone is a blob or something else is wrong | ||||
|             if player == self.player: | ||||
|                 break | ||||
| 
 | ||||
|             if player.role.properties.get("been_blobbed", False): | ||||
|                 self.blob_target = player | ||||
| 
 | ||||
|         if self.blob_target is not None: | ||||
|             await self.player.send_dm(f"**You will attempt to absorb {self.blob_target} tonight**") | ||||
|         else: | ||||
|             await self.player.send_dm(f"**No player will be absorbed tonight**") | ||||
| 
 | ||||
|     @wolflistener("at_night_end", priority=4) | ||||
|     async def _at_night_end(self): | ||||
|         if self.blob_target is None or not self.player.alive: | ||||
|             return | ||||
| 
 | ||||
|         target: "Player" = await self.game.visit(self.blob_target, self.player) | ||||
| 
 | ||||
|         if target is not None: | ||||
|             target.role.properties["been_blobbed"] = True | ||||
|             self.game.night_results.append("The Blob grows...") | ||||
| @ -1,26 +1,11 @@ | ||||
| import logging | ||||
| 
 | ||||
| from werewolf.constants import ( | ||||
|     ALIGNMENT_TOWN, | ||||
|     ALIGNMENT_WEREWOLF, | ||||
|     CATEGORY_TOWN_INVESTIGATIVE, | ||||
|     CATEGORY_TOWN_RANDOM, | ||||
| ) | ||||
| from werewolf.listener import wolflistener | ||||
| from werewolf.night_powers import pick_target | ||||
| from werewolf.role import Role | ||||
| 
 | ||||
| log = logging.getLogger("red.fox_v3.werewolf.role.seer") | ||||
| from ..night_powers import pick_target | ||||
| from ..role import Role | ||||
| 
 | ||||
| 
 | ||||
| class Seer(Role): | ||||
|     rand_choice = True | ||||
|     town_balance = 4 | ||||
|     category = [ | ||||
|         CATEGORY_TOWN_RANDOM, | ||||
|         CATEGORY_TOWN_INVESTIGATIVE, | ||||
|     ]  # List of enrolled categories (listed above) | ||||
|     alignment = ALIGNMENT_TOWN  # 1: Town, 2: Werewolf, 3: Neutral | ||||
|     rand_choice = True  # Determines if it can be picked as a random role (False for unusually disruptive roles) | ||||
|     category = [1, 2]  # List of enrolled categories (listed above) | ||||
|     alignment = 1  # 1: Town, 2: Werewolf, 3: Neutral | ||||
|     channel_id = ""  # Empty for no private channel | ||||
|     unique = False  # Only one of this role per game | ||||
|     game_start_message = ( | ||||
| @ -29,10 +14,8 @@ class Seer(Role): | ||||
|         "Lynch players during the day with `[p]ww vote <ID>`\n" | ||||
|         "Check for werewolves at night with `[p]ww choose <ID>`" | ||||
|     ) | ||||
|     description = ( | ||||
|         "A mystic in search of answers in a chaotic town.\n" | ||||
|         "Calls upon the cosmos to discern those of Lycan blood" | ||||
|     ) | ||||
|     description = "A mystic in search of answers in a chaotic town.\n" \ | ||||
|                   "Calls upon the cosmos to discern those of Lycan blood" | ||||
| 
 | ||||
|     def __init__(self, game): | ||||
|         super().__init__(game) | ||||
| @ -41,49 +24,47 @@ class Seer(Role): | ||||
|         # self.blocked = False | ||||
|         # self.properties = {}  # Extra data for other roles (i.e. arsonist) | ||||
|         self.see_target = None | ||||
|         # self.action_list = [ | ||||
|         #     (self._at_game_start, 1),  # (Action, Priority) | ||||
|         #     (self._at_day_start, 0), | ||||
|         #     (self._at_voted, 0), | ||||
|         #     (self._at_kill, 0), | ||||
|         #     (self._at_hang, 0), | ||||
|         #     (self._at_day_end, 0), | ||||
|         #     (self._at_night_start, 2), | ||||
|         #     (self._at_night_end, 4), | ||||
|         #     (self._at_visit, 0), | ||||
|         # ] | ||||
|         self.action_list = [ | ||||
|             (self._at_game_start, 1),  # (Action, Priority) | ||||
|             (self._at_day_start, 0), | ||||
|             (self._at_voted, 0), | ||||
|             (self._at_kill, 0), | ||||
|             (self._at_hang, 0), | ||||
|             (self._at_day_end, 0), | ||||
|             (self._at_night_start, 2), | ||||
|             (self._at_night_end, 4), | ||||
|             (self._at_visit, 0) | ||||
|         ] | ||||
| 
 | ||||
|     async def see_alignment(self, source=None): | ||||
|         """ | ||||
|         Interaction for investigative roles attempting | ||||
|         to see team (Village, Werewolf, Other) | ||||
|         to see team (Village, Werewolf Other) | ||||
|         """ | ||||
|         return ALIGNMENT_TOWN | ||||
|         return "Village" | ||||
| 
 | ||||
|     async def get_role(self, source=None): | ||||
|         """ | ||||
|         Interaction for powerful access of role | ||||
|         Unlikely to be able to deceive this | ||||
|         """ | ||||
|         return "Seer" | ||||
|         return "Villager" | ||||
| 
 | ||||
|     async def see_role(self, source=None): | ||||
|         """ | ||||
|         Interaction for investigative roles. | ||||
|         More common to be able to deceive these roles | ||||
|         """ | ||||
|         return "Seer" | ||||
|         return "Villager" | ||||
| 
 | ||||
|     @wolflistener("at_night_start", priority=2) | ||||
|     async def _at_night_start(self): | ||||
|     async def _at_night_start(self, data=None): | ||||
|         if not self.player.alive: | ||||
|             return | ||||
|         self.see_target = None | ||||
|         await self.game.generate_targets(self.player.member) | ||||
|         await self.player.send_dm("**Pick a target to see tonight**") | ||||
| 
 | ||||
|     @wolflistener("at_night_end", priority=4) | ||||
|     async def _at_night_end(self): | ||||
|     async def _at_night_end(self, data=None): | ||||
|         if self.see_target is None: | ||||
|             if self.player.alive: | ||||
|                 await self.player.send_dm("You will not use your powers tonight...") | ||||
| @ -94,9 +75,9 @@ class Seer(Role): | ||||
|         if target: | ||||
|             alignment = await target.role.see_alignment(self.player) | ||||
| 
 | ||||
|         if alignment == ALIGNMENT_WEREWOLF: | ||||
|         if alignment == "Werewolf": | ||||
|             out = "Your insight reveals this player to be a **Werewolf!**" | ||||
|         else:  # Don't reveal neutrals | ||||
|         else: | ||||
|             out = "You fail to find anything suspicious about this player..." | ||||
| 
 | ||||
|         await self.player.send_dm(out) | ||||
| @ -106,6 +87,4 @@ class Seer(Role): | ||||
|         await super().choose(ctx, data) | ||||
| 
 | ||||
|         self.see_target, target = await pick_target(self, ctx, data) | ||||
|         await ctx.send( | ||||
|             f"**You will attempt to see the role of {target.member.display_name} tonight...**" | ||||
|         ) | ||||
|         await ctx.send("**You will attempt to see the role of {} tonight...**".format(target.member.display_name)) | ||||
|  | ||||
| @ -1,41 +1,35 @@ | ||||
| import logging | ||||
| 
 | ||||
| from werewolf.constants import ALIGNMENT_NEUTRAL, CATEGORY_NEUTRAL_BENIGN | ||||
| from werewolf.listener import wolflistener | ||||
| from werewolf.night_powers import pick_target | ||||
| from werewolf.role import Role | ||||
| 
 | ||||
| log = logging.getLogger("red.fox_v3.werewolf.role.shifter") | ||||
| from ..night_powers import pick_target | ||||
| from ..role import Role | ||||
| 
 | ||||
| 
 | ||||
| class Shifter(Role): | ||||
|     """ | ||||
|     Base Role class for werewolf game | ||||
| 
 | ||||
|      | ||||
|     Category enrollment guide as follows (category property): | ||||
|         Town: | ||||
|         1: Random, 2: Investigative, 3: Protective, 4: Government, | ||||
|         5: Killing, 6: Power (Special night action) | ||||
| 
 | ||||
|          | ||||
|         Werewolf: | ||||
|         11: Random, 12: Deception, 15: Killing, 16: Support | ||||
| 
 | ||||
|          | ||||
|         Neutral: | ||||
|         21: Benign, 22: Evil, 23: Killing | ||||
| 
 | ||||
| 
 | ||||
|          | ||||
|          | ||||
|         Example category: | ||||
|         category = [1, 5, 6] Could be Veteran | ||||
|         category = [1, 5] Could be Bodyguard | ||||
|         category = [11, 16] Could be Werewolf Silencer | ||||
| 
 | ||||
| 
 | ||||
|          | ||||
|      | ||||
|     Action guide as follows (on_event function): | ||||
|         _at_night_start | ||||
|         0. No Action | ||||
|         1. Detain actions (Jailer/Kidnapper) | ||||
|         2. Group discussions and choose targets | ||||
| 
 | ||||
|          | ||||
|         _at_night_end | ||||
|         0. No Action | ||||
|         1. Self actions (Veteran) | ||||
| @ -43,13 +37,12 @@ class Shifter(Role): | ||||
|         3. Protection / Preempt actions (bodyguard/framer) | ||||
|         4. Non-disruptive actions (seer/silencer) | ||||
|         5. Disruptive actions (Killing) | ||||
|         6. Role altering actions (Cult / Mason / Shifter) | ||||
|         6. Role altering actions (Cult / Mason) | ||||
|     """ | ||||
| 
 | ||||
|     rand_choice = False  # Determines if it can be picked as a random role (False for unusually disruptive roles) | ||||
|     town_balance = -3 | ||||
|     category = [CATEGORY_NEUTRAL_BENIGN]  # List of enrolled categories (listed above) | ||||
|     alignment = ALIGNMENT_NEUTRAL  # 1: Town, 2: Werewolf, 3: Neutral | ||||
|     category = [22]  # List of enrolled categories (listed above) | ||||
|     alignment = 3  # 1: Town, 2: Werewolf, 3: Neutral | ||||
|     channel_id = ""  # Empty for no private channel | ||||
|     unique = False  # Only one of this role per game | ||||
|     game_start_message = ( | ||||
| @ -68,22 +61,22 @@ class Shifter(Role): | ||||
|         super().__init__(game) | ||||
| 
 | ||||
|         self.shift_target = None | ||||
|         # self.action_list = [ | ||||
|         #     (self._at_game_start, 1),  # (Action, Priority) | ||||
|         #     (self._at_day_start, 0), | ||||
|         #     (self._at_voted, 0), | ||||
|         #     (self._at_kill, 0), | ||||
|         #     (self._at_hang, 0), | ||||
|         #     (self._at_day_end, 0), | ||||
|         #     (self._at_night_start, 2),  # Chooses targets | ||||
|         #     (self._at_night_end, 6),  # Role Swap | ||||
|         #     (self._at_visit, 0), | ||||
|         # ] | ||||
|         self.action_list = [ | ||||
|             (self._at_game_start, 1),  # (Action, Priority) | ||||
|             (self._at_day_start, 0), | ||||
|             (self._at_voted, 0), | ||||
|             (self._at_kill, 0), | ||||
|             (self._at_hang, 0), | ||||
|             (self._at_day_end, 0), | ||||
|             (self._at_night_start, 2),  # Chooses targets | ||||
|             (self._at_night_end, 6),  # Role Swap | ||||
|             (self._at_visit, 0) | ||||
|         ] | ||||
| 
 | ||||
|     async def see_alignment(self, source=None): | ||||
|         """ | ||||
|         Interaction for investigative roles attempting | ||||
|         to see alignment (Village, Werewolf,, Other) | ||||
|         to see alignment (Village, Werewolf, Other) | ||||
|         """ | ||||
|         return "Other" | ||||
| 
 | ||||
| @ -101,14 +94,14 @@ class Shifter(Role): | ||||
|         """ | ||||
|         return "Shifter" | ||||
| 
 | ||||
|     @wolflistener("at_night_start", priority=2) | ||||
|     async def _at_night_start(self): | ||||
|     async def _at_night_start(self, data=None): | ||||
|         await super()._at_night_start(data) | ||||
|         self.shift_target = None | ||||
|         await self.game.generate_targets(self.player.member) | ||||
|         await self.player.send_dm("**Pick a target to shift into**") | ||||
| 
 | ||||
|     @wolflistener("at_night_end", priority=6) | ||||
|     async def _at_night_end(self): | ||||
|     async def _at_night_end(self, data=None): | ||||
|         await super()._at_night_end(data) | ||||
|         if self.shift_target is None: | ||||
|             if self.player.alive: | ||||
|                 await self.player.send_dm("You will not use your powers tonight...") | ||||
| @ -121,20 +114,16 @@ class Shifter(Role): | ||||
| 
 | ||||
|             # Roles have now been swapped | ||||
| 
 | ||||
|             await self.player.send_dm( | ||||
|                 "Your role has been stolen...\n" "You are now a **Shifter**." | ||||
|             ) | ||||
|             await self.player.send_dm("Your role has been stolen...\n" | ||||
|                                       "You are now a **Shifter**.") | ||||
|             await self.player.send_dm(self.game_start_message) | ||||
| 
 | ||||
|             await target.send_dm(target.role.game_start_message) | ||||
|         else: | ||||
|             await self.player.send_dm("**Your shift failed...**") | ||||
| 
 | ||||
|     async def choose(self, ctx, data): | ||||
|         """Handle night actions""" | ||||
|         await super().choose(ctx, data) | ||||
| 
 | ||||
|         self.shift_target, target = await pick_target(self, ctx, data) | ||||
|         await ctx.send( | ||||
|             f"**You will attempt to see the role of {target.member.display_name} tonight...**" | ||||
|         ) | ||||
|         await ctx.send("**You will attempt to see the role of {} tonight...**".format(target.member.display_name)) | ||||
|  | ||||
| @ -1,19 +1,13 @@ | ||||
| import logging | ||||
| from ..role import Role | ||||
| 
 | ||||
| from werewolf.constants import ALIGNMENT_WEREWOLF, CATEGORY_WW_KILLING, CATEGORY_WW_RANDOM | ||||
| from werewolf.listener import wolflistener | ||||
| from werewolf.role import Role | ||||
| from werewolf.votegroups.wolfvote import WolfVote | ||||
| 
 | ||||
| log = logging.getLogger("red.fox_v3.werewolf.role.vanillawerewolf") | ||||
| from ..votegroups.wolfvote import WolfVote | ||||
| 
 | ||||
| 
 | ||||
| class VanillaWerewolf(Role): | ||||
|     rand_choice = True | ||||
|     town_balance = -6 | ||||
|     category = [CATEGORY_WW_RANDOM, CATEGORY_WW_KILLING] | ||||
|     alignment = ALIGNMENT_WEREWOLF  # 1: Town, 2: Werewolf, 3: Neutral | ||||
|     channel_name = "werewolves" | ||||
|     category = [11, 15] | ||||
|     alignment = 2  # 1: Town, 2: Werewolf, 3: Neutral | ||||
|     channel_id = "werewolves" | ||||
|     unique = False | ||||
|     game_start_message = ( | ||||
|         "Your role is **Werewolf**\n" | ||||
| @ -22,19 +16,34 @@ class VanillaWerewolf(Role): | ||||
|         "Vote to kill players at night with `[p]ww vote <ID>`" | ||||
|     ) | ||||
| 
 | ||||
|     def __init__(self, game): | ||||
|         super().__init__(game) | ||||
| 
 | ||||
|         self.action_list = [ | ||||
|             (self._at_game_start, 1),  # (Action, Priority) | ||||
|             (self._at_day_start, 0), | ||||
|             (self._at_voted, 0), | ||||
|             (self._at_kill, 0), | ||||
|             (self._at_hang, 0), | ||||
|             (self._at_day_end, 0), | ||||
|             (self._at_night_start, 0), | ||||
|             (self._at_night_end, 0), | ||||
|             (self._at_visit, 0) | ||||
|         ] | ||||
| 
 | ||||
|     async def see_alignment(self, source=None): | ||||
|         """ | ||||
|         Interaction for investigative roles attempting | ||||
|         to see team (Village, Werewolf Other) | ||||
|         """ | ||||
|         return ALIGNMENT_WEREWOLF | ||||
|         return "Werewolf" | ||||
| 
 | ||||
|     async def get_role(self, source=None): | ||||
|         """ | ||||
|         Interaction for powerful access of role | ||||
|         Unlikely to be able to deceive this | ||||
|         """ | ||||
|         return "VanillaWerewolf" | ||||
|         return "Werewolf" | ||||
| 
 | ||||
|     async def see_role(self, source=None): | ||||
|         """ | ||||
| @ -43,13 +52,10 @@ class VanillaWerewolf(Role): | ||||
|         """ | ||||
|         return "Werewolf" | ||||
| 
 | ||||
|     @wolflistener("at_game_start", priority=2) | ||||
|     async def _at_game_start(self): | ||||
|         if self.channel_name: | ||||
|             log.debug("Wolf has channel_name: " + self.channel_name) | ||||
|             await self.game.register_channel( | ||||
|                 self.channel_name, self, WolfVote | ||||
|             )  # Add VoteGroup WolfVote | ||||
|     async def _at_game_start(self, data=None): | ||||
|         if self.channel_id: | ||||
|             print("Wolf has channel_id: " + self.channel_id) | ||||
|             await self.game.register_channel(self.channel_id, self, WolfVote)  # Add VoteGroup WolfVote | ||||
| 
 | ||||
|         await self.player.send_dm(self.game_start_message) | ||||
| 
 | ||||
|  | ||||
| @ -1,17 +1,10 @@ | ||||
| import logging | ||||
| 
 | ||||
| from werewolf.constants import ALIGNMENT_TOWN, CATEGORY_TOWN_RANDOM | ||||
| from werewolf.role import Role | ||||
| 
 | ||||
| log = logging.getLogger("red.fox_v3.werewolf.role.villager") | ||||
| from ..role import Role | ||||
| 
 | ||||
| 
 | ||||
| class Villager(Role): | ||||
|     # Determines if it can be picked as a random role (False for unusually disruptive roles) | ||||
|     rand_choice = True | ||||
|     town_balance = 1 | ||||
|     category = [CATEGORY_TOWN_RANDOM]  # List of enrolled categories (listed above) | ||||
|     alignment = ALIGNMENT_TOWN  # 1: Town, 2: Werewolf, 3: Neutral | ||||
|     rand_choice = True  # Determines if it can be picked as a random role (False for unusually disruptive roles) | ||||
|     category = [1]  # List of enrolled categories (listed above) | ||||
|     alignment = 1  # 1: Town, 2: Werewolf, 3: Neutral | ||||
|     channel_id = ""  # Empty for no private channel | ||||
|     unique = False  # Only one of this role per game | ||||
|     game_start_message = ( | ||||
| @ -20,12 +13,15 @@ class Villager(Role): | ||||
|         "Lynch players during the day with `[p]ww vote <ID>`" | ||||
|     ) | ||||
| 
 | ||||
|     def __init__(self, game): | ||||
|         super().__init__(game) | ||||
| 
 | ||||
|     async def see_alignment(self, source=None): | ||||
|         """ | ||||
|         Interaction for investigative roles attempting | ||||
|         to see team (Village, Werewolf, Other) | ||||
|         to see team (Village, Werewolf Other) | ||||
|         """ | ||||
|         return ALIGNMENT_TOWN | ||||
|         return "Village" | ||||
| 
 | ||||
|     async def get_role(self, source=None): | ||||
|         """ | ||||
|  | ||||
| @ -1,11 +1,4 @@ | ||||
| import logging | ||||
| 
 | ||||
| from werewolf.listener import WolfListener, wolflistener | ||||
| 
 | ||||
| log = logging.getLogger("red.fox_v3.werewolf.votegroup") | ||||
| 
 | ||||
| 
 | ||||
| class VoteGroup(WolfListener): | ||||
| class VoteGroup: | ||||
|     """ | ||||
|     Base VoteGroup class for werewolf game | ||||
|     Handles secret channels and group decisions | ||||
| @ -15,41 +8,58 @@ class VoteGroup(WolfListener): | ||||
|     channel_id = "" | ||||
| 
 | ||||
|     def __init__(self, game, channel): | ||||
|         super().__init__(game) | ||||
|         self.game = game | ||||
|         self.channel = channel | ||||
|         self.players = [] | ||||
|         self.vote_results = {} | ||||
|         self.properties = {}  # Extra data for other options | ||||
| 
 | ||||
|     def __repr__(self): | ||||
|         return f"{self.__class__.__name__}({self.channel},{self.players})" | ||||
|         self.action_list = [ | ||||
|             (self._at_game_start, 1),  # (Action, Priority) | ||||
|             (self._at_day_start, 0), | ||||
|             (self._at_voted, 0), | ||||
|             (self._at_kill, 1), | ||||
|             (self._at_hang, 1), | ||||
|             (self._at_day_end, 0), | ||||
|             (self._at_night_start, 2), | ||||
|             (self._at_night_end, 0), | ||||
|             (self._at_visit, 0) | ||||
|         ] | ||||
| 
 | ||||
|     @wolflistener("at_game_start", priority=1) | ||||
|     async def _at_game_start(self): | ||||
|     async def on_event(self, event, data): | ||||
|         """ | ||||
|         See Game class for event guide | ||||
|         """ | ||||
| 
 | ||||
|         await self.action_list[event][0](data) | ||||
| 
 | ||||
|     async def _at_game_start(self, data=None): | ||||
|         await self.channel.send(" ".join(player.mention for player in self.players)) | ||||
| 
 | ||||
|     @wolflistener("at_kill", priority=1) | ||||
|     async def _at_kill(self, player): | ||||
|         if player in self.players: | ||||
|             self.players.remove(player) | ||||
|     async def _at_day_start(self, data=None): | ||||
|         pass | ||||
| 
 | ||||
|     @wolflistener("at_hang", priority=1) | ||||
|     async def _at_hang(self, player): | ||||
|         if player in self.players: | ||||
|             self.players.remove(player) | ||||
|     async def _at_voted(self, data=None): | ||||
|         pass | ||||
| 
 | ||||
|     @wolflistener("at_night_start", priority=2) | ||||
|     async def _at_night_start(self): | ||||
|     async def _at_kill(self, data=None): | ||||
|         if data["player"] in self.players: | ||||
|             self.players.remove(data["player"]) | ||||
| 
 | ||||
|     async def _at_hang(self, data=None): | ||||
|         if data["player"] in self.players: | ||||
|             self.players.remove(data["player"]) | ||||
| 
 | ||||
|     async def _at_day_end(self, data=None): | ||||
|         pass | ||||
| 
 | ||||
|     async def _at_night_start(self, data=None): | ||||
|         if self.channel is None: | ||||
|             return | ||||
| 
 | ||||
|         self.vote_results = {} | ||||
| 
 | ||||
|         await self.game.generate_targets(self.channel) | ||||
| 
 | ||||
|     @wolflistener("at_night_end", priority=5) | ||||
|     async def _at_night_end(self): | ||||
|     async def _at_night_end(self, data=None): | ||||
|         if self.channel is None: | ||||
|             return | ||||
| 
 | ||||
| @ -60,8 +70,11 @@ class VoteGroup(WolfListener): | ||||
|             target = max(set(vote_list), key=vote_list.count) | ||||
| 
 | ||||
|         if target: | ||||
|             # Do what the votegroup votes on | ||||
|             raise NotImplementedError | ||||
|             # Do what you voted on | ||||
|             pass | ||||
| 
 | ||||
|     async def _at_visit(self, data=None): | ||||
|         pass | ||||
| 
 | ||||
|     async def register_players(self, *players): | ||||
|         """ | ||||
| @ -77,7 +90,7 @@ class VoteGroup(WolfListener): | ||||
|             self.players.remove(player) | ||||
| 
 | ||||
|         if not self.players: | ||||
|             # TODO: Confirm deletion | ||||
|             # ToDo: Trigger deletion of votegroup | ||||
|             pass | ||||
| 
 | ||||
|     async def vote(self, target, author, target_id): | ||||
|  | ||||
| @ -1 +0,0 @@ | ||||
| from .wolfvote import WolfVote | ||||
| @ -1,12 +1,6 @@ | ||||
| import logging | ||||
| import random | ||||
| 
 | ||||
| import discord | ||||
| 
 | ||||
| from werewolf.listener import wolflistener | ||||
| from werewolf.votegroup import VoteGroup | ||||
| 
 | ||||
| log = logging.getLogger("red.fox_v3.werewolf.votegroup.wolfvote") | ||||
| from ..votegroup import VoteGroup | ||||
| 
 | ||||
| 
 | ||||
| class WolfVote(VoteGroup): | ||||
| @ -19,29 +13,71 @@ class WolfVote(VoteGroup): | ||||
| 
 | ||||
|     kill_messages = [ | ||||
|         "**{ID}** - {target} was mauled by wolves", | ||||
|         "**{ID}** - {target} was found torn to shreds", | ||||
|     ] | ||||
|         "**{ID}** - {target} was found torn to shreds"] | ||||
| 
 | ||||
|     def __init__(self, game, channel): | ||||
|         super().__init__(game, channel) | ||||
|         # self.game = game | ||||
|         # self.channel = channel | ||||
|         # self.players = [] | ||||
|         # self.vote_results = {} | ||||
|         # self.properties = {}  # Extra data for other options | ||||
| 
 | ||||
|         self.killer = None  # Added killer | ||||
| 
 | ||||
|     @wolflistener("at_night_start", priority=2) | ||||
|     async def _at_night_start(self): | ||||
|         await super()._at_night_start() | ||||
|         self.action_list = [ | ||||
|             (self._at_game_start, 1),  # (Action, Priority) | ||||
|             (self._at_day_start, 0), | ||||
|             (self._at_voted, 0), | ||||
|             (self._at_kill, 1), | ||||
|             (self._at_hang, 1), | ||||
|             (self._at_day_end, 0), | ||||
|             (self._at_night_start, 2), | ||||
|             (self._at_night_end, 5),  # Kill priority | ||||
|             (self._at_visit, 0) | ||||
|         ] | ||||
| 
 | ||||
|         # async def on_event(self, event, data): | ||||
| 
 | ||||
|     #     """ | ||||
|     #     See Game class for event guide | ||||
|     #     """ | ||||
|     # | ||||
|     #     await action_list[event][0](data) | ||||
|     # | ||||
|     # async def _at_game_start(self, data=None): | ||||
|     #     await self.channel.send(" ".join(player.mention for player in self.players)) | ||||
|     # | ||||
|     # async def _at_day_start(self, data=None): | ||||
|     #     pass | ||||
|     # | ||||
|     # async def _at_voted(self, data=None): | ||||
|     #     pass | ||||
|     # | ||||
|     # async def _at_kill(self, data=None): | ||||
|     #     if data["player"] in self.players: | ||||
|     #         self.players.pop(data["player"]) | ||||
|     # | ||||
|     # async def _at_hang(self, data=None): | ||||
|     #     if data["player"] in self.players: | ||||
|     #         self.players.pop(data["player"]) | ||||
|     # | ||||
|     # async def _at_day_end(self, data=None): | ||||
|     #     pass | ||||
| 
 | ||||
|     async def _at_night_start(self, data=None): | ||||
|         if self.channel is None: | ||||
|             return | ||||
| 
 | ||||
|         await self.game.generate_targets(self.channel) | ||||
|         mention_list = " ".join(player.mention for player in self.players) | ||||
|         if mention_list != "": | ||||
|             await self.channel.send(mention_list) | ||||
|         self.killer = random.choice(self.players) | ||||
| 
 | ||||
|         await self.channel.send( | ||||
|             f"{self.killer.member.display_name} has been selected as tonight's killer" | ||||
|         ) | ||||
|         await self.channel.send("{} has been selected as tonight's killer".format(self.killer.member.display_name)) | ||||
| 
 | ||||
|     @wolflistener("at_night_end", priority=5) | ||||
|     async def _at_night_end(self): | ||||
|     async def _at_night_end(self, data=None): | ||||
|         if self.channel is None: | ||||
|             return | ||||
| 
 | ||||
| @ -51,23 +87,34 @@ class WolfVote(VoteGroup): | ||||
|         if vote_list: | ||||
|             target_id = max(set(vote_list), key=vote_list.count) | ||||
| 
 | ||||
|         log.debug(f"Target id: {target_id}\nKiller: {self.killer.member.display_name}") | ||||
|         print("Target id: {}\nKiller: {}".format(target_id, self.killer.member.display_name)) | ||||
|         if target_id is not None and self.killer: | ||||
|             await self.game.kill(target_id, self.killer, random.choice(self.kill_messages)) | ||||
|             await self.channel.send( | ||||
|                 "*{} has left to complete the kill...*".format(self.killer.member.display_name) | ||||
|             ) | ||||
|             await self.channel.send("**{} has left to complete the kill...**".format(self.killer.member.display_name)) | ||||
|         else: | ||||
|             await self.channel.send("*No kill will be attempted tonight...*") | ||||
|             await self.channel.send("**No kill will be attempted tonight...**") | ||||
| 
 | ||||
|     # async def _at_visit(self, data=None): | ||||
|     #     pass | ||||
|     # | ||||
|     # async def register_players(self, *players): | ||||
|     #     """ | ||||
|     #     Extend players by passed list | ||||
|     #     """ | ||||
|     #     self.players.extend(players) | ||||
|     # | ||||
|     # async def remove_player(self, player): | ||||
|     #     """ | ||||
|     #     Remove a player from player list | ||||
|     #     """ | ||||
|     #     if player.id in self.players: | ||||
|     #         self.players.remove(player) | ||||
| 
 | ||||
|     async def vote(self, target, author, target_id): | ||||
|         """ | ||||
|         Receive vote from game | ||||
|         """ | ||||
| 
 | ||||
|         await super().vote(target, author, target_id) | ||||
|         self.vote_results[author.id] = target_id | ||||
| 
 | ||||
|         await self.channel.send( | ||||
|             "{} has voted to kill {}".format(author.mention, target.member.display_name), | ||||
|             allowed_mentions=discord.AllowedMentions(everyone=False, users=[author]), | ||||
|         ) | ||||
|         await self.channel.send("{} has voted to kill {}".format(author.mention, target.member.display_name)) | ||||
|  | ||||
| @ -1,22 +1,17 @@ | ||||
| import logging | ||||
| from typing import Optional | ||||
| 
 | ||||
| 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.menus import DEFAULT_CONTROLS, menu | ||||
| 
 | ||||
| from werewolf.builder import ( | ||||
| from .builder import ( | ||||
|     GameBuilder, | ||||
|     role_from_alignment, | ||||
|     role_from_category, | ||||
|     role_from_id, | ||||
|     role_from_name, | ||||
| ) | ||||
| from werewolf.game import Game, anyone_has_role | ||||
| 
 | ||||
| log = logging.getLogger("red.fox_v3.werewolf") | ||||
| from .game import Game | ||||
| 
 | ||||
| 
 | ||||
| class Werewolf(Cog): | ||||
| @ -47,27 +42,25 @@ class Werewolf(Cog): | ||||
|         """Nothing to delete""" | ||||
|         return | ||||
| 
 | ||||
|     def cog_unload(self): | ||||
|         log.debug("Unload called") | ||||
|         for key in self.games.keys(): | ||||
|             del self.games[key] | ||||
|     def __unload(self): | ||||
|         print("Unload called") | ||||
|         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) | ||||
| 
 | ||||
|         if code != "": | ||||
|             await ctx.maybe_send_embed(f"Your game code is **{code}**") | ||||
|             await ctx.send("Your game code is **{}**".format(code)) | ||||
|         else: | ||||
|             await ctx.maybe_send_embed("No code generated") | ||||
|             await ctx.send("No code generated") | ||||
| 
 | ||||
|     @checks.guildowner() | ||||
|     @commands.group() | ||||
| @ -75,7 +68,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") | ||||
| @ -83,33 +77,31 @@ class Werewolf(Cog): | ||||
|         """ | ||||
|         Lists current guild settings | ||||
|         """ | ||||
|         valid, role, category, channel, log_channel = await self._get_settings(ctx) | ||||
|         success, role, category, channel, log_channel = await self._get_settings(ctx) | ||||
|         if not success: | ||||
|             await ctx.send("Failed to get settings") | ||||
|             return None | ||||
| 
 | ||||
|         embed = discord.Embed( | ||||
|             title="Current Guild Settings", | ||||
|             description=f"Valid: {valid}", | ||||
|             color=0x008000 if valid else 0xFF0000, | ||||
|         ) | ||||
|         embed = discord.Embed(title="Current Guild Settings") | ||||
|         embed.add_field(name="Role", value=str(role)) | ||||
|         embed.add_field(name="Category", value=str(category)) | ||||
|         embed.add_field(name="Channel", value=str(channel)) | ||||
|         embed.add_field(name="Log Channel", value=str(log_channel)) | ||||
| 
 | ||||
|         await ctx.send(embed=embed) | ||||
| 
 | ||||
|     @commands.guild_only() | ||||
|     @wwset.command(name="role") | ||||
|     async def wwset_role(self, ctx: commands.Context, role: discord.Role = None): | ||||
|         """ | ||||
|         Set the game role | ||||
|         Assign the game role | ||||
|         This role should not be manually assigned | ||||
|         """ | ||||
|         if role is None: | ||||
|             await self.config.guild(ctx.guild).role_id.set(None) | ||||
|             await ctx.maybe_send_embed("Cleared Game Role") | ||||
|             await ctx.send("Cleared Game Role") | ||||
|         else: | ||||
|             await self.config.guild(ctx.guild).role_id.set(role.id) | ||||
|             await ctx.maybe_send_embed("Game Role has been set to **{}**".format(role.name)) | ||||
|             await ctx.send("Game Role has been set to **{}**".format(role.name)) | ||||
| 
 | ||||
|     @commands.guild_only() | ||||
|     @wwset.command(name="category") | ||||
| @ -119,16 +111,14 @@ class Werewolf(Cog): | ||||
|         """ | ||||
|         if category_id is None: | ||||
|             await self.config.guild(ctx.guild).category_id.set(None) | ||||
|             await ctx.maybe_send_embed("Cleared Game Channel Category") | ||||
|             await ctx.send("Cleared Game Channel Category") | ||||
|         else: | ||||
|             category = discord.utils.get(ctx.guild.categories, id=int(category_id)) | ||||
|             if category is None: | ||||
|                 await ctx.maybe_send_embed("Category not found") | ||||
|                 await ctx.send("Category not found") | ||||
|                 return | ||||
|             await self.config.guild(ctx.guild).category_id.set(category.id) | ||||
|             await ctx.maybe_send_embed( | ||||
|                 "Game Channel Category has been set to **{}**".format(category.name) | ||||
|             ) | ||||
|             await ctx.send("Game Channel Category has been set to **{}**".format(category.name)) | ||||
| 
 | ||||
|     @commands.guild_only() | ||||
|     @wwset.command(name="channel") | ||||
| @ -138,12 +128,10 @@ class Werewolf(Cog): | ||||
|         """ | ||||
|         if channel is None: | ||||
|             await self.config.guild(ctx.guild).channel_id.set(None) | ||||
|             await ctx.maybe_send_embed("Cleared Game Channel") | ||||
|             await ctx.send("Cleared Game Channel") | ||||
|         else: | ||||
|             await self.config.guild(ctx.guild).channel_id.set(channel.id) | ||||
|             await ctx.maybe_send_embed( | ||||
|                 "Game Channel has been set to **{}**".format(channel.mention) | ||||
|             ) | ||||
|             await ctx.send("Game Channel has been set to **{}**".format(channel.mention)) | ||||
| 
 | ||||
|     @commands.guild_only() | ||||
|     @wwset.command(name="logchannel") | ||||
| @ -153,19 +141,18 @@ class Werewolf(Cog): | ||||
|         """ | ||||
|         if channel is None: | ||||
|             await self.config.guild(ctx.guild).log_channel_id.set(None) | ||||
|             await ctx.maybe_send_embed("Cleared Game Log Channel") | ||||
|             await ctx.send("Cleared Game Log Channel") | ||||
|         else: | ||||
|             await self.config.guild(ctx.guild).log_channel_id.set(channel.id) | ||||
|             await ctx.maybe_send_embed( | ||||
|                 "Game Log Channel has been set to **{}**".format(channel.mention) | ||||
|             ) | ||||
|             await ctx.send("Game Log Channel has been set to **{}**".format(channel.mention)) | ||||
| 
 | ||||
|     @commands.group() | ||||
|     async def ww(self, ctx: commands.Context): | ||||
|         """ | ||||
|         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") | ||||
| @ -175,9 +162,9 @@ class Werewolf(Cog): | ||||
|         """ | ||||
|         game = await self._get_game(ctx, game_code) | ||||
|         if not game: | ||||
|             await ctx.maybe_send_embed("Failed to start a new game") | ||||
|             await ctx.send("Failed to start a new game") | ||||
|         else: | ||||
|             await ctx.maybe_send_embed("Game is ready to join! Use `[p]ww join`") | ||||
|             await ctx.send("Game is ready to join! Use `[p]ww join`") | ||||
| 
 | ||||
|     @commands.guild_only() | ||||
|     @ww.command(name="join") | ||||
| @ -186,49 +173,28 @@ class Werewolf(Cog): | ||||
|         Joins a game of Werewolf | ||||
|         """ | ||||
| 
 | ||||
|         game: Game = await self._get_game(ctx) | ||||
|         game = await self._get_game(ctx) | ||||
| 
 | ||||
|         if not game: | ||||
|             await ctx.maybe_send_embed("Failed to join a game!") | ||||
|             await ctx.send("No game to join!\nCreate a new one with `[p]ww new`") | ||||
|             return | ||||
| 
 | ||||
|         await game.join(ctx, ctx.author) | ||||
|         await ctx.tick() | ||||
| 
 | ||||
|     @commands.guild_only() | ||||
|     @commands.admin() | ||||
|     @ww.command(name="forcejoin") | ||||
|     async def ww_forcejoin(self, ctx: commands.Context, target: discord.Member): | ||||
|         """ | ||||
|         Force someone to join a game of Werewolf | ||||
|         """ | ||||
| 
 | ||||
|         game: Game = await self._get_game(ctx) | ||||
| 
 | ||||
|         if not game: | ||||
|             await ctx.maybe_send_embed("Failed to join a game!") | ||||
|             return | ||||
| 
 | ||||
|         await game.join(ctx, target) | ||||
|         await ctx.tick() | ||||
|         await game.join(ctx.author, ctx.channel) | ||||
| 
 | ||||
|     @commands.guild_only() | ||||
|     @ww.command(name="code") | ||||
|     async def ww_code(self, ctx: commands.Context, code): | ||||
|         """ | ||||
|         Adjusts the game code. | ||||
| 
 | ||||
|         See `[p]buildgame` to generate a new code | ||||
|         Adjust game code | ||||
|         """ | ||||
| 
 | ||||
|         game = await self._get_game(ctx) | ||||
| 
 | ||||
|         if not game: | ||||
|             await ctx.maybe_send_embed("No game to join!\nCreate a new one with `[p]ww new`") | ||||
|             await ctx.send("No game to join!\nCreate a new one with `[p]ww new`") | ||||
|             return | ||||
| 
 | ||||
|         await game.set_code(ctx, code) | ||||
|         await ctx.tick() | ||||
| 
 | ||||
|     @commands.guild_only() | ||||
|     @ww.command(name="quit") | ||||
| @ -240,7 +206,6 @@ class Werewolf(Cog): | ||||
|         game = await self._get_game(ctx) | ||||
| 
 | ||||
|         await game.quit(ctx.author, ctx.channel) | ||||
|         await ctx.tick() | ||||
| 
 | ||||
|     @commands.guild_only() | ||||
|     @ww.command(name="start") | ||||
| @ -250,13 +215,10 @@ class Werewolf(Cog): | ||||
|         """ | ||||
|         game = await self._get_game(ctx) | ||||
|         if not game: | ||||
|             await ctx.maybe_send_embed("No game running, cannot start") | ||||
|             return | ||||
|             await ctx.send("No game running, cannot start") | ||||
| 
 | ||||
|         if not await game.setup(ctx): | ||||
|             pass  # ToDo something? | ||||
| 
 | ||||
|         await ctx.tick() | ||||
|             pass  # Do something? | ||||
| 
 | ||||
|     @commands.guild_only() | ||||
|     @ww.command(name="stop") | ||||
| @ -264,19 +226,17 @@ class Werewolf(Cog): | ||||
|         """ | ||||
|         Stops the current game | ||||
|         """ | ||||
|         # if ctx.guild is None: | ||||
|         #     # Private message, can't get guild | ||||
|         #     await ctx.send("Cannot stop game from PM!") | ||||
|         #     return | ||||
|         if ctx.guild is None: | ||||
|             # Private message, can't get guild | ||||
|             await ctx.send("Cannot start game from PM!") | ||||
|             return | ||||
|         if ctx.guild.id not in self.games or self.games[ctx.guild.id].game_over: | ||||
|             await ctx.maybe_send_embed("No game to stop") | ||||
|             await ctx.send("No game to stop") | ||||
|             return | ||||
| 
 | ||||
|         game = await self._get_game(ctx) | ||||
|         game.game_over = True | ||||
|         if game.current_action: | ||||
|             game.current_action.cancel() | ||||
|         await ctx.maybe_send_embed("Game has been stopped") | ||||
|         await ctx.send("Game has been stopped") | ||||
| 
 | ||||
|     @commands.guild_only() | ||||
|     @ww.command(name="vote") | ||||
| @ -290,7 +250,7 @@ class Werewolf(Cog): | ||||
|             target_id = None | ||||
| 
 | ||||
|         if target_id is None: | ||||
|             await ctx.maybe_send_embed("`id` must be an integer") | ||||
|             await ctx.send("`id` must be an integer") | ||||
|             return | ||||
| 
 | ||||
|         # if ctx.guild is None: | ||||
| @ -307,7 +267,7 @@ class Werewolf(Cog): | ||||
|         game = await self._get_game(ctx) | ||||
| 
 | ||||
|         if game is None: | ||||
|             await ctx.maybe_send_embed("No game running, cannot vote") | ||||
|             await ctx.send("No game running, cannot vote") | ||||
|             return | ||||
| 
 | ||||
|         # Game handles response now | ||||
| @ -317,7 +277,7 @@ class Werewolf(Cog): | ||||
|         elif channel in (c["channel"] for c in game.p_channels.values()): | ||||
|             await game.vote(ctx.author, target_id, channel) | ||||
|         else: | ||||
|             await ctx.maybe_send_embed("Nothing to vote for in this channel") | ||||
|             await ctx.send("Nothing to vote for in this channel") | ||||
| 
 | ||||
|     @ww.command(name="choose") | ||||
|     async def ww_choose(self, ctx: commands.Context, data): | ||||
| @ -328,7 +288,7 @@ class Werewolf(Cog): | ||||
|         """ | ||||
| 
 | ||||
|         if ctx.guild is not None: | ||||
|             await ctx.maybe_send_embed("This action is only available in DM's") | ||||
|             await ctx.send("This action is only available in DM's") | ||||
|             return | ||||
|         # DM nonsense, find their game | ||||
|         # If multiple games, panic | ||||
| @ -336,7 +296,7 @@ class Werewolf(Cog): | ||||
|             if await game.get_player_by_member(ctx.author): | ||||
|                 break  # game = game | ||||
|         else: | ||||
|             await ctx.maybe_send_embed("You're not part of any werewolf game") | ||||
|             await ctx.send("You're not part of any werewolf game") | ||||
|             return | ||||
| 
 | ||||
|         await game.choose(ctx, data) | ||||
| @ -346,7 +306,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): | ||||
| @ -356,7 +317,7 @@ class Werewolf(Cog): | ||||
|             if from_name: | ||||
|                 await menu(ctx, from_name, DEFAULT_CONTROLS) | ||||
|             else: | ||||
|                 await ctx.maybe_send_embed("No roles containing that name were found") | ||||
|                 await ctx.send("No roles containing that name were found") | ||||
| 
 | ||||
|     @ww_search.command(name="alignment") | ||||
|     async def ww_search_alignment(self, ctx: commands.Context, alignment: int): | ||||
| @ -366,7 +327,7 @@ class Werewolf(Cog): | ||||
|             if from_alignment: | ||||
|                 await menu(ctx, from_alignment, DEFAULT_CONTROLS) | ||||
|             else: | ||||
|                 await ctx.maybe_send_embed("No roles with that alignment were found") | ||||
|                 await ctx.send("No roles with that alignment were found") | ||||
| 
 | ||||
|     @ww_search.command(name="category") | ||||
|     async def ww_search_category(self, ctx: commands.Context, category: int): | ||||
| @ -376,7 +337,7 @@ class Werewolf(Cog): | ||||
|             if pages: | ||||
|                 await menu(ctx, pages, DEFAULT_CONTROLS) | ||||
|             else: | ||||
|                 await ctx.maybe_send_embed("No roles in that category were found") | ||||
|                 await ctx.send("No roles in that category were found") | ||||
| 
 | ||||
|     @ww_search.command(name="index") | ||||
|     async def ww_search_index(self, ctx: commands.Context, idx: int): | ||||
| @ -386,36 +347,28 @@ class Werewolf(Cog): | ||||
|             if idx_embed is not None: | ||||
|                 await ctx.send(embed=idx_embed) | ||||
|             else: | ||||
|                 await ctx.maybe_send_embed("Role ID not found") | ||||
|                 await ctx.send("Role ID not found") | ||||
| 
 | ||||
|     async def _get_game(self, ctx: commands.Context, game_code=None) -> Optional[Game]: | ||||
|         guild: discord.Guild = getattr(ctx, "guild", None) | ||||
|     async def _get_game(self, ctx: commands.Context, game_code=None): | ||||
|         guild: discord.Guild = ctx.guild | ||||
| 
 | ||||
|         if guild is None: | ||||
|             # Private message, can't get guild | ||||
|             await ctx.maybe_send_embed("Cannot start game from DM!") | ||||
|             await ctx.send("Cannot start game from PM!") | ||||
|             return None | ||||
|         if guild.id not in self.games or self.games[guild.id].game_over: | ||||
|             await ctx.maybe_send_embed("Starting a new game...") | ||||
|             valid, role, category, channel, log_channel = await self._get_settings(ctx) | ||||
|             await ctx.send("Starting a new game...") | ||||
|             success, role, category, channel, log_channel = await self._get_settings(ctx) | ||||
| 
 | ||||
|             if not valid: | ||||
|                 await ctx.maybe_send_embed("Cannot start a new game") | ||||
|             if not success: | ||||
|                 await ctx.send("Cannot start a new game") | ||||
|                 return None | ||||
| 
 | ||||
|             who_has_the_role = await anyone_has_role(guild.members, role) | ||||
|             if who_has_the_role: | ||||
|                 await ctx.maybe_send_embed( | ||||
|                     f"Cannot continue, {who_has_the_role.display_name} already has the game role." | ||||
|                 ) | ||||
|                 return None | ||||
|             self.games[guild.id] = Game( | ||||
|                 self.bot, guild, role, category, channel, log_channel, game_code | ||||
|             ) | ||||
|             self.games[guild.id] = Game(guild, role, category, channel, log_channel, game_code) | ||||
| 
 | ||||
|         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): | ||||
| @ -432,30 +385,23 @@ class Werewolf(Cog): | ||||
| 
 | ||||
|         if role_id is not None: | ||||
|             role = discord.utils.get(guild.roles, id=role_id) | ||||
|         # if role is None: | ||||
|         #     # await ctx.send("Game Role is invalid") | ||||
|         #     return False, None, None, None, None | ||||
|             if role is None: | ||||
|                 await ctx.send("Game Role is invalid") | ||||
|                 return False, None, None, None, None | ||||
|         if category_id is not None: | ||||
|             category = discord.utils.get(guild.categories, id=category_id) | ||||
|         # if category is None: | ||||
|         #     # await ctx.send("Game Category is invalid") | ||||
|         #     return False, role, None, None, None | ||||
|             if category is None: | ||||
|                 await ctx.send("Game Category is invalid") | ||||
|                 return False, None, None, None, None | ||||
|         if channel_id is not None: | ||||
|             channel = discord.utils.get(guild.text_channels, id=channel_id) | ||||
|         # if channel is None: | ||||
|         #     # await ctx.send("Village Channel is invalid") | ||||
|         #     return False, role, category, None, None | ||||
| 
 | ||||
|             if channel is None: | ||||
|                 await ctx.send("Village Channel is invalid") | ||||
|                 return False, None, None, None, None | ||||
|         if log_channel_id is not None: | ||||
|             log_channel = discord.utils.get(guild.text_channels, id=log_channel_id) | ||||
|             # if log_channel is None: | ||||
|             #     # await ctx.send("Log Channel is invalid") | ||||
|             #     return False, None, None, None, None | ||||
|             if log_channel is None: | ||||
|                 await ctx.send("Log Channel is invalid") | ||||
|                 return False, None, None, None, None | ||||
| 
 | ||||
|         return ( | ||||
|             role is not None and category is not None and channel is not None, | ||||
|             role, | ||||
|             category, | ||||
|             channel, | ||||
|             log_channel, | ||||
|         ) | ||||
|         return True, role, category, channel, log_channel | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user