diff --git a/README.md b/README.md index ce3bc62..098cd6d 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ Cog Function | Name | Status | Description (Click to see full status) | --- | --- | --- | | announcedaily | **Alpha** |
Send daily announcements to all servers at a specified timesCommissioned release, so suggestions will not be accepted
| +| audiotrivia | **Alpha** |
Guess the audio using the core trivia cogReplaces the core Trivia cog. Needs help adding audio trivia lists, please submit a PR to contribute
| | ccrole | **Beta** |
Create custom commands that also assign rolesMay have some bugs, please create an issue if you find any
| | chatter | **Alpha** |
Chat-bot trained to talk like your guildMissing some key features, but currently functional
| | coglint | **Alpha** |
Error check code in python syntax posted to discordWorks, but probably needs more turning to work for cogs
| diff --git a/audiotrivia/__init__.py b/audiotrivia/__init__.py new file mode 100644 index 0000000..327e34e --- /dev/null +++ b/audiotrivia/__init__.py @@ -0,0 +1,5 @@ +from .audiotrivia import AudioTrivia + + +def setup(bot): + bot.add_cog(AudioTrivia(bot)) diff --git a/audiotrivia/audiosession.py b/audiotrivia/audiosession.py new file mode 100644 index 0000000..780d4b9 --- /dev/null +++ b/audiotrivia/audiosession.py @@ -0,0 +1,74 @@ +"""Module to manage audio trivia sessions.""" +import asyncio + +import lavalink +from redbot.cogs.trivia import TriviaSession + + +class AudioSession(TriviaSession): + """Class to run a session of audio trivia""" + + def __init__(self, ctx, question_list: dict, settings: dict, player: lavalink.Player): + super().__init__(ctx, question_list, settings) + + self.player = player + + @classmethod + 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 + + 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`. + """ + await self._send_startup_msg() + max_score = self.settings["max_score"] + delay = self.settings["delay"] + timeout = self.settings["timeout"] + for question, answers in self._iter_questions(): + async with self.ctx.typing(): + await asyncio.sleep(3) + self.count += 1 + await self.player.stop() + + msg = "**Question number {}!**\n\nName this audio!".format(self.count) + await self.ctx.send(msg) + # print("Audio question: {}".format(question)) + + # await self.ctx.invoke(self.audio.play(ctx=self.ctx, query=question)) + # ctx_copy = copy(self.ctx) + + # 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.send("There are no more questions!") + await self.end_game() + + async def end_game(self): + await super().end_game() + await self.player.disconnect() diff --git a/audiotrivia/audiotrivia.py b/audiotrivia/audiotrivia.py new file mode 100644 index 0000000..6328e37 --- /dev/null +++ b/audiotrivia/audiotrivia.py @@ -0,0 +1,205 @@ +import datetime +import pathlib +from typing import List + +import lavalink +import yaml +from redbot.cogs.audio import Audio +from redbot.cogs.trivia import LOG +from redbot.cogs.trivia.trivia import InvalidListError, Trivia +from redbot.core import commands, Config, checks +from redbot.core.bot import Red +from redbot.core.data_manager import cog_data_path +from redbot.core.utils.chat_formatting import box + +from .audiosession import AudioSession + + +class AudioTrivia(Trivia): + """ + Custom commands + Creates commands used to display text and adjust roles + """ + + 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( + delay=30.0, + repeat=True + ) + + @commands.group() + @commands.guild_only() + @checks.mod_or_permissions(administrator=True) + async def atriviaset(self, ctx: commands.Context): + """Manage Audio Trivia settings.""" + audioset = self.audioconf.guild(ctx.guild) + settings_dict = await audioset.all() + msg = box( + "**Audio settings**\n" + "Answer time limit: {delay} seconds\n" + "Repeat Short Audio: {repeat}" + "".format(**settings_dict), + lang="py", + ) + await ctx.send(msg) + + @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.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.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. + + 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 = 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.send("There is already an ongoing trivia session in this channel.") + return + + if not Audio._player_check(ctx): + try: + if not ctx.author.voice.channel.permissions_for(ctx.me).connect or Audio._userlimit( + 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 + lavaplayer.store("guild", ctx.guild.id) + + 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 = [] + 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.send( + "Invalid category `{0}`. See `{1}audiotrivia list`" + " for a list of trivia categories." + "".format(category, ctx.prefix) + ) + except InvalidListError: + await ctx.send( + "There was an error parsing the trivia list for" + " the `{}` category. It may be formatted" + " incorrectly.".format(category) + ) + else: + trivia_dict.update(dict_) + authors.append(trivia_dict.pop("AUTHOR", None)) + continue + return + if not trivia_dict: + await ctx.send( + "The trivia list was parsed successfully, however it appears to be empty!" + ) + return + settings = await self.conf.guild(ctx.guild).all() + audiosettings = await self.audioconf.guild(ctx.guild).all() + config = trivia_dict.pop("CONFIG", None) + if config and settings["allow_override"]: + settings.update(config) + settings["lists"] = dict(zip(categories, reversed(authors))) + + # Delay in audiosettings overwrites delay in settings + combined_settings = {**settings, **audiosettings} + session = AudioSession.start(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) + + @audiotrivia.command(name="list") + @commands.guild_only() + async def audiotrivia_list(self, ctx: commands.Context): + """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. + + Parameters + ---------- + category : str + The desired category. Case sensitive. + + Returns + ------- + `dict` + A dict mapping questions (`str`) to answers (`list` of `str`). + + """ + try: + path = next(p for p in self._audio_lists() if p.stem == category) + except StopIteration: + raise FileNotFoundError("Could not find the `{}` category.".format(category)) + + with path.open(encoding="utf-8") as file: + try: + dict_ = yaml.load(file) + except yaml.error.YAMLError as exc: + raise InvalidListError("YAML parsing failed.") from exc + else: + return dict_ + + def _audio_lists(self) -> List[pathlib.Path]: + personal_lists = [p.resolve() for p in cog_data_path(self).glob("*.yaml")] + + return personal_lists + get_core_lists() + + +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")) diff --git a/audiotrivia/data/lists/csgo.yaml b/audiotrivia/data/lists/csgo.yaml new file mode 100644 index 0000000..ab726bb --- /dev/null +++ b/audiotrivia/data/lists/csgo.yaml @@ -0,0 +1,5 @@ +https://www.youtube.com/watch?v=DYWi8qdvWCk: +- AK47 +- AK 47 +https://www.youtube.com/watch?v=DmuK9Wml88E: +- P90 \ No newline at end of file diff --git a/audiotrivia/data/lists/games.yaml b/audiotrivia/data/lists/games.yaml new file mode 100644 index 0000000..eacde4b --- /dev/null +++ b/audiotrivia/data/lists/games.yaml @@ -0,0 +1,4 @@ +https://www.youtube.com/watch?v=FrceWR4XnVU: +- shovel knight +https://www.youtube.com/watch?v=Fn0khIn2wfc: +- super mario world \ No newline at end of file diff --git a/audiotrivia/data/lists/guitar.yaml b/audiotrivia/data/lists/guitar.yaml new file mode 100644 index 0000000..1c0d07e --- /dev/null +++ b/audiotrivia/data/lists/guitar.yaml @@ -0,0 +1,4 @@ +https://www.youtube.com/watch?v=hfyE220BsD0: +- holiday +https://www.youtube.com/watch?v=Hh3U9iPKeXQ: +- sultans of swing \ No newline at end of file diff --git a/audiotrivia/data/lists/league.yaml b/audiotrivia/data/lists/league.yaml new file mode 100644 index 0000000..323aadd --- /dev/null +++ b/audiotrivia/data/lists/league.yaml @@ -0,0 +1,4 @@ +https://www.youtube.com/watch?v=Hi1kUdreiWk: +- Jinx +https://www.youtube.com/watch?v=PNYHFluhOGI: +- Teemo \ No newline at end of file diff --git a/audiotrivia/info.json b/audiotrivia/info.json new file mode 100644 index 0000000..323a69d --- /dev/null +++ b/audiotrivia/info.json @@ -0,0 +1,20 @@ +{ + "author": [ + "Bobloy" + ], + "bot_version": [ + 3, + 0, + 0 + ], + "description": "Start an Audio Trivia game", + "hidden": false, + "install_msg": "Thank you for installing Audio trivia! Get started with `[p]help AudioTrivia`", + "requirements": [], + "short": "Start an Audio Trivia game", + "tags": [ + "fox", + "bobloy", + "games" + ] +} \ No newline at end of file