From 0d59a5220ff911990c831ed2a65c503f184c8932 Mon Sep 17 00:00:00 2001 From: bobloy Date: Fri, 5 Oct 2018 13:06:17 -0400 Subject: [PATCH 001/121] ctx.me instead of ctx.guild.me to work in dm's --- werewolf/builder.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/werewolf/builder.py b/werewolf/builder.py index da0bf1e..4364cb1 100644 --- a/werewolf/builder.py +++ b/werewolf/builder.py @@ -190,7 +190,7 @@ async def encode(roles, rand_roles): 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) + 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) @@ -210,7 +210,7 @@ async def next_group(ctx: commands.Context, pages: list, 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) + 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) @@ -295,7 +295,7 @@ class GameBuilder: 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) + 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) @@ -310,7 +310,7 @@ class GameBuilder: 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) + 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) From 0ee0199b11f852bbdc13e33cbacf66aafd184576 Mon Sep 17 00:00:00 2001 From: bobloy Date: Fri, 5 Oct 2018 13:50:55 -0400 Subject: [PATCH 002/121] role `__init__.py` experiment --- werewolf/builder.py | 4 +--- werewolf/roles/__init__.py | 5 +++++ 2 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 werewolf/roles/__init__.py diff --git a/werewolf/builder.py b/werewolf/builder.py index 4364cb1..ce00b28 100644 --- a/werewolf/builder.py +++ b/werewolf/builder.py @@ -8,9 +8,7 @@ import discord # Import all roles here from redbot.core import commands -from werewolf.roles.seer import Seer -from werewolf.roles.vanillawerewolf import VanillaWerewolf -from werewolf.roles.villager import Villager +from werewolf.roles import Seer, VanillaWerewolf, Villager from redbot.core.utils.menus import menu, prev_page, next_page, close_menu # All roles in this list for iterating diff --git a/werewolf/roles/__init__.py b/werewolf/roles/__init__.py new file mode 100644 index 0000000..ba929e5 --- /dev/null +++ b/werewolf/roles/__init__.py @@ -0,0 +1,5 @@ +from .seer import Seer +from .shifter import Shifter +from .vanillawerewolf import VanillaWerewolf +from .villager import Villager + From 849262969c2c1936a45a0ec5c3cd73d9740dc9aa Mon Sep 17 00:00:00 2001 From: bobloy Date: Wed, 5 Dec 2018 14:59:10 -0500 Subject: [PATCH 003/121] forgot some await's --- werewolf/game.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/werewolf/game.py b/werewolf/game.py index 181c198..78bd6f6 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -210,7 +210,7 @@ class Game: print("Pre-cycle") await asyncio.sleep(1) - asyncio.ensure_future(self._cycle()) # Start the loop + await asyncio.ensure_future(self._cycle()) # Start the loop ############START Notify structure############ async def _cycle(self): @@ -276,7 +276,7 @@ class Game: # Need a loop here to wait for trial to end (can_vote?) while self.ongoing_vote: - asyncio.sleep(5) + await asyncio.sleep(5) if check(): return From 9ef5836fa80138b8cd71229ac6c484bf3570ec8a Mon Sep 17 00:00:00 2001 From: bobloy Date: Thu, 30 Apr 2020 21:02:45 -0400 Subject: [PATCH 004/121] WIP fix to aiohttp payload error --- lovecalculator/lovecalculator.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/lovecalculator/lovecalculator.py b/lovecalculator/lovecalculator.py index 3a83cab..2038ccc 100644 --- a/lovecalculator/lovecalculator.py +++ b/lovecalculator/lovecalculator.py @@ -22,18 +22,19 @@ class LoveCalculator(Cog): x = lover.display_name y = loved.display_name - url = "https://www.lovecalculator.com/love.php?name1={}&name2={}".format( - x.replace(" ", "+"), y.replace(" ", "+") - ) + url = f"https://www.lovecalculator.com/love.php?name1={x}&name2={y}" + async with aiohttp.ClientSession() as session: async with session.get(url) as response: - soup_object = BeautifulSoup(await response.text(), "html.parser") - try: - description = ( - soup_object.find("div", attrs={"class": "result__score"}).get_text().strip() - ) - except: - description = "Dr. Love is busy right now" + resp = await response.text() + + soup_object = BeautifulSoup(resp, "html.parser") + try: + description = soup_object.find("div", attrs={"class": "result__score"}).get_text().strip() + img = soup_object.find("img", attrs={"class": "result__image"})['src'] + except: + description = "Dr. Love is busy right now" + img = None try: z = description[:2] @@ -48,5 +49,5 @@ class LoveCalculator(Cog): title = "Dr. Love has left a note for you." description = emoji + " " + description + " " + emoji - em = discord.Embed(title=title, description=description, color=discord.Color.red()) + em = discord.Embed(title=title, description=description, color=discord.Color.red(), url=img) await ctx.send(embed=em) From 4a9f0b9e7459b2423a5835c26b7505f56b6f18c0 Mon Sep 17 00:00:00 2001 From: bobloy Date: Wed, 19 Aug 2020 18:14:56 -0400 Subject: [PATCH 005/121] Template guilds --- stealemoji/stealemoji.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/stealemoji/stealemoji.py b/stealemoji/stealemoji.py index b1c7de5..70fc644 100644 --- a/stealemoji/stealemoji.py +++ b/stealemoji/stealemoji.py @@ -1,3 +1,5 @@ +from typing import Union + import discord from redbot.core import Config, checks, commands from redbot.core.bot import Red @@ -43,7 +45,7 @@ class StealEmoji(Cog): super().__init__() self.bot = red self.config = Config.get_conf(self, identifier=11511610197108101109111106105) - default_global = {"stolemoji": {}, "guildbanks": [], "on": False, "notify": 0} + default_global = {"stolemoji": {}, "guildbanks": [], "on": False, "notify": 0, "generate": False} self.config.register_global(**default_global) @@ -193,7 +195,7 @@ class StealEmoji(Cog): # This is now a custom emoji that the bot doesn't have access to, time to steal it # First, do I have an available guildbank? - guildbank = None + guildbank: Union[discord.Guild, None] = None banklist = await self.config.guildbanks() for guild_id in banklist: guild: discord.Guild = self.bot.get_guild(guild_id) @@ -203,9 +205,12 @@ class StealEmoji(Cog): break if guildbank is None: - # print("No guildbank to store emoji") - # Eventually make a new banklist - return + if await self.config.generate(): + guild_template = await self.bot.fetch_template("https://discord.new/S93bqTqKQ9rM") + guildbank: discord.Guild = await self.bot.create_guild("StealEmoji Guildbank", code=guild_template) + await self.bot.send_to_owners(guildbank.channels) + else: + return # Next, have I saved this emoji before (because uploaded emoji != orignal emoji) From 5693d690e1565185985f9acc3647afdc17e7a221 Mon Sep 17 00:00:00 2001 From: bobloy Date: Fri, 21 Aug 2020 17:22:56 -0400 Subject: [PATCH 006/121] Autobanking with templates --- stealemoji/stealemoji.py | 38 ++++++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/stealemoji/stealemoji.py b/stealemoji/stealemoji.py index 70fc644..5a08265 100644 --- a/stealemoji/stealemoji.py +++ b/stealemoji/stealemoji.py @@ -1,3 +1,4 @@ +import logging from typing import Union import discord @@ -5,7 +6,7 @@ from redbot.core import Config, checks, commands from redbot.core.bot import Red from redbot.core.commands import Cog - +log = logging.getLogger("red.fox_v3.stealemoji") # Replaced with discord.Asset.read() # async def fetch_img(session: aiohttp.ClientSession, url: StrOrURL): # async with session.get(url) as response: @@ -45,7 +46,13 @@ class StealEmoji(Cog): super().__init__() self.bot = red self.config = Config.get_conf(self, identifier=11511610197108101109111106105) - default_global = {"stolemoji": {}, "guildbanks": [], "on": False, "notify": 0, "generate": False} + default_global = { + "stolemoji": {}, + "guildbanks": [], + "on": False, + "notify": 0, + "autobank": False, + } self.config.register_global(**default_global) @@ -126,6 +133,17 @@ class StealEmoji(Cog): await ctx.maybe_send_embed("Collection is now " + str(not curr_setting)) + @checks.is_owner() + @stealemoji.command(name="autobank") + async def se_autobank(self, ctx): + """Toggles automatically creating new guilds as emoji banks""" + curr_setting = await self.config.autobank() + await self.config.autobank.set(not curr_setting) + + self.is_on = await self.config.autobank() + + await ctx.maybe_send_embed("AutoBanking is now " + str(not curr_setting)) + @checks.is_owner() @commands.guild_only() @stealemoji.command(name="bank") @@ -205,10 +223,18 @@ class StealEmoji(Cog): break if guildbank is None: - if await self.config.generate(): - guild_template = await self.bot.fetch_template("https://discord.new/S93bqTqKQ9rM") - guildbank: discord.Guild = await self.bot.create_guild("StealEmoji Guildbank", code=guild_template) - await self.bot.send_to_owners(guildbank.channels) + 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 + invite = await guildbank.text_channels[0].create_invite() + await self.bot.send_to_owners(invite) + log.info(f"Guild created id {guildbank.id}. Invite: {invite}") else: return From d8ec75701dc567f458ebd42efe29f5a031272996 Mon Sep 17 00:00:00 2001 From: bobloy Date: Fri, 21 Aug 2020 17:29:54 -0400 Subject: [PATCH 007/121] Problem with invites still, panic --- stealemoji/stealemoji.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/stealemoji/stealemoji.py b/stealemoji/stealemoji.py index 5a08265..2ee68eb 100644 --- a/stealemoji/stealemoji.py +++ b/stealemoji/stealemoji.py @@ -1,3 +1,4 @@ +import asyncio import logging from typing import Union @@ -232,7 +233,13 @@ class StealEmoji(Cog): 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) + + await asyncio.sleep(2) + invite = await guildbank.text_channels[0].create_invite() + await self.bot.send_to_owners(invite) log.info(f"Guild created id {guildbank.id}. Invite: {invite}") else: From a0042da1700b727bfa2d75fa30ca5635f066f04a Mon Sep 17 00:00:00 2001 From: bobloy Date: Mon, 24 Aug 2020 08:19:55 -0400 Subject: [PATCH 008/121] "fix" sending invites --- stealemoji/stealemoji.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/stealemoji/stealemoji.py b/stealemoji/stealemoji.py index 2ee68eb..492ef70 100644 --- a/stealemoji/stealemoji.py +++ b/stealemoji/stealemoji.py @@ -238,7 +238,14 @@ class StealEmoji(Cog): await asyncio.sleep(2) - invite = await guildbank.text_channels[0].create_invite() + 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() await self.bot.send_to_owners(invite) log.info(f"Guild created id {guildbank.id}. Invite: {invite}") From 2ea7819b8ba4f5f8990159f2c8117641ea28f310 Mon Sep 17 00:00:00 2001 From: bobloy Date: Mon, 24 Aug 2020 08:27:36 -0400 Subject: [PATCH 009/121] Library updated, use the new functioning async api calls --- launchlib/info.json | 2 +- launchlib/launchlib.py | 22 ++++++++++------------ 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/launchlib/info.json b/launchlib/info.json index 8df1059..c1c7ad7 100644 --- a/launchlib/info.json +++ b/launchlib/info.json @@ -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"], + "requirements": ["python-launch-library>=1.0.6"], "tags": [ "bobloy", "utils", diff --git a/launchlib/launchlib.py b/launchlib/launchlib.py index 0c6eeef..e34a23d 100644 --- a/launchlib/launchlib.py +++ b/launchlib/launchlib.py @@ -34,10 +34,10 @@ class LaunchLib(commands.Cog): """Nothing to delete""" return - async def _embed_launch_data(self, launch: ll.Launch): - status: ll.LaunchStatus = launch.get_status() + async def _embed_launch_data(self, launch: ll.AsyncLaunch): + status: ll.AsyncLaunchStatus = await launch.get_status() - rocket: ll.Rocket = launch.rocket + rocket: ll.AsyncRocket = launch.rocket title = launch.name description = status.description @@ -105,15 +105,13 @@ class LaunchLib(commands.Cog): @launchlib.command() async def next(self, ctx: commands.Context, num_launches: int = 1): # launches = await api.async_next_launches(num_launches) - loop = asyncio.get_running_loop() - - launches = await loop.run_in_executor( - None, functools.partial(self.api.fetch_launch, num=num_launches) - ) - - # launches = self.api.fetch_launch(num=num_launches) - - print(len(launches)) + # loop = asyncio.get_running_loop() + # + # launches = await loop.run_in_executor( + # None, functools.partial(self.api.fetch_launch, num=num_launches) + # ) + # + launches = await self.api.async_fetch_launch(num=num_launches) async with ctx.typing(): for x, launch in enumerate(launches): From 43e2a46c553ea862be20841ebf7b6c1ba556e3c8 Mon Sep 17 00:00:00 2001 From: bobloy Date: Mon, 24 Aug 2020 08:57:12 -0400 Subject: [PATCH 010/121] Chatter install instructions in install message --- chatter/info.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chatter/info.json b/chatter/info.json index e9749ff..b79e587 100644 --- a/chatter/info.json +++ b/chatter/info.json @@ -5,7 +5,7 @@ "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! Get started ith `[p]load chatter` and `[p]help Chatter`", + "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/gunthercox/chatterbot-corpus@master#egg=chatterbot_corpus", "mathparse>=0.1,<0.2", From 6f414be6ab52398bfaeb4a6154e2053b741e4c8e Mon Sep 17 00:00:00 2001 From: bobloy Date: Mon, 24 Aug 2020 12:51:57 -0400 Subject: [PATCH 011/121] Add chat channel --- chatter/chat.py | 52 ++++++++++++++++++++++++++++++++++++------------- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/chatter/chat.py b/chatter/chat.py index a464e40..a81e669 100644 --- a/chatter/chat.py +++ b/chatter/chat.py @@ -3,6 +3,7 @@ import logging import os import pathlib from datetime import datetime, timedelta +from typing import Optional import discord from chatterbot import ChatBot @@ -17,6 +18,13 @@ from redbot.core.utils.predicates import MessagePredicate log = logging.getLogger("red.fox_v3.chatter") +def my_local_get_prefix(prefixes, content): + for p in prefixes: + if content.startswith(p): + return p + return None + + class ENG_LG: ISO_639_1 = "en_core_web_lg" ISO_639 = "eng" @@ -45,7 +53,7 @@ class Chatter(Cog): self.bot = bot self.config = Config.get_conf(self, identifier=6710497116116101114) default_global = {} - default_guild = {"whitelist": None, "days": 1, "convo_delta": 15} + default_guild = {"whitelist": None, "days": 1, "convo_delta": 15, "chatchannel": None} path: pathlib.Path = cog_data_path(self) self.data_path = path / "database.sqlite3" @@ -183,6 +191,25 @@ class Chatter(Cog): if ctx.invoked_subcommand is None: pass + @chatter.command(name="channel") + async def chatter_channel( + self, ctx: commands.Context, channel: Optional[discord.TextChannel] = None + ): + """ + Set a channel that the bot will respond in without mentioning it + + Pass with no channel object to clear this guild's channel + """ + if channel is None: + await self.config.guild(ctx.guild).chatchannel.set(None) + await ctx.maybe_send_embed("Chat channel for guild is cleared") + else: + if channel.guild != ctx.guild: + await ctx.maybe_send_embed("What are you trying to pull here? :eyes:") + return + await self.config.guild(ctx.guild).chatchannel.set(channel.id) + await ctx.maybe_send_embed(f"Chat channel is now {channel.mention}") + @chatter.command(name="cleardata") async def chatter_cleardata(self, ctx: commands.Context, confirm: bool = False): """ @@ -434,24 +461,21 @@ class Chatter(Cog): ########### # Thank you Cog-Creators + channel: discord.TextChannel = message.channel - def my_local_get_prefix(prefixes, content): - for p in prefixes: - if content.startswith(p): - return p - return None - - when_mentionables = commands.when_mentioned(self.bot, message) + if channel.id == await self.config.guild(guild).chatchannel(): + pass # good to go + else: + when_mentionables = commands.when_mentioned(self.bot, message) - prefix = my_local_get_prefix(when_mentionables, message.content) + prefix = my_local_get_prefix(when_mentionables, message.content) - if prefix is None: - # print("not mentioned") - return + if prefix is None: + # print("not mentioned") + return - channel: discord.TextChannel = message.channel + message.content = message.content.replace(prefix, "", 1) - message.content = message.content.replace(prefix, "", 1) text = message.clean_content async with channel.typing(): From ea0cb8c51b6e12ec30843f58a55edef84d465661 Mon Sep 17 00:00:00 2001 From: bobloy Date: Mon, 24 Aug 2020 13:02:48 -0400 Subject: [PATCH 012/121] hours and days --- timerole/info.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/timerole/info.json b/timerole/info.json index ec74efb..ca9375b 100644 --- a/timerole/info.json +++ b/timerole/info.json @@ -3,10 +3,10 @@ "Bobloy" ], "min_bot_version": "3.3.0", - "description": "Apply roles based on the # of days on server", + "description": "Apply roles based on the # of hours or days on server", "hidden": false, "install_msg": "Thank you for installing timerole.\nGet started with `[p]load timerole`. Configure with `[p]timerole`", - "short": "Apply roles after # of days", + "short": "Apply roles after # of hours or days", "end_user_data_statement": "This cog does not store any End User Data", "tags": [ "bobloy", From 1a5aaff268f6106afa04b573c07969162cd68932 Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 25 Aug 2020 16:03:36 -0400 Subject: [PATCH 013/121] initial commit of FIFO, RedConfigJobStore is WIP --- fifo/__init__.py | 7 + fifo/fifo.py | 250 ++++++++++++++++++++++++++++++++++++ fifo/info.json | 18 +++ fifo/jobstores/redconfig.py | 35 +++++ 4 files changed, 310 insertions(+) create mode 100644 fifo/__init__.py create mode 100644 fifo/fifo.py create mode 100644 fifo/info.json create mode 100644 fifo/jobstores/redconfig.py diff --git a/fifo/__init__.py b/fifo/__init__.py new file mode 100644 index 0000000..dedd355 --- /dev/null +++ b/fifo/__init__.py @@ -0,0 +1,7 @@ +from .fifo import FIFO + + +async def setup(bot): + cog = FIFO(bot) + await cog.load_tasks() + bot.add_cog(cog) diff --git a/fifo/fifo.py b/fifo/fifo.py new file mode 100644 index 0000000..ae484d4 --- /dev/null +++ b/fifo/fifo.py @@ -0,0 +1,250 @@ +from typing import Dict, Union + +from apscheduler.executors.asyncio import AsyncIOExecutor +from apscheduler.jobstores.memory import MemoryJobStore +from apscheduler.triggers.base import BaseTrigger +from apscheduler.triggers.combining import AndTrigger, OrTrigger +from apscheduler.triggers.date import DateTrigger +from apscheduler.triggers.interval import IntervalTrigger +from dateutil import parser +from redbot.core import Config, checks, commands +from redbot.core.bot import Red +from apscheduler.schedulers.asyncio import AsyncIOScheduler +import discord +import asyncio +import datetime + +from redbot.core.commands import DictConverter, TimedeltaConverter, parse_timedelta +from redbot.core.utils import AsyncIter + + +def get_trigger(data): + if data["type"] == "interval": + parsed_time = parse_timedelta(data["timedelta_str"]) + return IntervalTrigger(days=parsed_time.days, seconds=parsed_time.seconds) + + if data["type"] == "date": + return DateTrigger(parser.parse(data["strtime"])) + + if data["type"] == "cron": + return None # TODO: Cron parsing + + +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 + return OrTrigger(get_trigger(t_data) for t_data in data["triggers"]) + + return get_trigger(data[0]) + + +class Task: + + default_task_data = {"triggers": [], "command_str": ""} + + default_trigger = { + "type": "", + "timedelta_str": "", + } + + def __init__(self, name: str, guild_id, config: Config): + self.name = name + self.guild_id = guild_id + self.config = config + + self.data = None + + async def load_from_data(self, data: Dict): + self.data = data.copy() + + async def load_from_config(self): + self.data = await self.config.guild_from_id(self.guild_id).tasks.get_raw( + self.name, default=None + ) + return self.data + + async def get_trigger(self) -> Union[BaseTrigger, None]: + if self.data is None: + await self.load_from_config() + + return parse_triggers(self.data) + + # async def set_job_id(self, job_id): + # if self.data is None: + # await self.load_from_config() + # + # self.data["job_id"] = job_id + + async def save_data(self): + await self.config.guild_from_id(self.guild_id).tasks.set_raw(self.name, value=self.data) + + async def execute(self): + pass # TODO: something something invoke command + + async def add_trigger(self, param, parsed_time): + pass + + +class FIFO(commands.Cog): + """ + Simple Scheduling Cog + + Named after the simplest scheduling algorithm: First In First Out + """ + + def __init__(self, bot: Red): + super().__init__() + self.bot = bot + self.config = Config.get_conf(self, identifier=70737079, force_registration=True) + + default_global = {"jobs_index": {}, "jobs": []} + default_guild = {"tasks": {}} + + self.config.register_global(**default_global) + self.config.register_guild(**default_guild) + + jobstores = {"default": MemoryJobStore()} + + job_defaults = {"coalesce": False, "max_instances": 1} + + # executors = {"default": AsyncIOExecutor()} + + # Default executor is already AsyncIOExecutor + self.scheduler = AsyncIOScheduler( + jobstores=jobstores, job_defaults=job_defaults + ) + + async def red_delete_data_for_user(self, **kwargs): + """Nothing to delete""" + return + + async def _parse_command(self, command_to_parse: str): + return False # TODO: parse commands somehow + + @checks.is_owner() # Will be reduced when I figure out permissions later + @commands.group() + async def fifo(self, ctx: commands.Context): + """ + Base command for handling scheduling of tasks + """ + if ctx.invoked_subcommand is None: + pass + + @fifo.command(name="list") + async def fifo_list(self, ctx: commands.Context, all_guilds: bool = False): + """ + Lists all current tasks and their triggers. + + Do `[p]fifo list True` to see tasks from all guilds + """ + if all_guilds: + pass + else: + pass # TODO: parse and display tasks + + @fifo.command(name="add") + async def fifo_add(self, ctx: commands.Context, task_name: str, *, command_to_execute: str): + """ + Add a new task to this guild's task list + """ + pass + + @fifo.command(name="delete") + async def fifo_delete(self, ctx: commands.Context, task_name: str, *, command_to_execute: str): + """ + Deletes a task from this guild's task list + """ + pass + + @fifo.group(name="trigger") + async def fifo_trigger(self, ctx: commands.Context): + """ + Add a new trigger for a task from the current guild. + """ + if ctx.invoked_subcommand is None: + pass + + @fifo_trigger.command(name="interval") + async def fifo_trigger_interval( + self, ctx: commands.Context, task_name: str, interval_str: TimedeltaConverter + ): + """ + Add an interval trigger to the specified task + """ + + task = Task(task_name, ctx.guild.id, self.config) + 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 + + result = await task.add_trigger("interval", interval_str) + if not result: + await ctx.maybe_send_embed( + "Failed to add an interval trigger to this task, see console for logs" + ) + return + await ctx.tick() + + @fifo_trigger.command(name="date") + async def fifo_trigger_date( + self, ctx: commands.Context, task_name: str, datetime_str: TimedeltaConverter + ): + """ + Add a "run once" datetime trigger to the specified task + """ + + task = Task(task_name, ctx.guild.id, self.config) + 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 + + 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" + ) + return + await ctx.tick() + + @fifo_trigger.command(name="cron") + async def fifo_trigger_cron( + self, ctx: commands.Context, task_name: str, cron_settings: DictConverter + ): + """ + Add a "time of day" trigger to the specified task + """ + await ctx.maybe_send_embed("Not yet implemented") + + async def load_tasks(self): + """ + Run once on cog load. + """ + all_guilds = await self.config.all_guilds() + async for guild_id, guild_data in AsyncIter(all_guilds["tasks"].items(), steps=100): + for task_name, task_data in guild_data["tasks"].items(): + task = Task(task_name, guild_id, self.config) + await task.load_from_data(task_data) + + job = self.scheduler.add_job( + task.execute, id=task_name + "_" + guild_id, trigger=await task.get_trigger(), + ) + + self.scheduler.start() + + # async def parent_loop(self): + # await asyncio.sleep(60) + # asyncio.create_task(self.process_tasks(datetime.datetime.utcnow())) + # + # async def process_tasks(self, now: datetime.datetime): + # for task in self.tasks: + # pass diff --git a/fifo/info.json b/fifo/info.json new file mode 100644 index 0000000..4a9cd1c --- /dev/null +++ b/fifo/info.json @@ -0,0 +1,18 @@ +{ + "author": [ + "Bobloy" + ], + "min_bot_version": "3.3.0", + "description": "Schedule commands to be run by certain 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": "Schedule commands to be run by certain at certain times or intervals\"", + "end_user_data_statement": "This cog does not store any End User Data", + "tags": [ + "bobloy", + "utilities", + "tools", + "tool", + "roles" + ] +} \ No newline at end of file diff --git a/fifo/jobstores/redconfig.py b/fifo/jobstores/redconfig.py new file mode 100644 index 0000000..0707b6e --- /dev/null +++ b/fifo/jobstores/redconfig.py @@ -0,0 +1,35 @@ +import asyncio + +from apscheduler.jobstores.base import BaseJobStore +from redbot.core import Config + + +class RedConfigJobStore(BaseJobStore): + def __init__(self, config: Config, loop): + super().__init__() + self.config = config + self.loop: asyncio.BaseEventLoop = loop + + def lookup_job(self, job_id): + task = self.loop.create_task(self.config.jobs_index.get_raw(job_id)) + + def get_due_jobs(self, now): + pass + + def get_next_run_time(self): + pass + + def get_all_jobs(self): + pass + + def add_job(self, job): + pass + + def update_job(self, job): + pass + + def remove_job(self, job_id): + pass + + def remove_all_jobs(self): + pass From c6a9116a9279b66178a6c0882a9cd4b6f24fe229 Mon Sep 17 00:00:00 2001 From: bobloy Date: Wed, 26 Aug 2020 17:36:04 -0400 Subject: [PATCH 014/121] Almost to adding triggers --- fifo/__init__.py | 1 - fifo/datetimeconverter.py | 16 +++ fifo/fifo.py | 252 +++++++++++++++++++++++++++++------- fifo/jobstores/redconfig.py | 35 ----- fifo/redconfigjobstore.py | 189 +++++++++++++++++++++++++++ 5 files changed, 410 insertions(+), 83 deletions(-) create mode 100644 fifo/datetimeconverter.py delete mode 100644 fifo/jobstores/redconfig.py create mode 100644 fifo/redconfigjobstore.py diff --git a/fifo/__init__.py b/fifo/__init__.py index dedd355..860ab97 100644 --- a/fifo/__init__.py +++ b/fifo/__init__.py @@ -3,5 +3,4 @@ from .fifo import FIFO async def setup(bot): cog = FIFO(bot) - await cog.load_tasks() bot.add_cog(cog) diff --git a/fifo/datetimeconverter.py b/fifo/datetimeconverter.py new file mode 100644 index 0000000..bdfbf88 --- /dev/null +++ b/fifo/datetimeconverter.py @@ -0,0 +1,16 @@ +from datetime import datetime +from typing import TYPE_CHECKING + +from discord.ext.commands import BadArgument, Converter +from dateutil import parser + + +if TYPE_CHECKING: + DatetimeConverter = datetime +else: + class DatetimeConverter(Converter): + async def convert(self, ctx, argument) -> datetime: + dt = parser.parse(argument) + if dt is not None: + return dt + raise BadArgument() diff --git a/fifo/fifo.py b/fifo/fifo.py index ae484d4..1ffb612 100644 --- a/fifo/fifo.py +++ b/fifo/fifo.py @@ -1,34 +1,34 @@ +from datetime import datetime, timedelta from typing import Dict, Union -from apscheduler.executors.asyncio import AsyncIOExecutor -from apscheduler.jobstores.memory import MemoryJobStore +import discord +from apscheduler.schedulers.asyncio import AsyncIOScheduler from apscheduler.triggers.base import BaseTrigger -from apscheduler.triggers.combining import AndTrigger, OrTrigger +from apscheduler.triggers.combining import OrTrigger from apscheduler.triggers.date import DateTrigger from apscheduler.triggers.interval import IntervalTrigger from dateutil import parser from redbot.core import Config, checks, commands from redbot.core.bot import Red -from apscheduler.schedulers.asyncio import AsyncIOScheduler -import discord -import asyncio -import datetime - from redbot.core.commands import DictConverter, TimedeltaConverter, parse_timedelta -from redbot.core.utils import AsyncIter + +from .datetimeconverter import DatetimeConverter +from .redconfigjobstore import RedConfigJobStore def get_trigger(data): if data["type"] == "interval": - parsed_time = parse_timedelta(data["timedelta_str"]) + parsed_time = data["time_data"] return IntervalTrigger(days=parsed_time.days, seconds=parsed_time.seconds) if data["type"] == "date": - return DateTrigger(parser.parse(data["strtime"])) + return DateTrigger(data["time_data"]) if data["type"] == "cron": return None # TODO: Cron parsing + return False + def parse_triggers(data: Union[Dict, None]): if data is None or not data.get("triggers", False): # No triggers @@ -40,33 +40,95 @@ def parse_triggers(data: Union[Dict, None]): return get_trigger(data[0]) -class Task: +class FakeMessage: + _state = None + +# class FakeMessage(discord.Message): +# def __init__(self): +# super().__init__(state=None, channel=None, data=None) + + +class Task: default_task_data = {"triggers": [], "command_str": ""} default_trigger = { "type": "", - "timedelta_str": "", + "time_data": None, # Used for Interval and Date Triggers } - def __init__(self, name: str, guild_id, config: Config): + def __init__(self, name: str, guild_id, config: Config, author_id=None, bot: Red = None): self.name = name self.guild_id = guild_id self.config = config - + self.bot = bot + self.author_id = author_id self.data = None - async def load_from_data(self, data: Dict): - self.data = data.copy() + async def _encode_time_data(self): + if not self.data or not self.data.get("triggers", None): + return None + + triggers = [] + for t in self.data["triggers"]: + if t["type"] == "interval": # Convert into timedelta + td: timedelta = t["time_data"] + + triggers.append({"type": t["type"], "time_data": {"days": td.days, "seconds": td.seconds} }) + + if t["type"] == "date": # Convert into datetime + dt: datetime = t["time_data"] + 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, + }}) + + if t["type"] == "cron": + raise NotImplemented + raise NotImplemented + + return triggers + + async def _decode_time_data(self): + if not self.data or not self.data.get("triggers", None): + return + + for t in self.data["triggers"]: + if t["type"] == "interval": # Convert into timedelta + t["time_data"] = timedelta(**t["time_data"]) + + if t["type"] == "date": # Convert into datetime + t["time_data"] = datetime(**t["time_data"]) + + if t["type"] == "cron": + raise NotImplemented + raise NotImplemented + + # async def load_from_data(self, data: Dict): + # self.data = data.copy() async def load_from_config(self): - self.data = await self.config.guild_from_id(self.guild_id).tasks.get_raw( + data = await self.config.guild_from_id(self.guild_id).tasks.get_raw( self.name, default=None ) + + if not data: + return + + self.author_id = data["author_id"] + self.guild_id = data["guild_id"] + + self.data = data["data"] + + await self._decode_time_data() return self.data async def get_trigger(self) -> Union[BaseTrigger, None]: - if self.data is None: + if not self.data: await self.load_from_config() return parse_triggers(self.data) @@ -77,14 +139,69 @@ class Task: # # self.data["job_id"] = job_id + async def save_all(self): + """To be used when creating an new task""" + + data_to_save = self.default_task_data.copy() + if self.data: + data_to_save["command_str"] = self.data.get("command_str", "") + data_to_save["triggers"] = await self._encode_time_data() + + to_save = { + "guild_id": self.guild_id, + "author_id": self.author_id, + "data": data_to_save, + } + await self.config.guild_from_id(self.guild_id).tasks.set_raw(self.name, value=to_save) + async def save_data(self): - await self.config.guild_from_id(self.guild_id).tasks.set_raw(self.name, value=self.data) + """To be used when updating triggers""" + if not self.data: + return + await self.config.guild_from_id(self.guild_id).tasks.set_raw( + self.name, "data", value=await self._encode_time_data() + ) async def execute(self): - pass # TODO: something something invoke command + if not self.data or self.data["command_str"]: + return False + message = FakeMessage() + message.guild = self.bot.get_guild(self.guild_id) # used for get_prefix + message.author = message.guild.get_member(self.author_id) + message.content = await self.bot.get_prefix(message) + self.data["command_str"] - async def add_trigger(self, param, parsed_time): - pass + if not message.guild or not message.author or not message.content: + return False + + new_ctx: commands.Context = await self.bot.get_context(message) + if not new_ctx.valid: + return False + + await self.bot.invoke(new_ctx) + return True + + async def set_bot(self, bot: Red): + self.bot = bot + + async def set_author(self, author: Union[discord.User, str]): + self.author_id = getattr(author, "id", None) or author + + async def set_commmand_str(self, command_str): + if not self.data: + self.data = self.default_task_data.copy() + self.data["command_str"] = command_str + return True + + async def add_trigger(self, param, parsed_time: Union[timedelta, datetime]): + trigger_data = {"type": param, "time_data": parsed_time} + if not get_trigger(trigger_data): + return False + + if not self.data: + self.data = self.default_task_data.copy() + + self.data["triggers"].append(trigger_data) + return True class FIFO(commands.Cog): @@ -105,23 +222,50 @@ class FIFO(commands.Cog): self.config.register_global(**default_global) self.config.register_guild(**default_guild) - jobstores = {"default": MemoryJobStore()} + jobstores = {"default": RedConfigJobStore(self.config, self.bot)} job_defaults = {"coalesce": False, "max_instances": 1} # executors = {"default": AsyncIOExecutor()} # Default executor is already AsyncIOExecutor - self.scheduler = AsyncIOScheduler( - jobstores=jobstores, job_defaults=job_defaults - ) + self.scheduler = AsyncIOScheduler(jobstores=jobstores, job_defaults=job_defaults) + + self.scheduler.start() async def red_delete_data_for_user(self, **kwargs): """Nothing to delete""" return - async def _parse_command(self, command_to_parse: str): - return False # TODO: parse commands somehow + def _assemble_job_id(self, task_name, guild_id): + return task_name + "_" + guild_id + + async def _check_parsable_command(self, ctx: commands.Context, command_to_parse: str): + message = FakeMessage() + message.content = ctx.prefix + command_to_parse + message.author = ctx.author + message.guild = ctx.guild + + new_ctx: commands.Context = await self.bot.get_context(message) + + return new_ctx.valid + + async def _get_job(self, task_name, guild_id): + return self.scheduler.get_job(self._assemble_job_id(task_name, guild_id)) + + async def _add_job(self, task): + return self.scheduler.add_job( + task.execute, + id=self._assemble_job_id(task.name, task.guild_id), + trigger=await task.get_trigger(), + ) + + @checks.is_owner() + @commands.command() + async def fifoclear(self, ctx: commands.Context): + """Debug command to clear fifo config""" + await self.config.guild(ctx.guild).tasks.clear() + await ctx.tick() @checks.is_owner() # Will be reduced when I figure out permissions later @commands.group() @@ -149,10 +293,21 @@ class FIFO(commands.Cog): """ Add a new task to this guild's task list """ - pass + if (await self.config.guild(ctx.guild).tasks.get_raw(task_name, default=None)) is not None: + await ctx.maybe_send_embed(f"Task already exists with {task_name=}") + return + + if not await self._check_parsable_command(ctx, command_to_execute): + await ctx.maybe_send_embed("Failed to parse command. Make sure to include the prefix") + return + + task = Task(task_name, ctx.guild.id, self.config, ctx.author.id) + await task.set_commmand_str(command_to_execute) + await task.save_all() + await ctx.tick() @fifo.command(name="delete") - async def fifo_delete(self, ctx: commands.Context, task_name: str, *, command_to_execute: str): + async def fifo_delete(self, ctx: commands.Context, task_name: str): """ Deletes a task from this guild's task list """ @@ -189,11 +344,12 @@ class FIFO(commands.Cog): "Failed to add an interval trigger to this task, see console for logs" ) return + await task.save_data() await ctx.tick() @fifo_trigger.command(name="date") async def fifo_trigger_date( - self, ctx: commands.Context, task_name: str, datetime_str: TimedeltaConverter + self, ctx: commands.Context, task_name: str, datetime_str: DatetimeConverter ): """ Add a "run once" datetime trigger to the specified task @@ -214,6 +370,8 @@ class FIFO(commands.Cog): "Failed to add a date trigger to this task, see console for logs" ) return + + await task.save_data() await ctx.tick() @fifo_trigger.command(name="cron") @@ -225,21 +383,21 @@ class FIFO(commands.Cog): """ await ctx.maybe_send_embed("Not yet implemented") - async def load_tasks(self): - """ - Run once on cog load. - """ - all_guilds = await self.config.all_guilds() - async for guild_id, guild_data in AsyncIter(all_guilds["tasks"].items(), steps=100): - for task_name, task_data in guild_data["tasks"].items(): - task = Task(task_name, guild_id, self.config) - await task.load_from_data(task_data) - - job = self.scheduler.add_job( - task.execute, id=task_name + "_" + guild_id, trigger=await task.get_trigger(), - ) - - self.scheduler.start() + # async def load_tasks(self): + # """ + # Run once on cog load. + # """ + # all_guilds = await self.config.all_guilds() + # async for guild_id, guild_data in AsyncIter(all_guilds["tasks"].items(), steps=100): + # for task_name, task_data in guild_data["tasks"].items(): + # task = Task(task_name, guild_id, self.config) + # await task.load_from_data(task_data) + # + # job = self.scheduler.add_job( + # task.execute, id=task_name + "_" + guild_id, trigger=await task.get_trigger(), + # ) + # + # self.scheduler.start() # async def parent_loop(self): # await asyncio.sleep(60) diff --git a/fifo/jobstores/redconfig.py b/fifo/jobstores/redconfig.py deleted file mode 100644 index 0707b6e..0000000 --- a/fifo/jobstores/redconfig.py +++ /dev/null @@ -1,35 +0,0 @@ -import asyncio - -from apscheduler.jobstores.base import BaseJobStore -from redbot.core import Config - - -class RedConfigJobStore(BaseJobStore): - def __init__(self, config: Config, loop): - super().__init__() - self.config = config - self.loop: asyncio.BaseEventLoop = loop - - def lookup_job(self, job_id): - task = self.loop.create_task(self.config.jobs_index.get_raw(job_id)) - - def get_due_jobs(self, now): - pass - - def get_next_run_time(self): - pass - - def get_all_jobs(self): - pass - - def add_job(self, job): - pass - - def update_job(self, job): - pass - - def remove_job(self, job_id): - pass - - def remove_all_jobs(self): - pass diff --git a/fifo/redconfigjobstore.py b/fifo/redconfigjobstore.py new file mode 100644 index 0000000..9db7213 --- /dev/null +++ b/fifo/redconfigjobstore.py @@ -0,0 +1,189 @@ +import asyncio + +from apscheduler.jobstores.base import ConflictingIdError, JobLookupError +from apscheduler.jobstores.memory import MemoryJobStore +from apscheduler.util import datetime_to_utc_timestamp +from redbot.core import Config + + +# TODO: use get_lock on config +from redbot.core.bot import Red + + + +class RedConfigJobStore(MemoryJobStore): + def __init__(self, config: Config, bot: Red): + super().__init__() + self.config = config + # nest_asyncio.apply() + self.bot = bot + asyncio.ensure_future(self._load_from_config(), loop=self.bot.loop) + + async def _load_from_config(self): + self._jobs = await self.config.jobs() + self._jobs_index = await self.config.jobs_index.all() + + def add_job(self, job): + if job.id in self._jobs_index: + raise ConflictingIdError(job.id) + + 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)) + + async def _async_add_job(self, job, index, timestamp): + async with self.config.jobs() as jobs: + jobs.insert(index, (job, timestamp)) + await self.config.jobs_index.set_raw(job.id, value=(job, timestamp)) + return True + + def update_job(self, job): + old_job, old_timestamp = self._jobs_index.get(job.id, (None, None)) + 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.ensure_future(self._async_update_job(job, new_timestamp, old_index, old_job, old_timestamp), loop=self.bot.loop) + + async def _async_update_job(self, job, new_timestamp, old_index, old_job, old_timestamp): + if old_timestamp == new_timestamp: + self._jobs[old_index] = (job, new_timestamp) + async with self.config.jobs() as jobs: + jobs[old_index] = (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, (job, new_timestamp)) + self._jobs_index[old_job.id] = (job, new_timestamp) + await self.config.jobs_index.set_raw(old_job.id, value=(job, new_timestamp)) + + 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) + + def remove_all_jobs(self): + super().remove_all_jobs() + asyncio.create_task(self._async_remove_all_jobs()) + + async def _async_remove_all_jobs(self): + await self.config.jobs.clear() + await self.config.jobs_index.clear() + + +# import asyncio +# +# from apscheduler.jobstores.base import BaseJobStore, ConflictingIdError +# from apscheduler.util import datetime_to_utc_timestamp +# from redbot.core import Config +# from redbot.core.utils import AsyncIter +# +# +# class RedConfigJobStore(BaseJobStore): +# def __init__(self, config: Config, loop): +# super().__init__() +# self.config = config +# self.loop: asyncio.BaseEventLoop = loop +# +# self._jobs = [] +# self._jobs_index = {} # id -> (job, timestamp) lookup table +# +# def lookup_job(self, job_id): +# return asyncio.run(self._async_lookup_job(job_id)) +# +# async def _async_lookup_job(self, job_id): +# return (await self.config.jobs_index.get_raw(job_id, default=(None, None)))[0] +# +# def get_due_jobs(self, now): +# return asyncio.run(self._async_get_due_jobs(now)) +# +# async def _async_get_due_jobs(self, now): +# now_timestamp = datetime_to_utc_timestamp(now) +# pending = [] +# all_jobs = await self.config.jobs() +# async for job, timestamp in AsyncIter(all_jobs, steps=100): +# if timestamp is None or timestamp > now_timestamp: +# break +# pending.append(job) +# +# return pending +# +# def get_next_run_time(self): +# return asyncio.run(self._async_get_next_run_time()) +# +# async def _async_get_next_run_time(self): +# _jobs = await self.config.jobs() +# return _jobs[0][0].next_run_time if _jobs else None +# +# def get_all_jobs(self): +# return asyncio.run(self._async_get_all_jobs()) +# +# async def _async_get_all_jobs(self): +# return [j[0] for j in (await self.config.jobs())] +# +# def add_job(self, job): +# return asyncio.run(self._async_add_job(job)) +# +# async def _async_add_job(self, job): +# if await self.config.jobs_index.get_raw(job.id, default=None) is not None: +# raise ConflictingIdError(job.id) +# +# timestamp = datetime_to_utc_timestamp(job.next_run_time) +# index = self._get_job_index(timestamp, job.id) +# self._jobs.insert(index, (job, timestamp)) +# self._jobs_index[job.id] = (job, timestamp) +# +# def update_job(self, job): +# pass +# +# def remove_job(self, job_id): +# pass +# +# def remove_all_jobs(self): +# pass +# +# def _get_job_index(self, timestamp, job_id): +# """ +# Returns the index of the given job, or if it's not found, the index where the job should be +# inserted based on the given timestamp. +# +# :type timestamp: int +# :type job_id: str +# +# """ +# lo, hi = 0, len(self._jobs) +# timestamp = float('inf') if timestamp is None else timestamp +# while lo < hi: +# mid = (lo + hi) // 2 +# mid_job, mid_timestamp = self._jobs[mid] +# mid_timestamp = float('inf') if mid_timestamp is None else mid_timestamp +# if mid_timestamp > timestamp: +# hi = mid +# elif mid_timestamp < timestamp: +# lo = mid + 1 +# elif mid_job.id > job_id: +# hi = mid +# elif mid_job.id < job_id: +# lo = mid + 1 +# else: +# return mid +# +# return lo From e602b5c868e546bcdd3d736b2fb28b8f5fc3b713 Mon Sep 17 00:00:00 2001 From: bobloy Date: Thu, 27 Aug 2020 16:22:36 -0400 Subject: [PATCH 015/121] Almost working. Date time is only date, figure out what's going on with time. --- fifo/fifo.py | 214 ++++++++++++++++++++++++++++++-------- fifo/info.json | 3 + fifo/redconfigjobstore.py | 83 +++++++++++++-- fifo/redjob.py | 44 ++++++++ 4 files changed, 287 insertions(+), 57 deletions(-) create mode 100644 fifo/redjob.py diff --git a/fifo/fifo.py b/fifo/fifo.py index 1ffb612..c5ea125 100644 --- a/fifo/fifo.py +++ b/fifo/fifo.py @@ -1,19 +1,32 @@ +import logging from datetime import datetime, timedelta from typing import Dict, Union import discord +from apscheduler.job import Job from apscheduler.schedulers.asyncio import AsyncIOScheduler from apscheduler.triggers.base import BaseTrigger from apscheduler.triggers.combining import OrTrigger from apscheduler.triggers.date import DateTrigger from apscheduler.triggers.interval import IntervalTrigger -from dateutil import parser from redbot.core import Config, checks, commands from redbot.core.bot import Red -from redbot.core.commands import DictConverter, TimedeltaConverter, parse_timedelta +from redbot.core.commands import DictConverter, TimedeltaConverter from .datetimeconverter import DatetimeConverter -from .redconfigjobstore import RedConfigJobStore + +log = logging.getLogger("red.fox_v3.fifo") +schedule_log = logging.getLogger("red.fox_v3.fifo.scheduler") +schedule_log.setLevel(logging.DEBUG) +log.setLevel(logging.DEBUG) + + +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() + return False def get_trigger(data): @@ -37,11 +50,14 @@ def parse_triggers(data: Union[Dict, None]): if len(data["triggers"]) > 1: # Multiple triggers return OrTrigger(get_trigger(t_data) for t_data in data["triggers"]) - return get_trigger(data[0]) + return get_trigger(data["triggers"][0]) -class FakeMessage: - _state = None +# class FakeMessage(discord.Message): +# def __init__(self, *, state, channel, data): +# super().__init__(state=state, channel=channel, data=data) +# +# _state = None # class FakeMessage(discord.Message): @@ -57,35 +73,47 @@ class Task: "time_data": None, # Used for Interval and Date Triggers } - def __init__(self, name: str, guild_id, config: Config, author_id=None, bot: Red = None): + def __init__( + self, name: str, guild_id, config: Config, author_id=None, channel_id=None, bot: Red = None + ): self.name = name self.guild_id = guild_id self.config = config self.bot = bot self.author_id = author_id + self.channel_id = channel_id self.data = None - async def _encode_time_data(self): + async def _encode_time_triggers(self): if not self.data or not self.data.get("triggers", None): - return None + return [] triggers = [] for t in self.data["triggers"]: if t["type"] == "interval": # Convert into timedelta td: timedelta = t["time_data"] - triggers.append({"type": t["type"], "time_data": {"days": td.days, "seconds": td.seconds} }) + triggers.append( + {"type": t["type"], "time_data": {"days": td.days, "seconds": td.seconds}} + ) + continue if t["type"] == "date": # Convert into datetime dt: datetime = t["time_data"] - 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, - }}) + 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, + }, + } + ) + continue if t["type"] == "cron": raise NotImplemented @@ -93,16 +121,18 @@ class Task: return triggers - async def _decode_time_data(self): + async def _decode_time_triggers(self): if not self.data or not self.data.get("triggers", None): return - for t in self.data["triggers"]: + 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 - t["time_data"] = datetime(**t["time_data"]) + self.data["triggers"][n]["time_data"] = datetime(**t["time_data"]) + continue if t["type"] == "cron": raise NotImplemented @@ -121,10 +151,11 @@ class Task: self.author_id = data["author_id"] self.guild_id = data["guild_id"] + self.channel_id = data["channel_id"] self.data = data["data"] - await self._decode_time_data() + await self._decode_time_triggers() return self.data async def get_trigger(self) -> Union[BaseTrigger, None]: @@ -145,11 +176,12 @@ class Task: data_to_save = self.default_task_data.copy() if self.data: data_to_save["command_str"] = self.data.get("command_str", "") - data_to_save["triggers"] = await self._encode_time_data() + data_to_save["triggers"] = await self._encode_time_triggers() to_save = { "guild_id": self.guild_id, "author_id": self.author_id, + "channel_id": self.channel_id, "data": data_to_save, } await self.config.guild_from_id(self.guild_id).tasks.set_raw(self.name, value=to_save) @@ -158,23 +190,54 @@ class Task: """To be used when updating triggers""" if not self.data: return + + data_to_save = self.data.copy() + 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=await self._encode_time_data() + self.name, "data", value=data_to_save ) async def execute(self): - if not self.data or self.data["command_str"]: + if not self.data or not self.data.get("command_str", False): + log.warning(f"Could not execute task due to data problem: {self.data=}") return False - message = FakeMessage() - message.guild = self.bot.get_guild(self.guild_id) # used for get_prefix - message.author = message.guild.get_member(self.author_id) - message.content = await self.bot.get_prefix(message) + self.data["command_str"] + + guild: discord.Guild = self.bot.get_guild(self.guild_id) # used for get_prefix + if guild is None: + log.warning(f"Could not execute task due to missing guild: {self.guild_id}") + return False + channel: discord.TextChannel = guild.get_channel(self.channel_id) + if channel is None: + log.warning(f"Could not execute task due to missing channel: {self.channel_id}") + return False + author: discord.User = guild.get_member(self.author_id) + if author is None: + log.warning(f"Could not execute task due to missing author: {self.author_id}") + return False + + message = channel.last_message + if message is None: + log.warning("No message found in channel cache yet, skipping execution") + return + message.author = author + + prefixes = await self.bot.get_prefix(message) + if isinstance(prefixes, str): + prefix = prefixes + else: + prefix = prefixes[0] + + message.content = f"{prefix}{self.data['command_str']}" if not message.guild or not message.author or not message.content: + log.warning(f"Could not execute task due to message problem: {message}") 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 due invalid context: {new_ctx}") return False await self.bot.invoke(new_ctx) @@ -203,6 +266,31 @@ class Task: self.data["triggers"].append(trigger_data) return True + def __setstate__(self, task_state): + self.name = task_state["name"] + self.guild_id = task_state["guild_id"] + self.config = task_state["config"] + self.bot = None + self.author_id = None + self.channel_id = None + self.data = None + + def __getstate__(self): + return { + "name": self.name, + "guild_id": self.guild_id, + "config": self.config, + "bot": self.bot, + } + + +def _assemble_job_id(task_name, guild_id): + return f"{task_name}_{guild_id}" + + +def _disassemble_job_id(job_id: str): + return job_id.split("_") + class FIFO(commands.Cog): """ @@ -222,6 +310,8 @@ class FIFO(commands.Cog): self.config.register_global(**default_global) self.config.register_guild(**default_guild) + from .redconfigjobstore import RedConfigJobStore + jobstores = {"default": RedConfigJobStore(self.config, self.bot)} job_defaults = {"coalesce": False, "max_instances": 1} @@ -229,7 +319,9 @@ class FIFO(commands.Cog): # executors = {"default": AsyncIOExecutor()} # Default executor is already AsyncIOExecutor - self.scheduler = AsyncIOScheduler(jobstores=jobstores, job_defaults=job_defaults) + self.scheduler = AsyncIOScheduler( + jobstores=jobstores, job_defaults=job_defaults, logger=schedule_log + ) self.scheduler.start() @@ -237,34 +329,48 @@ class FIFO(commands.Cog): """Nothing to delete""" return - def _assemble_job_id(self, task_name, guild_id): - return task_name + "_" + guild_id - async def _check_parsable_command(self, ctx: commands.Context, command_to_parse: str): - message = FakeMessage() + message: discord.Message = ctx.message + message.content = ctx.prefix + command_to_parse message.author = ctx.author - message.guild = ctx.guild new_ctx: commands.Context = await self.bot.get_context(message) return new_ctx.valid - async def _get_job(self, task_name, guild_id): - return self.scheduler.get_job(self._assemble_job_id(task_name, guild_id)) + async def _process_task(self, task: Task): + job = await self._get_job(task) + if job is not None: + job.remove() - async def _add_job(self, task): + 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): return self.scheduler.add_job( - task.execute, - id=self._assemble_job_id(task.name, task.guild_id), + _execute_task, + args=[task.__getstate__()], + id=_assemble_job_id(task.name, task.guild_id), trigger=await task.get_trigger(), ) + async def _pause_job(self, task: Task): + return self.scheduler.pause_job(job_id=_assemble_job_id(task.name, task.guild_id)) + + async def _remove_job(self, task: Task): + return self.scheduler.remove_job(job_id=_assemble_job_id(task.name, task.guild_id)) + @checks.is_owner() @commands.command() async def fifoclear(self, ctx: commands.Context): - """Debug command to clear fifo config""" + """Debug command to clear all current fifo data""" await self.config.guild(ctx.guild).tasks.clear() + await self.config.jobs.clear() + await self.config.jobs_index.clear() + self.scheduler.remove_all_jobs() await ctx.tick() @checks.is_owner() # Will be reduced when I figure out permissions later @@ -286,7 +392,15 @@ class FIFO(commands.Cog): if all_guilds: pass else: - pass # TODO: parse and display tasks + 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" + + if out: + await ctx.maybe_send_embed(out) + else: + await ctx.maybe_send_embed("No tasks to list") @fifo.command(name="add") async def fifo_add(self, ctx: commands.Context, task_name: str, *, command_to_execute: str): @@ -297,11 +411,17 @@ class FIFO(commands.Cog): await ctx.maybe_send_embed(f"Task already exists with {task_name=}") return + if "_" in task_name: # See _disassemble_job_id + await ctx.maybe_send_embed("Task name cannot contain underscores") + return + if not await self._check_parsable_command(ctx, command_to_execute): - await ctx.maybe_send_embed("Failed to parse command. Make sure to include the prefix") + await ctx.maybe_send_embed( + "Failed to parse command. Make sure not to include the prefix" + ) return - task = Task(task_name, ctx.guild.id, self.config, ctx.author.id) + task = Task(task_name, ctx.guild.id, self.config, ctx.author.id, ctx.channel.id, self.bot) await task.set_commmand_str(command_to_execute) await task.save_all() await ctx.tick() @@ -329,7 +449,7 @@ class FIFO(commands.Cog): Add an interval trigger to the specified task """ - task = Task(task_name, ctx.guild.id, self.config) + task = Task(task_name, ctx.guild.id, self.config, bot=self.bot) await task.load_from_config() if task.data is None: @@ -345,6 +465,7 @@ class FIFO(commands.Cog): ) return await task.save_data() + await self._process_task(task) await ctx.tick() @fifo_trigger.command(name="date") @@ -372,6 +493,7 @@ class FIFO(commands.Cog): return await task.save_data() + await self._process_task(task) await ctx.tick() @fifo_trigger.command(name="cron") diff --git a/fifo/info.json b/fifo/info.json index 4a9cd1c..6cc6f9d 100644 --- a/fifo/info.json +++ b/fifo/info.json @@ -8,6 +8,9 @@ "install_msg": "Thank you for installing FIFO.\nGet started with `[p]load fifo`, then `[p]help FIFO`", "short": "Schedule commands to be run by certain at certain times or intervals\"", "end_user_data_statement": "This cog does not store any End User Data", + "requirements": [ + "apscheduler" + ], "tags": [ "bobloy", "utilities", diff --git a/fifo/redconfigjobstore.py b/fifo/redconfigjobstore.py index 9db7213..4921dca 100644 --- a/fifo/redconfigjobstore.py +++ b/fifo/redconfigjobstore.py @@ -1,42 +1,96 @@ import asyncio +import base64 +import logging +import pickle +from apscheduler.job import Job from apscheduler.jobstores.base import ConflictingIdError, JobLookupError from apscheduler.jobstores.memory import MemoryJobStore from apscheduler.util import datetime_to_utc_timestamp from redbot.core import Config - # TODO: use get_lock on config from redbot.core.bot import Red +log = logging.getLogger("red.fox_v3.fifo.jobstore") +log.setLevel(logging.DEBUG) + +save_task_objects = [] class RedConfigJobStore(MemoryJobStore): def __init__(self, config: Config, bot: Red): super().__init__() self.config = config - # nest_asyncio.apply() self.bot = bot + self.pickle_protocol = pickle.HIGHEST_PROTOCOL asyncio.ensure_future(self._load_from_config(), loop=self.bot.loop) async def _load_from_config(self): self._jobs = await self.config.jobs() + self._jobs = [ + (await self._decode_job(job["job_state"]), timestamp) + for (job, timestamp) in self._jobs + ] self._jobs_index = await self.config.jobs_index.all() - - def add_job(self, job): + self._jobs_index = {job.id: (job, timestamp) for job, timestamp in self._jobs} + + def _encode_job(self, job: Job): + log.info(f"Encoding job id: {job.id}") + job_state = job.__getstate__() + 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"), + } + 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.info(f"After encode: Check job args: {job.args=}") + return out + + async def _decode_job(self, job_state): + job_state = pickle.loads(base64.b64decode(job_state)) + 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 + job._jobstore_alias = self._alias + # task_name, guild_id = _disassemble_job_id(job.id) + # task = Task(task_name, guild_id, self.config) + # await task.load_from_config() + # save_task_objects.append(task) + # + # job.func = task.execute + + log.info(f"Decoded job id: {job.id}") + + return job + + def add_job(self, job: Job): if job.id in self._jobs_index: raise ConflictingIdError(job.id) - + log.info(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.info(f"Added job: {self._jobs[index][0].args}") async def _async_add_job(self, job, index, timestamp): async with self.config.jobs() as jobs: - jobs.insert(index, (job, timestamp)) - await self.config.jobs_index.set_raw(job.id, value=(job, timestamp)) + jobs.insert(index, (self._encode_job(job), timestamp)) + await self.config.jobs_index.set_raw(job.id, value=(self._encode_job(job), timestamp)) return True def update_job(self, job): @@ -48,22 +102,29 @@ class RedConfigJobStore(MemoryJobStore): # 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.ensure_future(self._async_update_job(job, new_timestamp, old_index, old_job, old_timestamp), loop=self.bot.loop) + asyncio.ensure_future( + self._async_update_job(job, new_timestamp, old_index, old_job, old_timestamp), + loop=self.bot.loop, + ) 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] = (job, new_timestamp) + 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, (job, new_timestamp)) + 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=(job, new_timestamp)) + await self.config.jobs_index.set_raw(old_job.id, value=(encoded_job, new_timestamp)) + + log.info(f"Async Updated {job.id=}") + log.info(f"Check job args: {job.args=}") def remove_job(self, job_id): job, timestamp = self._jobs_index.get(job_id, (None, None)) diff --git a/fifo/redjob.py b/fifo/redjob.py new file mode 100644 index 0000000..c276aa4 --- /dev/null +++ b/fifo/redjob.py @@ -0,0 +1,44 @@ +import six +from apscheduler.job import Job +from apscheduler.schedulers.asyncio import AsyncIOScheduler +from apscheduler.schedulers.base import STATE_STOPPED +from apscheduler.util import undefined + + +class RedJob(Job): + pass + + +class RedAsyncIOScheduler(AsyncIOScheduler): + + def add_job(self, func, trigger=None, args=None, kwargs=None, id=None, name=None, + misfire_grace_time=undefined, coalesce=undefined, max_instances=undefined, + next_run_time=undefined, jobstore='default', executor='default', + replace_existing=False, **trigger_args): + job_kwargs = { + 'trigger': self._create_trigger(trigger, trigger_args), + 'executor': executor, + 'func': func, + 'args': tuple(args) if args is not None else (), + 'kwargs': dict(kwargs) if kwargs is not None else {}, + 'id': id, + 'name': name, + 'misfire_grace_time': misfire_grace_time, + 'coalesce': coalesce, + 'max_instances': max_instances, + 'next_run_time': next_run_time + } + job_kwargs = dict((key, value) for key, value in six.iteritems(job_kwargs) if + value is not undefined) + job = RedJob(self, **job_kwargs) + + # Don't really add jobs to job stores before the scheduler is up and running + with self._jobstores_lock: + if self.state == STATE_STOPPED: + self._pending_jobs.append((job, jobstore, replace_existing)) + self._logger.info('Adding job tentatively -- it will be properly scheduled when ' + 'the scheduler starts') + else: + self._real_add_job(job, jobstore, replace_existing) + + return job From 636b3ee9753ff9a22645af7ac6a7907442794d48 Mon Sep 17 00:00:00 2001 From: bobloy Date: Mon, 31 Aug 2020 07:28:10 -0400 Subject: [PATCH 016/121] Further attempts at fake message object --- fifo/fifo.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/fifo/fifo.py b/fifo/fifo.py index c5ea125..da88283 100644 --- a/fifo/fifo.py +++ b/fifo/fifo.py @@ -21,6 +21,10 @@ schedule_log.setLevel(logging.DEBUG) log.setLevel(logging.DEBUG) +async def _do_nothing(*args, **kwargs): + pass + + async def _execute_task(task_state): log.info(f"Executing {task_state=}") task = Task(**task_state) @@ -60,9 +64,9 @@ def parse_triggers(data: Union[Dict, None]): # _state = None -# class FakeMessage(discord.Message): -# def __init__(self): -# super().__init__(state=None, channel=None, data=None) +class FakeMessage: + def __init__(self, **kwargs): + self.__dict__.update(kwargs) class Task: @@ -216,11 +220,15 @@ class Task: log.warning(f"Could not execute task due to missing author: {self.author_id}") return False - message = channel.last_message - if message is None: + actual_message: discord.Message = channel.last_message + if actual_message is None: log.warning("No message found in channel cache yet, skipping execution") return + + message = FakeMessage(**actual_message.__dict__) message.author = author + message.id = None + message.add_reaction = _do_nothing prefixes = await self.bot.get_prefix(message) if isinstance(prefixes, str): From 53eda2d9a88ab38d42b814e79f04e0f8c8ce3c49 Mon Sep 17 00:00:00 2001 From: bobloy Date: Mon, 31 Aug 2020 08:50:46 -0400 Subject: [PATCH 017/121] Add logging and support for no response message with `ctx.tick` instead --- ccrole/ccrole.py | 82 +++++++++++++++++++++++++----------------------- 1 file changed, 42 insertions(+), 40 deletions(-) diff --git a/ccrole/ccrole.py b/ccrole/ccrole.py index bd41a67..190050d 100644 --- a/ccrole/ccrole.py +++ b/ccrole/ccrole.py @@ -1,4 +1,5 @@ import asyncio +import logging import re import discord @@ -7,13 +8,14 @@ from redbot.core import Config, checks, commands from redbot.core.bot import Red from redbot.core.utils.chat_formatting import box, pagify +log = logging.getLogger("red.fox_v3.ccrole") + async def _get_roles_from_content(ctx, content): content_list = content.split(",") try: role_list = [ - discord.utils.get(ctx.guild.roles, name=role.strip(" ")).id - for role in content_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 @@ -55,6 +57,12 @@ class CCRole(commands.Cog): When adding text, put arguments in `{}` to eval them Options: `{author}`, `{target}`, `{server}`, `{channel}`, `{message}`""" + + # TODO: Clean this up so it's not so repetitive + # The call/answer format has better options as well + # Saying "none" over and over can trigger automod actions as well + # Also, allow `ctx.tick()` instead of sending a message + command = command.lower() if command in self.bot.all_commands: await ctx.send("That command is already a standard command.") @@ -76,7 +84,8 @@ class CCRole(commands.Cog): # Roles to add await ctx.send( - "What roles should it add? (Must be **comma separated**)\nSay `None` to skip adding roles" + "What roles should it add? (Must be **comma separated**)\n" + "Say `None` to skip adding roles" ) def check(m): @@ -97,7 +106,8 @@ class CCRole(commands.Cog): # Roles to remove await ctx.send( - "What roles should it remove? (Must be comma separated)\nSay `None` to skip removing roles" + "What roles should it remove? (Must be comma separated)\n" + "Say `None` to skip removing roles" ) try: answer = await self.bot.wait_for("message", timeout=120, check=check) @@ -114,7 +124,8 @@ class CCRole(commands.Cog): # Roles to use await ctx.send( - "What roles are allowed to use this command? (Must be comma separated)\nSay `None` to allow all roles" + "What roles are allowed to use this command? (Must be comma separated)\n" + "Say `None` to allow all roles" ) try: @@ -131,7 +142,8 @@ class CCRole(commands.Cog): return # Selfrole - await ctx.send("Is this a targeted command?(yes/no)\nNo 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) @@ -149,7 +161,7 @@ class CCRole(commands.Cog): # Message to send await ctx.send( "What message should the bot say when using this command?\n" - "Say `None` to send the default `Success!` message\n" + "Say `None` to send no message and just react with ✅\n" "Eval Options: `{author}`, `{target}`, `{server}`, `{channel}`, `{message}`\n" "For example: `Welcome {target.mention} to {server.name}!`" ) @@ -160,7 +172,7 @@ class CCRole(commands.Cog): await ctx.send("Timed out, canceling") return - text = "Success!" + text = None if answer.content.upper() != "NONE": text = answer.content @@ -193,7 +205,7 @@ class CCRole(commands.Cog): await self.config.guild(guild).cmdlist.set_raw(command, value=None) await ctx.send("Custom command successfully deleted.") - @ccrole.command(name="details") + @ccrole.command(name="details", aliases=["detail"]) async def ccrole_details(self, ctx, command: str): """Provide details about passed custom command""" guild = ctx.guild @@ -217,10 +229,10 @@ class CCRole(commands.Cog): [discord.utils.get(ctx.guild.roles, id=roleid).name for roleid in role_list] ) - embed.add_field(name="Text", value="```{}```".format(cmd["text"])) - embed.add_field(name="Adds Roles", value=process_roles(cmd["aroles"]), inline=True) - embed.add_field(name="Removes Roles", value=process_roles(cmd["rroles"]), inline=True) - embed.add_field(name="Role Restrictions", value=process_roles(cmd["proles"]), inline=True) + embed.add_field(name="Text", value="```{}```".format(cmd["text"]), inline=False) + embed.add_field(name="Adds Roles", value=process_roles(cmd["aroles"]), inline=False) + embed.add_field(name="Removes Roles", value=process_roles(cmd["rroles"]), inline=False) + embed.add_field(name="Role Restrictions", value=process_roles(cmd["proles"]), inline=False) await ctx.send(embed=embed) @@ -288,6 +300,8 @@ class CCRole(commands.Cog): if cmd is not None: await self.eval_cc(cmd, message, ctx) + else: + log.debug(f"No custom command named {ctx.invoked_with} found") async def get_prefix(self, message: discord.Message) -> str: """ @@ -312,18 +326,10 @@ class CCRole(commands.Cog): 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 if cmd["targeted"]: - # try: - # arg1 = message.content.split(maxsplit=1)[1] - # except IndexError: # .split() return list of len<2 - # target = None - # else: - # target = discord.utils.get( - # message.guild.members, mention=arg1 - # ) - view: StringView = ctx.view view.skip_ws() @@ -342,20 +348,12 @@ class CCRole(commands.Cog): else: target = None - # try: - # arg1 = ctx.args[1] - # except IndexError: # args is list of len<2 - # target = None - # else: - # target = discord.utils.get( - # message.guild.members, mention=arg1 - # ) - if not target: - out_message = "This custom command is targeted! @mention a target\n`{} `".format( - ctx.invoked_with + out_message = ( + f"This custom command is targeted! @mention a target\n`" + f"{ctx.invoked_with} `" ) - await message.channel.send(out_message) + await ctx.send(out_message) return else: target = message.author @@ -364,25 +362,28 @@ class CCRole(commands.Cog): arole_list = [ discord.utils.get(message.guild.roles, id=roleid) for roleid in cmd["aroles"] ] - # await self.bot.send_message(message.channel, "Adding: "+str([str(arole) for arole in arole_list])) try: await target.add_roles(*arole_list) except discord.Forbidden: - await message.channel.send("Permission error: Unable to add roles") + log.exception(f"Permission error: Unable to add roles") + await ctx.send("Permission error: Unable to add roles") await asyncio.sleep(1) if cmd["rroles"]: rrole_list = [ discord.utils.get(message.guild.roles, id=roleid) for roleid in cmd["rroles"] ] - # await self.bot.send_message(message.channel, "Removing: "+str([str(rrole) for rrole in rrole_list])) try: await target.remove_roles(*rrole_list) except discord.Forbidden: - await message.channel.send("Permission error: Unable to remove roles") + log.exception(f"Permission error: Unable to remove roles") + await ctx.send("Permission error: Unable to remove roles") - out_message = self.format_cc(cmd, message, target) - await message.channel.send(out_message, allowed_mentions=discord.AllowedMentions()) + if cmd["text"] is not None: + out_message = self.format_cc(cmd, message, target) + await ctx.send(out_message, allowed_mentions=discord.AllowedMentions()) + else: + await ctx.tick() def format_cc(self, cmd, message, target): out = cmd["text"] @@ -396,6 +397,7 @@ class CCRole(commands.Cog): """ For security reasons only specific objects are allowed Internals are ignored + Copied from customcom.CustomCommands.transform_parameter and added `target` """ raw_result = "{" + result + "}" objects = { From 2c38e05ed0b64acb36bc38b52153732922a6723c Mon Sep 17 00:00:00 2001 From: bobloy Date: Mon, 31 Aug 2020 10:43:43 -0400 Subject: [PATCH 018/121] Add logging and running timerole on the hour instead of an hour after loading --- timerole/timerole.py | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/timerole/timerole.py b/timerole/timerole.py index a103bb3..0692d7d 100644 --- a/timerole/timerole.py +++ b/timerole/timerole.py @@ -1,12 +1,23 @@ import asyncio +import logging from datetime import datetime, timedelta import discord from redbot.core import Config, checks, commands from redbot.core.bot import Red from redbot.core.commands import Cog, parse_timedelta +from redbot.core.utils import AsyncIter from redbot.core.utils.chat_formatting import pagify +log = logging.getLogger("red.fox_v3.timerole") + + +async def sleep_till_next_hour(): + now = datetime.utcnow() + next_hour = datetime(year=now.year, month=now.month, day=now.day, hour=now.hour + 1) + log.debug("Sleeping for {} seconds".format((next_hour - datetime.utcnow()).seconds)) + await asyncio.sleep((next_hour - datetime.utcnow()).seconds) + class Timerole(Cog): """Add roles to users based on time on server""" @@ -20,7 +31,7 @@ class Timerole(Cog): self.config.register_global(**default_global) self.config.register_guild(**default_guild) - self.updating = asyncio.create_task(self.check_day()) + self.updating = asyncio.create_task(self.check_hour()) async def red_delete_data_for_user(self, **kwargs): """Nothing to delete""" @@ -34,13 +45,14 @@ class Timerole(Cog): @commands.guild_only() async def runtimerole(self, ctx: commands.Context): """ - Trigger the daily timerole + Trigger the hourly timerole Useful for troubleshooting the initial setup """ - await self.timerole_update() - await ctx.tick() + async with ctx.typing(): + await self.timerole_update() + await ctx.tick() @commands.group() @checks.mod_or_permissions(administrator=True) @@ -129,7 +141,7 @@ class Timerole(Cog): guild = ctx.guild role_dict = await self.config.guild(guild).roles() - out = "" + out = "Current Timeroles:\n" for r_id, r_data in role_dict.items(): if r_data is not None: role = discord.utils.get(guild.roles, id=int(r_id)) @@ -141,7 +153,7 @@ class Timerole(Cog): str(discord.utils.get(guild.roles, id=int(new_id))) for new_id in r_data["required"] ] - out += "{} || {} days || requires: {}\n".format(str(role), r_data["days"], r_roles) + out += "{} | {} days | requires: {}\n".format(str(role), r_data["days"], r_roles) await ctx.maybe_send_embed(out) async def timerole_update(self): @@ -153,7 +165,7 @@ class Timerole(Cog): if not any(role_data for role_data in role_dict.values()): # No roles continue - for member in guild.members: + async for member in AsyncIter(guild.members): has_roles = [r.id for r in member.roles] add_roles = [ @@ -203,6 +215,8 @@ class Timerole(Cog): 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): for role_id in check_roles: @@ -223,7 +237,11 @@ class Timerole(Cog): # Qualifies role_list.append((member, role_id)) - async def check_day(self): + async def check_hour(self): + await sleep_till_next_hour() while self is self.bot.get_cog("Timerole"): await self.timerole_update() - await asyncio.sleep(3600) + await sleep_till_next_hour() + +from moviepy.editor import VideoFileClip + From 71aa4c3048c409265816214ac40fc099f84f7604 Mon Sep 17 00:00:00 2001 From: bobloy Date: Mon, 31 Aug 2020 11:30:18 -0400 Subject: [PATCH 019/121] Fix error with beautifulsoup connection, add result image and text --- lovecalculator/lovecalculator.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/lovecalculator/lovecalculator.py b/lovecalculator/lovecalculator.py index 1b7e5c1..d43146d 100644 --- a/lovecalculator/lovecalculator.py +++ b/lovecalculator/lovecalculator.py @@ -1,9 +1,13 @@ +import logging + import aiohttp import discord from bs4 import BeautifulSoup from redbot.core import commands from redbot.core.commands import Cog +log = logging.getLogger("red.fox_v3.chatter") + class LoveCalculator(Cog): """Calculate the love percentage for two users!""" @@ -28,18 +32,23 @@ class LoveCalculator(Cog): url = "https://www.lovecalculator.com/love.php?name1={}&name2={}".format( x.replace(" ", "+"), y.replace(" ", "+") ) - async with aiohttp.ClientSession() as session: + async with aiohttp.ClientSession(headers={"Connection": "keep-alive"}) as session: async with session.get(url) as response: + log.debug(f"{response=}") + assert response.status == 200 soup_object = BeautifulSoup(await response.text(), "html.parser") try: description = ( - soup_object.find("div", attrs={"class": "result__score"}) - .get_text() - .strip() + soup_object.find("div", class_="result__score").get_text().strip() ) except: description = "Dr. Love is busy right now" + result_image = soup_object.find("img", class_="result__image").get("src") + + result_text = soup_object.find("div", class_="result-text").get_text() + result_text = " ".join(result_text.split()) + try: z = description[:2] z = int(z) @@ -47,11 +56,12 @@ class LoveCalculator(Cog): emoji = "❤" else: emoji = "💔" - title = "Dr. Love says that the love percentage for {} and {} is:".format(x, y) + title = f"Dr. Love says that the love percentage for {x} and {y} is: {emoji} {description} {emoji}" except: - emoji = "" title = "Dr. Love has left a note for you." - description = emoji + " " + description + " " + emoji - em = discord.Embed(title=title, description=description, color=discord.Color.red()) + em = discord.Embed(title=title, description=result_text, color=discord.Color.red()) + if result_image: + em.set_image(url=f"https://www.lovecalculator.com/{result_image}") + await ctx.send(embed=em) From c74736966733b0e8bf34436d718213253227a503 Mon Sep 17 00:00:00 2001 From: bobloy Date: Mon, 31 Aug 2020 11:37:46 -0400 Subject: [PATCH 020/121] Merge missing changes, better error handling --- lovecalculator/lovecalculator.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/lovecalculator/lovecalculator.py b/lovecalculator/lovecalculator.py index 4425cf7..95e9f97 100644 --- a/lovecalculator/lovecalculator.py +++ b/lovecalculator/lovecalculator.py @@ -36,14 +36,16 @@ class LoveCalculator(Cog): 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") - try: - description = ( - soup_object.find("div", class_="result__score").get_text().strip() - ) - except: + + description = soup_object.find("div", class_="result__score").get_text() + + if description is None: description = "Dr. Love is busy right now" + else: + description = description.strip() result_image = soup_object.find("img", class_="result__image").get("src") @@ -61,6 +63,11 @@ class LoveCalculator(Cog): except: title = "Dr. Love has left a note for you." - em = discord.Embed(title=title, description=result_text, color=discord.Color.red(), url=f"https://www.lovecalculator.com/{result_image}") + em = discord.Embed( + title=title, + description=result_text, + color=discord.Color.red(), + url=f"https://www.lovecalculator.com/{result_image}", + ) await ctx.send(embed=em) From 16af7c06c876cffae7a2121bd21b3966e77803ba Mon Sep 17 00:00:00 2001 From: bobloy Date: Mon, 31 Aug 2020 12:25:39 -0400 Subject: [PATCH 021/121] Better description --- lovecalculator/info.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lovecalculator/info.json b/lovecalculator/info.json index 543ff92..6425715 100644 --- a/lovecalculator/info.json +++ b/lovecalculator/info.json @@ -4,13 +4,13 @@ "SnappyDragon" ], "min_bot_version": "3.3.0", - "description": "Calculate the love percentage for two users", + "description": "Calculate the love percentage for two users. Shows gif result and description of their love", "hidden": false, "install_msg": "Thank you for installing LoveCalculator. Love is in the air.\n Get started with `[p]load lovecalculator`, then `[p]help LoveCalculator`", "requirements": [ "beautifulsoup4" ], - "short": "Calculate love percentage", + "short": "Calculate love percentage for two users.", "end_user_data_statement": "This cog uses the core Bank cog. It store no End User Data otherwise.", "tags": [ "bobloy", From 68690473c08da148042974ce9951c22445a06e0a Mon Sep 17 00:00:00 2001 From: bobloy Date: Mon, 31 Aug 2020 17:39:08 -0400 Subject: [PATCH 022/121] More WIP fake message --- fifo/datetimeconverter.py | 2 +- fifo/fifo.py | 27 +++++++++++++++------------ 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/fifo/datetimeconverter.py b/fifo/datetimeconverter.py index bdfbf88..c3f96ee 100644 --- a/fifo/datetimeconverter.py +++ b/fifo/datetimeconverter.py @@ -10,7 +10,7 @@ if TYPE_CHECKING: else: class DatetimeConverter(Converter): async def convert(self, ctx, argument) -> datetime: - dt = parser.parse(argument) + dt = parser.parse(argument, fuzzy=True) if dt is not None: return dt raise BadArgument() diff --git a/fifo/fifo.py b/fifo/fifo.py index da88283..b65b681 100644 --- a/fifo/fifo.py +++ b/fifo/fifo.py @@ -57,16 +57,14 @@ def parse_triggers(data: Union[Dict, None]): return get_trigger(data["triggers"][0]) -# class FakeMessage(discord.Message): -# def __init__(self, *, state, channel, data): -# super().__init__(state=state, channel=channel, data=data) -# -# _state = None +class FakeMessage2(discord.Message): + __slots__ = ("__dict__",) class FakeMessage: - def __init__(self, **kwargs): - self.__dict__.update(kwargs) + def __init__(self, message: discord.Message): + d = {k: getattr(message, k) for k in dir(message)} + self.__dict__.update(**d) class Task: @@ -114,6 +112,7 @@ class Task: "hour": dt.hour, "minute": dt.minute, "second": dt.second, + "tzinfo": dt.tzinfo }, } ) @@ -225,7 +224,8 @@ class Task: log.warning("No message found in channel cache yet, skipping execution") return - message = FakeMessage(**actual_message.__dict__) + message = FakeMessage(actual_message) + # message = FakeMessage2 message.author = author message.id = None message.add_reaction = _do_nothing @@ -451,7 +451,7 @@ class FIFO(commands.Cog): @fifo_trigger.command(name="interval") async def fifo_trigger_interval( - self, ctx: commands.Context, task_name: str, interval_str: TimedeltaConverter + self, ctx: commands.Context, task_name: str, *, interval_str: TimedeltaConverter ): """ Add an interval trigger to the specified task @@ -478,7 +478,7 @@ class FIFO(commands.Cog): @fifo_trigger.command(name="date") async def fifo_trigger_date( - self, ctx: commands.Context, task_name: str, datetime_str: DatetimeConverter + self, ctx: commands.Context, task_name: str, *, datetime_str: DatetimeConverter ): """ Add a "run once" datetime trigger to the specified task @@ -501,8 +501,11 @@ class FIFO(commands.Cog): return await task.save_data() - await self._process_task(task) - await ctx.tick() + job: Job = await self._process_task(task) + await ctx.maybe_send_embed( + f"Task `{task_name}` added {datetime_str} to its scheduled runtimes\n" + f"Next run time: {job.next_run_time}" + ) @fifo_trigger.command(name="cron") async def fifo_trigger_cron( From 19fcf7bc06ea9ca8d5acf85ea0bc45362feb7a5b Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 1 Sep 2020 04:08:27 -0400 Subject: [PATCH 023/121] Update timerole.py Haha whoops --- timerole/timerole.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/timerole/timerole.py b/timerole/timerole.py index 0692d7d..3815dcd 100644 --- a/timerole/timerole.py +++ b/timerole/timerole.py @@ -243,5 +243,5 @@ class Timerole(Cog): await self.timerole_update() await sleep_till_next_hour() -from moviepy.editor import VideoFileClip + From c34929a93ed5be9ca14673e21a1da30b35d0a946 Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 1 Sep 2020 16:47:03 -0400 Subject: [PATCH 024/121] Timezone support, working fake message --- fifo/datetimeconverter.py | 3 +- fifo/fifo.py | 120 +++++++++++++++++------ fifo/redconfigjobstore.py | 21 ++-- fifo/timezones.py | 195 +++++++++++++++++++++++++++++++++++++ infochannel/infochannel.py | 6 +- 5 files changed, 303 insertions(+), 42 deletions(-) create mode 100644 fifo/timezones.py diff --git a/fifo/datetimeconverter.py b/fifo/datetimeconverter.py index c3f96ee..def0403 100644 --- a/fifo/datetimeconverter.py +++ b/fifo/datetimeconverter.py @@ -4,13 +4,14 @@ from typing import TYPE_CHECKING from discord.ext.commands import BadArgument, Converter from dateutil import parser +from fifo.timezones import assemble_timezones if TYPE_CHECKING: DatetimeConverter = datetime else: class DatetimeConverter(Converter): async def convert(self, ctx, argument) -> datetime: - dt = parser.parse(argument, fuzzy=True) + dt = parser.parse(argument, fuzzy=True, tzinfos=assemble_timezones()) if dt is not None: return dt raise BadArgument() diff --git a/fifo/fifo.py b/fifo/fifo.py index b65b681..4c174ee 100644 --- a/fifo/fifo.py +++ b/fifo/fifo.py @@ -1,6 +1,6 @@ import logging from datetime import datetime, timedelta -from typing import Dict, Union +from typing import Dict, List, Union import discord from apscheduler.job import Job @@ -63,7 +63,7 @@ class FakeMessage2(discord.Message): class FakeMessage: def __init__(self, message: discord.Message): - d = {k: getattr(message, k) for k in dir(message)} + d = {k: getattr(message, k, None) for k in dir(message)} self.__dict__.update(**d) @@ -102,20 +102,21 @@ class Task: if t["type"] == "date": # Convert into datetime dt: datetime = t["time_data"] - 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 - }, - } - ) + 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": @@ -134,7 +135,8 @@ class Task: continue if t["type"] == "date": # Convert into datetime - self.data["triggers"][n]["time_data"] = datetime(**t["time_data"]) + # self.data["triggers"][n]["time_data"] = datetime(**t["time_data"]) + self.data["triggers"][n]["time_data"] = datetime.fromisoformat(t["time_data"]) continue if t["type"] == "cron": @@ -161,7 +163,16 @@ class Task: await self._decode_time_triggers() return self.data - async def get_trigger(self) -> Union[BaseTrigger, None]: + 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 [get_trigger(t) for t in self.data["triggers"]] + + async def get_combined_trigger(self) -> Union[BaseTrigger, None]: if not self.data: await self.load_from_config() @@ -178,7 +189,7 @@ class Task: data_to_save = self.default_task_data.copy() if self.data: - data_to_save["command_str"] = self.data.get("command_str", "") + data_to_save["command_str"] = self.get_command_str() data_to_save["triggers"] = await self._encode_time_triggers() to_save = { @@ -202,7 +213,7 @@ class Task: ) async def execute(self): - if not self.data or not self.data.get("command_str", False): + if not self.data or not self.get_command_str(): log.warning(f"Could not execute task due to data problem: {self.data=}") return False @@ -236,7 +247,7 @@ class Task: else: prefix = prefixes[0] - message.content = f"{prefix}{self.data['command_str']}" + message.content = f"{prefix}{self.get_command_str()}" if not message.guild or not message.author or not message.content: log.warning(f"Could not execute task due to message problem: {message}") @@ -257,6 +268,9 @@ class Task: async def set_author(self, author: Union[discord.User, str]): self.author_id = getattr(author, "id", None) or author + def get_command_str(self): + return self.data.get("command_str", "") + async def set_commmand_str(self, command_str): if not self.data: self.data = self.default_task_data.copy() @@ -348,10 +362,10 @@ class FIFO(commands.Cog): return new_ctx.valid async def _process_task(self, task: Task): - job = await self._get_job(task) + job: Union[Job, None] = await self._get_job(task) if job is not None: - job.remove() - + job.reschedule(await task.get_combined_trigger()) + return job return await self._add_job(task) async def _get_job(self, task: Task) -> Job: @@ -362,7 +376,7 @@ class FIFO(commands.Cog): _execute_task, args=[task.__getstate__()], id=_assemble_job_id(task.name, task.guild_id), - trigger=await task.get_trigger(), + trigger=await task.get_combined_trigger(), ) async def _pause_job(self, task: Task): @@ -375,10 +389,10 @@ class FIFO(commands.Cog): @commands.command() async def fifoclear(self, ctx: commands.Context): """Debug command to clear all current fifo data""" + self.scheduler.remove_all_jobs() await self.config.guild(ctx.guild).tasks.clear() await self.config.jobs.clear() await self.config.jobs_index.clear() - self.scheduler.remove_all_jobs() await ctx.tick() @checks.is_owner() # Will be reduced when I figure out permissions later @@ -390,6 +404,47 @@ class FIFO(commands.Cog): if ctx.invoked_subcommand is None: pass + @fifo.command(name="details") + async def fifo_details(self, ctx: commands.Context, task_name: str): + """ + Provide all the details on the specified task name + + """ + 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 + + embed = discord.Embed(title=task_name) + + embed.add_field( + name="Task command", value=f"{ctx.prefix}{task.get_command_str()}", inline=False + ) + + guild: discord.Guild = self.bot.get_guild(task.guild_id) + + if guild is not None: + author: discord.Member = guild.get_member(task.author_id) + channel: discord.TextChannel = guild.get_channel(task.channel_id) + embed.add_field(name="Server", value=guild.name) + if author is not None: + embed.add_field(name="Author", value=author.mention) + if channel is not None: + embed.add_field(name="Channel", value=channel.mention) + + else: + embed.add_field(name="Server", value="Server not found") + + 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) + + await ctx.send(embed=embed) + @fifo.command(name="list") async def fifo_list(self, ctx: commands.Context, all_guilds: bool = False): """ @@ -473,8 +528,12 @@ class FIFO(commands.Cog): ) return await task.save_data() - await self._process_task(task) - await ctx.tick() + 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" + f"Next run time: {job.next_run_time} ({delta_from_now.total_seconds()} seconds)" + ) @fifo_trigger.command(name="date") async def fifo_trigger_date( @@ -502,9 +561,10 @@ class FIFO(commands.Cog): 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 {datetime_str} to its scheduled runtimes\n" - f"Next run time: {job.next_run_time}" + f"Next run time: {job.next_run_time} ({delta_from_now.total_seconds()} seconds)" ) @fifo_trigger.command(name="cron") @@ -527,7 +587,7 @@ class FIFO(commands.Cog): # await task.load_from_data(task_data) # # job = self.scheduler.add_job( - # task.execute, id=task_name + "_" + guild_id, trigger=await task.get_trigger(), + # task.execute, id=task_name + "_" + guild_id, trigger=await task.get_combined_trigger(), # ) # # self.scheduler.start() diff --git a/fifo/redconfigjobstore.py b/fifo/redconfigjobstore.py index 4921dca..cbbe0ab 100644 --- a/fifo/redconfigjobstore.py +++ b/fifo/redconfigjobstore.py @@ -2,6 +2,8 @@ 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 @@ -36,7 +38,7 @@ class RedConfigJobStore(MemoryJobStore): self._jobs_index = {job.id: (job, timestamp) for job, timestamp in self._jobs} def _encode_job(self, job: Job): - log.info(f"Encoding job id: {job.id}") + # log.debug(f"Encoding job id: {job.id}") job_state = job.__getstate__() new_args = list(job_state["args"]) new_args[0]["config"] = None @@ -52,7 +54,6 @@ class RedConfigJobStore(MemoryJobStore): new_args[0]["config"] = self.config new_args[0]["bot"] = self.bot job_state["args"] = tuple(new_args) - log.info(f"After encode: Check job args: {job.args=}") return out async def _decode_job(self, job_state): @@ -72,20 +73,20 @@ class RedConfigJobStore(MemoryJobStore): # # job.func = task.execute - log.info(f"Decoded job id: {job.id}") + # log.debug(f"Decoded job id: {job.id}") return job def add_job(self, job: Job): if job.id in self._jobs_index: raise ConflictingIdError(job.id) - log.info(f"Check job args: {job.args=}") + # 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.info(f"Added job: {self._jobs[index][0].args}") + # log.debug(f"Added job: {self._jobs[index][0].args}") async def _async_add_job(self, job, index, timestamp): async with self.config.jobs() as jobs: @@ -94,7 +95,11 @@ class RedConfigJobStore(MemoryJobStore): return True def update_job(self, job): - old_job, old_timestamp = self._jobs_index.get(job.id, (None, None)) + 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) @@ -123,8 +128,8 @@ class RedConfigJobStore(MemoryJobStore): 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.info(f"Async Updated {job.id=}") - log.info(f"Check job args: {job.args=}") + log.debug(f"Async Updated {job.id=}") + log.debug(f"Check job args: {job.args=}") def remove_job(self, job_id): job, timestamp = self._jobs_index.get(job_id, (None, None)) diff --git a/fifo/timezones.py b/fifo/timezones.py new file mode 100644 index 0000000..5a322a4 --- /dev/null +++ b/fifo/timezones.py @@ -0,0 +1,195 @@ +""" +Timezone information for the dateutil parser + +All credit to https://github.com/prefrontal/dateutil-parser-timezones +""" + +from dateutil.tz import gettz + + +def assemble_timezones(): + """ + Assembles a dictionary of timezone abbreviations and values + :return: Dictionary of abbreviation keys and timezone values + """ + timezones = {} + + 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) + + return timezones \ No newline at end of file diff --git a/infochannel/infochannel.py b/infochannel/infochannel.py index c7297b8..b8d36a3 100644 --- a/infochannel/infochannel.py +++ b/infochannel/infochannel.py @@ -1,4 +1,5 @@ import asyncio +from typing import Union import discord from redbot.core import Config, checks, commands @@ -61,10 +62,9 @@ class InfoChannel(Cog): guild: discord.Guild = ctx.guild channel_id = await self.config.guild(guild).channel_id() + channel = None if channel_id is not None: - channel: discord.VoiceChannel = guild.get_channel(channel_id) - else: - channel: discord.VoiceChannel = None + channel: Union[discord.VoiceChannel, None] = guild.get_channel(channel_id) if channel_id is not None and channel is None: await ctx.send("Info channel has been deleted, recreate it?") From f24183d4f238b4ee32aca4d63aa727f2e072704f Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 1 Sep 2020 17:42:13 -0400 Subject: [PATCH 025/121] Pausing and resuming, discord ID --- fifo/fifo.py | 74 ++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 69 insertions(+), 5 deletions(-) diff --git a/fifo/fifo.py b/fifo/fifo.py index 4c174ee..e210f90 100644 --- a/fifo/fifo.py +++ b/fifo/fifo.py @@ -1,14 +1,17 @@ import logging from datetime import datetime, timedelta -from typing import Dict, List, Union +from typing import Dict, List, Optional, Union import discord from apscheduler.job import Job +from apscheduler.jobstores.base import JobLookupError from apscheduler.schedulers.asyncio import AsyncIOScheduler +from apscheduler.schedulers.base import STATE_PAUSED, STATE_RUNNING from apscheduler.triggers.base import BaseTrigger from apscheduler.triggers.combining import OrTrigger from apscheduler.triggers.date import DateTrigger from apscheduler.triggers.interval import IntervalTrigger +from discord.utils import time_snowflake from redbot.core import Config, checks, commands from redbot.core.bot import Red from redbot.core.commands import DictConverter, TimedeltaConverter @@ -238,7 +241,7 @@ class Task: message = FakeMessage(actual_message) # message = FakeMessage2 message.author = author - message.id = None + message.id = time_snowflake(datetime.now()) # Pretend to be now message.add_reaction = _do_nothing prefixes = await self.bot.get_prefix(message) @@ -345,12 +348,16 @@ class FIFO(commands.Cog): jobstores=jobstores, job_defaults=job_defaults, logger=schedule_log ) - self.scheduler.start() + self.scheduler.start() # TODO: Jobs are not receiving next_run_times async def red_delete_data_for_user(self, **kwargs): """Nothing to delete""" return + def cog_unload(self): + # self.scheduler.remove_all_jobs() + self.scheduler.shutdown() + async def _check_parsable_command(self, ctx: commands.Context, command_to_parse: str): message: discord.Message = ctx.message @@ -379,6 +386,13 @@ class FIFO(commands.Cog): trigger=await task.get_combined_trigger(), ) + async def _resume_job(self, task: Task): + 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): return self.scheduler.pause_job(job_id=_assemble_job_id(task.name, task.guild_id)) @@ -404,6 +418,52 @@ class FIFO(commands.Cog): if ctx.invoked_subcommand is None: pass + @fifo.command(name="resume") + async def fifo_resume(self, ctx: commands.Context, task_name: Optional[str] = None): + if task_name is None: + if self.scheduler.state == STATE_PAUSED: + self.scheduler.resume() + await ctx.maybe_send_embed("All task execution for all guilds has been resumed") + else: + await ctx.maybe_send_embed("Task execution is not paused, can't resume") + else: + 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 + + if await self._resume_job(task): + await ctx.maybe_send_embed(f"Execution of {task_name=} has been resumed") + else: + await ctx.maybe_send_embed(f"Failed to resume {task_name=}") + + @fifo.command(name="pause") + async def fifo_pause(self, ctx: commands.Context, task_name: Optional[str] = None): + if task_name is None: + if self.scheduler.state == STATE_RUNNING: + self.scheduler.pause() + await ctx.maybe_send_embed("All task execution for all guilds has been paused") + else: + await ctx.maybe_send_embed("Task execution is not running, can't pause") + else: + 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 + + if await self._pause_job(task): + await ctx.maybe_send_embed(f"Execution of {task_name=} has been paused") + else: + await ctx.maybe_send_embed(f"Failed to pause {task_name=}") + @fifo.command(name="details") async def fifo_details(self, ctx: commands.Context, task_name: str): """ @@ -419,7 +479,7 @@ class FIFO(commands.Cog): ) return - embed = discord.Embed(title=task_name) + embed = discord.Embed(title=f"Task: {task_name}") embed.add_field( name="Task command", value=f"{ctx.prefix}{task.get_command_str()}", inline=False @@ -437,12 +497,16 @@ class FIFO(commands.Cog): embed.add_field(name="Channel", value=channel.mention) else: - embed.add_field(name="Server", value="Server not found") + embed.add_field(name="Server", value="Server not found", inline=False) 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) + job = await self._get_job(task) + if job and job.next_run_time: + embed.timestamp = job.next_run_time + await ctx.send(embed=embed) @fifo.command(name="list") From e1d314cc83bd09a4b19708d03f9cccfdda353491 Mon Sep 17 00:00:00 2001 From: bobloy Date: Wed, 2 Sep 2020 17:17:29 -0400 Subject: [PATCH 026/121] Start of Cron, fixed jobstore, add pause and resume, split task, --- fifo/__init__.py | 5 + ...nverter.py => datetime_cron_converters.py} | 11 + fifo/fifo.py | 415 ++++-------------- fifo/redconfigjobstore.py | 57 ++- fifo/task.py | 325 ++++++++++++++ 5 files changed, 475 insertions(+), 338 deletions(-) rename fifo/{datetimeconverter.py => datetime_cron_converters.py} (61%) create mode 100644 fifo/task.py diff --git a/fifo/__init__.py b/fifo/__init__.py index 860ab97..34cfd7b 100644 --- a/fifo/__init__.py +++ b/fifo/__init__.py @@ -4,3 +4,8 @@ from .fifo import FIFO async def setup(bot): cog = FIFO(bot) bot.add_cog(cog) + await cog.initialize() + + +def teardown(bot): + pass diff --git a/fifo/datetimeconverter.py b/fifo/datetime_cron_converters.py similarity index 61% rename from fifo/datetimeconverter.py rename to fifo/datetime_cron_converters.py index def0403..5382b07 100644 --- a/fifo/datetimeconverter.py +++ b/fifo/datetime_cron_converters.py @@ -1,6 +1,7 @@ from datetime import datetime from typing import TYPE_CHECKING +from apscheduler.triggers.cron import CronTrigger from discord.ext.commands import BadArgument, Converter from dateutil import parser @@ -8,6 +9,7 @@ from fifo.timezones import assemble_timezones if TYPE_CHECKING: DatetimeConverter = datetime + CronConverter = str else: class DatetimeConverter(Converter): async def convert(self, ctx, argument) -> datetime: @@ -15,3 +17,12 @@ else: if dt is not None: return dt raise BadArgument() + + class CronConverter(Converter): + async def convert(self, ctx, argument) -> str: + try: + CronTrigger.from_crontab(argument) + except ValueError: + raise BadArgument() + + return argument \ No newline at end of file diff --git a/fifo/fifo.py b/fifo/fifo.py index e210f90..10b72af 100644 --- a/fifo/fifo.py +++ b/fifo/fifo.py @@ -1,31 +1,23 @@ import logging from datetime import datetime, timedelta -from typing import Dict, List, Optional, Union +from typing import Optional, Union import discord from apscheduler.job import Job from apscheduler.jobstores.base import JobLookupError from apscheduler.schedulers.asyncio import AsyncIOScheduler from apscheduler.schedulers.base import STATE_PAUSED, STATE_RUNNING -from apscheduler.triggers.base import BaseTrigger -from apscheduler.triggers.combining import OrTrigger -from apscheduler.triggers.date import DateTrigger -from apscheduler.triggers.interval import IntervalTrigger -from discord.utils import time_snowflake from redbot.core import Config, checks, commands from redbot.core.bot import Red from redbot.core.commands import DictConverter, TimedeltaConverter -from .datetimeconverter import DatetimeConverter +from .datetime_cron_converters import CronConverter, DatetimeConverter +from .task import Task -log = logging.getLogger("red.fox_v3.fifo") schedule_log = logging.getLogger("red.fox_v3.fifo.scheduler") schedule_log.setLevel(logging.DEBUG) -log.setLevel(logging.DEBUG) - -async def _do_nothing(*args, **kwargs): - pass +log = logging.getLogger("red.fox_v3.fifo") async def _execute_task(task_state): @@ -36,279 +28,6 @@ async def _execute_task(task_state): return False -def get_trigger(data): - if data["type"] == "interval": - parsed_time = data["time_data"] - return IntervalTrigger(days=parsed_time.days, seconds=parsed_time.seconds) - - if data["type"] == "date": - return DateTrigger(data["time_data"]) - - if data["type"] == "cron": - return None # TODO: Cron parsing - - return False - - -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 - return OrTrigger(get_trigger(t_data) for t_data in data["triggers"]) - - return get_trigger(data["triggers"][0]) - - -class FakeMessage2(discord.Message): - __slots__ = ("__dict__",) - - -class FakeMessage: - def __init__(self, message: discord.Message): - d = {k: getattr(message, k, None) for k in dir(message)} - self.__dict__.update(**d) - - -class Task: - default_task_data = {"triggers": [], "command_str": ""} - - default_trigger = { - "type": "", - "time_data": None, # Used for Interval and Date Triggers - } - - def __init__( - self, name: str, guild_id, config: Config, author_id=None, channel_id=None, bot: Red = None - ): - self.name = name - self.guild_id = guild_id - self.config = config - self.bot = bot - self.author_id = author_id - self.channel_id = channel_id - self.data = None - - async def _encode_time_triggers(self): - if not self.data or not self.data.get("triggers", None): - return [] - - triggers = [] - for t in self.data["triggers"]: - if t["type"] == "interval": # Convert into timedelta - td: timedelta = t["time_data"] - - triggers.append( - {"type": t["type"], "time_data": {"days": td.days, "seconds": td.seconds}} - ) - continue - - if t["type"] == "date": # Convert into datetime - dt: datetime = t["time_data"] - 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": - raise NotImplemented - raise NotImplemented - - return triggers - - async def _decode_time_triggers(self): - if not self.data or not self.data.get("triggers", None): - return - - for n, t in enumerate(self.data["triggers"]): - if t["type"] == "interval": # Convert into timedelta - 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"]) - self.data["triggers"][n]["time_data"] = datetime.fromisoformat(t["time_data"]) - continue - - if t["type"] == "cron": - raise NotImplemented - raise NotImplemented - - # async def load_from_data(self, data: Dict): - # self.data = data.copy() - - async def load_from_config(self): - data = await self.config.guild_from_id(self.guild_id).tasks.get_raw( - self.name, default=None - ) - - if not data: - return - - self.author_id = data["author_id"] - self.guild_id = data["guild_id"] - self.channel_id = data["channel_id"] - - self.data = data["data"] - - await self._decode_time_triggers() - return self.data - - 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 [get_trigger(t) for t in self.data["triggers"]] - - async def get_combined_trigger(self) -> Union[BaseTrigger, None]: - if not self.data: - await self.load_from_config() - - return parse_triggers(self.data) - - # async def set_job_id(self, job_id): - # if self.data is None: - # await self.load_from_config() - # - # self.data["job_id"] = job_id - - async def save_all(self): - """To be used when creating an new 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"] = await self._encode_time_triggers() - - to_save = { - "guild_id": self.guild_id, - "author_id": self.author_id, - "channel_id": self.channel_id, - "data": data_to_save, - } - await self.config.guild_from_id(self.guild_id).tasks.set_raw(self.name, value=to_save) - - async def save_data(self): - """To be used when updating triggers""" - if not self.data: - return - - data_to_save = self.data.copy() - 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 - ) - - async def execute(self): - if not self.data or not self.get_command_str(): - log.warning(f"Could not execute task due to data problem: {self.data=}") - return False - - guild: discord.Guild = self.bot.get_guild(self.guild_id) # used for get_prefix - if guild is None: - log.warning(f"Could not execute task due to missing guild: {self.guild_id}") - return False - channel: discord.TextChannel = guild.get_channel(self.channel_id) - if channel is None: - log.warning(f"Could not execute task due to missing channel: {self.channel_id}") - return False - author: discord.User = guild.get_member(self.author_id) - if author is None: - log.warning(f"Could not execute task due to missing author: {self.author_id}") - return False - - actual_message: discord.Message = channel.last_message - if actual_message is None: - log.warning("No message found in channel cache yet, skipping execution") - return - - message = FakeMessage(actual_message) - # message = FakeMessage2 - message.author = author - message.id = time_snowflake(datetime.now()) # Pretend to be now - message.add_reaction = _do_nothing - - prefixes = await self.bot.get_prefix(message) - if isinstance(prefixes, str): - prefix = prefixes - else: - prefix = prefixes[0] - - message.content = f"{prefix}{self.get_command_str()}" - - if not message.guild or not message.author or not message.content: - log.warning(f"Could not execute task due to message problem: {message}") - 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 due invalid context: {new_ctx}") - return False - - await self.bot.invoke(new_ctx) - return True - - async def set_bot(self, bot: Red): - self.bot = bot - - async def set_author(self, author: Union[discord.User, str]): - self.author_id = getattr(author, "id", None) or author - - def get_command_str(self): - return self.data.get("command_str", "") - - async def set_commmand_str(self, command_str): - if not self.data: - self.data = self.default_task_data.copy() - self.data["command_str"] = command_str - return True - - async def add_trigger(self, param, parsed_time: Union[timedelta, datetime]): - trigger_data = {"type": param, "time_data": parsed_time} - if not get_trigger(trigger_data): - return False - - if not self.data: - self.data = self.default_task_data.copy() - - self.data["triggers"].append(trigger_data) - return True - - def __setstate__(self, task_state): - self.name = task_state["name"] - self.guild_id = task_state["guild_id"] - self.config = task_state["config"] - self.bot = None - self.author_id = None - self.channel_id = None - self.data = None - - def __getstate__(self): - return { - "name": self.name, - "guild_id": self.guild_id, - "config": self.config, - "bot": self.bot, - } - - def _assemble_job_id(task_name, guild_id): return f"{task_name}_{guild_id}" @@ -335,28 +54,34 @@ class FIFO(commands.Cog): self.config.register_global(**default_global) self.config.register_guild(**default_guild) - from .redconfigjobstore import RedConfigJobStore + self.scheduler = None + self.jobstore = None - jobstores = {"default": RedConfigJobStore(self.config, self.bot)} + async def red_delete_data_for_user(self, **kwargs): + """Nothing to delete""" + return + + def cog_unload(self): + # self.scheduler.remove_all_jobs() + if self.scheduler is not None: + self.scheduler.shutdown() + + async def initialize(self): job_defaults = {"coalesce": False, "max_instances": 1} # executors = {"default": AsyncIOExecutor()} # Default executor is already AsyncIOExecutor - self.scheduler = AsyncIOScheduler( - jobstores=jobstores, job_defaults=job_defaults, logger=schedule_log - ) + self.scheduler = AsyncIOScheduler(job_defaults=job_defaults, logger=schedule_log) - self.scheduler.start() # TODO: Jobs are not receiving next_run_times + from .redconfigjobstore import RedConfigJobStore - async def red_delete_data_for_user(self, **kwargs): - """Nothing to delete""" - return + self.jobstore = RedConfigJobStore(self.config, self.bot) + await self.jobstore.load_from_config(self.scheduler, "default") + self.scheduler.add_jobstore(self.jobstore, "default") - def cog_unload(self): - # self.scheduler.remove_all_jobs() - self.scheduler.shutdown() + self.scheduler.start() # TODO: Jobs are not receiving next_run_times async def _check_parsable_command(self, ctx: commands.Context, command_to_parse: str): message: discord.Message = ctx.message @@ -400,6 +125,7 @@ class FIFO(commands.Cog): return self.scheduler.remove_job(job_id=_assemble_job_id(task.name, task.guild_id)) @checks.is_owner() + @commands.guild_only() @commands.command() async def fifoclear(self, ctx: commands.Context): """Debug command to clear all current fifo data""" @@ -410,6 +136,7 @@ class FIFO(commands.Cog): await ctx.tick() @checks.is_owner() # Will be reduced when I figure out permissions later + @commands.guild_only() @commands.group() async def fifo(self, ctx: commands.Context): """ @@ -418,8 +145,46 @@ class FIFO(commands.Cog): if ctx.invoked_subcommand is None: pass + @fifo.command(name="set") + async def fifo_setauthor(self, ctx: commands.Context, task_name: str, author_or_channel: Union[discord.Member, discord.TextChannel]): + """ + Sets the task to be executed as a different author or in a different channel. + """ + 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 + + if isinstance(author_or_channel, discord.Member): + if task.author_id == author_or_channel.id: + await ctx.maybe_send_embed("Already executing as that member") + return + + await task.set_author(author_or_channel) # also saves + elif isinstance(author_or_channel, discord.TextChannel): + if task.channel_id == author_or_channel.id: + await ctx.maybe_send_embed("Already executing in that channel") + return + + await task.set_channel(author_or_channel) + else: + await ctx.maybe_send_embed("Unsupported result") + return + + await ctx.tick() + @fifo.command(name="resume") async def fifo_resume(self, ctx: commands.Context, task_name: Optional[str] = None): + """ + Provide a task name to resume execution of a task. + + Otherwise resumes execution of all tasks on all guilds + If the task isn't currently scheduled, will schedule it + """ if task_name is None: if self.scheduler.state == STATE_PAUSED: self.scheduler.resume() @@ -443,6 +208,11 @@ class FIFO(commands.Cog): @fifo.command(name="pause") async def fifo_pause(self, ctx: commands.Context, task_name: Optional[str] = None): + """ + Provide a task name to pause execution of a task + + Otherwise pauses execution of all tasks on all guilds + """ if task_name is None: if self.scheduler.state == STATE_RUNNING: self.scheduler.pause() @@ -468,7 +238,6 @@ class FIFO(commands.Cog): async def fifo_details(self, ctx: commands.Context, task_name: str): """ Provide all the details on the specified task name - """ task = Task(task_name, ctx.guild.id, self.config, bot=self.bot) await task.load_from_config() @@ -633,33 +402,31 @@ class FIFO(commands.Cog): @fifo_trigger.command(name="cron") async def fifo_trigger_cron( - self, ctx: commands.Context, task_name: str, cron_settings: DictConverter + self, ctx: commands.Context, task_name: str, *, cron_str: CronConverter ): """ Add a "time of day" trigger to the specified task """ - await ctx.maybe_send_embed("Not yet implemented") - - # async def load_tasks(self): - # """ - # Run once on cog load. - # """ - # all_guilds = await self.config.all_guilds() - # async for guild_id, guild_data in AsyncIter(all_guilds["tasks"].items(), steps=100): - # for task_name, task_data in guild_data["tasks"].items(): - # task = Task(task_name, guild_id, self.config) - # await task.load_from_data(task_data) - # - # job = self.scheduler.add_job( - # task.execute, id=task_name + "_" + guild_id, trigger=await task.get_combined_trigger(), - # ) - # - # self.scheduler.start() - - # async def parent_loop(self): - # await asyncio.sleep(60) - # asyncio.create_task(self.process_tasks(datetime.datetime.utcnow())) - # - # async def process_tasks(self, now: datetime.datetime): - # for task in self.tasks: - # pass + task = Task(task_name, ctx.guild.id, self.config) + 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 + + 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" + ) + 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 cron_str to its scheduled runtimes\n" + f"Next run time: {job.next_run_time} ({delta_from_now.total_seconds()} seconds)" + ) diff --git a/fifo/redconfigjobstore.py b/fifo/redconfigjobstore.py index cbbe0ab..3747a97 100644 --- a/fifo/redconfigjobstore.py +++ b/fifo/redconfigjobstore.py @@ -3,16 +3,19 @@ import base64 import logging import pickle from datetime import datetime +from time import sleep 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 from redbot.core.bot import Red +from redbot.core.utils import AsyncIter log = logging.getLogger("red.fox_v3.fifo.jobstore") log.setLevel(logging.DEBUG) @@ -26,19 +29,29 @@ class RedConfigJobStore(MemoryJobStore): self.config = config self.bot = bot self.pickle_protocol = pickle.HIGHEST_PROTOCOL - asyncio.ensure_future(self._load_from_config(), loop=self.bot.loop) + self._eventloop = self.bot.loop + # TODO: self.config.jobs_index is never read from, + # either remove or replace self._jobs_index - async def _load_from_config(self): - self._jobs = await self.config.jobs() + # 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) + + async def load_from_config(self, scheduler, alias): + super().start(scheduler, alias) + _jobs = await self.config.jobs() self._jobs = [ - (await self._decode_job(job["job_state"]), timestamp) - for (job, timestamp) in self._jobs + (await self._decode_job(job), timestamp) async for (job, timestamp) in AsyncIter(_jobs) ] - self._jobs_index = await self.config.jobs_index.all() + # 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} def _encode_job(self, job: Job): - # log.debug(f"Encoding job id: {job.id}") job_state = job.__getstate__() new_args = list(job_state["args"]) new_args[0]["config"] = None @@ -54,9 +67,15 @@ class RedConfigJobStore(MemoryJobStore): 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}") + return out - async def _decode_job(self, job_state): + async def _decode_job(self, in_job): + if in_job is None: + return None + job_state = in_job["job_state"] job_state = pickle.loads(base64.b64decode(job_state)) new_args = list(job_state["args"]) new_args[0]["config"] = self.config @@ -73,10 +92,12 @@ class RedConfigJobStore(MemoryJobStore): # # job.func = task.execute - # log.debug(f"Decoded job id: {job.id}") + # log.debug(f"Decoded job id: {job.id}\n" + # f"Decoded as {job_state}") return job + @run_in_event_loop def add_job(self, job: Job): if job.id in self._jobs_index: raise ConflictingIdError(job.id) @@ -89,11 +110,14 @@ class RedConfigJobStore(MemoryJobStore): # 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, (self._encode_job(job), timestamp)) - await self.config.jobs_index.set_raw(job.id, value=(self._encode_job(job), timestamp)) + 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) @@ -107,9 +131,8 @@ class RedConfigJobStore(MemoryJobStore): # 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.ensure_future( - self._async_update_job(job, new_timestamp, old_index, old_job, old_timestamp), - loop=self.bot.loop, + 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): @@ -131,6 +154,7 @@ class RedConfigJobStore(MemoryJobStore): 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: @@ -146,6 +170,7 @@ class RedConfigJobStore(MemoryJobStore): 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() asyncio.create_task(self._async_remove_all_jobs()) @@ -154,6 +179,10 @@ class RedConfigJobStore(MemoryJobStore): await self.config.jobs.clear() await self.config.jobs_index.clear() + def shutdown(self): + """Removes all jobs without clearing config""" + super().remove_all_jobs() + # import asyncio # diff --git a/fifo/task.py b/fifo/task.py new file mode 100644 index 0000000..62d50ca --- /dev/null +++ b/fifo/task.py @@ -0,0 +1,325 @@ +import logging +from datetime import datetime, timedelta +from typing import Dict, List, Union + +import discord +from apscheduler.triggers.base import BaseTrigger +from apscheduler.triggers.combining import OrTrigger +from apscheduler.triggers.date import DateTrigger +from apscheduler.triggers.interval import IntervalTrigger +from apscheduler.triggers.cron import CronTrigger +from discord.utils import time_snowflake +from redbot.core import Config, commands +from redbot.core.bot import Red + +log = logging.getLogger("red.fox_v3.fifo.task") + + +async def _do_nothing(*args, **kwargs): + pass + + +def get_trigger(data): + if data["type"] == "interval": + parsed_time = data["time_data"] + return IntervalTrigger(days=parsed_time.days, seconds=parsed_time.seconds) + + if data["type"] == "date": + return DateTrigger(data["time_data"]) + + if data["type"] == "cron": + return CronTrigger.from_crontab(data["time_data"]) + + return False + + +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 + 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) + + +def neuter_message(message: FakeMessage): + message.delete = _do_nothing + message.edit = _do_nothing + message.publish = _do_nothing + message.pin = _do_nothing + message.unpin = _do_nothing + message.add_reaction = _do_nothing + message.remove_reaction = _do_nothing + message.clear_reaction = _do_nothing + message.clear_reactions = _do_nothing + message.ack = _do_nothing + + return message + + +class Task: + default_task_data = {"triggers": [], "command_str": ""} + + default_trigger = { + "type": "", + "time_data": None, # Used for Interval and Date Triggers + } + + def __init__( + self, name: str, guild_id, config: Config, author_id=None, channel_id=None, bot: Red = None + ): + self.name = name + self.guild_id = guild_id + self.config = config + self.bot = bot + self.author_id = author_id + self.channel_id = channel_id + self.data = None + + async def _encode_time_triggers(self): + if not self.data or not self.data.get("triggers", None): + return [] + + triggers = [] + for t in self.data["triggers"]: + if t["type"] == "interval": # Convert into timedelta + td: timedelta = t["time_data"] + + triggers.append( + {"type": t["type"], "time_data": {"days": td.days, "seconds": td.seconds}} + ) + continue + + if t["type"] == "date": # Convert into datetime + dt: datetime = t["time_data"] + 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": # TODO: Implement this, should be easy + raise NotImplemented + raise NotImplemented + + return triggers + + async def _decode_time_triggers(self): + if not self.data or not self.data.get("triggers", None): + return + + for n, t in enumerate(self.data["triggers"]): + if t["type"] == "interval": # Convert into timedelta + 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"]) + self.data["triggers"][n]["time_data"] = datetime.fromisoformat(t["time_data"]) + continue + + if t["type"] == "cron": + raise NotImplemented + raise NotImplemented + + # async def load_from_data(self, data: Dict): + # self.data = data.copy() + + async def load_from_config(self): + data = await self.config.guild_from_id(self.guild_id).tasks.get_raw( + self.name, default=None + ) + + if not data: + return + + self.author_id = data["author_id"] + self.guild_id = data["guild_id"] + self.channel_id = data["channel_id"] + + self.data = data["data"] + + await self._decode_time_triggers() + return self.data + + 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 [get_trigger(t) for t in self.data["triggers"]] + + async def get_combined_trigger(self) -> Union[BaseTrigger, None]: + if not self.data: + await self.load_from_config() + + return parse_triggers(self.data) + + # async def set_job_id(self, job_id): + # if self.data is None: + # await self.load_from_config() + # + # self.data["job_id"] = job_id + + async def save_all(self): + """To be used when creating an new 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"] = await self._encode_time_triggers() + + to_save = { + "guild_id": self.guild_id, + "author_id": self.author_id, + "channel_id": self.channel_id, + "data": data_to_save, + } + await self.config.guild_from_id(self.guild_id).tasks.set_raw(self.name, value=to_save) + + async def save_data(self): + """To be used when updating triggers""" + if not self.data: + return + + data_to_save = self.data.copy() + 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 + ) + + async def execute(self): + if not self.data or not self.get_command_str(): + log.warning(f"Could not execute task due to data problem: {self.data=}") + return False + + guild: discord.Guild = self.bot.get_guild(self.guild_id) # used for get_prefix + if guild is None: + log.warning(f"Could not execute task due to missing guild: {self.guild_id}") + return False + channel: discord.TextChannel = guild.get_channel(self.channel_id) + if channel is None: + log.warning(f"Could not execute task due to missing channel: {self.channel_id}") + return False + author: discord.User = guild.get_member(self.author_id) + if author is None: + log.warning(f"Could not execute task due to missing author: {self.author_id}") + return False + + 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 + 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 + actual_message = actual_message[0] + + 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(message) + if isinstance(prefixes, str): + prefix = prefixes + else: + prefix = prefixes[0] + + message.content = f"{prefix}{self.get_command_str()}" + + if not message.guild or not message.author or not message.content: + log.warning(f"Could not execute task due to message problem: {message}") + 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 due invalid context: {new_ctx}") + return False + + await self.bot.invoke(new_ctx) + return True + + async def set_bot(self, bot: Red): + self.bot = bot + + async def set_author(self, author: Union[discord.User, discord.Member, str]): + self.author_id = getattr(author, "id", None) or author + await self.config.guild_from_id(self.guild_id).tasks.set_raw( + self.name, "author_id", value=self.author_id + ) + + async def set_channel(self, channel: Union[discord.TextChannel, str]): + self.channel_id = getattr(channel, "id", None) or channel + await self.config.guild_from_id(self.guild_id).tasks.set_raw( + self.name, "channel_id", value=self.author_id + ) + + def get_command_str(self): + return self.data.get("command_str", "") + + async def set_commmand_str(self, command_str): + if not self.data: + self.data = self.default_task_data.copy() + self.data["command_str"] = command_str + return True + + 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 + + if not self.data: + self.data = self.default_task_data.copy() + + self.data["triggers"].append(trigger_data) + return True + + def __setstate__(self, task_state): + self.name = task_state["name"] + self.guild_id = task_state["guild_id"] + self.config = task_state["config"] + self.bot = None + self.author_id = None + self.channel_id = None + self.data = None + + def __getstate__(self): + return { + "name": self.name, + "guild_id": self.guild_id, + "config": self.config, + "bot": self.bot, + } From 607b7b67183bc12667d6f4db9b4e2b9ad298f49d Mon Sep 17 00:00:00 2001 From: bobloy Date: Thu, 3 Sep 2020 10:04:10 -0400 Subject: [PATCH 027/121] Ready for release? --- fifo/fifo.py | 54 +++++++++++++++++++++++++++++++++++++++++++++----- fifo/redjob.py | 44 ---------------------------------------- fifo/task.py | 20 +++++++++++++++---- 3 files changed, 65 insertions(+), 53 deletions(-) delete mode 100644 fifo/redjob.py diff --git a/fifo/fifo.py b/fifo/fifo.py index 10b72af..d4156ea 100644 --- a/fifo/fifo.py +++ b/fifo/fifo.py @@ -93,6 +93,13 @@ class FIFO(commands.Cog): return new_ctx.valid + async def _delete_task(self, task: Task): + job: Union[Job, None] = await self._get_job(task) + if job is not None: + job.remove() + + await task.delete_self() + async def _process_task(self, task: Task): job: Union[Job, None] = await self._get_job(task) if job is not None: @@ -146,9 +153,14 @@ class FIFO(commands.Cog): pass @fifo.command(name="set") - async def fifo_setauthor(self, ctx: commands.Context, task_name: str, author_or_channel: Union[discord.Member, discord.TextChannel]): + async def fifo_set( + self, + ctx: commands.Context, + task_name: str, + author_or_channel: Union[discord.Member, discord.TextChannel], + ): """ - Sets the task to be executed as a different author or in a different channel. + Sets a different author or in a different channel for execution of a task. """ task = Task(task_name, ctx.guild.id, self.config, bot=self.bot) await task.load_from_config() @@ -327,9 +339,39 @@ class FIFO(commands.Cog): """ Deletes a task from this guild's task list """ - pass + 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 + + await self._delete_task(task) + await ctx.maybe_send_embed(f"Task[{task_name}] has been deleted from this guild") + + @fifo.command(name="cleartriggers", aliases=["cleartrigger"]) + async def fifo_cleartriggers(self, ctx: commands.Context, task_name: str): + """ + Removes all triggers from specified task + + Useful to start over with new trigger + """ + + task = Task(task_name, ctx.guild.id, self.config, bot=self.bot) + await task.load_from_config() - @fifo.group(name="trigger") + 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 + + await task.clear_triggers() + await ctx.tick() + + @fifo.group(name="addtrigger", aliases=["trigger"]) async def fifo_trigger(self, ctx: commands.Context): """ Add a new trigger for a task from the current guild. @@ -405,7 +447,9 @@ class FIFO(commands.Cog): self, ctx: commands.Context, task_name: str, *, cron_str: CronConverter ): """ - Add a "time of day" trigger to the specified task + 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) await task.load_from_config() diff --git a/fifo/redjob.py b/fifo/redjob.py deleted file mode 100644 index c276aa4..0000000 --- a/fifo/redjob.py +++ /dev/null @@ -1,44 +0,0 @@ -import six -from apscheduler.job import Job -from apscheduler.schedulers.asyncio import AsyncIOScheduler -from apscheduler.schedulers.base import STATE_STOPPED -from apscheduler.util import undefined - - -class RedJob(Job): - pass - - -class RedAsyncIOScheduler(AsyncIOScheduler): - - def add_job(self, func, trigger=None, args=None, kwargs=None, id=None, name=None, - misfire_grace_time=undefined, coalesce=undefined, max_instances=undefined, - next_run_time=undefined, jobstore='default', executor='default', - replace_existing=False, **trigger_args): - job_kwargs = { - 'trigger': self._create_trigger(trigger, trigger_args), - 'executor': executor, - 'func': func, - 'args': tuple(args) if args is not None else (), - 'kwargs': dict(kwargs) if kwargs is not None else {}, - 'id': id, - 'name': name, - 'misfire_grace_time': misfire_grace_time, - 'coalesce': coalesce, - 'max_instances': max_instances, - 'next_run_time': next_run_time - } - job_kwargs = dict((key, value) for key, value in six.iteritems(job_kwargs) if - value is not undefined) - job = RedJob(self, **job_kwargs) - - # Don't really add jobs to job stores before the scheduler is up and running - with self._jobstores_lock: - if self.state == STATE_STOPPED: - self._pending_jobs.append((job, jobstore, replace_existing)) - self._logger.info('Adding job tentatively -- it will be properly scheduled when ' - 'the scheduler starts') - else: - self._real_add_job(job, jobstore, replace_existing) - - return job diff --git a/fifo/task.py b/fifo/task.py index 62d50ca..9ed4e12 100644 --- a/fifo/task.py +++ b/fifo/task.py @@ -117,7 +117,9 @@ class Task: continue if t["type"] == "cron": # TODO: Implement this, should be easy - raise NotImplemented + triggers.append(t) # already a string, nothing to do + + continue raise NotImplemented return triggers @@ -137,7 +139,7 @@ class Task: continue if t["type"] == "cron": - raise NotImplemented + continue # already a string raise NotImplemented # async def load_from_data(self, data: Dict): @@ -266,7 +268,9 @@ class Task: 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 due invalid context: {new_ctx}") + log.warning( + f"Could not execute Task[{self.name}] due invalid context: {new_ctx.invoked_with}" + ) return False await self.bot.invoke(new_ctx) @@ -284,7 +288,7 @@ class Task: async def set_channel(self, channel: Union[discord.TextChannel, str]): self.channel_id = getattr(channel, "id", None) or channel await self.config.guild_from_id(self.guild_id).tasks.set_raw( - self.name, "channel_id", value=self.author_id + self.name, "channel_id", value=self.channel_id ) def get_command_str(self): @@ -323,3 +327,11 @@ class Task: "config": self.config, "bot": self.bot, } + + async def clear_triggers(self): + self.data["triggers"] = [] + await self.save_data() + + async def delete_self(self): + """Hopefully nothing uses the object after running this...""" + await self.config.guild_from_id(self.guild_id).tasks.clear_raw(self.name) From 4f494d115d8607981f0cd71d4e662d697aba0d7b Mon Sep 17 00:00:00 2001 From: bobloy Date: Thu, 3 Sep 2020 10:17:05 -0400 Subject: [PATCH 028/121] Fifo release ready --- README.md | 1 + fifo/fifo.py | 2 +- fifo/info.json | 17 ++++-- fifo/redconfigjobstore.py | 112 ++------------------------------------ fifo/task.py | 2 +- 5 files changed, 21 insertions(+), 113 deletions(-) diff --git a/README.md b/README.md index d8399cc..3a9ab8f 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ Cog Function | conquest | **Alpha** |
Manage maps for war games and RPGsLots of additional features are planned, currently function with simple map
| | dad | **Beta** |
Tell dad jokesWorks great!
| | exclusiverole | **Alpha** |
Prevent certain roles from getting any other rolesFully functional, but pretty simple
| +| fifo | **Alpha** |
Schedule commands to be run at certain times or intervalsJust released, please report bugs as you find them. Only works for bot owner for now
| | fight | **Incomplete** |
Organize bracket tournaments within discordStill in-progress, a massive project
| | flag | **Alpha** |
Create temporary marks on users that expire after specified timePorted, will not import old data. Please report bugs
| | forcemention | **Alpha** |
Mentions unmentionable rolesVery simple cog, mention doesn't persist
| diff --git a/fifo/fifo.py b/fifo/fifo.py index d4156ea..9e0580e 100644 --- a/fifo/fifo.py +++ b/fifo/fifo.py @@ -81,7 +81,7 @@ class FIFO(commands.Cog): await self.jobstore.load_from_config(self.scheduler, "default") self.scheduler.add_jobstore(self.jobstore, "default") - self.scheduler.start() # TODO: Jobs are not receiving next_run_times + self.scheduler.start() async def _check_parsable_command(self, ctx: commands.Context, command_to_parse: str): message: discord.Message = ctx.message diff --git a/fifo/info.json b/fifo/info.json index 6cc6f9d..c8c2ed9 100644 --- a/fifo/info.json +++ b/fifo/info.json @@ -3,19 +3,26 @@ "Bobloy" ], "min_bot_version": "3.3.0", - "description": "Schedule commands to be run by certain 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": "Schedule commands to be run by certain 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" + "apscheduler", + "dateutil" ], "tags": [ "bobloy", "utilities", - "tools", "tool", - "roles" + "roles", + "schedule", + "cron", + "interval", + "date", + "datetime", + "time", + "calendar" ] } \ No newline at end of file diff --git a/fifo/redconfigjobstore.py b/fifo/redconfigjobstore.py index 3747a97..aa6d967 100644 --- a/fifo/redconfigjobstore.py +++ b/fifo/redconfigjobstore.py @@ -30,8 +30,8 @@ class RedConfigJobStore(MemoryJobStore): self.bot = bot self.pickle_protocol = pickle.HIGHEST_PROTOCOL self._eventloop = self.bot.loop - # TODO: self.config.jobs_index is never read from, - # either remove or replace self._jobs_index + # 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(): @@ -114,7 +114,7 @@ class RedConfigJobStore(MemoryJobStore): 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) + # await self.config.jobs_index.set_raw(job.id, value=job_tuple) return True @run_in_event_loop @@ -149,7 +149,7 @@ class RedConfigJobStore(MemoryJobStore): 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)) + # 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=}") @@ -168,7 +168,7 @@ class RedConfigJobStore(MemoryJobStore): 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) + # await self.config.jobs_index.clear_raw(job.id) @run_in_event_loop def remove_all_jobs(self): @@ -177,108 +177,8 @@ class RedConfigJobStore(MemoryJobStore): async def _async_remove_all_jobs(self): await self.config.jobs.clear() - await self.config.jobs_index.clear() + # await self.config.jobs_index.clear() def shutdown(self): """Removes all jobs without clearing config""" super().remove_all_jobs() - - -# import asyncio -# -# from apscheduler.jobstores.base import BaseJobStore, ConflictingIdError -# from apscheduler.util import datetime_to_utc_timestamp -# from redbot.core import Config -# from redbot.core.utils import AsyncIter -# -# -# class RedConfigJobStore(BaseJobStore): -# def __init__(self, config: Config, loop): -# super().__init__() -# self.config = config -# self.loop: asyncio.BaseEventLoop = loop -# -# self._jobs = [] -# self._jobs_index = {} # id -> (job, timestamp) lookup table -# -# def lookup_job(self, job_id): -# return asyncio.run(self._async_lookup_job(job_id)) -# -# async def _async_lookup_job(self, job_id): -# return (await self.config.jobs_index.get_raw(job_id, default=(None, None)))[0] -# -# def get_due_jobs(self, now): -# return asyncio.run(self._async_get_due_jobs(now)) -# -# async def _async_get_due_jobs(self, now): -# now_timestamp = datetime_to_utc_timestamp(now) -# pending = [] -# all_jobs = await self.config.jobs() -# async for job, timestamp in AsyncIter(all_jobs, steps=100): -# if timestamp is None or timestamp > now_timestamp: -# break -# pending.append(job) -# -# return pending -# -# def get_next_run_time(self): -# return asyncio.run(self._async_get_next_run_time()) -# -# async def _async_get_next_run_time(self): -# _jobs = await self.config.jobs() -# return _jobs[0][0].next_run_time if _jobs else None -# -# def get_all_jobs(self): -# return asyncio.run(self._async_get_all_jobs()) -# -# async def _async_get_all_jobs(self): -# return [j[0] for j in (await self.config.jobs())] -# -# def add_job(self, job): -# return asyncio.run(self._async_add_job(job)) -# -# async def _async_add_job(self, job): -# if await self.config.jobs_index.get_raw(job.id, default=None) is not None: -# raise ConflictingIdError(job.id) -# -# timestamp = datetime_to_utc_timestamp(job.next_run_time) -# index = self._get_job_index(timestamp, job.id) -# self._jobs.insert(index, (job, timestamp)) -# self._jobs_index[job.id] = (job, timestamp) -# -# def update_job(self, job): -# pass -# -# def remove_job(self, job_id): -# pass -# -# def remove_all_jobs(self): -# pass -# -# def _get_job_index(self, timestamp, job_id): -# """ -# Returns the index of the given job, or if it's not found, the index where the job should be -# inserted based on the given timestamp. -# -# :type timestamp: int -# :type job_id: str -# -# """ -# lo, hi = 0, len(self._jobs) -# timestamp = float('inf') if timestamp is None else timestamp -# while lo < hi: -# mid = (lo + hi) // 2 -# mid_job, mid_timestamp = self._jobs[mid] -# mid_timestamp = float('inf') if mid_timestamp is None else mid_timestamp -# if mid_timestamp > timestamp: -# hi = mid -# elif mid_timestamp < timestamp: -# lo = mid + 1 -# elif mid_job.id > job_id: -# hi = mid -# elif mid_job.id < job_id: -# lo = mid + 1 -# else: -# return mid -# -# return lo diff --git a/fifo/task.py b/fifo/task.py index 9ed4e12..5d16ec9 100644 --- a/fifo/task.py +++ b/fifo/task.py @@ -116,7 +116,7 @@ class Task: # ) continue - if t["type"] == "cron": # TODO: Implement this, should be easy + if t["type"] == "cron": triggers.append(t) # already a string, nothing to do continue From 3d64bcf768f8518bc34ffd048e80ea4f77a5cf5c Mon Sep 17 00:00:00 2001 From: bobloy Date: Thu, 3 Sep 2020 10:17:43 -0400 Subject: [PATCH 029/121] jobs_index is unused --- fifo/fifo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fifo/fifo.py b/fifo/fifo.py index 9e0580e..38bef79 100644 --- a/fifo/fifo.py +++ b/fifo/fifo.py @@ -48,7 +48,7 @@ class FIFO(commands.Cog): self.bot = bot self.config = Config.get_conf(self, identifier=70737079, force_registration=True) - default_global = {"jobs_index": {}, "jobs": []} + default_global = {"jobs": []} default_guild = {"tasks": {}} self.config.register_global(**default_global) From 12d0b2944ed511d6119c16d848f5e21ad1cd1bc4 Mon Sep 17 00:00:00 2001 From: bobloy Date: Thu, 3 Sep 2020 10:26:10 -0400 Subject: [PATCH 030/121] Reformatting --- fifo/datetime_cron_converters.py | 5 +++-- fifo/fifo.py | 2 +- fifo/redconfigjobstore.py | 1 - fifo/task.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/fifo/datetime_cron_converters.py b/fifo/datetime_cron_converters.py index 5382b07..d59ef37 100644 --- a/fifo/datetime_cron_converters.py +++ b/fifo/datetime_cron_converters.py @@ -2,8 +2,8 @@ from datetime import datetime from typing import TYPE_CHECKING from apscheduler.triggers.cron import CronTrigger -from discord.ext.commands import BadArgument, Converter from dateutil import parser +from discord.ext.commands import BadArgument, Converter from fifo.timezones import assemble_timezones @@ -11,6 +11,7 @@ if TYPE_CHECKING: DatetimeConverter = datetime CronConverter = str else: + class DatetimeConverter(Converter): async def convert(self, ctx, argument) -> datetime: dt = parser.parse(argument, fuzzy=True, tzinfos=assemble_timezones()) @@ -25,4 +26,4 @@ else: except ValueError: raise BadArgument() - return argument \ No newline at end of file + return argument diff --git a/fifo/fifo.py b/fifo/fifo.py index 38bef79..e84e342 100644 --- a/fifo/fifo.py +++ b/fifo/fifo.py @@ -9,7 +9,7 @@ from apscheduler.schedulers.asyncio import AsyncIOScheduler 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 DictConverter, TimedeltaConverter +from redbot.core.commands import TimedeltaConverter from .datetime_cron_converters import CronConverter, DatetimeConverter from .task import Task diff --git a/fifo/redconfigjobstore.py b/fifo/redconfigjobstore.py index aa6d967..7e68697 100644 --- a/fifo/redconfigjobstore.py +++ b/fifo/redconfigjobstore.py @@ -3,7 +3,6 @@ import base64 import logging import pickle from datetime import datetime -from time import sleep from typing import Tuple, Union from apscheduler.job import Job diff --git a/fifo/task.py b/fifo/task.py index 5d16ec9..83158d8 100644 --- a/fifo/task.py +++ b/fifo/task.py @@ -5,9 +5,9 @@ from typing import Dict, List, Union import discord 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 apscheduler.triggers.cron import CronTrigger from discord.utils import time_snowflake from redbot.core import Config, commands from redbot.core.bot import Red From e13518dc42cf02d5972dd2d4835fea0ac8f12048 Mon Sep 17 00:00:00 2001 From: bobloy Date: Thu, 3 Sep 2020 13:53:27 -0400 Subject: [PATCH 031/121] Typo in short description --- fifo/info.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fifo/info.json b/fifo/info.json index c8c2ed9..a1255cd 100644 --- a/fifo/info.json +++ b/fifo/info.json @@ -6,7 +6,7 @@ "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": "[ALPHA] 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", From 0ec877d5f98cc29a942c016ff10efeb5edc14993 Mon Sep 17 00:00:00 2001 From: bobloy Date: Thu, 3 Sep 2020 15:09:51 -0400 Subject: [PATCH 032/121] Reformatting and f string syntax --- audiotrivia/audiotrivia.py | 42 ++++++++++++-------------------------- 1 file changed, 13 insertions(+), 29 deletions(-) diff --git a/audiotrivia/audiotrivia.py b/audiotrivia/audiotrivia.py index 9465d9a..c0c88fd 100644 --- a/audiotrivia/audiotrivia.py +++ b/audiotrivia/audiotrivia.py @@ -5,18 +5,17 @@ from typing import List import lavalink import yaml from redbot.cogs.audio import Audio -from redbot.cogs.audio.core.utilities import validation from redbot.cogs.trivia import LOG from redbot.cogs.trivia.trivia import InvalidListError, Trivia -from redbot.core import commands, Config, checks +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 box -# from redbot.cogs.audio.utils import userlimit +from .audiosession import AudioSession -from .audiosession import AudioSession +# from redbot.cogs.audio.utils import userlimit class AudioTrivia(Trivia): @@ -66,18 +65,16 @@ class AudioTrivia(Trivia): """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) - ) + 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. - """ + 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 @@ -92,25 +89,19 @@ class AudioTrivia(Trivia): 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." - ) + 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( - "It is recommended to disable audio status with `{}audioset status`".format( - ctx.prefix - ) + f"It is recommended to disable audio status with `{ctx.prefix}audioset status`" ) if notify: await ctx.send( - "It is recommended to disable audio notify with `{}audioset notify`".format( - ctx.prefix - ) + f"It is recommended to disable audio notify with `{ctx.prefix}audioset notify`" ) if not self.audio._player_check(ctx): @@ -118,9 +109,7 @@ class AudioTrivia(Trivia): 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." - ) + 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()) @@ -177,10 +166,7 @@ class AudioTrivia(Trivia): # 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, + 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) @@ -214,9 +200,7 @@ class AudioTrivia(Trivia): 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) - ) + raise FileNotFoundError("Could not find the `{}` category.".format(category)) with path.open(encoding="utf-8") as file: try: From 7c43d6c8ac046310f0a122b6b28ea8156e083d63 Mon Sep 17 00:00:00 2001 From: bobloy Date: Thu, 3 Sep 2020 15:10:03 -0400 Subject: [PATCH 033/121] python-dateutil, not just dateutil --- fifo/info.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fifo/info.json b/fifo/info.json index a1255cd..f19aa63 100644 --- a/fifo/info.json +++ b/fifo/info.json @@ -10,7 +10,7 @@ "end_user_data_statement": "This cog does not store any End User Data", "requirements": [ "apscheduler", - "dateutil" + "python-dateutil" ], "tags": [ "bobloy", From d619c9a502590d725edce6b01943bfdedcb0ffc5 Mon Sep 17 00:00:00 2001 From: bobloy Date: Thu, 3 Sep 2020 15:10:52 -0400 Subject: [PATCH 034/121] probably 3.4 with all the jack stuff I did --- fifo/info.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fifo/info.json b/fifo/info.json index f19aa63..c9a80dd 100644 --- a/fifo/info.json +++ b/fifo/info.json @@ -2,7 +2,7 @@ "author": [ "Bobloy" ], - "min_bot_version": "3.3.0", + "min_bot_version": "3.4.0", "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`", From 9f6a05ae88b651dcc0571c03260ea6e0afced0ad Mon Sep 17 00:00:00 2001 From: bobloy Date: Sat, 5 Sep 2020 15:26:38 -0400 Subject: [PATCH 035/121] Update README.md More verbose downloader instructions --- chatter/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/chatter/README.md b/chatter/README.md index e8c03d6..ecaf625 100644 --- a/chatter/README.md +++ b/chatter/README.md @@ -83,6 +83,7 @@ pip install --no-deps "chatterbot>=1.1" #### Step 3: Load Chatter ``` +[p]repo add Fox https://github.com/bobloy/Fox-V3 [p]cog install Fox chatter [p]load chatter ``` @@ -92,7 +93,7 @@ pip install --no-deps "chatterbot>=1.1" #### Step 1: Built-in Downloader ``` -[p]cog install Chatter +[p]cog install Chatter ``` #### Step 2: Install Requirements From 5eb31a277dcfb73207b45bbcb3e0cceb18d07c64 Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 8 Sep 2020 12:27:19 -0400 Subject: [PATCH 036/121] Update README.md --- chatter/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chatter/README.md b/chatter/README.md index ecaf625..9b0eb6b 100644 --- a/chatter/README.md +++ b/chatter/README.md @@ -83,7 +83,7 @@ pip install --no-deps "chatterbot>=1.1" #### Step 3: Load Chatter ``` -[p]repo add Fox https://github.com/bobloy/Fox-V3 +[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 ``` From d377461602378a20f57dfea864d535579dc36a40 Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 8 Sep 2020 13:56:28 -0400 Subject: [PATCH 037/121] WIP adding timezone to Cron triggers --- fifo/datetime_cron_converters.py | 15 +++++++++++++-- fifo/fifo.py | 11 ++++++++--- fifo/task.py | 5 ++++- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/fifo/datetime_cron_converters.py b/fifo/datetime_cron_converters.py index d59ef37..b7f3dc4 100644 --- a/fifo/datetime_cron_converters.py +++ b/fifo/datetime_cron_converters.py @@ -1,8 +1,8 @@ -from datetime import datetime +from datetime import datetime, tzinfo from typing import TYPE_CHECKING from apscheduler.triggers.cron import CronTrigger -from dateutil import parser +from dateutil import parser, tz from discord.ext.commands import BadArgument, Converter from fifo.timezones import assemble_timezones @@ -11,6 +11,17 @@ if TYPE_CHECKING: DatetimeConverter = datetime 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 = tz.gettz(argument) + + if timez is not None: + return timez + raise BadArgument() class DatetimeConverter(Converter): async def convert(self, ctx, argument) -> datetime: diff --git a/fifo/fifo.py b/fifo/fifo.py index e84e342..91991b0 100644 --- a/fifo/fifo.py +++ b/fifo/fifo.py @@ -11,7 +11,7 @@ from redbot.core import Config, checks, commands from redbot.core.bot import Red from redbot.core.commands import TimedeltaConverter -from .datetime_cron_converters import CronConverter, DatetimeConverter +from .datetime_cron_converters import CronConverter, DatetimeConverter, TimezoneConverter from .task import Task schedule_log = logging.getLogger("red.fox_v3.fifo.scheduler") @@ -444,7 +444,12 @@ class FIFO(commands.Cog): @fifo_trigger.command(name="cron") async def fifo_trigger_cron( - self, ctx: commands.Context, task_name: str, *, cron_str: CronConverter + self, + ctx: commands.Context, + task_name: str, + optional_tz: Optional[TimezoneConverter] = None, + *, + cron_str: CronConverter, ): """ Add a cron "time of day" trigger to the specified task @@ -460,7 +465,7 @@ class FIFO(commands.Cog): ) return - result = await task.add_trigger("cron", cron_str) + result = await task.add_trigger("cron", cron_str, optional_tz) if not result: await ctx.maybe_send_embed( "Failed to add a cron trigger to this task, see console for logs" diff --git a/fifo/task.py b/fifo/task.py index 83158d8..764ab8f 100644 --- a/fifo/task.py +++ b/fifo/task.py @@ -300,7 +300,10 @@ class Task: self.data["command_str"] = command_str return True - async def add_trigger(self, param, parsed_time: Union[timedelta, datetime, str]): + 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 = {"type": param, "time_data": parsed_time} if not get_trigger(trigger_data): return False From 2e65c137f3c082083d901876543ec2f1f5eb3a80 Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 8 Sep 2020 15:40:40 -0400 Subject: [PATCH 038/121] Update README.md Added 64 bit python requirement for Windows --- chatter/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/chatter/README.md b/chatter/README.md index 9b0eb6b..f82fd0b 100644 --- a/chatter/README.md +++ b/chatter/README.md @@ -50,7 +50,9 @@ Linux is a bit easier, but only tested on Debian and Ubuntu. ## Windows Prerequisites -Install these on your windows machine before attempting the installation +**Requires 64 Bit Python to continue on Windows.** + +Install these on your windows machine before attempting the installation: [Visual Studio C++ Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) From b8aceb003ea494edc52eeef76f4b0c2580eb5bc8 Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 8 Sep 2020 15:41:24 -0400 Subject: [PATCH 039/121] Update README.md Fix tyop --- chatter/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chatter/README.md b/chatter/README.md index f82fd0b..8ef6734 100644 --- a/chatter/README.md +++ b/chatter/README.md @@ -29,7 +29,7 @@ Chatter by default uses spaCy's `en_core_web_md` training model, which is ~50 MB Chatter can potential use spaCy's `en_core_web_lg` training model, which is ~800 MB -Chatter uses as sqlite database that can potentially take up a large amount os disk space, +Chatter uses as sqlite database that can potentially take up a large amount of disk space, depending on how much training Chatter has done. The sqlite database can be safely deleted at any time. Deletion will only erase training data. From 6af1d06b2c0105393638d82ec475876e31718ab4 Mon Sep 17 00:00:00 2001 From: bobloy Date: Thu, 10 Sep 2020 09:15:55 -0400 Subject: [PATCH 040/121] Add identifier to launchlib config in case I use it later --- launchlib/launchlib.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/launchlib/launchlib.py b/launchlib/launchlib.py index e34a23d..ae870fd 100644 --- a/launchlib/launchlib.py +++ b/launchlib/launchlib.py @@ -22,7 +22,9 @@ class LaunchLib(commands.Cog): def __init__(self, bot: Red): super().__init__() self.bot = bot - self.config = Config.get_conf(self, identifier=0, force_registration=True) + self.config = Config.get_conf( + self, identifier=7697117110991047610598, force_registration=True + ) default_guild = {} From 92caf16fe977ba168ec343f7484a6ce099c42732 Mon Sep 17 00:00:00 2001 From: bobloy Date: Thu, 10 Sep 2020 09:17:02 -0400 Subject: [PATCH 041/121] FirstMessage initial commit --- firstmessage/__init__.py | 5 ++++ firstmessage/firstmessage.py | 48 ++++++++++++++++++++++++++++++++++++ firstmessage/info.json | 16 ++++++++++++ 3 files changed, 69 insertions(+) create mode 100644 firstmessage/__init__.py create mode 100644 firstmessage/firstmessage.py create mode 100644 firstmessage/info.json diff --git a/firstmessage/__init__.py b/firstmessage/__init__.py new file mode 100644 index 0000000..4dab2ed --- /dev/null +++ b/firstmessage/__init__.py @@ -0,0 +1,5 @@ +from .firstmessage import FirstMessage + + +async def setup(bot): + bot.add_cog(FirstMessage(bot)) diff --git a/firstmessage/firstmessage.py b/firstmessage/firstmessage.py new file mode 100644 index 0000000..b74ea40 --- /dev/null +++ b/firstmessage/firstmessage.py @@ -0,0 +1,48 @@ +import logging + +import discord +from redbot.core import Config, commands +from redbot.core.bot import Red + +log = logging.getLogger("red.fox_v3.firstmessage") + + +class FirstMessage(commands.Cog): + """ + Provides a link to the first message in the provided channel + + Less important information about the cog + """ + + def __init__(self, bot: Red): + super().__init__() + self.bot = bot + self.config = Config.get_conf( + self, identifier=701051141151167710111511597103101, force_registration=True + ) + + default_guild = {} + + self.config.register_guild(**default_guild) + + async def red_delete_data_for_user(self, **kwargs): + """Nothing to delete""" + return + + @commands.command() + async def firstmessage(self, ctx: commands.Context, channel: discord.TextChannel = None): + if channel is None: + channel = ctx.channel + try: + message: discord.Message = ( + await channel.history(limit=1, oldest_first=True).flatten() + )[0] + except (discord.Forbidden, discord.HTTPException): + log.exception(f"Unable to read message history for {channel.id=}") + await ctx.maybe_send_embed("Unable to read message history for that channel") + return + + em = discord.Embed(description=f"[First Message in {channel.mention}]({message.jump_url})") + em.set_author(name=message.author.display_name, icon_url=message.author.avatar_url) + + await ctx.send(embed=em) diff --git a/firstmessage/info.json b/firstmessage/info.json new file mode 100644 index 0000000..c01766f --- /dev/null +++ b/firstmessage/info.json @@ -0,0 +1,16 @@ +{ + "author": [ + "Bobloy" + ], + "min_bot_version": "3.4.0", + "description": "Simple cog to jump to the first message of a channel easily", + "hidden": false, + "install_msg": "Thank you for installing FirstMessage.\nGet started with `[p]load firstmessage`, then `[p]help FirstMessage`", + "short": "Simple cog to jump to first message of a channel", + "end_user_data_statement": "This cog does not store any End User Data", + "tags": [ + "bobloy", + "utilities", + "tool" + ] +} \ No newline at end of file From f9388454a5cae33f47ba79c32c3989cf943fdd68 Mon Sep 17 00:00:00 2001 From: bobloy Date: Thu, 10 Sep 2020 21:22:50 -0400 Subject: [PATCH 042/121] Another guild bug --- chatter/chat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chatter/chat.py b/chatter/chat.py index a81e669..ad8e37b 100644 --- a/chatter/chat.py +++ b/chatter/chat.py @@ -463,7 +463,7 @@ class Chatter(Cog): # Thank you Cog-Creators channel: discord.TextChannel = message.channel - if channel.id == await self.config.guild(guild).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) From cb6693f3829abe36f6c467d04501bd7920a33a04 Mon Sep 17 00:00:00 2001 From: bobloy Date: Fri, 11 Sep 2020 09:35:08 -0400 Subject: [PATCH 043/121] Add description of first message --- firstmessage/firstmessage.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/firstmessage/firstmessage.py b/firstmessage/firstmessage.py index b74ea40..f13bd60 100644 --- a/firstmessage/firstmessage.py +++ b/firstmessage/firstmessage.py @@ -10,8 +10,6 @@ log = logging.getLogger("red.fox_v3.firstmessage") class FirstMessage(commands.Cog): """ Provides a link to the first message in the provided channel - - Less important information about the cog """ def __init__(self, bot: Red): @@ -31,6 +29,9 @@ class FirstMessage(commands.Cog): @commands.command() async def firstmessage(self, ctx: commands.Context, channel: discord.TextChannel = None): + """ + Provide a link to the first message in current or provided channel. + """ if channel is None: channel = ctx.channel try: From 7092bd590b070afaba89182069f2a55068a5462f Mon Sep 17 00:00:00 2001 From: bobloy Date: Fri, 11 Sep 2020 09:43:16 -0400 Subject: [PATCH 044/121] FirstMessage added to cog list --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3a9ab8f..26e39d1 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Cog Function | exclusiverole | **Alpha** |
Prevent certain roles from getting any other rolesFully functional, but pretty simple
| | fifo | **Alpha** |
Schedule commands to be run at certain times or intervalsJust released, please report bugs as you find them. Only works for bot owner for now
| | fight | **Incomplete** |
Organize bracket tournaments within discordStill in-progress, a massive project
| +| firstmessage | **Release** |
Simple cog to provide a jump link to the first message in a channel/summary>Just released, please report bugs as you find them.
| | flag | **Alpha** |
Create temporary marks on users that expire after specified timePorted, will not import old data. Please report bugs
| | forcemention | **Alpha** |
Mentions unmentionable rolesVery simple cog, mention doesn't persist
| | hangman | **Beta** |
Play a game of hangmanSome visual glitches and needs more customization
| @@ -38,7 +39,7 @@ Cog Function | unicode | **Alpha** |
Encode and Decode unicode characters[Snap-Ons] Just updated to V3
| | werewolf | **Pre-Alpha** |
Play the classic party game Werewolf within discordAnother massive project currently being developed, will be fully customizable
| -Check out my V2 cogs at [Fox-Cogs v2](https://github.com/bobloy/Fox-Cogs) +Check out *Deprecated* my V2 cogs at [Fox-Cogs v2](https://github.com/bobloy/Fox-Cogs) # Installation ### Recommended - Built-in Downloader From 260a3bc62df8bbc58ac91e4f5870c3935d29c251 Mon Sep 17 00:00:00 2001 From: bobloy Date: Fri, 11 Sep 2020 14:58:34 -0400 Subject: [PATCH 045/121] IsItDown initial commit --- isitdown/__init__.py | 5 ++++ isitdown/isitdown.py | 58 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 isitdown/__init__.py create mode 100644 isitdown/isitdown.py diff --git a/isitdown/__init__.py b/isitdown/__init__.py new file mode 100644 index 0000000..fdebc2a --- /dev/null +++ b/isitdown/__init__.py @@ -0,0 +1,5 @@ +from .isitdown import IsItDown + + +async def setup(bot): + bot.add_cog(IsItDown(bot)) diff --git a/isitdown/isitdown.py b/isitdown/isitdown.py new file mode 100644 index 0000000..f786928 --- /dev/null +++ b/isitdown/isitdown.py @@ -0,0 +1,58 @@ +import logging +import re + +import aiohttp +from redbot.core import Config, commands +from redbot.core.bot import Red + +log = logging.getLogger("red.fox_v3.isitdown") + + +class IsItDown(commands.Cog): + """ + Cog Description + + Less important information about the cog + """ + + def __init__(self, bot: Red): + super().__init__() + self.bot = bot + self.config = Config.get_conf(self, identifier=0, force_registration=True) + + default_guild = {"iids": []} # List of tuple pairs (channel_id, website) + + self.config.register_guild(**default_guild) + + async def red_delete_data_for_user(self, **kwargs): + """Nothing to delete""" + return + + @commands.command(alias=["iid"]) + async def isitdown(self, ctx: commands.Context, url_to_check): + """ + Check if the provided url is down + + Alias: iid + """ + try: + 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 + + if resp["isitdown"]: + await ctx.maybe_send_embed(f"{url_to_check} is DOWN!") + else: + await ctx.maybe_send_embed(f"{url_to_check} is UP!") + + async def _check_if_down(self, url_to_check): + url = re.compile(r"https?://(www\.)?") + url.sub("", url_to_check).strip().strip("/") + + url = f"https://isitdown.site/api/v3/{url}" + async with aiohttp.ClientSession() as session: + async with session.get(url) as response: + assert response.status == 200 + resp = await response.json() + return resp From 360f294ca0b7c0798e5063146244f2442362fd38 Mon Sep 17 00:00:00 2001 From: bobloy Date: Fri, 11 Sep 2020 15:06:30 -0400 Subject: [PATCH 046/121] IsItDown initial commit --- README.md | 3 ++- isitdown/info.json | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 isitdown/info.json diff --git a/README.md b/README.md index 26e39d1..ec76ead 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,8 @@ Cog Function | forcemention | **Alpha** |
Mentions unmentionable rolesVery simple cog, mention doesn't persist
| | hangman | **Beta** |
Play a game of hangmanSome visual glitches and needs more customization
| | howdoi | **Incomplete** |
Ask coding questions and get results from StackExchangeNot yet functional
| -| infochannel | **Beta** |
Create a channel to display server infoJust released, please report bugs
| +| infochannel | **Beta** |
Create a channel to display server infoDue to rate limits, this does not update as often as it once did
| +| isitdown | **Beta** |
Check if a website/url is downJust released, please report bugs
| | launchlib | **Beta** |
Access rocket launch dataJust released, please report bugs
| | leaver | **Beta** |
Send a message in a channel when a user leaves the serverSeems to be functional, please report any bugs or suggestions
| | lovecalculator | **Alpha** |
Calculate the love between two users[Snap-Ons] Just updated to V3
| diff --git a/isitdown/info.json b/isitdown/info.json new file mode 100644 index 0000000..0a2fb07 --- /dev/null +++ b/isitdown/info.json @@ -0,0 +1,16 @@ +{ + "author": [ + "Bobloy" + ], + "min_bot_version": "3.4.0", + "description": "Check if a website/url is down using the https://isitdown.site/ api", + "hidden": false, + "install_msg": "Thank you for installing IsItDown.\nGet started with `[p]load isitdown`, then `[p]help IsItDown`", + "short": "Check if a website/url is down", + "end_user_data_statement": "This cog does not store any End User Data", + "tags": [ + "bobloy", + "utilities", + "tool" + ] +} \ No newline at end of file From 2e000b11904f2759bcfa175c73bcbb2214c8d9b9 Mon Sep 17 00:00:00 2001 From: bobloy Date: Mon, 14 Sep 2020 10:00:38 -0400 Subject: [PATCH 047/121] "tools" is more common --- fifo/info.json | 1 + firstmessage/info.json | 3 ++- isitdown/info.json | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/fifo/info.json b/fifo/info.json index c9a80dd..dda63ce 100644 --- a/fifo/info.json +++ b/fifo/info.json @@ -16,6 +16,7 @@ "bobloy", "utilities", "tool", + "tools", "roles", "schedule", "cron", diff --git a/firstmessage/info.json b/firstmessage/info.json index c01766f..f656d32 100644 --- a/firstmessage/info.json +++ b/firstmessage/info.json @@ -11,6 +11,7 @@ "tags": [ "bobloy", "utilities", - "tool" + "tool", + "tools" ] } \ No newline at end of file diff --git a/isitdown/info.json b/isitdown/info.json index 0a2fb07..d321732 100644 --- a/isitdown/info.json +++ b/isitdown/info.json @@ -11,6 +11,7 @@ "tags": [ "bobloy", "utilities", - "tool" + "tool", + "tools" ] } \ No newline at end of file From 58054c7a92ddca8176ae739dd9bdafd3ef397e12 Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 15 Sep 2020 15:39:41 -0400 Subject: [PATCH 048/121] Rate limits are for NERDS --- ccrole/ccrole.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ccrole/ccrole.py b/ccrole/ccrole.py index 190050d..77a7fd3 100644 --- a/ccrole/ccrole.py +++ b/ccrole/ccrole.py @@ -367,7 +367,6 @@ class CCRole(commands.Cog): except discord.Forbidden: log.exception(f"Permission error: Unable to add roles") await ctx.send("Permission error: Unable to add roles") - await asyncio.sleep(1) if cmd["rroles"]: rrole_list = [ From eddac5b8b2ad8df9df6f33a5315fed272ce2d800 Mon Sep 17 00:00:00 2001 From: jack1142 <6032823+jack1142@users.noreply.github.com> Date: Tue, 15 Sep 2020 23:56:07 +0200 Subject: [PATCH 049/121] [ccrole] Specify an audit log reason --- ccrole/ccrole.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ccrole/ccrole.py b/ccrole/ccrole.py index 77a7fd3..eb654b1 100644 --- a/ccrole/ccrole.py +++ b/ccrole/ccrole.py @@ -7,6 +7,7 @@ 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") @@ -358,12 +359,14 @@ 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) + await target.add_roles(*arole_list, reason=reason) except discord.Forbidden: log.exception(f"Permission error: Unable to add roles") await ctx.send("Permission error: Unable to add roles") @@ -373,7 +376,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) + await target.remove_roles(*rrole_list, reason=reason) except discord.Forbidden: log.exception(f"Permission error: Unable to remove roles") await ctx.send("Permission error: Unable to remove roles") From 88ef4753398778b1881060dddb1c835197c4c99b Mon Sep 17 00:00:00 2001 From: bobloy Date: Wed, 16 Sep 2020 13:42:35 -0400 Subject: [PATCH 050/121] Bring reactrestrict into the modern discord era --- reactrestrict/reactrestrict.py | 43 ++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/reactrestrict/reactrestrict.py b/reactrestrict/reactrestrict.py index 4030538..226cf24 100644 --- a/reactrestrict/reactrestrict.py +++ b/reactrestrict/reactrestrict.py @@ -1,3 +1,4 @@ +import logging from typing import List, Union import discord @@ -5,6 +6,7 @@ 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): @@ -131,10 +133,12 @@ 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 channel found.") from e + raise LookupError("No member found.") from e if member is None: raise LookupError("No member found.") @@ -168,7 +172,7 @@ class ReactRestrict(Cog): """ channel = self.bot.get_channel(channel_id) try: - return await channel.get_message(message_id) + return await channel.fetch_message(message_id) except discord.NotFound: pass except AttributeError: # VoiceChannel object has no attribute 'get_message' @@ -186,9 +190,11 @@ class ReactRestrict(Cog): :param message_id: :return: """ - for channel in ctx.guild.channels: + + guild: discord.Guild = ctx.guild + for channel in guild.text_channels: try: - return await channel.get_message(message_id) + return await channel.fetch_message(message_id) except discord.NotFound: pass except AttributeError: # VoiceChannel object has no attribute 'get_message' @@ -251,34 +257,35 @@ class ReactRestrict(Cog): await ctx.send("Reaction removed.") @commands.Cog.listener() - async def on_raw_reaction_add( - self, emoji: discord.PartialEmoji, message_id: int, channel_id: int, user_id: int - ): + async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent): """ Event handler for long term reaction watching. - - :param discord.PartialReactionEmoji emoji: - :param int message_id: - :param int channel_id: - :param int user_id: - :return: """ - if emoji.is_custom_emoji(): - emoji_id = emoji.id - else: - emoji_id = emoji.name + + 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 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): @@ -287,10 +294,12 @@ 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) From 8ecdf45fa796ca2d90f5a0d50d3fa1cc562d0766 Mon Sep 17 00:00:00 2001 From: bobloy Date: Wed, 16 Sep 2020 13:43:56 -0400 Subject: [PATCH 051/121] One more error handler --- reactrestrict/reactrestrict.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/reactrestrict/reactrestrict.py b/reactrestrict/reactrestrict.py index 226cf24..25a32c1 100644 --- a/reactrestrict/reactrestrict.py +++ b/reactrestrict/reactrestrict.py @@ -303,7 +303,10 @@ class ReactRestrict(Cog): return message = await self._get_message_from_channel(channel_id, message_id) - await message.remove_reaction(emoji, member) + try: + await message.remove_reaction(emoji, member) + except (discord.Forbidden, discord.NotFound, discord.HTTPException): + log.exception("Unable to remove reaction") # try: # await member.add_roles(*roles) From 18e5cc12ff570b966bee92cc7a394fc055a7e1f7 Mon Sep 17 00:00:00 2001 From: bobloy Date: Wed, 16 Sep 2020 13:46:22 -0400 Subject: [PATCH 052/121] Black formatting --- reactrestrict/reactrestrict.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/reactrestrict/reactrestrict.py b/reactrestrict/reactrestrict.py index 25a32c1..79c3c1c 100644 --- a/reactrestrict/reactrestrict.py +++ b/reactrestrict/reactrestrict.py @@ -8,6 +8,7 @@ from redbot.core.commands import Cog log = logging.getLogger("red.fox_v3.reactrestrict") + class ReactRestrictCombo: def __init__(self, message_id, role_id): self.message_id = message_id @@ -238,7 +239,7 @@ class ReactRestrict(Cog): # noinspection PyTypeChecker await self.add_reactrestrict(message_id, role) - await ctx.maybe_send_embed("Message|Role combo added.") + await ctx.maybe_send_embed("Message|Role restriction added.") @reactrestrict.command() async def remove(self, ctx: commands.Context, message_id: int, role: discord.Role): @@ -254,7 +255,7 @@ class ReactRestrict(Cog): # noinspection PyTypeChecker await self.remove_react(message_id, role) - await ctx.send("Reaction removed.") + await ctx.send("React restriction removed.") @commands.Cog.listener() async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent): From 339492d6d923f421b187cc33d0d65837bc022b1f Mon Sep 17 00:00:00 2001 From: bobloy Date: Fri, 18 Sep 2020 14:10:04 -0400 Subject: [PATCH 053/121] Black formatting --- werewolf/builder.py | 153 ++++++++++++++++++-------- werewolf/game.py | 250 ++++++++++++++++++++++++++++-------------- werewolf/role.py | 18 +-- werewolf/votegroup.py | 2 +- 4 files changed, 281 insertions(+), 142 deletions(-) diff --git a/werewolf/builder.py b/werewolf/builder.py index b6a52df..1648b45 100644 --- a/werewolf/builder.py +++ b/werewolf/builder.py @@ -17,7 +17,7 @@ from redbot.core.utils.menus import menu, prev_page, next_page, close_menu ROLE_LIST = sorted([Villager, Seer, VanillaWerewolf], key=lambda x: x.alignment) -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]] @@ -26,21 +26,38 @@ 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"} + 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, 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) + 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 @@ -60,7 +77,11 @@ def setup(): 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)) + ROLE_PAGES.append( + discord.Embed( + title="RANDOM:Town Role", description="Town {}".format(v), color=0x008000 + ) + ) CATEGORY_COUNT.append(k) # Random WW Roles @@ -69,7 +90,12 @@ def setup(): 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)) + 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: @@ -77,7 +103,10 @@ def setup(): 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)) + discord.Embed( + title="RANDOM:Neutral Role", description="Neutral {}".format(v), color=0xC0C0C0 + ) + ) CATEGORY_COUNT.append(k) @@ -187,9 +216,15 @@ async def encode(roles, 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): +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.me) if perms.manage_messages: # Can manage messages, so remove react try: @@ -203,13 +238,18 @@ async def next_group(ctx: commands.Context, pages: list, else: page = PAGE_GROUPS[page] - 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 prev_group(ctx: commands.Context, pages: list, - controls: dict, message: discord.Message, page: int, - timeout: float, emoji: str): +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.me) if perms.manage_messages: # Can manage messages, so remove react try: @@ -218,18 +258,23 @@ async def prev_group(ctx: commands.Context, pages: list, pass page = PAGE_GROUPS[bisect.bisect_left(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) 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): @@ -242,8 +287,11 @@ 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): @@ -268,7 +316,6 @@ def say_role_list(code_list, rand_roles): class GameBuilder: - def __init__(self): self.code = [] self.rand_roles = [] @@ -276,13 +323,13 @@ class GameBuilder: async def build_game(self, ctx: commands.Context): new_controls = { - '⏪': prev_group, + "⏪": prev_group, "⬅": prev_page, - '☑': self.select_page, + "☑": self.select_page, "➡": next_page, - '⏩': 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") @@ -292,9 +339,16 @@ 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): + 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) if perms.manage_messages: # Can manage messages, so remove react try: @@ -304,12 +358,18 @@ 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) - - async def select_page(self, ctx: commands.Context, pages: list, - controls: dict, message: discord.Message, page: int, - timeout: float, emoji: str): + 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) if perms.manage_messages: # Can manage messages, so remove react try: @@ -322,5 +382,4 @@ class GameBuilder: else: self.code.append(page) - return await menu(ctx, pages, controls, message=message, - page=page, timeout=timeout) + return await menu(ctx, pages, controls, message=message, page=page, timeout=timeout) diff --git a/werewolf/game.py b/werewolf/game.py index a31518f..95807a1 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -15,6 +15,7 @@ class Game: """ Base class to run a single game of Werewolf """ + vote_groups: Dict[str, VoteGroup] roles: List[Role] players: List[Player] @@ -22,19 +23,25 @@ class Game: default_secret_channel = { "channel": None, "players": [], - "votegroup": None # uninitialized VoteGroup + "votegroup": None, # uninitialized VoteGroup } morning_messages = [ "**The sun rises on day {} in the village..**", - "**Morning has arrived on day {}..**" + "**Morning has arrived on day {}..**", ] day_vote_count = 3 - def __init__(self, guild: discord.Guild, role: discord.Role = None, - category: discord.CategoryChannel = None, village: discord.TextChannel = None, - log_channel: discord.TextChannel = None, game_code=None): + def __init__( + self, + guild: discord.Guild, + role: discord.Role = None, + category: discord.CategoryChannel = None, + village: discord.TextChannel = None, + log_channel: discord.TextChannel = None, + game_code=None, + ): self.guild = guild self.game_code = game_code @@ -97,22 +104,28 @@ class Game: await self.get_roles(ctx) if len(self.players) != len(self.roles): - await ctx.maybe_send_embed("Player count does not match role count, cannot start\n" - "Currently **{} / {}**\n" - "Use `{}ww code` to pick a new game" - "".format(len(self.players), len(self.roles), ctx.prefix)) + await ctx.maybe_send_embed( + "Player count does not match role count, cannot start\n" + "Currently **{} / {}**\n" + "Use `{}ww code` to pick a new game" + "".format(len(self.players), len(self.roles), ctx.prefix) + ) self.roles = [] return False if self.game_role is None: try: - self.game_role = await ctx.guild.create_role(name="WW Players", - hoist=True, - mentionable=True, - reason="(BOT) Werewolf game role") + self.game_role = await ctx.guild.create_role( + name="WW Players", + hoist=True, + mentionable=True, + reason="(BOT) Werewolf game role", + ) self.to_delete.add(self.game_role) except (discord.Forbidden, discord.HTTPException): - await ctx.maybe_send_embed("Game role not configured and unable to generate one, cannot start") + await ctx.maybe_send_embed( + "Game role not configured and unable to generate one, cannot start" + ) self.roles = [] return False try: @@ -120,24 +133,33 @@ class Game: await player.member.add_roles(*[self.game_role]) except discord.Forbidden: await ctx.send( - "Unable to add role **{}**\nBot is missing `manage_roles` permissions".format(self.game_role.name)) + "Unable to add role **{}**\nBot is missing `manage_roles` permissions".format( + self.game_role.name + ) + ) return False await self.assign_roles() # Create category and channel with individual overwrites overwrite = { - self.guild.default_role: discord.PermissionOverwrite(read_messages=True, send_messages=False, - add_reactions=False), - self.guild.me: discord.PermissionOverwrite(read_messages=True, send_messages=True, add_reactions=True, - manage_messages=True, manage_channels=True, - manage_roles=True), - self.game_role: discord.PermissionOverwrite(read_messages=True, send_messages=True) + self.guild.default_role: discord.PermissionOverwrite( + read_messages=True, send_messages=False, add_reactions=False + ), + self.guild.me: discord.PermissionOverwrite( + read_messages=True, + send_messages=True, + add_reactions=True, + manage_messages=True, + manage_channels=True, + manage_roles=True, + ), + self.game_role: discord.PermissionOverwrite(read_messages=True, send_messages=True), } if self.channel_category is None: - self.channel_category = await self.guild.create_category("Werewolf Game", - overwrites=overwrite, - reason="(BOT) New game of werewolf") + self.channel_category = await self.guild.create_category( + "Werewolf Game", overwrites=overwrite, reason="(BOT) New game of werewolf" + ) else: # No need to modify categories pass # await self.channel_category.edit(name="🔴 Werewolf Game (ACTIVE)", reason="(BOT) New game of werewolf") @@ -147,20 +169,26 @@ class Game: # reason="(BOT) New game of werewolf") if self.village_channel is None: try: - self.village_channel = await self.guild.create_text_channel("🔵Werewolf", - overwrites=overwrite, - reason="(BOT) New game of werewolf", - category=self.channel_category) + self.village_channel = await self.guild.create_text_channel( + "🔵Werewolf", + overwrites=overwrite, + reason="(BOT) New game of werewolf", + category=self.channel_category, + ) except discord.Forbidden: - await ctx.send("Unable to create Game Channel and none was provided\n" - "Grant Bot appropriate permissions or assign a game_channel") + await ctx.send( + "Unable to create Game Channel and none was provided\n" + "Grant Bot appropriate permissions or assign a game_channel" + ) return False else: self.save_perms[self.village_channel] = self.village_channel.overwrites try: - await self.village_channel.edit(name="🔵Werewolf", - category=self.channel_category, - reason="(BOT) New game of werewolf") + await self.village_channel.edit( + name="🔵Werewolf", + category=self.channel_category, + reason="(BOT) New game of werewolf", + ) except discord.Forbidden as e: print("Unable to rename Game Channel") print(e) @@ -170,12 +198,14 @@ class Game: for target, ow in overwrite.items(): curr = self.village_channel.overwrites_for(target) curr.update(**{perm: value for perm, value in ow}) - await self.village_channel.set_permissions(target=target, - overwrite=curr, - reason="(BOT) New game of werewolf") + await self.village_channel.set_permissions( + target=target, overwrite=curr, reason="(BOT) New game of werewolf" + ) except discord.Forbidden: - await ctx.send("Unable to edit Game Channel permissions\n" - "Grant Bot appropriate permissions to manage permissions") + await ctx.send( + "Unable to edit Game Channel permissions\n" + "Grant Bot appropriate permissions to manage permissions" + ) return self.started = True # Assuming everything worked so far @@ -186,18 +216,25 @@ class Game: print("Channel id: " + channel_id) overwrite = { self.guild.default_role: discord.PermissionOverwrite(read_messages=False), - self.guild.me: discord.PermissionOverwrite(read_messages=True, send_messages=True, add_reactions=True, - manage_messages=True, manage_channels=True, - manage_roles=True) + self.guild.me: discord.PermissionOverwrite( + read_messages=True, + send_messages=True, + add_reactions=True, + manage_messages=True, + manage_channels=True, + manage_roles=True, + ), } for player in self.p_channels[channel_id]["players"]: overwrite[player.member] = discord.PermissionOverwrite(read_messages=True) - channel = await self.guild.create_text_channel(channel_id, - overwrites=overwrite, - reason="(BOT) WW game secret channel", - category=self.channel_category) + channel = await self.guild.create_text_channel( + channel_id, + overwrites=overwrite, + reason="(BOT) WW game secret channel", + category=self.channel_category, + ) self.p_channels[channel_id]["channel"] = channel @@ -216,8 +253,8 @@ class Game: async def _cycle(self): """ Each event calls the next event - - + + _at_day_start() _at_voted() @@ -225,7 +262,7 @@ class Game: _at_day_end() _at_night_begin() _at_night_end() - + and repeat with _at_day_start() again """ await self._at_day_start() @@ -237,7 +274,8 @@ class Game: return await self.village_channel.send( - embed=discord.Embed(title="Game is starting, please wait for setup to complete")) + embed=discord.Embed(title="Game is starting, please wait for setup to complete") + ) await self._notify(0) @@ -271,7 +309,9 @@ class Game: await asyncio.sleep(24) # 4 minute days FixMe to 120 later if check(): return - await self.village_channel.send(embed=discord.Embed(title="**Two minutes of daylight remain...**")) + await self.village_channel.send( + embed=discord.Embed(title="**Two minutes of daylight remain...**") + ) await asyncio.sleep(24) # 4 minute days FixMe to 120 later # Need a loop here to wait for trial to end (can_vote?) @@ -295,7 +335,10 @@ class Game: await self.speech_perms(self.village_channel, target.member) # Only target can talk await self.village_channel.send( - "**{} will be put to trial and has 30 seconds to defend themselves**".format(target.mention)) + "**{} will be put to trial and has 30 seconds to defend themselves**".format( + target.mention + ) + ) await asyncio.sleep(30) @@ -305,7 +348,8 @@ class Game: "Everyone will now vote whether to lynch {}\n" "👍 to save, 👎 to lynch\n" "*Majority rules, no-lynch on ties, " - "vote both or neither to abstain, 15 seconds to vote*".format(target.mention)) + "vote both or neither to abstain, 15 seconds to vote*".format(target.mention) + ) await message.add_reaction("👍") await message.add_reaction("👎") @@ -317,9 +361,9 @@ class Game: down_votes = sum(p for p in reaction_list if p.emoji == "👎" and not p.me) if down_votes > up_votes: - embed = discord.Embed(title="Vote Results", color=0xff0000) + embed = discord.Embed(title="Vote Results", color=0xFF0000) else: - embed = discord.Embed(title="Vote Results", color=0x80ff80) + embed = discord.Embed(title="Vote Results", color=0x80FF80) embed.add_field(name="👎", value="**{}**".format(up_votes), inline=True) embed.add_field(name="👍", value="**{}**".format(down_votes), inline=True) @@ -339,7 +383,8 @@ class Game: else: await self.village_channel.send( "**{}**/**{}** of today's votes have been used!\n" - "Nominate carefully..".format(self.used_votes, self.day_vote_count)) + "Nominate carefully..".format(self.used_votes, self.day_vote_count) + ) self.ongoing_vote = False @@ -373,7 +418,9 @@ class Game: await self.night_perms(self.village_channel) - await self.village_channel.send(embed=discord.Embed(title="**The sun sets on the village...**")) + await self.village_channel.send( + embed=discord.Embed(title="**The sun sets on the village...**") + ) await self._notify(5) await asyncio.sleep(5) @@ -385,9 +432,13 @@ class Game: await self._notify(6) await asyncio.sleep(12) # 2 minutes FixMe to 120 later - await self.village_channel.send(embed=discord.Embed(title="**Two minutes of night remain...**")) + await self.village_channel.send( + embed=discord.Embed(title="**Two minutes of night remain...**") + ) await asyncio.sleep(9) # 1.5 minutes FixMe to 90 later - await self.village_channel.send(embed=discord.Embed(title="**Thirty seconds until sunrise...**")) + await self.village_channel.send( + embed=discord.Embed(title="**Thirty seconds until sunrise...**") + ) await asyncio.sleep(3) # .5 minutes FixMe to 3 Later await self._at_night_end() @@ -413,10 +464,12 @@ class Game: role_order = [role for role in self.roles if role.action_list[event][1] == i] for role in role_order: tasks.append(asyncio.ensure_future(role.on_event(event, data), loop=self.loop)) - # VoteGroup priorities + # VoteGroup priorities vote_order = [vg for vg in self.vote_groups.values() if vg.action_list[event][1] == i] for vote_group in vote_order: - tasks.append(asyncio.ensure_future(vote_group.on_event(event, data), loop=self.loop)) + tasks.append( + asyncio.ensure_future(vote_group.on_event(event, data), loop=self.loop) + ) if tasks: await asyncio.gather(*tasks) # Run same-priority task simultaneously @@ -432,13 +485,17 @@ class Game: else: status = "*[Dead]*-" if with_roles or not player.alive: - embed.add_field(name="ID# **{}**".format(i), - value="{}{}-{}".format(status, player.member.display_name, str(player.role)), - inline=True) + embed.add_field( + name="ID# **{}**".format(i), + value="{}{}-{}".format(status, player.member.display_name, str(player.role)), + inline=True, + ) else: - embed.add_field(name="ID# **{}**".format(i), - value="{}{}".format(status, player.member.display_name), - inline=True) + embed.add_field( + name="ID# **{}**".format(i), + value="{}{}".format(status, player.member.display_name), + inline=True, + ) return await channel.send(embed=embed) @@ -479,10 +536,15 @@ class Game: await member.add_roles(*[self.game_role]) except discord.Forbidden: await channel.send( - "Unable to add role **{}**\nBot is missing `manage_roles` permissions".format(self.game_role.name)) + "Unable to add role **{}**\nBot is missing `manage_roles` permissions".format( + self.game_role.name + ) + ) - await channel.send("{} has been added to the game, " - "total players is **{}**".format(member.mention, len(self.players))) + await channel.send( + "{} has been added to the game, " + "total players is **{}**".format(member.mention, len(self.players)) + ) async def quit(self, member: discord.Member, channel: discord.TextChannel = None): """ @@ -499,7 +561,11 @@ class Game: else: self.players = [player for player in self.players if player.member != member] await member.remove_roles(*[self.game_role]) - await channel.send("{} chickened out, player count is now **{}**".format(member.mention, len(self.players))) + await channel.send( + "{} chickened out, player count is now **{}**".format( + member.mention, len(self.players) + ) + ) async def choose(self, ctx, data): """ @@ -598,14 +664,20 @@ class Game: required_votes = len([player for player in self.players if player.alive]) // 7 + 2 if self.vote_totals[target_id] < required_votes: - await self.village_channel.send("" - "{} has voted to put {} to trial. " - "{} more votes needed".format(author.mention, - target.member.mention, - required_votes - self.vote_totals[target_id])) + await self.village_channel.send( + "" + "{} has voted to put {} to trial. " + "{} more votes needed".format( + author.mention, + target.member.mention, + required_votes - self.vote_totals[target_id], + ) + ) else: self.vote_totals[target_id] = 0 - self.day_vote = {k: v for k, v in self.day_vote.items() if v != target_id} # Remove votes for this id + self.day_vote = { + k: v for k, v in self.day_vote.items() if v != target_id + } # Remove votes for this id await self._at_voted(target) async def eval_results(self, target, source=None, method=None): @@ -613,9 +685,9 @@ class Game: out = "**{ID}** - " + method return out.format(ID=target.id, target=target.member.display_name) else: - return "**{ID}** - {target} the {role} was found dead".format(ID=target.id, - target=target.member.display_name, - role=await target.role.get_role()) + return "**{ID}** - {target} the {role} was found dead".format( + ID=target.id, target=target.member.display_name, role=await target.role.get_role() + ) async def _quit(self, player): """ @@ -748,7 +820,9 @@ class Game: alive_players = [player for player in self.players if player.alive] if len(alive_players) <= 0: - await self.village_channel.send(embed=discord.Embed(title="**Everyone is dead! Game Over!**")) + await self.village_channel.send( + embed=discord.Embed(title="**Everyone is dead! Game Over!**") + ) self.game_over = True elif len(alive_players) == 1: self.game_over = True @@ -780,17 +854,19 @@ class Game: async def _announce_winners(self, winnerlist): await self.village_channel.send(self.game_role.mention) - embed = discord.Embed(title='Game Over', description='The Following Players have won!') + embed = discord.Embed(title="Game Over", description="The Following Players have won!") for player in winnerlist: embed.add_field(name=player.member.display_name, value=str(player.role), inline=True) - embed.set_thumbnail(url='https://emojipedia-us.s3.amazonaws.com/thumbs/160/twitter/134/trophy_1f3c6.png') + embed.set_thumbnail( + url="https://emojipedia-us.s3.amazonaws.com/thumbs/160/twitter/134/trophy_1f3c6.png" + ) await self.village_channel.send(embed=embed) await self.generate_targets(self.village_channel, True) async def _end_game(self): # Remove game_role access for potential archiving for now - reason = '(BOT) End of WW game' + reason = "(BOT) End of WW game" for obj in self.to_delete: print(obj) await obj.delete(reason=reason) @@ -798,8 +874,12 @@ class Game: try: await self.village_channel.edit(reason=reason, name="Werewolf") for target, overwrites in self.save_perms[self.village_channel]: - await self.village_channel.set_permissions(target, overwrite=overwrites, reason=reason) - await self.village_channel.set_permissions(self.game_role, overwrite=None, reason=reason) + await self.village_channel.set_permissions( + target, overwrite=overwrites, reason=reason + ) + await self.village_channel.set_permissions( + self.game_role, overwrite=None, reason=reason + ) except (discord.HTTPException, discord.NotFound, discord.errors.NotFound): pass diff --git a/werewolf/role.py b/werewolf/role.py index 3e4124d..af2d38b 100644 --- a/werewolf/role.py +++ b/werewolf/role.py @@ -1,31 +1,31 @@ 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 - - + + 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) @@ -68,7 +68,7 @@ class Role: (self._at_day_end, 0), (self._at_night_start, 0), (self._at_night_end, 0), - (self._at_visit, 0) + (self._at_visit, 0), ] def __repr__(self): diff --git a/werewolf/votegroup.py b/werewolf/votegroup.py index bf07c8c..a754247 100644 --- a/werewolf/votegroup.py +++ b/werewolf/votegroup.py @@ -23,7 +23,7 @@ class VoteGroup: (self._at_day_end, 0), (self._at_night_start, 2), (self._at_night_end, 0), - (self._at_visit, 0) + (self._at_visit, 0), ] async def on_event(self, event, data): From 7e1a6e108ea804fc3a35601d8740cc555f3db6ed Mon Sep 17 00:00:00 2001 From: bobloy Date: Fri, 18 Sep 2020 17:02:37 -0400 Subject: [PATCH 054/121] Logs mainly --- werewolf/builder.py | 3 +++ werewolf/game.py | 3 +++ werewolf/night_powers.py | 4 ++++ werewolf/player.py | 4 ++++ werewolf/role.py | 5 +++++ werewolf/votegroup.py | 5 +++++ werewolf/werewolf.py | 4 ++++ 7 files changed, 28 insertions(+) diff --git a/werewolf/builder.py b/werewolf/builder.py index 1648b45..4632a21 100644 --- a/werewolf/builder.py +++ b/werewolf/builder.py @@ -1,4 +1,5 @@ import bisect +import logging from collections import defaultdict from random import choice @@ -13,6 +14,8 @@ from .roles.vanillawerewolf import VanillaWerewolf from .roles.villager import Villager from redbot.core.utils.menus import menu, prev_page, next_page, close_menu +log = logging.getLogger("red.fox_v3.werewolf.builder") + # All roles in this list for iterating ROLE_LIST = sorted([Villager, Seer, VanillaWerewolf], key=lambda x: x.alignment) diff --git a/werewolf/game.py b/werewolf/game.py index 95807a1..7c6a5c8 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -1,4 +1,5 @@ import asyncio +import logging import random from typing import List, Any, Dict, Set, Union @@ -10,6 +11,8 @@ from .player import Player from .role import Role from .votegroup import VoteGroup +log = logging.getLogger("red.fox_v3.werewolf.game") + class Game: """ diff --git a/werewolf/night_powers.py b/werewolf/night_powers.py index b50929b..a39ad26 100644 --- a/werewolf/night_powers.py +++ b/werewolf/night_powers.py @@ -1,5 +1,9 @@ +import logging + from .role import Role +log = logging.getLogger("red.fox_v3.werewolf.night_powers") + def night_immune(role: Role): role.player.alive = True diff --git a/werewolf/player.py b/werewolf/player.py index c84d87f..8ee437e 100644 --- a/werewolf/player.py +++ b/werewolf/player.py @@ -1,5 +1,9 @@ +import logging + import discord +log = logging.getLogger("red.fox_v3.werewolf.player") + class Player: """ diff --git a/werewolf/role.py b/werewolf/role.py index af2d38b..8c4d989 100644 --- a/werewolf/role.py +++ b/werewolf/role.py @@ -1,3 +1,8 @@ +import logging + +log = logging.getLogger("red.fox_v3.werewolf.role") + + class Role: """ Base Role class for werewolf game diff --git a/werewolf/votegroup.py b/werewolf/votegroup.py index a754247..d60f451 100644 --- a/werewolf/votegroup.py +++ b/werewolf/votegroup.py @@ -1,3 +1,8 @@ +import logging + +log = logging.getLogger("red.fox_v3.werewolf.votegroup") + + class VoteGroup: """ Base VoteGroup class for werewolf game diff --git a/werewolf/werewolf.py b/werewolf/werewolf.py index 1f8fc3f..c90982f 100644 --- a/werewolf/werewolf.py +++ b/werewolf/werewolf.py @@ -1,3 +1,5 @@ +import logging + import discord from redbot.core import Config, checks, commands from redbot.core.bot import Red @@ -13,6 +15,8 @@ from .builder import ( ) from .game import Game +log = logging.getLogger("red.fox_v3.werewolf") + class Werewolf(Cog): """ From fe1f11b2ebc1b27c9461d9a632aa69c520744acf Mon Sep 17 00:00:00 2001 From: bobloy Date: Sat, 19 Sep 2020 20:21:28 -0400 Subject: [PATCH 055/121] Maybe dispatch? WIP --- werewolf/game.py | 46 +++++++++++++++++++++------------ werewolf/role.py | 2 +- werewolf/roles/seer.py | 14 +++++++--- werewolf/roles/shifter.py | 2 +- werewolf/votegroups/wolfvote.py | 17 ++++++++---- werewolf/werewolf.py | 18 ++++++------- 6 files changed, 63 insertions(+), 36 deletions(-) diff --git a/werewolf/game.py b/werewolf/game.py index 7c6a5c8..ba63211 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -5,6 +5,7 @@ from typing import List, Any, Dict, Set, Union import discord from redbot.core import commands +from redbot.core.bot import Red from .builder import parse_code from .player import Player @@ -34,10 +35,15 @@ class Game: "**Morning has arrived on day {}..**", ] + night_messages = [ + "**Dawn falls on day {}..****" + ] + day_vote_count = 3 def __init__( self, + bot: Red, guild: discord.Guild, role: discord.Role = None, category: discord.CategoryChannel = None, @@ -45,6 +51,7 @@ class Game: log_channel: discord.TextChannel = None, game_code=None, ): + self.bot = bot self.guild = guild self.game_code = game_code @@ -340,6 +347,9 @@ class Game: await self.village_channel.send( "**{} will be put to trial and has 30 seconds to defend themselves**".format( target.mention + ), + allowed_mentions=discord.AllowedMentions( + everyone=False, users=[target] ) ) @@ -347,11 +357,14 @@ class Game: await self.speech_perms(self.village_channel, target.member, undo=True) # No one can talk - message = await self.village_channel.send( + message: discord.Message = await self.village_channel.send( "Everyone will now vote whether to lynch {}\n" "👍 to save, 👎 to lynch\n" "*Majority rules, no-lynch on ties, " - "vote both or neither to abstain, 15 seconds to vote*".format(target.mention) + "vote both or neither to abstain, 15 seconds to vote*".format(target.mention), + allowed_mentions=discord.AllowedMentions( + everyone=False, users=[target] + ) ) await message.add_reaction("👍") @@ -442,7 +455,7 @@ class Game: await self.village_channel.send( embed=discord.Embed(title="**Thirty seconds until sunrise...**") ) - await asyncio.sleep(3) # .5 minutes FixMe to 3 Later + await asyncio.sleep(3) # .5 minutes FixMe to 30 Later await self._at_night_end() @@ -462,19 +475,20 @@ class Game: async def _notify(self, event, data=None): for i in range(1, 7): # action guide 1-6 (0 is no action) - tasks = [] - # Role priorities - role_order = [role for role in self.roles if role.action_list[event][1] == i] - for role in role_order: - tasks.append(asyncio.ensure_future(role.on_event(event, data), loop=self.loop)) - # VoteGroup priorities - vote_order = [vg for vg in self.vote_groups.values() if vg.action_list[event][1] == i] - for vote_group in vote_order: - tasks.append( - asyncio.ensure_future(vote_group.on_event(event, data), loop=self.loop) - ) - if tasks: - await asyncio.gather(*tasks) + self.bot.dispatch(f"red.fox.werewolf.{event}", data=data, priority=i) + # tasks = [] + # # Role priorities + # role_order = [role for role in self.roles if role.action_list[event][1] == i] + # for role in role_order: + # tasks.append(asyncio.ensure_future(role.on_event(event, data), loop=self.loop)) + # # VoteGroup priorities + # vote_order = [vg for vg in self.vote_groups.values() if vg.action_list[event][1] == i] + # for vote_group in vote_order: + # tasks.append( + # asyncio.ensure_future(vote_group.on_event(event, data), loop=self.loop) + # ) + # if tasks: + # await asyncio.gather(*tasks) # Run same-priority task simultaneously ############END Notify structure############ diff --git a/werewolf/role.py b/werewolf/role.py index 8c4d989..2a08f71 100644 --- a/werewolf/role.py +++ b/werewolf/role.py @@ -38,7 +38,7 @@ class Role: 3. Protection / Preempt actions (bodyguard/framer) 4. Non-disruptive actions (seer/silencer) 5. Disruptive actions (Killing) - 6. Role altering actions (Cult / Mason) + 6. Role altering actions (Cult / Mason / Shifter) """ rand_choice = False # Determines if it can be picked as a random role (False for unusually disruptive roles) diff --git a/werewolf/roles/seer.py b/werewolf/roles/seer.py index 35c8271..8d0fc1f 100644 --- a/werewolf/roles/seer.py +++ b/werewolf/roles/seer.py @@ -14,8 +14,10 @@ class Seer(Role): "Lynch players during the day with `[p]ww vote `\n" "Check for werewolves at night with `[p]ww choose `" ) - 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) @@ -33,7 +35,7 @@ class Seer(Role): (self._at_day_end, 0), (self._at_night_start, 2), (self._at_night_end, 4), - (self._at_visit, 0) + (self._at_visit, 0), ] async def see_alignment(self, source=None): @@ -87,4 +89,8 @@ class Seer(Role): await super().choose(ctx, data) self.see_target, target = await pick_target(self, ctx, data) - await ctx.send("**You will attempt to see the role of {} tonight...**".format(target.member.display_name)) + await ctx.send( + "**You will attempt to see the role of {} tonight...**".format( + target.member.display_name + ) + ) diff --git a/werewolf/roles/shifter.py b/werewolf/roles/shifter.py index 4c550dc..8d4b4f5 100644 --- a/werewolf/roles/shifter.py +++ b/werewolf/roles/shifter.py @@ -37,7 +37,7 @@ 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) + 6. Role altering actions (Cult / Mason / Shifter) """ rand_choice = False # Determines if it can be picked as a random role (False for unusually disruptive roles) diff --git a/werewolf/votegroups/wolfvote.py b/werewolf/votegroups/wolfvote.py index 9c068d5..fb98b20 100644 --- a/werewolf/votegroups/wolfvote.py +++ b/werewolf/votegroups/wolfvote.py @@ -13,7 +13,8 @@ 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) @@ -34,7 +35,7 @@ class WolfVote(VoteGroup): (self._at_day_end, 0), (self._at_night_start, 2), (self._at_night_end, 5), # Kill priority - (self._at_visit, 0) + (self._at_visit, 0), ] # async def on_event(self, event, data): @@ -75,7 +76,9 @@ class WolfVote(VoteGroup): await self.channel.send(mention_list) self.killer = random.choice(self.players) - await self.channel.send("{} has been selected as tonight's killer".format(self.killer.member.display_name)) + await self.channel.send( + "{} has been selected as tonight's killer".format(self.killer.member.display_name) + ) async def _at_night_end(self, data=None): if self.channel is None: @@ -90,7 +93,9 @@ class WolfVote(VoteGroup): 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...**") @@ -117,4 +122,6 @@ class WolfVote(VoteGroup): self.vote_results[author.id] = target_id - await self.channel.send("{} has voted to kill {}".format(author.mention, target.member.display_name)) + await self.channel.send( + "{} has voted to kill {}".format(author.mention, target.member.display_name) + ) diff --git a/werewolf/werewolf.py b/werewolf/werewolf.py index c90982f..0c8374c 100644 --- a/werewolf/werewolf.py +++ b/werewolf/werewolf.py @@ -97,7 +97,7 @@ class Werewolf(Cog): @wwset.command(name="role") async def wwset_role(self, ctx: commands.Context, role: discord.Role = None): """ - Assign the game role + Set the game role This role should not be manually assigned """ if role is None: @@ -222,7 +222,7 @@ class Werewolf(Cog): await ctx.send("No game running, cannot start") if not await game.setup(ctx): - pass # Do something? + pass # ToDo something? @commands.guild_only() @ww.command(name="stop") @@ -230,10 +230,10 @@ class Werewolf(Cog): """ Stops the current game """ - if ctx.guild is None: - # Private message, can't get guild - await ctx.send("Cannot start game from PM!") - return + # if ctx.guild is None: + # # Private message, can't get guild + # await ctx.send("Cannot stop game from PM!") + # return if ctx.guild.id not in self.games or self.games[ctx.guild.id].game_over: await ctx.send("No game to stop") return @@ -354,11 +354,11 @@ class Werewolf(Cog): await ctx.send("Role ID not found") async def _get_game(self, ctx: commands.Context, game_code=None): - guild: discord.Guild = ctx.guild + guild: discord.Guild = getattr(ctx, "guild", None) if guild is None: # Private message, can't get guild - await ctx.send("Cannot start game from PM!") + await ctx.send("Cannot start game from DM!") return None if guild.id not in self.games or self.games[guild.id].game_over: await ctx.send("Starting a new game...") @@ -368,7 +368,7 @@ class Werewolf(Cog): await ctx.send("Cannot start a new game") return None - self.games[guild.id] = Game(guild, role, category, channel, log_channel, game_code) + self.games[guild.id] = Game(self.bot, guild, role, category, channel, log_channel, game_code) return self.games[guild.id] From a046102549da655aff84aee909c73c135daea713 Mon Sep 17 00:00:00 2001 From: bobloy Date: Sat, 19 Sep 2020 20:24:41 -0400 Subject: [PATCH 056/121] Hotfix maybe_send_embed in lseen --- lseen/lseen.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lseen/lseen.py b/lseen/lseen.py index a451f57..3348b65 100644 --- a/lseen/lseen.py +++ b/lseen/lseen.py @@ -75,9 +75,7 @@ class LastSeen(Cog): else: last_seen = await self.config.member(member).seen() if last_seen is None: - await ctx.maybe_send_embed( - embed=discord.Embed(description="I've never seen this user") - ) + await ctx.maybe_send_embed("I've never seen this user") return last_seen = self.get_date_time(last_seen) From 28bf2a73e15625564dbe2d28b0a2c19879f54ede Mon Sep 17 00:00:00 2001 From: bobloy Date: Mon, 21 Sep 2020 11:17:59 -0400 Subject: [PATCH 057/121] Still going on events --- chatter/chat.py | 2 +- werewolf/game.py | 52 +++++++++++++++++++++++++++++------------------- 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/chatter/chat.py b/chatter/chat.py index ad8e37b..ef75bb8 100644 --- a/chatter/chat.py +++ b/chatter/chat.py @@ -434,7 +434,7 @@ class Chatter(Cog): else: await ctx.maybe_send_embed("Error occurred :(") - @commands.Cog.listener() + @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 diff --git a/werewolf/game.py b/werewolf/game.py index ba63211..c3aa813 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -1,6 +1,7 @@ import asyncio import logging import random +from collections import deque from typing import List, Any, Dict, Set, Union import discord @@ -85,6 +86,8 @@ class Game: self.loop = asyncio.get_event_loop() + self.action_queue = deque() + # def __del__(self): # """ # Cleanup channels as necessary @@ -275,8 +278,14 @@ class Game: and repeat with _at_day_start() again """ - await self._at_day_start() - # Once cycle ends, this will trigger end_game + + self.action_queue.append(self._at_day_start()) + + while self.action_queue: + await self.action_queue.popleft() + # + # await self._at_day_start() + # # Once cycle ends, this will trigger end_game await self._end_game() # Handle open channels async def _at_game_start(self): # ID 0 @@ -331,7 +340,7 @@ class Game: if check(): return - await self._at_day_end() + self.action_queue.append(self._at_day_end()) async def _at_voted(self, target): # ID 2 if self.game_over: @@ -405,7 +414,7 @@ class Game: self.ongoing_vote = False if not self.can_vote: - await self._at_day_end() + self.action_queue.append(self._at_day_end()) else: await self.normal_perms(self.village_channel) # No point if about to be night @@ -440,7 +449,7 @@ class Game: await self._notify(5) await asyncio.sleep(5) - await self._at_night_start() + self.action_queue.append(self._at_night_start()) async def _at_night_start(self): # ID 6 if self.game_over: @@ -457,7 +466,7 @@ class Game: ) await asyncio.sleep(3) # .5 minutes FixMe to 30 Later - await self._at_night_end() + self.action_queue.append(self._at_night_end()) async def _at_night_end(self): # ID 7 if self.game_over: @@ -465,7 +474,7 @@ class Game: await self._notify(7) await asyncio.sleep(10) - await self._at_day_start() + self.action_queue.append(self._at_day_start()) async def _at_visit(self, target, source): # ID 8 if self.game_over: @@ -475,20 +484,21 @@ class Game: async def _notify(self, event, data=None): for i in range(1, 7): # action guide 1-6 (0 is no action) - self.bot.dispatch(f"red.fox.werewolf.{event}", data=data, priority=i) - # tasks = [] - # # Role priorities - # role_order = [role for role in self.roles if role.action_list[event][1] == i] - # for role in role_order: - # tasks.append(asyncio.ensure_future(role.on_event(event, data), loop=self.loop)) - # # VoteGroup priorities - # vote_order = [vg for vg in self.vote_groups.values() if vg.action_list[event][1] == i] - # for vote_group in vote_order: - # tasks.append( - # asyncio.ensure_future(vote_group.on_event(event, data), loop=self.loop) - # ) - # if tasks: - # await asyncio.gather(*tasks) + # self.bot.dispatch(f"red.fox.werewolf.{event}", data=data, priority=i) + # self.bot.extra_events + tasks = [] + # Role priorities + role_order = [role for role in self.roles if role.action_list[event][1] == i] + for role in role_order: + tasks.append(asyncio.ensure_future(role.on_event(event, data), loop=self.loop)) + # VoteGroup priorities + vote_order = [vg for vg in self.vote_groups.values() if vg.action_list[event][1] == i] + for vote_group in vote_order: + tasks.append( + asyncio.ensure_future(vote_group.on_event(event, data), loop=self.loop) + ) + if tasks: + await asyncio.gather(*tasks) # Run same-priority task simultaneously ############END Notify structure############ From f69e8fdb1a039c1b5f8f3e376d669188d4d22d73 Mon Sep 17 00:00:00 2001 From: bobloy Date: Mon, 21 Sep 2020 11:35:35 -0400 Subject: [PATCH 058/121] Handle track errors gracefully --- audiotrivia/audiosession.py | 21 +++++++++++++++------ audiotrivia/audiotrivia.py | 8 ++++++-- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/audiotrivia/audiosession.py b/audiotrivia/audiosession.py index 780d4b9..4207fab 100644 --- a/audiotrivia/audiosession.py +++ b/audiotrivia/audiosession.py @@ -1,8 +1,12 @@ """Module to manage audio trivia sessions.""" import asyncio +import logging import lavalink from redbot.cogs.trivia import TriviaSession +from redbot.core.utils.chat_formatting import bold + +log = logging.getLogger("red.fox_v3.audiotrivia.audiosession") class AudioSession(TriviaSession): @@ -23,9 +27,9 @@ 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"] @@ -36,8 +40,8 @@ class AudioSession(TriviaSession): self.count += 1 await self.player.stop() - msg = "**Question number {}!**\n\nName this audio!".format(self.count) - await self.ctx.send(msg) + msg = bold(f"Question number {self.count}!") + "\n\nName this audio!" + await self.ctx.maybe_send_embed(msg) # print("Audio question: {}".format(question)) # await self.ctx.invoke(self.audio.play(ctx=self.ctx, query=question)) @@ -45,7 +49,12 @@ class AudioSession(TriviaSession): # await self.ctx.invoke(self.player.play, query=question) query = question.strip("<>") - tracks = await self.player.get_tracks(query) + load_result = await self.player.load_tracks(query) + if load_result.has_error: + await self.ctx.maybe_send_embed(f"Track has error, skipping. See logs for details") + log.info(f"Track has error: {load_result.exception_message}") + continue # Skip tracks with error + tracks = load_result.tracks seconds = tracks[0].length / 1000 if self.settings["repeat"] and seconds < delay: diff --git a/audiotrivia/audiotrivia.py b/audiotrivia/audiotrivia.py index c0c88fd..ec7b5ea 100644 --- a/audiotrivia/audiotrivia.py +++ b/audiotrivia/audiotrivia.py @@ -1,4 +1,5 @@ import datetime +import logging import pathlib from typing import List @@ -15,7 +16,7 @@ from redbot.core.utils.chat_formatting import box from .audiosession import AudioSession -# from redbot.cogs.audio.utils import userlimit +log = logging.getLogger("red.fox_v3.audiotrivia") class AudioTrivia(Trivia): @@ -166,7 +167,10 @@ class AudioTrivia(Trivia): # 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, + 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) From 1723dc381d4a49fc5f2c14a666499967c8bd7f33 Mon Sep 17 00:00:00 2001 From: bobloy Date: Mon, 21 Sep 2020 13:42:43 -0400 Subject: [PATCH 059/121] More listener --- werewolf/game.py | 183 ++++++++++++++++++++++++++++++++++++++--------- werewolf/role.py | 84 +++++++++++----------- 2 files changed, 192 insertions(+), 75 deletions(-) diff --git a/werewolf/game.py b/werewolf/game.py index c3aa813..e9d3763 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -1,4 +1,5 @@ import asyncio +import inspect import logging import random from collections import deque @@ -36,9 +37,7 @@ class Game: "**Morning has arrived on day {}..**", ] - night_messages = [ - "**Dawn falls on day {}..****" - ] + night_messages = ["**Dawn falls on day {}..****"] day_vote_count = 3 @@ -87,6 +86,7 @@ class Game: self.loop = asyncio.get_event_loop() self.action_queue = deque() + self.listeners = {} # def __del__(self): # """ @@ -265,9 +265,7 @@ class Game: ############START Notify structure############ async def _cycle(self): """ - Each event calls the next event - - + Each event enqueues the next event _at_day_start() _at_voted() @@ -296,7 +294,7 @@ class Game: embed=discord.Embed(title="Game is starting, please wait for setup to complete") ) - await self._notify(0) + await self._notify("at_game_start") async def _at_day_start(self): # ID 1 if self.game_over: @@ -318,7 +316,7 @@ class Game: await self.generate_targets(self.village_channel) await self.day_perms(self.village_channel) - await self._notify(1) + await self._notify("at_day_start") await self._check_game_over() if self.game_over: @@ -346,7 +344,7 @@ class Game: if self.game_over: return data = {"player": target} - await self._notify(2, data) + await self._notify("at_voted", player=target) self.ongoing_vote = True @@ -357,9 +355,7 @@ class Game: "**{} will be put to trial and has 30 seconds to defend themselves**".format( target.mention ), - allowed_mentions=discord.AllowedMentions( - everyone=False, users=[target] - ) + allowed_mentions=discord.AllowedMentions(everyone=False, users=[target]), ) await asyncio.sleep(30) @@ -371,9 +367,7 @@ class Game: "👍 to save, 👎 to lynch\n" "*Majority rules, no-lynch on ties, " "vote both or neither to abstain, 15 seconds to vote*".format(target.mention), - allowed_mentions=discord.AllowedMentions( - everyone=False, users=[target] - ) + allowed_mentions=discord.AllowedMentions(everyone=False, users=[target]), ) await message.add_reaction("👍") @@ -422,13 +416,13 @@ class Game: if self.game_over: return data = {"player": target} - await self._notify(3, data) + await self._notify("at_kill", player=target) async def _at_hang(self, target): # ID 4 if self.game_over: return data = {"player": target} - await self._notify(4, data) + await self._notify("at_hang", player=target) async def _at_day_end(self): # ID 5 await self._check_game_over() @@ -447,14 +441,14 @@ class Game: embed=discord.Embed(title="**The sun sets on the village...**") ) - await self._notify(5) + await self._notify("at_day_end") await asyncio.sleep(5) self.action_queue.append(self._at_night_start()) async def _at_night_start(self): # ID 6 if self.game_over: return - await self._notify(6) + await self._notify("at_night_start") await asyncio.sleep(12) # 2 minutes FixMe to 120 later await self.village_channel.send( @@ -471,7 +465,7 @@ class Game: async def _at_night_end(self): # ID 7 if self.game_over: return - await self._notify(7) + await self._notify("at_night_end") await asyncio.sleep(10) self.action_queue.append(self._at_day_start()) @@ -480,25 +474,30 @@ class Game: if self.game_over: return data = {"target": target, "source": source} - await self._notify(8, data) + await self._notify("at_visit", target=target, source=source) - async def _notify(self, event, data=None): + async def _notify(self, event, **kwargs): for i in range(1, 7): # action guide 1-6 (0 is no action) + tasks = [] + for event in self.listeners.get(event, []): + tasks.append(asyncio.ensure_future(event(**kwargs), loop=self.loop)) + await asyncio.gather(*tasks) + # self.bot.dispatch(f"red.fox.werewolf.{event}", data=data, priority=i) # self.bot.extra_events - tasks = [] - # Role priorities - role_order = [role for role in self.roles if role.action_list[event][1] == i] - for role in role_order: - tasks.append(asyncio.ensure_future(role.on_event(event, data), loop=self.loop)) - # VoteGroup priorities - vote_order = [vg for vg in self.vote_groups.values() if vg.action_list[event][1] == i] - for vote_group in vote_order: - tasks.append( - asyncio.ensure_future(vote_group.on_event(event, data), loop=self.loop) - ) - if tasks: - await asyncio.gather(*tasks) + # tasks = [] + # # Role priorities + # role_order = [role for role in self.roles if role.action_list[event][1] == i] + # for role in role_order: + # tasks.append(asyncio.ensure_future(role.on_event(event, data), loop=self.loop)) + # # VoteGroup priorities + # vote_order = [vg for vg in self.vote_groups.values() if vg.action_list[event][1] == i] + # for vote_group in vote_order: + # tasks.append( + # asyncio.ensure_future(vote_group.on_event(event, data), loop=self.loop) + # ) + # if tasks: + # await asyncio.gather(*tasks) # Run same-priority task simultaneously ############END Notify structure############ @@ -911,3 +910,117 @@ class Game: pass # Optional dynamic channels/categories + + @classmethod + def wolflistener(cls, name=None): + """A decorator that marks a function as a listener. + + This is the cog equivalent of :meth:`.Bot.listen`. + + Parameters + ------------ + name: :class:`str` + The name of the event being listened to. If not provided, it + defaults to the function's name. + + 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( + "Cog.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.__werewolf_listener__ = True + to_assign = name or actual.__name__ + try: + actual.__cog_listener_names__.append(to_assign) + except AttributeError: + actual.__cog_listener_names__ = [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 + + def wolflisten(self, name=None): + """A decorator that registers another function as an external + event listener. Basically this allows you to listen to multiple + events from different places e.g. such as :func:`.on_ready` + + The functions being listened to must be a :ref:`coroutine `. + + Example + -------- + + .. code-block:: python3 + + @bot.listen() + async def on_message(message): + print('one') + + # in some other file... + + @bot.listen('on_message') + async def my_message(message): + print('two') + + Would print one and two in an unspecified order. + + Raises + ------- + TypeError + The function being listened to is not a coroutine. + """ + + def decorator(func): + self.add_wolflistener(func, name) + return func + + return decorator + + def add_wolflistener(self, func, name=None): + """The non decorator alternative to :meth:`.listen`. + + Parameters + ----------- + func: :ref:`coroutine ` + The function to call. + name: Optional[:class:`str`] + The name of the event to listen for. Defaults to ``func.__name__``. + + Example + -------- + + .. code-block:: python3 + + async def on_ready(): pass + async def my_message(message): pass + + bot.add_listener(on_ready) + bot.add_listener(my_message, 'on_message') + + """ + name = func.__name__ if name is None else name + + if not asyncio.iscoroutinefunction(func): + raise TypeError('Listeners must be coroutines') + + if name in self.listeners: + self.listeners[name].append(func) + else: + self.listeners[name] = [func] diff --git a/werewolf/role.py b/werewolf/role.py index 2a08f71..bdcec71 100644 --- a/werewolf/role.py +++ b/werewolf/role.py @@ -1,5 +1,8 @@ +import inspect import logging +from werewolf import Werewolf + log = logging.getLogger("red.fox_v3.werewolf.role") @@ -64,27 +67,27 @@ class Role: self.blocked = False self.properties = {} # Extra data for other roles (i.e. arsonist) - 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), - ] + # 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 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 on_event(self, event, data): + # """ + # See Game class for event guide + # """ + # + # await self.action_list[event][0](data) async def assign_player(self, player): """ @@ -124,35 +127,36 @@ class Role: """ return "Default" + @wolflistener("at_game_start") async def _at_game_start(self, data=None): if self.channel_id: await self.game.register_channel(self.channel_id, self) 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 _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): """ From ec5d713fa01285d2f315ce9ebbcb128e4dfe78d4 Mon Sep 17 00:00:00 2001 From: bobloy Date: Mon, 21 Sep 2020 13:49:33 -0400 Subject: [PATCH 060/121] Correct listeners --- hangman/__init__.py | 1 - hangman/hangman.py | 48 ++++++++++++++++++++++----------------------- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/hangman/__init__.py b/hangman/__init__.py index dbc62e7..35012c4 100644 --- a/hangman/__init__.py +++ b/hangman/__init__.py @@ -6,4 +6,3 @@ 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") diff --git a/hangman/hangman.py b/hangman/hangman.py index c7d005d..2b6ab07 100644 --- a/hangman/hangman.py +++ b/hangman/hangman.py @@ -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""", ) @@ -255,7 +255,7 @@ class Hangman(Cog): 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 @@ -286,10 +286,10 @@ class Hangman(Cog): await self._reprintgame(message) - @commands.Cog.listener() + @commands.Cog.listener("on_reaction_add") 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 From 8a3f45bdc1231b606f7dfecd096dbd2c670eda13 Mon Sep 17 00:00:00 2001 From: bobloy Date: Mon, 21 Sep 2020 17:11:55 -0400 Subject: [PATCH 061/121] Listener structure major change Still need priority --- werewolf/game.py | 113 +++++++----------------------- werewolf/listener.py | 91 ++++++++++++++++++++++++ werewolf/role.py | 7 +- werewolf/roles/seer.py | 3 + werewolf/roles/shifter.py | 55 ++++++++------- werewolf/roles/vanillawerewolf.py | 24 ++++--- werewolf/votegroup.py | 67 +++++++++--------- 7 files changed, 200 insertions(+), 160 deletions(-) create mode 100644 werewolf/listener.py diff --git a/werewolf/game.py b/werewolf/game.py index e9d3763..df5d263 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -16,6 +16,7 @@ from .votegroup import VoteGroup log = logging.getLogger("red.fox_v3.werewolf.game") +HALF_DAY_LENGTH = 24 # FixMe: to 120 later for 4 minute days class Game: """ @@ -262,7 +263,7 @@ class Game: await asyncio.sleep(1) await asyncio.ensure_future(self._cycle()) # Start the loop - ############START Notify structure############ + # ###########START Notify structure############ async def _cycle(self): """ Each event enqueues the next event @@ -323,13 +324,13 @@ class Game: return self.can_vote = True - await asyncio.sleep(24) # 4 minute days FixMe to 120 later + await asyncio.sleep(HALF_DAY_LENGTH) # 4 minute days FixMe to 120 later if check(): return await self.village_channel.send( embed=discord.Embed(title="**Two minutes of daylight remain...**") ) - await asyncio.sleep(24) # 4 minute days FixMe to 120 later + await asyncio.sleep(HALF_DAY_LENGTH) # 4 minute days FixMe to 120 later # Need a loop here to wait for trial to end (can_vote?) while self.ongoing_vote: @@ -500,7 +501,7 @@ class Game: # await asyncio.gather(*tasks) # Run same-priority task simultaneously - ############END Notify structure############ + # ###########END Notify structure############ async def generate_targets(self, channel, with_roles=False): embed = discord.Embed(title="Remaining Players") @@ -911,89 +912,7 @@ class Game: # Optional dynamic channels/categories - @classmethod - def wolflistener(cls, name=None): - """A decorator that marks a function as a listener. - - This is the cog equivalent of :meth:`.Bot.listen`. - - Parameters - ------------ - name: :class:`str` - The name of the event being listened to. If not provided, it - defaults to the function's name. - - 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( - "Cog.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.__werewolf_listener__ = True - to_assign = name or actual.__name__ - try: - actual.__cog_listener_names__.append(to_assign) - except AttributeError: - actual.__cog_listener_names__ = [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 - - def wolflisten(self, name=None): - """A decorator that registers another function as an external - event listener. Basically this allows you to listen to multiple - events from different places e.g. such as :func:`.on_ready` - - The functions being listened to must be a :ref:`coroutine `. - - Example - -------- - - .. code-block:: python3 - - @bot.listen() - async def on_message(message): - print('one') - - # in some other file... - - @bot.listen('on_message') - async def my_message(message): - print('two') - - Would print one and two in an unspecified order. - - Raises - ------- - TypeError - The function being listened to is not a coroutine. - """ - - def decorator(func): - self.add_wolflistener(func, name) - return func - - return decorator - - def add_wolflistener(self, func, name=None): + def add_listener(self, func, name=None): """The non decorator alternative to :meth:`.listen`. Parameters @@ -1024,3 +943,23 @@ class Game: self.listeners[name].append(func) else: self.listeners[name] = [func] + + def remove_listener(self, func, name=None): + """Removes a listener from the pool of listeners. + + Parameters + ----------- + func + The function that was used as a listener to remove. + name: :class:`str` + The name of the event we want to remove. Defaults to + ``func.__name__``. + """ + + name = func.__name__ if name is None else name + + if name in self.listeners: + try: + self.listeners[name].remove(func) + except ValueError: + pass diff --git a/werewolf/listener.py b/werewolf/listener.py new file mode 100644 index 0000000..9c36400 --- /dev/null +++ b/werewolf/listener.py @@ -0,0 +1,91 @@ +import inspect + + +def wolflistener(name=None): + """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. + + 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__ = True + to_assign = name or actual.__name__ + try: + actual.__wolf_listener_names__.append(to_assign) + except AttributeError: + actual.__wolf_listener_names__ = [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, cls, *args, **kwargs): + name, bases = args + + commands = {} + listeners = {} + need_at_msg = "Listeners must start with at_ (in method {0.__name__}.{1})" + + new_cls = super().__new__(cls, name, bases, **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(mcs, elem)) + listeners[elem] = value + + listeners_as_list = [] + for listener in listeners.values(): + for 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((listener_name, listener.__name__)) + + new_cls.__wolf_listeners__ = listeners_as_list + return new_cls + + +class WolfListener(metaclass=WolfListenerMeta): + def __init__(self, game): + for name, method_name in self.__wolf_listeners__: + game.add_listener(getattr(self, method_name), name) diff --git a/werewolf/role.py b/werewolf/role.py index bdcec71..4ae10ad 100644 --- a/werewolf/role.py +++ b/werewolf/role.py @@ -1,12 +1,12 @@ import inspect import logging -from werewolf import Werewolf +from werewolf.listener import WolfListener, wolflistener log = logging.getLogger("red.fox_v3.werewolf.role") -class Role: +class Role(WolfListener): """ Base Role class for werewolf game @@ -28,7 +28,7 @@ class Role: category = [11, 16] Could be Werewolf Silencer - Action guide as follows (on_event function): + Action priority guide as follows (on_event function): _at_night_start 0. No Action 1. Detain actions (Jailer/Kidnapper) @@ -62,6 +62,7 @@ class Role: 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 diff --git a/werewolf/roles/seer.py b/werewolf/roles/seer.py index 8d0fc1f..603d197 100644 --- a/werewolf/roles/seer.py +++ b/werewolf/roles/seer.py @@ -1,3 +1,4 @@ +from ..listener import wolflistener from ..night_powers import pick_target from ..role import Role @@ -59,6 +60,7 @@ class Seer(Role): """ return "Villager" + @wolflistener("at_night_start") async def _at_night_start(self, data=None): if not self.player.alive: return @@ -66,6 +68,7 @@ class Seer(Role): await self.game.generate_targets(self.player.member) await self.player.send_dm("**Pick a target to see tonight**") + @wolflistener("at_night_end") async def _at_night_end(self, data=None): if self.see_target is None: if self.player.alive: diff --git a/werewolf/roles/shifter.py b/werewolf/roles/shifter.py index 8d4b4f5..85c3ec5 100644 --- a/werewolf/roles/shifter.py +++ b/werewolf/roles/shifter.py @@ -1,3 +1,4 @@ +from ..listener import wolflistener from ..night_powers import pick_target from ..role import Role @@ -5,31 +6,31 @@ 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) @@ -61,17 +62,17 @@ 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): """ @@ -94,14 +95,14 @@ class Shifter(Role): """ return "Shifter" + @wolflistener("at_night_start") 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") 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...") @@ -114,16 +115,22 @@ 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("**You will attempt to see the role of {} tonight...**".format(target.member.display_name)) + await ctx.send( + "**You will attempt to see the role of {} tonight...**".format( + target.member.display_name + ) + ) diff --git a/werewolf/roles/vanillawerewolf.py b/werewolf/roles/vanillawerewolf.py index c8050da..5f7407b 100644 --- a/werewolf/roles/vanillawerewolf.py +++ b/werewolf/roles/vanillawerewolf.py @@ -1,3 +1,4 @@ +from ..listener import wolflistener from ..role import Role from ..votegroups.wolfvote import WolfVote @@ -19,17 +20,17 @@ class VanillaWerewolf(Role): 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) - ] + # 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): """ @@ -52,6 +53,7 @@ class VanillaWerewolf(Role): """ return "Werewolf" + @wolflistener("at_game_start") async def _at_game_start(self, data=None): if self.channel_id: print("Wolf has channel_id: " + self.channel_id) diff --git a/werewolf/votegroup.py b/werewolf/votegroup.py index d60f451..19ebd9e 100644 --- a/werewolf/votegroup.py +++ b/werewolf/votegroup.py @@ -1,9 +1,11 @@ import logging +from werewolf.listener import WolfListener, wolflistener + log = logging.getLogger("red.fox_v3.werewolf.votegroup") -class VoteGroup: +class VoteGroup(WolfListener): """ Base VoteGroup class for werewolf game Handles secret channels and group decisions @@ -13,57 +15,55 @@ class VoteGroup: 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 - 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), - ] - - async def on_event(self, event, data): - """ - See Game class for event guide - """ - - await self.action_list[event][0](data) - + # 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), + # ] + + # async def on_event(self, event, data): + # """ + # See Game class for event guide + # """ + # + # await self.action_list[event][0](data) + + @wolflistener("at_game_start") 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 - + @wolflistener("at_kill") 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 + # Removed, only if they actually die + # @wolflistener("at_hang") + # async def _at_hang(self, data=None): + # if data["player"] in self.players: + # self.players.remove(data["player"]) + @wolflistener("at_night_start") async def _at_night_start(self, data=None): if self.channel is None: return await self.game.generate_targets(self.channel) + @wolflistener("at_night_end") async def _at_night_end(self, data=None): if self.channel is None: return @@ -78,9 +78,6 @@ class VoteGroup: # Do what you voted on pass - async def _at_visit(self, data=None): - pass - async def register_players(self, *players): """ Extend players by passed list From 06af229a62f1a81c57f2c233bd96f3dfea57b3e9 Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 22 Sep 2020 16:23:26 -0400 Subject: [PATCH 062/121] non-relative imports --- werewolf/roles/shifter.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/werewolf/roles/shifter.py b/werewolf/roles/shifter.py index 85c3ec5..6e2a850 100644 --- a/werewolf/roles/shifter.py +++ b/werewolf/roles/shifter.py @@ -1,6 +1,6 @@ -from ..listener import wolflistener -from ..night_powers import pick_target -from ..role import Role +from werewolf.listener import wolflistener +from werewolf.night_powers import pick_target +from werewolf.role import Role class Shifter(Role): From a2eaf555159682478398228a465e5de3b34c86fb Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 22 Sep 2020 16:46:38 -0400 Subject: [PATCH 063/121] Priority update for listeners --- werewolf/game.py | 73 +++++++++++++++---------------- werewolf/listener.py | 33 ++++++++++---- werewolf/night_powers.py | 2 +- werewolf/roles/seer.py | 32 +++++++------- werewolf/roles/vanillawerewolf.py | 7 ++- werewolf/roles/villager.py | 2 +- werewolf/votegroups/__init__.py | 1 + werewolf/votegroups/wolfvote.py | 2 +- werewolf/werewolf.py | 16 ++++--- 9 files changed, 92 insertions(+), 76 deletions(-) create mode 100644 werewolf/votegroups/__init__.py diff --git a/werewolf/game.py b/werewolf/game.py index df5d263..98475ba 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -9,10 +9,10 @@ import discord from redbot.core import commands from redbot.core.bot import Red -from .builder import parse_code -from .player import Player -from .role import Role -from .votegroup import VoteGroup +from werewolf.builder import parse_code +from werewolf.player import Player +from werewolf.role import Role +from werewolf.votegroup import VoteGroup log = logging.getLogger("red.fox_v3.werewolf.game") @@ -480,7 +480,7 @@ class Game: async def _notify(self, event, **kwargs): for i in range(1, 7): # action guide 1-6 (0 is no action) tasks = [] - for event in self.listeners.get(event, []): + for event in self.listeners.get(event, {}).get(i, []): tasks.append(asyncio.ensure_future(event(**kwargs), loop=self.loop)) await asyncio.gather(*tasks) @@ -912,26 +912,19 @@ class Game: # Optional dynamic channels/categories - def add_listener(self, func, name=None): - """The non decorator alternative to :meth:`.listen`. + def add_ww_listener(self, func, priority=0, name=None): + """Adds a listener from the pool of listeners. Parameters ----------- func: :ref:`coroutine ` The function to call. + priority: Optional[:class:`int`] + Priority of the listener. Defaults to 0 (no-action) name: Optional[:class:`str`] The name of the event to listen for. Defaults to ``func.__name__``. - - Example - -------- - - .. code-block:: python3 - - async def on_ready(): pass - async def my_message(message): pass - - bot.add_listener(on_ready) - bot.add_listener(my_message, 'on_message') + do_sort: Optional[:class:`bool`] + Whether or not to sort listeners after. Skip sorting during mass appending """ name = func.__name__ if name is None else name @@ -940,26 +933,32 @@ class Game: raise TypeError('Listeners must be coroutines') if name in self.listeners: - self.listeners[name].append(func) + if priority in self.listeners[name]: + self.listeners[name][priority].append(func) + else: + self.listeners[name][priority] = [func] else: - self.listeners[name] = [func] - - def remove_listener(self, func, name=None): - """Removes a listener from the pool of listeners. + self.listeners[name] = {priority: [func]} - Parameters - ----------- - func - The function that was used as a listener to remove. - name: :class:`str` - The name of the event we want to remove. Defaults to - ``func.__name__``. - """ + # self.listeners[name].sort(reverse=True) - name = func.__name__ if name is None else name - if name in self.listeners: - try: - self.listeners[name].remove(func) - except ValueError: - pass + # def remove_wolf_listener(self, func, name=None): + # """Removes a listener from the pool of listeners. + # + # Parameters + # ----------- + # func + # The function that was used as a listener to remove. + # name: :class:`str` + # The name of the event we want to remove. Defaults to + # ``func.__name__``. + # """ + # + # name = func.__name__ if name is None else name + # + # if name in self.listeners: + # try: + # self.listeners[name].remove(func) + # except ValueError: + # pass diff --git a/werewolf/listener.py b/werewolf/listener.py index 9c36400..e14994a 100644 --- a/werewolf/listener.py +++ b/werewolf/listener.py @@ -1,7 +1,7 @@ import inspect -def wolflistener(name=None): +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`. @@ -11,6 +11,22 @@ def wolflistener(name=None): 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 -------- @@ -32,12 +48,12 @@ def wolflistener(name=None): actual = actual.__func__ if not inspect.iscoroutinefunction(actual): raise TypeError("Listener function must be a coroutine function.") - actual.__wolf_listener__ = True + actual.__wolf_listener__ = priority to_assign = name or actual.__name__ try: - actual.__wolf_listener_names__.append(to_assign) + actual.__wolf_listener_names__.append((priority, to_assign)) except AttributeError: - actual.__wolf_listener_names__ = [to_assign] + 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 @@ -51,7 +67,6 @@ class WolfListenerMeta(type): def __new__(mcs, cls, *args, **kwargs): name, bases = args - commands = {} listeners = {} need_at_msg = "Listeners must start with at_ (in method {0.__name__}.{1})" @@ -76,10 +91,10 @@ class WolfListenerMeta(type): listeners_as_list = [] for listener in listeners.values(): - for listener_name in listener.__wolf_listener_names__: + 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((listener_name, listener.__name__)) + listeners_as_list.append((priority, listener_name, listener.__name__)) new_cls.__wolf_listeners__ = listeners_as_list return new_cls @@ -87,5 +102,5 @@ class WolfListenerMeta(type): class WolfListener(metaclass=WolfListenerMeta): def __init__(self, game): - for name, method_name in self.__wolf_listeners__: - game.add_listener(getattr(self, method_name), name) + for priority, name, method_name in self.__wolf_listeners__: + game.add_ww_listener(getattr(self, method_name), priority, name) diff --git a/werewolf/night_powers.py b/werewolf/night_powers.py index a39ad26..ab82e87 100644 --- a/werewolf/night_powers.py +++ b/werewolf/night_powers.py @@ -1,6 +1,6 @@ import logging -from .role import Role +from werewolf.role import Role log = logging.getLogger("red.fox_v3.werewolf.night_powers") diff --git a/werewolf/roles/seer.py b/werewolf/roles/seer.py index 603d197..a7a9aa8 100644 --- a/werewolf/roles/seer.py +++ b/werewolf/roles/seer.py @@ -1,6 +1,6 @@ -from ..listener import wolflistener -from ..night_powers import pick_target -from ..role import Role +from werewolf.listener import wolflistener +from werewolf.night_powers import pick_target +from werewolf.role import Role class Seer(Role): @@ -27,17 +27,17 @@ 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): """ @@ -60,7 +60,7 @@ class Seer(Role): """ return "Villager" - @wolflistener("at_night_start") + @wolflistener("at_night_start", priority=2) async def _at_night_start(self, data=None): if not self.player.alive: return @@ -68,7 +68,7 @@ class Seer(Role): await self.game.generate_targets(self.player.member) await self.player.send_dm("**Pick a target to see tonight**") - @wolflistener("at_night_end") + @wolflistener("at_night_end", priority=4) async def _at_night_end(self, data=None): if self.see_target is None: if self.player.alive: diff --git a/werewolf/roles/vanillawerewolf.py b/werewolf/roles/vanillawerewolf.py index 5f7407b..e6938eb 100644 --- a/werewolf/roles/vanillawerewolf.py +++ b/werewolf/roles/vanillawerewolf.py @@ -1,7 +1,6 @@ -from ..listener import wolflistener -from ..role import Role - -from ..votegroups.wolfvote import WolfVote +from werewolf.listener import wolflistener +from werewolf.role import Role +from werewolf.votegroups.wolfvote import WolfVote class VanillaWerewolf(Role): diff --git a/werewolf/roles/villager.py b/werewolf/roles/villager.py index bda51d2..040e34d 100644 --- a/werewolf/roles/villager.py +++ b/werewolf/roles/villager.py @@ -1,4 +1,4 @@ -from ..role import Role +from werewolf.role import Role class Villager(Role): diff --git a/werewolf/votegroups/__init__.py b/werewolf/votegroups/__init__.py new file mode 100644 index 0000000..03abc1b --- /dev/null +++ b/werewolf/votegroups/__init__.py @@ -0,0 +1 @@ +from .wolfvote import WolfVote \ No newline at end of file diff --git a/werewolf/votegroups/wolfvote.py b/werewolf/votegroups/wolfvote.py index fb98b20..f990eef 100644 --- a/werewolf/votegroups/wolfvote.py +++ b/werewolf/votegroups/wolfvote.py @@ -1,6 +1,6 @@ import random -from ..votegroup import VoteGroup +from werewolf.votegroup import VoteGroup class WolfVote(VoteGroup): diff --git a/werewolf/werewolf.py b/werewolf/werewolf.py index 0c8374c..abed258 100644 --- a/werewolf/werewolf.py +++ b/werewolf/werewolf.py @@ -6,14 +6,14 @@ from redbot.core.bot import Red from redbot.core.commands import Cog from redbot.core.utils.menus import DEFAULT_CONTROLS, menu -from .builder import ( +from werewolf.builder import ( GameBuilder, role_from_alignment, role_from_category, role_from_id, role_from_name, ) -from .game import Game +from werewolf.game import Game log = logging.getLogger("red.fox_v3.werewolf") @@ -81,8 +81,8 @@ class Werewolf(Cog): """ Lists current guild settings """ - success, role, category, channel, log_channel = await self._get_settings(ctx) - if not success: + valid, role, category, channel, log_channel = await self._get_settings(ctx) + if not valid: await ctx.send("Failed to get settings") return None @@ -362,13 +362,15 @@ class Werewolf(Cog): return None if guild.id not in self.games or self.games[guild.id].game_over: await ctx.send("Starting a new game...") - success, role, category, channel, log_channel = await self._get_settings(ctx) + valid, role, category, channel, log_channel = await self._get_settings(ctx) - if not success: + if not valid: await ctx.send("Cannot start a new game") return None - self.games[guild.id] = Game(self.bot, guild, role, category, channel, log_channel, game_code) + self.games[guild.id] = Game( + self.bot, guild, role, category, channel, log_channel, game_code + ) return self.games[guild.id] From 7109471c35194f284ea8f58a107b6ed89c05a37c Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 22 Sep 2020 17:30:58 -0400 Subject: [PATCH 064/121] WIP listeners, switch to f strings, and overall rewrite --- werewolf/builder.py | 16 +++---- werewolf/game.py | 73 ++++++++++++++++--------------- werewolf/listener.py | 10 ++--- werewolf/player.py | 2 +- werewolf/role.py | 2 +- werewolf/roles/seer.py | 8 ++-- werewolf/roles/shifter.py | 8 ++-- werewolf/roles/vanillawerewolf.py | 8 +++- werewolf/roles/villager.py | 4 ++ werewolf/votegroups/wolfvote.py | 7 ++- werewolf/werewolf.py | 2 +- 11 files changed, 80 insertions(+), 60 deletions(-) diff --git a/werewolf/builder.py b/werewolf/builder.py index 4632a21..ca90eca 100644 --- a/werewolf/builder.py +++ b/werewolf/builder.py @@ -49,7 +49,7 @@ CATEGORY_COUNT = [] def role_embed(idx, role, color): embed = discord.Embed( - title="**{}** - {}".format(idx, str(role.__name__)), + title=f"**{idx}** - {role.__name__}", description=role.game_start_message, color=color, ) @@ -82,7 +82,7 @@ def setup(): if 0 < k <= 6: ROLE_PAGES.append( discord.Embed( - title="RANDOM:Town Role", description="Town {}".format(v), color=0x008000 + title="RANDOM:Town Role", description=f"Town {v}", color=0x008000 ) ) CATEGORY_COUNT.append(k) @@ -95,7 +95,7 @@ def setup(): ROLE_PAGES.append( discord.Embed( title="RANDOM:Werewolf Role", - description="Werewolf {}".format(v), + description=f"Werewolf {v}", color=0xFF0000, ) ) @@ -107,7 +107,7 @@ def setup(): if 20 < k <= 26: ROLE_PAGES.append( discord.Embed( - title="RANDOM:Neutral Role", description="Neutral {}".format(v), color=0xC0C0C0 + title=f"RANDOM:Neutral Role", description="Neutral {v}", color=0xC0C0C0 ) ) CATEGORY_COUNT.append(k) @@ -306,14 +306,14 @@ def say_role_list(code_list, rand_roles): for role in rand_roles: if 0 < role <= 6: - role_dict["Town {}".format(ROLE_CATEGORIES[role])] += 1 + role_dict[f"Town {ROLE_CATEGORIES[role]}"] += 1 if 10 < role <= 16: - role_dict["Werewolf {}".format(ROLE_CATEGORIES[role])] += 1 + role_dict[f"Werewolf {ROLE_CATEGORIES[role]}"] += 1 if 20 < role <= 26: - role_dict["Neutral {}".format(ROLE_CATEGORIES[role])] += 1 + role_dict[f"Neutral {ROLE_CATEGORIES[role]}"] += 1 for k, v in role_dict.items(): - embed.add_field(name=k, value="Count: {}".format(v), inline=True) + embed.add_field(name=k, value=f"Count: {v}", inline=True) return embed diff --git a/werewolf/game.py b/werewolf/game.py index 98475ba..7372923 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -18,6 +18,7 @@ log = logging.getLogger("red.fox_v3.werewolf.game") HALF_DAY_LENGTH = 24 # FixMe: to 120 later for 4 minute days + class Game: """ Base class to run a single game of Werewolf @@ -119,10 +120,10 @@ class Game: if len(self.players) != len(self.roles): await ctx.maybe_send_embed( - "Player count does not match role count, cannot start\n" - "Currently **{} / {}**\n" - "Use `{}ww code` to pick a new game" - "".format(len(self.players), len(self.roles), ctx.prefix) + f"Player count does not match role count, cannot start\n" + f"Currently **{len(self.players)} / {len(self.roles)}**\n" + f"Use `{ctx.prefix}ww code` to pick a game setup\n" + f"Use `{ctx.prefix}buildgame` to generate a new game" ) self.roles = [] return False @@ -147,9 +148,7 @@ class Game: await player.member.add_roles(*[self.game_role]) except discord.Forbidden: await ctx.send( - "Unable to add role **{}**\nBot is missing `manage_roles` permissions".format( - self.game_role.name - ) + f"Unable to add role **{self.game_role.name}**\nBot is missing `manage_roles` permissions" ) return False @@ -353,9 +352,7 @@ class Game: await self.speech_perms(self.village_channel, target.member) # Only target can talk await self.village_channel.send( - "**{} will be put to trial and has 30 seconds to defend themselves**".format( - target.mention - ), + f"**{target.mention} will be put to trial and has 30 seconds to defend themselves**", allowed_mentions=discord.AllowedMentions(everyone=False, users=[target]), ) @@ -364,10 +361,10 @@ class Game: await self.speech_perms(self.village_channel, target.member, undo=True) # No one can talk message: discord.Message = await self.village_channel.send( - "Everyone will now vote whether to lynch {}\n" + f"Everyone will now vote whether to lynch {target.mention}\n" "👍 to save, 👎 to lynch\n" "*Majority rules, no-lynch on ties, " - "vote both or neither to abstain, 15 seconds to vote*".format(target.mention), + "vote both or neither to abstain, 15 seconds to vote*", allowed_mentions=discord.AllowedMentions(everyone=False, users=[target]), ) @@ -385,25 +382,31 @@ class Game: else: embed = discord.Embed(title="Vote Results", color=0x80FF80) - embed.add_field(name="👎", value="**{}**".format(up_votes), inline=True) - embed.add_field(name="👍", value="**{}**".format(down_votes), inline=True) + embed.add_field(name="👎", value=f"**{up_votes}**", inline=True) + embed.add_field(name="👍", value=f"**{down_votes}**", inline=True) await self.village_channel.send(embed=embed) if down_votes > up_votes: - await self.village_channel.send("**Voted to lynch {}!**".format(target.mention)) + await self.village_channel.send( + f"**Voted to lynch {target.mention}!**", + allowed_mentions=discord.AllowedMentions(everyone=False, users=[target]), + ) await self.lynch(target) self.can_vote = False else: - await self.village_channel.send("**{} has been spared!**".format(target.mention)) + await self.village_channel.send( + f"**{target.mention} has been spared!**", + allowed_mentions=discord.AllowedMentions(everyone=False, users=[target]), + ) if self.used_votes >= self.day_vote_count: await self.village_channel.send("**All votes have been used! Day is now over!**") self.can_vote = False else: await self.village_channel.send( - "**{}**/**{}** of today's votes have been used!\n" - "Nominate carefully..".format(self.used_votes, self.day_vote_count) + f"**{self.used_votes}**/**{self.day_vote_count}** of today's votes have been used!\n" + "Nominate carefully.." ) self.ongoing_vote = False @@ -513,14 +516,14 @@ class Game: status = "*[Dead]*-" if with_roles or not player.alive: embed.add_field( - name="ID# **{}**".format(i), - value="{}{}-{}".format(status, player.member.display_name, str(player.role)), + name=f"ID# **{i}**", + value=f"{status}{player.member.display_name}-{player.role}", inline=True, ) else: embed.add_field( - name="ID# **{}**".format(i), - value="{}{}".format(status, player.member.display_name), + name=f"ID# **{i}**", + value=f"{status}{player.member.display_name}", inline=True, ) @@ -553,7 +556,7 @@ class Game: return if await self.get_player_by_member(member) is not None: - await channel.send("{} is already in the game!".format(member.mention)) + await channel.send(f"{member.display_name} is already in the game!") return self.players.append(Player(member)) @@ -563,14 +566,12 @@ class Game: await member.add_roles(*[self.game_role]) except discord.Forbidden: await channel.send( - "Unable to add role **{}**\nBot is missing `manage_roles` permissions".format( - self.game_role.name - ) + f"Unable to add role **{self.game_role.name}**\nBot is missing `manage_roles` permissions" ) await channel.send( - "{} has been added to the game, " - "total players is **{}**".format(member.mention, len(self.players)) + f"{member.display_name} has been added to the game, " + f"total players is **{len(self.players)}**" ) async def quit(self, member: discord.Member, channel: discord.TextChannel = None): @@ -584,14 +585,16 @@ class Game: if self.started: await self._quit(player) - await channel.send("{} has left the game".format(member.mention)) + await channel.send( + f"{member.mention} has left the game", + allowed_mentions=discord.AllowedMentions(everyone=False, users=[member]), + ) else: self.players = [player for player in self.players if player.member != member] await member.remove_roles(*[self.game_role]) await channel.send( - "{} chickened out, player count is now **{}**".format( - member.mention, len(self.players) - ) + f"{member.mention} chickened out, player count is now **{len(self.players)}**", + allowed_mentions=discord.AllowedMentions(everyone=False, users=[member]), ) async def choose(self, ctx, data): @@ -698,7 +701,8 @@ class Game: author.mention, target.member.mention, required_votes - self.vote_totals[target_id], - ) + ), + allowed_mentions=discord.AllowedMentions(everyone=False, users=[author, target]), ) else: self.vote_totals[target_id] = 0 @@ -930,7 +934,7 @@ class Game: name = func.__name__ if name is None else name if not asyncio.iscoroutinefunction(func): - raise TypeError('Listeners must be coroutines') + raise TypeError("Listeners must be coroutines") if name in self.listeners: if priority in self.listeners[name]: @@ -942,7 +946,6 @@ class Game: # self.listeners[name].sort(reverse=True) - # def remove_wolf_listener(self, func, name=None): # """Removes a listener from the pool of listeners. # diff --git a/werewolf/listener.py b/werewolf/listener.py index e14994a..29ef7dd 100644 --- a/werewolf/listener.py +++ b/werewolf/listener.py @@ -64,13 +64,13 @@ def wolflistener(name=None, priority=0): class WolfListenerMeta(type): - def __new__(mcs, cls, *args, **kwargs): - name, bases = args + 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__(cls, name, bases, **kwargs) + 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: @@ -85,8 +85,8 @@ class WolfListenerMeta(type): except AttributeError: continue else: - if not elem.startswith("at_"): - raise TypeError(need_at_msg.format(mcs, elem)) + # if not elem.startswith("at_"): + # raise TypeError(need_at_msg.format(base, elem)) listeners[elem] = value listeners_as_list = [] diff --git a/werewolf/player.py b/werewolf/player.py index 8ee437e..201b781 100644 --- a/werewolf/player.py +++ b/werewolf/player.py @@ -34,4 +34,4 @@ class Player: try: await self.member.send(message) # Lets do embeds later except discord.Forbidden: - await self.role.game.village_channel.send("Couldn't DM {}, uh oh".format(self.mention)) + await self.role.game.village_channel.send(f"Couldn't DM {self.mention}, uh oh", allowed_mentions=discord.AllowedMentions(users=[self.member])) diff --git a/werewolf/role.py b/werewolf/role.py index 4ae10ad..0693f46 100644 --- a/werewolf/role.py +++ b/werewolf/role.py @@ -128,7 +128,7 @@ class Role(WolfListener): """ return "Default" - @wolflistener("at_game_start") + @wolflistener("at_game_start", priority=1) async def _at_game_start(self, data=None): if self.channel_id: await self.game.register_channel(self.channel_id, self) diff --git a/werewolf/roles/seer.py b/werewolf/roles/seer.py index a7a9aa8..f01c5c2 100644 --- a/werewolf/roles/seer.py +++ b/werewolf/roles/seer.py @@ -1,7 +1,11 @@ +import logging + 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") + class Seer(Role): rand_choice = True # Determines if it can be picked as a random role (False for unusually disruptive roles) @@ -93,7 +97,5 @@ class Seer(Role): self.see_target, target = await pick_target(self, ctx, data) await ctx.send( - "**You will attempt to see the role of {} tonight...**".format( - target.member.display_name - ) + f"**You will attempt to see the role of {target.member.display_name} tonight...**" ) diff --git a/werewolf/roles/shifter.py b/werewolf/roles/shifter.py index 6e2a850..8f93d76 100644 --- a/werewolf/roles/shifter.py +++ b/werewolf/roles/shifter.py @@ -1,7 +1,11 @@ +import logging + 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") + class Shifter(Role): """ @@ -130,7 +134,5 @@ class Shifter(Role): self.shift_target, target = await pick_target(self, ctx, data) await ctx.send( - "**You will attempt to see the role of {} tonight...**".format( - target.member.display_name - ) + f"**You will attempt to see the role of {target.member.display_name} tonight...**" ) diff --git a/werewolf/roles/vanillawerewolf.py b/werewolf/roles/vanillawerewolf.py index e6938eb..db70eb5 100644 --- a/werewolf/roles/vanillawerewolf.py +++ b/werewolf/roles/vanillawerewolf.py @@ -1,7 +1,11 @@ +import logging + 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") + class VanillaWerewolf(Role): rand_choice = True @@ -56,7 +60,9 @@ class VanillaWerewolf(Role): 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.game.register_channel( + self.channel_id, self, WolfVote + ) # Add VoteGroup WolfVote await self.player.send_dm(self.game_start_message) diff --git a/werewolf/roles/villager.py b/werewolf/roles/villager.py index 040e34d..f225e0d 100644 --- a/werewolf/roles/villager.py +++ b/werewolf/roles/villager.py @@ -1,5 +1,9 @@ +import logging + from werewolf.role import Role +log = logging.getLogger("red.fox_v3.werewolf.role.villager") + class Villager(Role): rand_choice = True # Determines if it can be picked as a random role (False for unusually disruptive roles) diff --git a/werewolf/votegroups/wolfvote.py b/werewolf/votegroups/wolfvote.py index f990eef..0823d77 100644 --- a/werewolf/votegroups/wolfvote.py +++ b/werewolf/votegroups/wolfvote.py @@ -1,7 +1,10 @@ +import logging import random from werewolf.votegroup import VoteGroup +log = logging.getLogger("red.fox_v3.werewolf.votegroup.wolfvote") + class WolfVote(VoteGroup): """ @@ -77,7 +80,7 @@ class WolfVote(VoteGroup): self.killer = random.choice(self.players) await self.channel.send( - "{} has been selected as tonight's killer".format(self.killer.member.display_name) + f"{self.killer.member.display_name} has been selected as tonight's killer" ) async def _at_night_end(self, data=None): @@ -90,7 +93,7 @@ class WolfVote(VoteGroup): if vote_list: target_id = max(set(vote_list), key=vote_list.count) - print("Target id: {}\nKiller: {}".format(target_id, self.killer.member.display_name)) + log.debug("Target id: {target_id}\nKiller: {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( diff --git a/werewolf/werewolf.py b/werewolf/werewolf.py index abed258..70574d2 100644 --- a/werewolf/werewolf.py +++ b/werewolf/werewolf.py @@ -62,7 +62,7 @@ class Werewolf(Cog): code = await gb.build_game(ctx) if code != "": - await ctx.send("Your game code is **{}**".format(code)) + await ctx.send(f"Your game code is **{code}**") else: await ctx.send("No code generated") From 29aa4930333d96f97049e88fb3b6eb1820273448 Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 22 Sep 2020 17:50:05 -0400 Subject: [PATCH 065/121] Better checking of valid settings --- werewolf/werewolf.py | 46 +++++++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/werewolf/werewolf.py b/werewolf/werewolf.py index 70574d2..677e5c0 100644 --- a/werewolf/werewolf.py +++ b/werewolf/werewolf.py @@ -82,15 +82,18 @@ class Werewolf(Cog): Lists current guild settings """ valid, role, category, channel, log_channel = await self._get_settings(ctx) - if not valid: - await ctx.send("Failed to get settings") - return None + # if not valid: + # await ctx.send("Failed to get settings") + # return None - embed = discord.Embed(title="Current Guild Settings") + embed = discord.Embed( + title="Current Guild Settings", description=f"Valid: {valid}", color=0xFF0000 + ) 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() @@ -391,23 +394,30 @@ 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, None, None, None, None + # if category is None: + # # await ctx.send("Game Category is invalid") + # return False, role, 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, None, None, None, None + # if channel is None: + # # await ctx.send("Village Channel is invalid") + # return False, role, category, 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 - - return True, role, category, channel, log_channel + # 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, + ) From 98ae481d14e5851201ec740da530c69f185fe92a Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 22 Sep 2020 17:53:54 -0400 Subject: [PATCH 066/121] Show valid settings correctly --- werewolf/werewolf.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/werewolf/werewolf.py b/werewolf/werewolf.py index 677e5c0..a011ccb 100644 --- a/werewolf/werewolf.py +++ b/werewolf/werewolf.py @@ -87,7 +87,9 @@ class Werewolf(Cog): # return None embed = discord.Embed( - title="Current Guild Settings", description=f"Valid: {valid}", color=0xFF0000 + title="Current Guild Settings", + description=f"Valid: {valid}", + color=0x008000 if valid else 0xFF0000, ) embed.add_field(name="Role", value=str(role)) embed.add_field(name="Category", value=str(category)) From e0042780a1f9db38e29e42862da2972bd42a46fe Mon Sep 17 00:00:00 2001 From: bobloy Date: Wed, 23 Sep 2020 11:39:34 -0400 Subject: [PATCH 067/121] Better detection of bad questions in trivia --- audiotrivia/audiosession.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/audiotrivia/audiosession.py b/audiotrivia/audiosession.py index 4207fab..04d53c2 100644 --- a/audiotrivia/audiosession.py +++ b/audiotrivia/audiosession.py @@ -3,6 +3,7 @@ import asyncio import logging import lavalink +from lavalink.enums import LoadType from redbot.cogs.trivia import TriviaSession from redbot.core.utils.chat_formatting import bold @@ -42,6 +43,7 @@ class AudioSession(TriviaSession): msg = bold(f"Question number {self.count}!") + "\n\nName this audio!" await self.ctx.maybe_send_embed(msg) + log.debug(f"Audio question: {question}") # print("Audio question: {}".format(question)) # await self.ctx.invoke(self.audio.play(ctx=self.ctx, query=question)) @@ -50,7 +52,8 @@ class AudioSession(TriviaSession): # await self.ctx.invoke(self.player.play, query=question) query = question.strip("<>") load_result = await self.player.load_tracks(query) - if load_result.has_error: + log.debug(f"{load_result.load_type=}") + if load_result.has_error or load_result.load_type != LoadType.TRACK_LOADED: await self.ctx.maybe_send_embed(f"Track has error, skipping. See logs for details") log.info(f"Track has error: {load_result.exception_message}") continue # Skip tracks with error @@ -66,6 +69,7 @@ class AudioSession(TriviaSession): self.player.add(self.ctx.author, tracks[0]) if not self.player.current: + log.debug("Pressing play") await self.player.play() continue_ = await self.wait_for_answer(answers, delay, timeout) From bf81d7c1573a86026d26fc3c15befde72486b34e Mon Sep 17 00:00:00 2001 From: bobloy Date: Wed, 23 Sep 2020 11:57:17 -0400 Subject: [PATCH 068/121] Embeds and track variable --- audiotrivia/audiosession.py | 9 ++++++--- audiotrivia/audiotrivia.py | 20 ++++++++++---------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/audiotrivia/audiosession.py b/audiotrivia/audiosession.py index 04d53c2..1bdff02 100644 --- a/audiotrivia/audiosession.py +++ b/audiotrivia/audiosession.py @@ -58,15 +58,18 @@ class AudioSession(TriviaSession): log.info(f"Track has error: {load_result.exception_message}") continue # Skip tracks with error tracks = load_result.tracks - seconds = tracks[0].length / 1000 + + track = tracks[0] + seconds = track.length / 1000 if self.settings["repeat"] and seconds < delay: + # Append it until it's longer than the delay tot_length = seconds + 0 while tot_length < delay: - self.player.add(self.ctx.author, tracks[0]) + self.player.add(self.ctx.author, track) tot_length += seconds else: - self.player.add(self.ctx.author, tracks[0]) + self.player.add(self.ctx.author, track) if not self.player.current: log.debug("Pressing play") diff --git a/audiotrivia/audiotrivia.py b/audiotrivia/audiotrivia.py index ec7b5ea..0bab980 100644 --- a/audiotrivia/audiotrivia.py +++ b/audiotrivia/audiotrivia.py @@ -84,24 +84,24 @@ class AudioTrivia(Trivia): 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") + await ctx.maybe_send_embed("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.") + await ctx.maybe_send_embed("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( + await ctx.maybe_send_embed( f"It is recommended to disable audio status with `{ctx.prefix}audioset status`" ) if notify: - await ctx.send( + await ctx.maybe_send_embed( f"It is recommended to disable audio notify with `{ctx.prefix}audioset notify`" ) @@ -110,12 +110,12 @@ class AudioTrivia(Trivia): 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.") + return await ctx.maybe_send_embed("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.") + return await ctx.maybe_send_embed("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 @@ -123,7 +123,7 @@ class AudioTrivia(Trivia): await self.audio.set_player_settings(ctx) if not ctx.author.voice or ctx.author.voice.channel != lavaplayer.channel: - return await ctx.send( + return await ctx.maybe_send_embed( "You must be in the voice channel to use the audiotrivia command." ) @@ -135,13 +135,13 @@ class AudioTrivia(Trivia): try: dict_ = self.get_audio_list(category) except FileNotFoundError: - await ctx.send( + await ctx.maybe_send_embed( "Invalid category `{0}`. See `{1}audiotrivia list`" " for a list of trivia categories." "".format(category, ctx.prefix) ) except InvalidListError: - await ctx.send( + await ctx.maybe_send_embed( "There was an error parsing the trivia list for" " the `{}` category. It may be formatted" " incorrectly.".format(category) @@ -152,7 +152,7 @@ class AudioTrivia(Trivia): continue return if not trivia_dict: - await ctx.send( + await ctx.maybe_send_embed( "The trivia list was parsed successfully, however it appears to be empty!" ) return From f05a8bf4f60f0f2fcb7f9188c3514d080a3f385b Mon Sep 17 00:00:00 2001 From: bobloy Date: Wed, 23 Sep 2020 12:07:20 -0400 Subject: [PATCH 069/121] *some* games fixed --- audiotrivia/data/lists/games.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/audiotrivia/data/lists/games.yaml b/audiotrivia/data/lists/games.yaml index c3a9078..4de795a 100644 --- a/audiotrivia/data/lists/games.yaml +++ b/audiotrivia/data/lists/games.yaml @@ -1,13 +1,13 @@ AUTHOR: Plab -https://www.youtube.com/watch?v=--bWm9hhoZo: +https://www.youtube.com/watch?v=f9O2Rjn1azc: - Transistor -https://www.youtube.com/watch?v=-4nCbgayZNE: +https://www.youtube.com/watch?v=PgUhYFkVdSY: - Dark Cloud 2 - Dark Cloud II -https://www.youtube.com/watch?v=-64NlME4lJU: +https://www.youtube.com/watch?v=1T1RZttyMwU: - Mega Man 7 - Mega Man VII -https://www.youtube.com/watch?v=-AesqnudNuw: +https://www.youtube.com/watch?v=AdDbbzuq1vY: - Mega Man 9 - Mega Man IX https://www.youtube.com/watch?v=-BmGDtP2t7M: From 608f4259658aaa1d46d0bbb8df939c280bbcd460 Mon Sep 17 00:00:00 2001 From: bobloy Date: Wed, 23 Sep 2020 12:15:53 -0400 Subject: [PATCH 070/121] Fix very long lists --- fifo/fifo.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/fifo/fifo.py b/fifo/fifo.py index e84e342..91c9190 100644 --- a/fifo/fifo.py +++ b/fifo/fifo.py @@ -10,6 +10,7 @@ from apscheduler.schedulers.base import STATE_PAUSED, STATE_RUNNING from redbot.core import Config, checks, commands from redbot.core.bot import Red from redbot.core.commands import TimedeltaConverter +from redbot.core.utils.chat_formatting import pagify from .datetime_cron_converters import CronConverter, DatetimeConverter from .task import Task @@ -306,7 +307,11 @@ class FIFO(commands.Cog): out += f"{task_name}: {task_data}\n" if out: - await ctx.maybe_send_embed(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("No tasks to list") From d13331d52d3f18c3098e500057b9d35519a94fcc Mon Sep 17 00:00:00 2001 From: bobloy Date: Wed, 23 Sep 2020 20:26:20 -0400 Subject: [PATCH 071/121] black --- planttycoon/planttycoon.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/planttycoon/planttycoon.py b/planttycoon/planttycoon.py index 61e5e06..665fc9a 100644 --- a/planttycoon/planttycoon.py +++ b/planttycoon/planttycoon.py @@ -360,7 +360,9 @@ 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( @@ -525,7 +527,8 @@ 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"]) @@ -583,7 +586,8 @@ 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( @@ -616,8 +620,11 @@ 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." From 8531ff5f91203862cbba0bf959bc887cfc84a17a Mon Sep 17 00:00:00 2001 From: bobloy Date: Thu, 24 Sep 2020 10:46:46 -0400 Subject: [PATCH 072/121] Timezone support and Aik Timezone cog integration --- fifo/datetime_cron_converters.py | 4 +- fifo/fifo.py | 33 ++- fifo/task.py | 49 ++++- fifo/timezones.py | 359 ++++++++++++++++--------------- 4 files changed, 252 insertions(+), 193 deletions(-) diff --git a/fifo/datetime_cron_converters.py b/fifo/datetime_cron_converters.py index b7f3dc4..0f03eff 100644 --- a/fifo/datetime_cron_converters.py +++ b/fifo/datetime_cron_converters.py @@ -1,6 +1,7 @@ from datetime import datetime, tzinfo from typing import TYPE_CHECKING +from pytz import timezone from apscheduler.triggers.cron import CronTrigger from dateutil import parser, tz from discord.ext.commands import BadArgument, Converter @@ -11,13 +12,14 @@ if TYPE_CHECKING: DatetimeConverter = datetime 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 = tz.gettz(argument) + timez = timezone(argument) if timez is not None: return timez diff --git a/fifo/fifo.py b/fifo/fifo.py index 30346d7..acd01ac 100644 --- a/fifo/fifo.py +++ b/fifo/fifo.py @@ -1,5 +1,5 @@ import logging -from datetime import datetime, timedelta +from datetime import datetime, timedelta, tzinfo from typing import Optional, Union import discord @@ -58,6 +58,8 @@ class FIFO(commands.Cog): self.scheduler = None self.jobstore = None + self.tz_cog = None + async def red_delete_data_for_user(self, **kwargs): """Nothing to delete""" return @@ -132,6 +134,24 @@ class FIFO(commands.Cog): async def _remove_job(self, task: Task): return self.scheduler.remove_job(job_id=_assemble_job_id(task.name, task.guild_id)) + async def _get_tz(self, user: Union[discord.User, discord.Member]) -> Union[None, tzinfo]: + if self.tz_cog is None: + self.tz_cog = self.bot.get_cog("Timezone") + if self.tz_cog is None: + self.tz_cog = False # only try once to get the timezone cog + + if 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 + @checks.is_owner() @commands.guild_only() @commands.command() @@ -140,7 +160,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 @@ -411,7 +431,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" + 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)" ) @@ -432,7 +452,9 @@ class FIFO(commands.Cog): ) return - result = await task.add_trigger("date", datetime_str) + maybe_tz = await self._get_tz(ctx.author) + + result = await task.add_trigger("date", datetime_str, maybe_tz) if not result: await ctx.maybe_send_embed( "Failed to add a date trigger to this task, see console for logs" @@ -470,6 +492,9 @@ 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) if not result: await ctx.maybe_send_embed( diff --git a/fifo/task.py b/fifo/task.py index 764ab8f..f7dc45a 100644 --- a/fifo/task.py +++ b/fifo/task.py @@ -9,6 +9,7 @@ from apscheduler.triggers.cron import CronTrigger from apscheduler.triggers.date import DateTrigger from apscheduler.triggers.interval import IntervalTrigger from discord.utils import time_snowflake +from pytz import timezone from redbot.core import Config, commands from redbot.core.bot import Red @@ -25,10 +26,10 @@ def get_trigger(data): return IntervalTrigger(days=parsed_time.days, seconds=parsed_time.seconds) if data["type"] == "date": - return DateTrigger(data["time_data"]) + return DateTrigger(data["time_data"], timezone=data["tzinfo"]) if data["type"] == "cron": - return CronTrigger.from_crontab(data["time_data"]) + return CronTrigger.from_crontab(data["time_data"], timezone=data["tzinfo"]) return False @@ -70,6 +71,7 @@ class Task: default_trigger = { "type": "", "time_data": None, # Used for Interval and Date Triggers + "tzinfo": None, } def __init__( @@ -99,7 +101,13 @@ class Task: if t["type"] == "date": # Convert into datetime dt: datetime = t["time_data"] - triggers.append({"type": t["type"], "time_data": dt.isoformat()}) + triggers.append( + { + "type": t["type"], + "time_data": dt.isoformat(), + "tzinfo": getattr(t["tzinfo"], "zone", None), + } + ) # triggers.append( # { # "type": t["type"], @@ -117,9 +125,18 @@ class Task: continue if t["type"] == "cron": - triggers.append(t) # already a string, nothing to do - + 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 + raise NotImplemented return triggers @@ -128,18 +145,27 @@ class Task: if not self.data or not self.data.get("triggers", None): return - for n, t in enumerate(self.data["triggers"]): + 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"] = timezone(t["tzinfo"]) + if t["type"] == "interval": # Convert into timedelta - self.data["triggers"][n]["time_data"] = timedelta(**t["time_data"]) + t["time_data"] = timedelta(**t["time_data"]) continue if t["type"] == "date": # Convert into datetime # self.data["triggers"][n]["time_data"] = datetime(**t["time_data"]) - self.data["triggers"][n]["time_data"] = datetime.fromisoformat(t["time_data"]) + t["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): @@ -304,7 +330,12 @@ class Task: self, param, parsed_time: Union[timedelta, datetime, str], timezone=None ): # TODO: Save timezone separately for cron and date triggers - trigger_data = {"type": param, "time_data": parsed_time} + 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 + if not get_trigger(trigger_data): return False diff --git a/fifo/timezones.py b/fifo/timezones.py index 5a322a4..5fdbdba 100644 --- a/fifo/timezones.py +++ b/fifo/timezones.py @@ -4,7 +4,8 @@ Timezone information for the dateutil parser All credit to https://github.com/prefrontal/dateutil-parser-timezones """ -from dateutil.tz import gettz +# from dateutil.tz import gettz +from pytz import timezone def assemble_timezones(): @@ -14,182 +15,182 @@ def assemble_timezones(): """ timezones = {} - 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) + 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) - return timezones \ No newline at end of file + return timezones From bdcb74587ee64c219acc622adf3fe143074fe522 Mon Sep 17 00:00:00 2001 From: bobloy Date: Thu, 24 Sep 2020 11:00:08 -0400 Subject: [PATCH 073/121] Bump to BETA, change requirements --- fifo/info.json | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/fifo/info.json b/fifo/info.json index dda63ce..eb2a576 100644 --- a/fifo/info.json +++ b/fifo/info.json @@ -3,14 +3,14 @@ "Bobloy" ], "min_bot_version": "3.4.0", - "description": "[ALPHA] Schedule commands to be run at certain times or intervals", + "description": "[BETA] 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": "[ALPHA] Schedule commands to be run at certain times or intervals", + "short": "[BETA] 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", - "python-dateutil" + "pytz" ], "tags": [ "bobloy", @@ -24,6 +24,7 @@ "date", "datetime", "time", - "calendar" + "calendar", + "timezone" ] } \ No newline at end of file From 9f17bca226feb232e92f27a994acc420ef0d5f6f Mon Sep 17 00:00:00 2001 From: bobloy Date: Thu, 24 Sep 2020 11:02:04 -0400 Subject: [PATCH 074/121] Change imports --- fifo/datetime_cron_converters.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fifo/datetime_cron_converters.py b/fifo/datetime_cron_converters.py index 0f03eff..9e01cc8 100644 --- a/fifo/datetime_cron_converters.py +++ b/fifo/datetime_cron_converters.py @@ -1,10 +1,10 @@ from datetime import datetime, tzinfo from typing import TYPE_CHECKING -from pytz import timezone from apscheduler.triggers.cron import CronTrigger -from dateutil import parser, tz +from dateutil import parser from discord.ext.commands import BadArgument, Converter +from pytz import timezone from fifo.timezones import assemble_timezones From 596865e49d6e0a8a43b8cd5bceea519c34d84d90 Mon Sep 17 00:00:00 2001 From: bobloy Date: Thu, 24 Sep 2020 12:11:32 -0400 Subject: [PATCH 075/121] Dad hotfix. Don't listen to bots --- dad/dad.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dad/dad.py b/dad/dad.py index edf98c5..0be7fce 100644 --- a/dad/dad.py +++ b/dad/dad.py @@ -85,6 +85,8 @@ 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 From 84ed2728e7ce29f3e97282b43f8af0b8776a742e Mon Sep 17 00:00:00 2001 From: bobloy Date: Thu, 24 Sep 2020 13:36:42 -0400 Subject: [PATCH 076/121] switch to log --- werewolf/game.py | 16 ++++++++-------- werewolf/role.py | 2 +- werewolf/roles/vanillawerewolf.py | 4 ++-- werewolf/werewolf.py | 4 ++-- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/werewolf/game.py b/werewolf/game.py index 7372923..45f0cbd 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -203,8 +203,7 @@ class Game: reason="(BOT) New game of werewolf", ) except discord.Forbidden as e: - print("Unable to rename Game Channel") - print(e) + log.exception("Unable to rename Game Channel") await ctx.send("Unable to rename Game Channel, ignoring") try: @@ -222,11 +221,11 @@ class Game: return self.started = True # Assuming everything worked so far - print("Pre at_game_start") + log.debug("Pre at_game_start") await self._at_game_start() # This will queue channels and votegroups to be made - print("Post at_game_start") + log.debug("Post at_game_start") for channel_id in self.p_channels: - print("Channel id: " + channel_id) + log.debug("Setup Channel id: " + channel_id) overwrite = { self.guild.default_role: discord.PermissionOverwrite(read_messages=False), self.guild.me: discord.PermissionOverwrite( @@ -258,7 +257,7 @@ class Game: self.vote_groups[channel_id] = vote_group - print("Pre-cycle") + log.debug("Pre-cycle") await asyncio.sleep(1) await asyncio.ensure_future(self._cycle()) # Start the loop @@ -566,7 +565,8 @@ class Game: await member.add_roles(*[self.game_role]) except discord.Forbidden: await channel.send( - f"Unable to add role **{self.game_role.name}**\nBot is missing `manage_roles` permissions" + f"Unable to add role **{self.game_role.name}**\n" + f"Bot is missing `manage_roles` permissions" ) await channel.send( @@ -899,7 +899,7 @@ class Game: # Remove game_role access for potential archiving for now reason = "(BOT) End of WW game" for obj in self.to_delete: - print(obj) + log.debug(f"End_game: Deleting object {obj}") await obj.delete(reason=reason) try: diff --git a/werewolf/role.py b/werewolf/role.py index 0693f46..ccc20ae 100644 --- a/werewolf/role.py +++ b/werewolf/role.py @@ -129,7 +129,7 @@ class Role(WolfListener): return "Default" @wolflistener("at_game_start", priority=1) - async def _at_game_start(self, data=None): + async def _at_game_start(self): if self.channel_id: await self.game.register_channel(self.channel_id, self) diff --git a/werewolf/roles/vanillawerewolf.py b/werewolf/roles/vanillawerewolf.py index db70eb5..f6eea81 100644 --- a/werewolf/roles/vanillawerewolf.py +++ b/werewolf/roles/vanillawerewolf.py @@ -57,9 +57,9 @@ class VanillaWerewolf(Role): return "Werewolf" @wolflistener("at_game_start") - async def _at_game_start(self, data=None): + async def _at_game_start(self): if self.channel_id: - print("Wolf has channel_id: " + self.channel_id) + log.debug("Wolf has channel_id: " + self.channel_id) await self.game.register_channel( self.channel_id, self, WolfVote ) # Add VoteGroup WolfVote diff --git a/werewolf/werewolf.py b/werewolf/werewolf.py index a011ccb..742a890 100644 --- a/werewolf/werewolf.py +++ b/werewolf/werewolf.py @@ -47,7 +47,7 @@ class Werewolf(Cog): return def __unload(self): - print("Unload called") + log.debug("Unload called") for game in self.games.values(): del game @@ -182,7 +182,7 @@ class Werewolf(Cog): Joins a game of Werewolf """ - game = await self._get_game(ctx) + game: Game = await self._get_game(ctx) if not game: await ctx.send("No game to join!\nCreate a new one with `[p]ww new`") From 8ffc8cc707b7c3fd666949b4b94584a36b95cda5 Mon Sep 17 00:00:00 2001 From: bobloy Date: Thu, 24 Sep 2020 17:03:39 -0400 Subject: [PATCH 077/121] Missed the listener update --- werewolf/votegroups/wolfvote.py | 79 +++++---------------------------- 1 file changed, 11 insertions(+), 68 deletions(-) diff --git a/werewolf/votegroups/wolfvote.py b/werewolf/votegroups/wolfvote.py index 0823d77..7f6bbde 100644 --- a/werewolf/votegroups/wolfvote.py +++ b/werewolf/votegroups/wolfvote.py @@ -1,6 +1,9 @@ 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") @@ -21,59 +24,13 @@ class WolfVote(VoteGroup): 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 - 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 + @wolflistener("at_night_start", priority=2) + async def _at_night_start(self): + await super()._at_night_start() - 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) @@ -83,7 +40,8 @@ class WolfVote(VoteGroup): f"{self.killer.member.display_name} has been selected as tonight's killer" ) - async def _at_night_end(self, data=None): + @wolflistener("at_night_end", priority=5) + async def _at_night_end(self): if self.channel is None: return @@ -102,29 +60,14 @@ class WolfVote(VoteGroup): else: 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 """ - self.vote_results[author.id] = target_id + await super().vote(target, author, target_id) await self.channel.send( - "{} has voted to kill {}".format(author.mention, target.member.display_name) + "{} has voted to kill {}".format(author.mention, target.member.display_name), + allowed_mentions=discord.AllowedMentions(everyone=False, users=[author]) ) From eaa3e0a2f792d1f4dba535f60c4aa9c7475ea5f7 Mon Sep 17 00:00:00 2001 From: bobloy Date: Thu, 24 Sep 2020 17:04:11 -0400 Subject: [PATCH 078/121] Fix listener parameters and priority --- werewolf/votegroup.py | 57 +++++++++++++++---------------------------- 1 file changed, 20 insertions(+), 37 deletions(-) diff --git a/werewolf/votegroup.py b/werewolf/votegroup.py index 19ebd9e..2f0b3a0 100644 --- a/werewolf/votegroup.py +++ b/werewolf/votegroup.py @@ -22,49 +22,31 @@ class VoteGroup(WolfListener): self.vote_results = {} self.properties = {} # Extra data for other options - # 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), - # ] - - # async def on_event(self, event, data): - # """ - # See Game class for event guide - # """ - # - # await self.action_list[event][0](data) - - @wolflistener("at_game_start") - async def _at_game_start(self, data=None): + @wolflistener("at_game_start", priority=1) + async def _at_game_start(self): await self.channel.send(" ".join(player.mention for player in self.players)) - @wolflistener("at_kill") - async def _at_kill(self, data=None): - if data["player"] in self.players: - self.players.remove(data["player"]) + @wolflistener("at_kill", priority=1) + async def _at_kill(self, player): + if player in self.players: + self.players.remove(player) - # Removed, only if they actually die - # @wolflistener("at_hang") - # async def _at_hang(self, data=None): - # if data["player"] in self.players: - # self.players.remove(data["player"]) + @wolflistener("at_hang", priority=1) + async def _at_hang(self, player): + if player in self.players: + self.players.remove(player) - @wolflistener("at_night_start") - async def _at_night_start(self, data=None): + @wolflistener("at_night_start", priority=2) + async def _at_night_start(self): if self.channel is None: return + self.vote_results = {} + await self.game.generate_targets(self.channel) - @wolflistener("at_night_end") - async def _at_night_end(self, data=None): + @wolflistener("at_night_end", priority=5) + async def _at_night_end(self): if self.channel is None: return @@ -75,8 +57,8 @@ class VoteGroup(WolfListener): target = max(set(vote_list), key=vote_list.count) if target: - # Do what you voted on - pass + # Do what the votegroup votes on + raise NotImplementedError async def register_players(self, *players): """ @@ -92,7 +74,8 @@ class VoteGroup(WolfListener): self.players.remove(player) if not self.players: - # ToDo: Trigger deletion of votegroup + # TODO: Confirm deletion + self.game.to_delete.add(self) pass async def vote(self, target, author, target_id): From 39801aada912b3ac4bf61226f7207f8518dbdac2 Mon Sep 17 00:00:00 2001 From: bobloy Date: Thu, 24 Sep 2020 17:04:25 -0400 Subject: [PATCH 079/121] Missed priority --- werewolf/roles/vanillawerewolf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/werewolf/roles/vanillawerewolf.py b/werewolf/roles/vanillawerewolf.py index f6eea81..58b474e 100644 --- a/werewolf/roles/vanillawerewolf.py +++ b/werewolf/roles/vanillawerewolf.py @@ -56,7 +56,7 @@ class VanillaWerewolf(Role): """ return "Werewolf" - @wolflistener("at_game_start") + @wolflistener("at_game_start", priority=1) async def _at_game_start(self): if self.channel_id: log.debug("Wolf has channel_id: " + self.channel_id) From cb0a7f1041a76202a1622d61c7af77a592b667d7 Mon Sep 17 00:00:00 2001 From: bobloy Date: Thu, 24 Sep 2020 17:04:49 -0400 Subject: [PATCH 080/121] Add priority and parameters --- werewolf/roles/seer.py | 4 ++-- werewolf/roles/shifter.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/werewolf/roles/seer.py b/werewolf/roles/seer.py index f01c5c2..56624c9 100644 --- a/werewolf/roles/seer.py +++ b/werewolf/roles/seer.py @@ -65,7 +65,7 @@ class Seer(Role): return "Villager" @wolflistener("at_night_start", priority=2) - async def _at_night_start(self, data=None): + async def _at_night_start(self): if not self.player.alive: return self.see_target = None @@ -73,7 +73,7 @@ class Seer(Role): await self.player.send_dm("**Pick a target to see tonight**") @wolflistener("at_night_end", priority=4) - async def _at_night_end(self, data=None): + async def _at_night_end(self): if self.see_target is None: if self.player.alive: await self.player.send_dm("You will not use your powers tonight...") diff --git a/werewolf/roles/shifter.py b/werewolf/roles/shifter.py index 8f93d76..a7ea058 100644 --- a/werewolf/roles/shifter.py +++ b/werewolf/roles/shifter.py @@ -99,14 +99,14 @@ class Shifter(Role): """ return "Shifter" - @wolflistener("at_night_start") - async def _at_night_start(self, data=None): + @wolflistener("at_night_start", priority=2) + async def _at_night_start(self): 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") - async def _at_night_end(self, data=None): + @wolflistener("at_night_end", priority=6) + async def _at_night_end(self): if self.shift_target is None: if self.player.alive: await self.player.send_dm("You will not use your powers tonight...") From 61d131341154401c659e5f717b5b4af8a201e3fd Mon Sep 17 00:00:00 2001 From: bobloy Date: Thu, 24 Sep 2020 17:05:52 -0400 Subject: [PATCH 081/121] Switch game to handle daytime smoother allowing cancellation --- werewolf/game.py | 121 +++++++++++++++++++++++++---------------------- 1 file changed, 65 insertions(+), 56 deletions(-) diff --git a/werewolf/game.py b/werewolf/game.py index 45f0cbd..dd95edb 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -65,7 +65,7 @@ class Game: self.started = False self.game_over = False - self.can_vote = False + self.any_votes_remaining = False self.used_votes = 0 self.day_time = False @@ -88,6 +88,7 @@ class Game: self.loop = asyncio.get_event_loop() self.action_queue = deque() + self.current_action = None self.listeners = {} # def __del__(self): @@ -278,8 +279,12 @@ class Game: self.action_queue.append(self._at_day_start()) - while self.action_queue: - await self.action_queue.popleft() + while self.action_queue and not self.game_over: + current_action = asyncio.create_task(self.action_queue.popleft()) + try: + await current_action + except asyncio.CancelledError: + log.debug("Cancelled task") # # await self._at_day_start() # # Once cycle ends, this will trigger end_game @@ -299,52 +304,57 @@ class Game: if self.game_over: return + self.action_queue.append(self._at_day_end()) # Get this ready in case day is cancelled + def check(): - return not self.can_vote or not self.day_time or self.game_over + return not self.any_votes_remaining or not self.day_time or self.game_over self.day_count += 1 + + # Print the results of who died during the night embed = discord.Embed(title=random.choice(self.morning_messages).format(self.day_count)) for result in self.night_results: embed.add_field(name=result, value="________", inline=False) - self.day_time = True + self.day_time = True # True while day self.night_results = [] # Clear for next day await self.village_channel.send(embed=embed) - await self.generate_targets(self.village_channel) + await self.generate_targets(self.village_channel) # Print remaining players for voting await self.day_perms(self.village_channel) - await self._notify("at_day_start") + await self._notify("at_day_start") # Wait for day_start actions await self._check_game_over() - if self.game_over: + if self.game_over: # If game ended because of _notify return - self.can_vote = True + self.any_votes_remaining = True + + # Now we sleep and let the day happen. Print the remaining daylight half way through await asyncio.sleep(HALF_DAY_LENGTH) # 4 minute days FixMe to 120 later if check(): return await self.village_channel.send( - embed=discord.Embed(title="**Two minutes of daylight remain...**") + embed=discord.Embed(title=f"**{HALF_DAY_LENGTH/60} minutes of daylight remain...**") ) await asyncio.sleep(HALF_DAY_LENGTH) # 4 minute days FixMe to 120 later - # Need a loop here to wait for trial to end (can_vote?) + # Need a loop here to wait for trial to end while self.ongoing_vote: await asyncio.sleep(5) - if check(): - return - - self.action_queue.append(self._at_day_end()) + # Abruptly ends, assuming _day_end is next in queue async def _at_voted(self, target): # ID 2 if self.game_over: return - data = {"player": target} + + # Notify that a target has been chosen await self._notify("at_voted", player=target) + # TODO: Support pre-vote target modifying roles self.ongoing_vote = True self.used_votes += 1 @@ -359,7 +369,7 @@ class Game: await self.speech_perms(self.village_channel, target.member, undo=True) # No one can talk - message: discord.Message = await self.village_channel.send( + vote_message: discord.Message = await self.village_channel.send( f"Everyone will now vote whether to lynch {target.mention}\n" "👍 to save, 👎 to lynch\n" "*Majority rules, no-lynch on ties, " @@ -367,41 +377,47 @@ class Game: allowed_mentions=discord.AllowedMentions(everyone=False, users=[target]), ) - await message.add_reaction("👍") - await message.add_reaction("👎") + await vote_message.add_reaction("👍") + await vote_message.add_reaction("👎") await asyncio.sleep(15) - reaction_list = message.reactions + reaction_list = vote_message.reactions + + if True: # TODO: Allow customizable vote history deletion. + await vote_message.delete() - up_votes = sum(p for p in reaction_list if p.emoji == "👍" and not p.me) - down_votes = sum(p for p in reaction_list if p.emoji == "👎" and not p.me) + raw_up_votes = sum(p for p in reaction_list if p.emoji == "👍" and not p.me) + raw_down_votes = sum(p for p in reaction_list if p.emoji == "👎" and not p.me) - if down_votes > up_votes: - embed = discord.Embed(title="Vote Results", color=0xFF0000) + # TODO: Support vote count modifying roles. (Need notify and count function) + voted_to_lynch = raw_down_votes > raw_up_votes + + if voted_to_lynch: + embed = discord.Embed( + title="Vote Results", + description=f"**Voted to lynch {target.mention}!**", + color=0xFF0000, + ) else: - embed = discord.Embed(title="Vote Results", color=0x80FF80) + embed = discord.Embed( + title="Vote Results", + description=f"**{target.mention} has been spared!**", + color=0x80FF80, + ) - embed.add_field(name="👎", value=f"**{up_votes}**", inline=True) - embed.add_field(name="👍", value=f"**{down_votes}**", inline=True) + embed.add_field(name="👎", value=f"**{raw_up_votes}**", inline=True) + embed.add_field(name="👍", value=f"**{raw_down_votes}**", inline=True) await self.village_channel.send(embed=embed) - if down_votes > up_votes: - await self.village_channel.send( - f"**Voted to lynch {target.mention}!**", - allowed_mentions=discord.AllowedMentions(everyone=False, users=[target]), - ) + if voted_to_lynch: await self.lynch(target) - self.can_vote = False + self.any_votes_remaining = False else: - await self.village_channel.send( - f"**{target.mention} has been spared!**", - allowed_mentions=discord.AllowedMentions(everyone=False, users=[target]), - ) if self.used_votes >= self.day_vote_count: await self.village_channel.send("**All votes have been used! Day is now over!**") - self.can_vote = False + self.any_votes_remaining = False else: await self.village_channel.send( f"**{self.used_votes}**/**{self.day_vote_count}** of today's votes have been used!\n" @@ -410,21 +426,19 @@ class Game: self.ongoing_vote = False - if not self.can_vote: - self.action_queue.append(self._at_day_end()) + if not self.any_votes_remaining and self.day_time: + self.current_action.cancel() else: await self.normal_perms(self.village_channel) # No point if about to be night async def _at_kill(self, target): # ID 3 if self.game_over: return - data = {"player": target} await self._notify("at_kill", player=target) async def _at_hang(self, target): # ID 4 if self.game_over: return - data = {"player": target} await self._notify("at_hang", player=target) async def _at_day_end(self): # ID 5 @@ -433,7 +447,7 @@ class Game: if self.game_over: return - self.can_vote = False + self.any_votes_remaining = False self.day_vote = {} self.vote_totals = {} self.day_time = False @@ -476,7 +490,6 @@ class Game: async def _at_visit(self, target, source): # ID 8 if self.game_over: return - data = {"target": target, "source": source} await self._notify("at_visit", target=target, source=source) async def _notify(self, event, **kwargs): @@ -484,6 +497,8 @@ class Game: tasks = [] for event in self.listeners.get(event, {}).get(i, []): tasks.append(asyncio.ensure_future(event(**kwargs), loop=self.loop)) + + # Run same-priority task simultaneously await asyncio.gather(*tasks) # self.bot.dispatch(f"red.fox.werewolf.{event}", data=data, priority=i) @@ -507,8 +522,7 @@ class Game: async def generate_targets(self, channel, with_roles=False): embed = discord.Embed(title="Remaining Players") - for i in range(len(self.players)): - player = self.players[i] + for i, player in enumerate(self.players): if player.alive: status = "" else: @@ -653,7 +667,7 @@ class Game: return if channel == self.village_channel: - if not self.can_vote: + if not self.any_votes_remaining: await channel.send("Voting is not allowed right now") return elif channel.name in self.p_channels: @@ -695,13 +709,8 @@ class Game: if self.vote_totals[target_id] < required_votes: await self.village_channel.send( - "" - "{} has voted to put {} to trial. " - "{} more votes needed".format( - author.mention, - target.member.mention, - required_votes - self.vote_totals[target_id], - ), + f"{author.mention} has voted to put {target.member.mention} to trial. " + f"{required_votes - self.vote_totals[target_id]} more votes needed", allowed_mentions=discord.AllowedMentions(everyone=False, users=[author, target]), ) else: @@ -771,8 +780,8 @@ class Game: Attempt to lynch a target Important to finish execution before triggering notify """ - target = await self.get_day_target(target_id) - target.alive = False + target = await self.get_day_target(target_id) # Allows target modification + target.alive = False # Kill them, await self._at_hang(target) if not target.alive: # Still dead after notifying await self.dead_perms(self.village_channel, target.member) From f263f97cc27bef8af33cd5998fd632a97db38845 Mon Sep 17 00:00:00 2001 From: bobloy Date: Thu, 24 Sep 2020 17:06:03 -0400 Subject: [PATCH 082/121] Update builder to accept any number of roles --- werewolf/builder.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/werewolf/builder.py b/werewolf/builder.py index ca90eca..62d290f 100644 --- a/werewolf/builder.py +++ b/werewolf/builder.py @@ -9,21 +9,33 @@ 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 .roles.seer import Seer +# from .roles.vanillawerewolf import VanillaWerewolf +# from .roles.villager import Villager + +from werewolf import roles from redbot.core.utils.menus import menu, prev_page, next_page, close_menu +from werewolf.role import Role + log = logging.getLogger("red.fox_v3.werewolf.builder") # All roles in this list for iterating -ROLE_LIST = sorted([Villager, Seer, VanillaWerewolf], key=lambda x: x.alignment) +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=lambda x: x.alignment, +) + +log.debug(f"{ROLE_DICT=}") 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]] +# TOWN_ROLES = [(idx, role) for idx, r_tuple in enumerate(ROLE_LIST) if role.alignment == 1] +# WW_ROLES = [(idx, role) for idx, r_tuple in enumerate(ROLE_LIST) if role.alignment == 2] +# OTHER_ROLES = [ +# (idx, role) for idx, r_tuple in enumerate(ROLE_LIST) if role.alignment not in [0, 1] +# ] ROLE_PAGES = [] PAGE_GROUPS = [0] @@ -81,9 +93,7 @@ def setup(): for k, v in ROLE_CATEGORIES.items(): if 0 < k <= 6: ROLE_PAGES.append( - discord.Embed( - title="RANDOM:Town Role", description=f"Town {v}", color=0x008000 - ) + discord.Embed(title="RANDOM:Town Role", description=f"Town {v}", color=0x008000) ) CATEGORY_COUNT.append(k) From 224ff93531179c1ccc73dbbd060172ca4e26b7dd Mon Sep 17 00:00:00 2001 From: bobloy Date: Thu, 24 Sep 2020 17:06:17 -0400 Subject: [PATCH 083/121] black and __all__ --- werewolf/roles/__init__.py | 1 + werewolf/votegroups/__init__.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/werewolf/roles/__init__.py b/werewolf/roles/__init__.py index ba929e5..201799a 100644 --- a/werewolf/roles/__init__.py +++ b/werewolf/roles/__init__.py @@ -3,3 +3,4 @@ from .shifter import Shifter from .vanillawerewolf import VanillaWerewolf from .villager import Villager +__all__ = ["Seer", "Shifter", "VanillaWerewolf", "Villager"] diff --git a/werewolf/votegroups/__init__.py b/werewolf/votegroups/__init__.py index 03abc1b..6b99b1e 100644 --- a/werewolf/votegroups/__init__.py +++ b/werewolf/votegroups/__init__.py @@ -1 +1 @@ -from .wolfvote import WolfVote \ No newline at end of file +from .wolfvote import WolfVote From a0c645bd28f9aa07029776e7b1c99baae4f6919d Mon Sep 17 00:00:00 2001 From: bobloy Date: Fri, 25 Sep 2020 15:56:33 -0400 Subject: [PATCH 084/121] Precarious import order --- werewolf/roles/__init__.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/werewolf/roles/__init__.py b/werewolf/roles/__init__.py index 201799a..3f58a76 100644 --- a/werewolf/roles/__init__.py +++ b/werewolf/roles/__init__.py @@ -1,6 +1,11 @@ +from .villager import Villager from .seer import Seer -from .shifter import Shifter + from .vanillawerewolf import VanillaWerewolf -from .villager import Villager + +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"] From eb0c79ef1d57924c650c96efc90998fe326d4d01 Mon Sep 17 00:00:00 2001 From: bobloy Date: Fri, 25 Sep 2020 15:56:49 -0400 Subject: [PATCH 085/121] Introduction of The Blob --- werewolf/roles/blob.py | 101 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 werewolf/roles/blob.py diff --git a/werewolf/roles/blob.py b/werewolf/roles/blob.py new file mode 100644 index 0000000..bd7b598 --- /dev/null +++ b/werewolf/roles/blob.py @@ -0,0 +1,101 @@ +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 `\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_messages.append("The Blob grows...") From 61049c23432cd8262e5f1805a30cdae7cc2af569 Mon Sep 17 00:00:00 2001 From: bobloy Date: Fri, 25 Sep 2020 15:59:21 -0400 Subject: [PATCH 086/121] Adding constants --- werewolf/builder.py | 258 +++++++++++++++--------------- werewolf/constants.py | 91 +++++++++++ werewolf/game.py | 10 +- werewolf/role.py | 59 ++----- werewolf/roles/seer.py | 23 +-- werewolf/roles/shifter.py | 8 +- werewolf/roles/vanillawerewolf.py | 20 +-- werewolf/roles/villager.py | 16 +- werewolf/werewolf.py | 2 +- 9 files changed, 273 insertions(+), 214 deletions(-) create mode 100644 werewolf/constants.py diff --git a/werewolf/builder.py b/werewolf/builder.py index 62d290f..4c803cc 100644 --- a/werewolf/builder.py +++ b/werewolf/builder.py @@ -1,6 +1,7 @@ import bisect import logging from collections import defaultdict +from operator import attrgetter from random import choice import discord @@ -16,6 +17,7 @@ from redbot.core import commands from werewolf import roles 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") @@ -25,104 +27,40 @@ log = logging.getLogger("red.fox_v3.werewolf.builder") 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=lambda x: x.alignment, + key=attrgetter('alignment'), ) log.debug(f"{ROLE_DICT=}") +# Town, Werewolf, Neutral ALIGNMENT_COLORS = [0x008000, 0xFF0000, 0xC0C0C0] -# TOWN_ROLES = [(idx, role) for idx, r_tuple in enumerate(ROLE_LIST) if role.alignment == 1] -# WW_ROLES = [(idx, role) for idx, r_tuple in enumerate(ROLE_LIST) if role.alignment == 2] -# OTHER_ROLES = [ -# (idx, role) for idx, r_tuple 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, color): + + +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=True + name="Alignment", value=["Town", "Werewolf", "Neutral"][role.alignment - 1], inline=False ) - embed.add_field(name="Multiples Allowed", value=str(not role.unique), inline=True) + embed.add_field(name="Multiples Allowed", value=str(not role.unique), inline=False) embed.add_field( - name="Role Type", value=", ".join(ROLE_CATEGORIES[x] for x in role.category), inline=True + 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=True) + embed.add_field(name="Random Option", value=str(role.rand_choice), inline=False) 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=f"Town {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=f"Werewolf {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=f"RANDOM:Neutral Role", description="Neutral {v}", color=0xC0C0C0 - ) - ) - CATEGORY_COUNT.append(k) - - """ Example code: 0 = Villager @@ -189,15 +127,15 @@ async def parse_code(code, game): return decode -async def encode(roles, rand_roles): +async def encode(role_list, rand_roles): """Convert role list to code""" out_code = "" - digit_sort = sorted(role for role in roles if role < 10) + digit_sort = sorted(role for role in role_list if role < 10) for role in digit_sort: out_code += str(role) - digit_sort = sorted(role for role in roles if 10 <= role < 100) + digit_sort = sorted(role for role in role_list if 10 <= role < 100) if digit_sort: out_code += "-" for role in digit_sort: @@ -229,51 +167,6 @@ async def encode(roles, 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.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.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]) @@ -316,11 +209,11 @@ def say_role_list(code_list, rand_roles): for role in rand_roles: if 0 < role <= 6: - role_dict[f"Town {ROLE_CATEGORIES[role]}"] += 1 + role_dict[f"Town {ROLE_CATEGORY_DESCRIPTIONS[role]}"] += 1 if 10 < role <= 16: - role_dict[f"Werewolf {ROLE_CATEGORIES[role]}"] += 1 + role_dict[f"Werewolf {ROLE_CATEGORY_DESCRIPTIONS[role]}"] += 1 if 20 < role <= 26: - role_dict[f"Neutral {ROLE_CATEGORIES[role]}"] += 1 + role_dict[f"Neutral {ROLE_CATEGORY_DESCRIPTIONS[role]}"] += 1 for k, v in role_dict.items(): embed.add_field(name=k, value=f"Count: {v}", inline=True) @@ -332,15 +225,69 @@ class GameBuilder: def __init__(self): self.code = [] self.rand_roles = [] - setup() + 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) async def build_game(self, ctx: commands.Context): new_controls = { - "⏪": prev_group, + "⏪": self.prev_group, "⬅": prev_page, "☑": self.select_page, "➡": next_page, - "⏩": next_group, + "⏩": self.next_group, "📇": self.list_roles, "❌": close_menu, } @@ -391,8 +338,53 @@ class GameBuilder: pass if page >= len(ROLE_LIST): - self.rand_roles.append(CATEGORY_COUNT[page - len(ROLE_LIST)]) + self.rand_roles.append(self.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) diff --git a/werewolf/constants.py b/werewolf/constants.py new file mode 100644 index 0000000..bb77421 --- /dev/null +++ b/werewolf/constants.py @@ -0,0 +1,91 @@ +""" +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) +""" diff --git a/werewolf/game.py b/werewolf/game.py index dd95edb..13db415 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -639,14 +639,14 @@ class Game: await target.role.visit(source) await self._at_visit(target, source) - async def visit(self, target_id, source): + async def visit(self, target_id, source) -> Union[Player, None]: """ Night visit target_id Returns a target for role information (i.e. Seer) """ if source.role.blocked: # Blocker handles text - return + return None target = await self.get_night_target(target_id, source) await self._visit(target, source) return target @@ -786,10 +786,10 @@ class Game: if not target.alive: # Still dead after notifying await self.dead_perms(self.village_channel, target.member) - async def get_night_target(self, target_id, source=None): + async def get_night_target(self, target_id, source=None) -> Player: return self.players[target_id] # ToDo check source - async def get_day_target(self, target_id, source=None): + async def get_day_target(self, target_id, source=None) -> Player: return self.players[target_id] # ToDo check source async def set_code(self, ctx: commands.Context, game_code): @@ -829,7 +829,7 @@ class Game: # Sorted players, now assign id's await self.players[idx].assign_id(idx) - async def get_player_by_member(self, member): + async def get_player_by_member(self, member: discord.Member): for player in self.players: if player.member == member: return player diff --git a/werewolf/role.py b/werewolf/role.py index ccc20ae..db7b852 100644 --- a/werewolf/role.py +++ b/werewolf/role.py @@ -26,6 +26,8 @@ class Role(WolfListener): 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): @@ -44,10 +46,12 @@ class Role(WolfListener): 6. Role altering actions (Cult / Mason / Shifter) """ - rand_choice = False # Determines if it can be picked as a random role (False for unusually disruptive roles) + # 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 category = [0] # List of enrolled categories (listed above) alignment = 0 # 1: Town, 2: Werewolf, 3: Neutral - channel_id = "" # Empty for no private channel + channel_name = "" # Empty for no private channel unique = False # Only one of this role per game game_start_message = ( "Your role is **Default**\n" @@ -68,28 +72,9 @@ class Role(WolfListener): self.blocked = False self.properties = {} # Extra data for other roles (i.e. arsonist) - # 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 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): """ Give this role a player @@ -110,7 +95,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" @@ -128,37 +113,13 @@ class Role(WolfListener): """ return "Default" - @wolflistener("at_game_start", priority=1) + @wolflistener("at_game_start", priority=2) async def _at_game_start(self): - if self.channel_id: - await self.game.register_channel(self.channel_id, self) + if self.channel_name: + await self.game.register_channel(self.channel_name, self) 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): """ Called when someone is trying to kill you! diff --git a/werewolf/roles/seer.py b/werewolf/roles/seer.py index 56624c9..32ace18 100644 --- a/werewolf/roles/seer.py +++ b/werewolf/roles/seer.py @@ -1,5 +1,7 @@ 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 @@ -8,9 +10,12 @@ log = logging.getLogger("red.fox_v3.werewolf.role.seer") class Seer(Role): - 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 + rand_choice = True + category = [ + CATEGORY_TOWN_RANDOM, + CATEGORY_TOWN_INVESTIGATIVE, + ] # List of enrolled categories (listed above) + alignment = ALIGNMENT_TOWN # 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 = ( @@ -46,23 +51,23 @@ class Seer(Role): 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 "Village" + return ALIGNMENT_TOWN async def get_role(self, source=None): """ Interaction for powerful access of role Unlikely to be able to deceive this """ - return "Villager" + return "Seer" async def see_role(self, source=None): """ Interaction for investigative roles. More common to be able to deceive these roles """ - return "Villager" + return "Seer" @wolflistener("at_night_start", priority=2) async def _at_night_start(self): @@ -84,9 +89,9 @@ class Seer(Role): if target: alignment = await target.role.see_alignment(self.player) - if alignment == "Werewolf": + if alignment == ALIGNMENT_WEREWOLF: out = "Your insight reveals this player to be a **Werewolf!**" - else: + else: # Don't reveal neutrals out = "You fail to find anything suspicious about this player..." await self.player.send_dm(out) diff --git a/werewolf/roles/shifter.py b/werewolf/roles/shifter.py index a7ea058..9685e20 100644 --- a/werewolf/roles/shifter.py +++ b/werewolf/roles/shifter.py @@ -1,5 +1,6 @@ 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 @@ -46,8 +47,9 @@ class Shifter(Role): """ rand_choice = False # Determines if it can be picked as a random role (False for unusually disruptive roles) - category = [22] # List of enrolled categories (listed above) - alignment = 3 # 1: Town, 2: Werewolf, 3: Neutral + town_balance = -3 + category = [CATEGORY_NEUTRAL_BENIGN] # List of enrolled categories (listed above) + alignment = ALIGNMENT_NEUTRAL # 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 = ( @@ -81,7 +83,7 @@ class Shifter(Role): 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" diff --git a/werewolf/roles/vanillawerewolf.py b/werewolf/roles/vanillawerewolf.py index 58b474e..74e8d96 100644 --- a/werewolf/roles/vanillawerewolf.py +++ b/werewolf/roles/vanillawerewolf.py @@ -1,5 +1,6 @@ import logging +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 @@ -9,9 +10,10 @@ log = logging.getLogger("red.fox_v3.werewolf.role.vanillawerewolf") class VanillaWerewolf(Role): rand_choice = True - category = [11, 15] - alignment = 2 # 1: Town, 2: Werewolf, 3: Neutral - channel_id = "werewolves" + town_balance = -6 + category = [CATEGORY_WW_RANDOM, CATEGORY_WW_KILLING] + alignment = ALIGNMENT_WEREWOLF # 1: Town, 2: Werewolf, 3: Neutral + channel_name = "werewolves" unique = False game_start_message = ( "Your role is **Werewolf**\n" @@ -40,14 +42,14 @@ class VanillaWerewolf(Role): Interaction for investigative roles attempting to see team (Village, Werewolf Other) """ - return "Werewolf" + return ALIGNMENT_WEREWOLF async def get_role(self, source=None): """ Interaction for powerful access of role Unlikely to be able to deceive this """ - return "Werewolf" + return "VanillaWerewolf" async def see_role(self, source=None): """ @@ -56,12 +58,12 @@ class VanillaWerewolf(Role): """ return "Werewolf" - @wolflistener("at_game_start", priority=1) + @wolflistener("at_game_start", priority=2) async def _at_game_start(self): - if self.channel_id: - log.debug("Wolf has channel_id: " + self.channel_id) + if self.channel_name: + log.debug("Wolf has channel_name: " + self.channel_name) await self.game.register_channel( - self.channel_id, self, WolfVote + self.channel_name, self, WolfVote ) # Add VoteGroup WolfVote await self.player.send_dm(self.game_start_message) diff --git a/werewolf/roles/villager.py b/werewolf/roles/villager.py index f225e0d..d669ef9 100644 --- a/werewolf/roles/villager.py +++ b/werewolf/roles/villager.py @@ -1,14 +1,20 @@ 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") class Villager(Role): - 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 + + # 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 channel_id = "" # Empty for no private channel unique = False # Only one of this role per game game_start_message = ( @@ -23,9 +29,9 @@ class Villager(Role): 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 "Village" + return ALIGNMENT_TOWN async def get_role(self, source=None): """ diff --git a/werewolf/werewolf.py b/werewolf/werewolf.py index 742a890..d26dc38 100644 --- a/werewolf/werewolf.py +++ b/werewolf/werewolf.py @@ -33,7 +33,7 @@ class Werewolf(Cog): default_guild = { "role_id": None, "category_id": None, - "channel_id": None, + "channel_name": None, "log_channel_id": None, } From 029b6a51b141c4c21c425bf8d3f8b771042dd5ee Mon Sep 17 00:00:00 2001 From: bobloy Date: Fri, 25 Sep 2020 15:59:29 -0400 Subject: [PATCH 087/121] black --- werewolf/player.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/werewolf/player.py b/werewolf/player.py index 201b781..7f10758 100644 --- a/werewolf/player.py +++ b/werewolf/player.py @@ -34,4 +34,7 @@ class Player: try: await self.member.send(message) # Lets do embeds later except discord.Forbidden: - await self.role.game.village_channel.send(f"Couldn't DM {self.mention}, uh oh", allowed_mentions=discord.AllowedMentions(users=[self.member])) + await self.role.game.village_channel.send( + f"Couldn't DM {self.mention}, uh oh", + allowed_mentions=discord.AllowedMentions(users=[self.member]), + ) From db1d64ae3e3865a00443c0d53773dc2c8ba7f4c8 Mon Sep 17 00:00:00 2001 From: bobloy Date: Fri, 25 Sep 2020 16:59:30 -0400 Subject: [PATCH 088/121] More async iters --- timerole/timerole.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/timerole/timerole.py b/timerole/timerole.py index 3815dcd..7484267 100644 --- a/timerole/timerole.py +++ b/timerole/timerole.py @@ -157,7 +157,7 @@ class Timerole(Cog): await ctx.maybe_send_embed(out) async def timerole_update(self): - for guild in self.bot.guilds: + async for guild in AsyncIter(self.bot.guilds): addlist = [] removelist = [] @@ -200,7 +200,7 @@ class Timerole(Cog): async def announce_roles(self, title, role_list, channel, guild, to_add: True): results = "" - for member, role_id in role_list: + async for member, role_id in AsyncIter(role_list): role = discord.utils.get(guild.roles, id=role_id) try: if to_add: @@ -219,7 +219,7 @@ class Timerole(Cog): log.info(results) async def check_required_and_date(self, role_list, check_roles, has_roles, member, role_dict): - for role_id in check_roles: + 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): @@ -242,6 +242,3 @@ class Timerole(Cog): while self is self.bot.get_cog("Timerole"): await self.timerole_update() await sleep_till_next_hour() - - - From 03f0ef17be2d8b3ea937cfcc14eb22aa14ddf90c Mon Sep 17 00:00:00 2001 From: bobloy Date: Fri, 25 Sep 2020 22:04:35 -0400 Subject: [PATCH 089/121] Fix aggressive refactor --- werewolf/werewolf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/werewolf/werewolf.py b/werewolf/werewolf.py index d26dc38..742a890 100644 --- a/werewolf/werewolf.py +++ b/werewolf/werewolf.py @@ -33,7 +33,7 @@ class Werewolf(Cog): default_guild = { "role_id": None, "category_id": None, - "channel_name": None, + "channel_id": None, "log_channel_id": None, } From af4cd92488ac3aae415aff445a3a2673ae5d67e4 Mon Sep 17 00:00:00 2001 From: bobloy Date: Fri, 25 Sep 2020 22:05:22 -0400 Subject: [PATCH 090/121] Correctly reset channels --- werewolf/game.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/werewolf/game.py b/werewolf/game.py index 13db415..df345af 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -8,6 +8,7 @@ from typing import List, Any, Dict, Set, Union import discord from redbot.core import commands from redbot.core.bot import Red +from redbot.core.utils import AsyncIter from werewolf.builder import parse_code from werewolf.player import Player @@ -913,10 +914,13 @@ class Game: try: await self.village_channel.edit(reason=reason, name="Werewolf") - for target, overwrites in self.save_perms[self.village_channel]: - await self.village_channel.set_permissions( - target, overwrite=overwrites, reason=reason - ) + async for channel, overwrites in AsyncIter(self.save_perms.items()): + async for target, overwrite in AsyncIter(overwrites.items()): + await channel.set_permissions(target, overwrite=overwrite, reason=reason) + # for target, overwrites in self.save_perms[self.village_channel]: + # await self.village_channel.set_permissions( + # target, overwrite=overwrites, reason=reason + # ) await self.village_channel.set_permissions( self.game_role, overwrite=None, reason=reason ) From ab1b069ee98435ff14db8a27e1eb41941d1e85f6 Mon Sep 17 00:00:00 2001 From: bobloy Date: Mon, 28 Sep 2020 08:44:03 -0400 Subject: [PATCH 091/121] Move games to non-functional --- audiotrivia/data/lists/{ => non_funcitonal_lists}/games.yaml | 1 + 1 file changed, 1 insertion(+) rename audiotrivia/data/lists/{ => non_funcitonal_lists}/games.yaml (99%) diff --git a/audiotrivia/data/lists/games.yaml b/audiotrivia/data/lists/non_funcitonal_lists/games.yaml similarity index 99% rename from audiotrivia/data/lists/games.yaml rename to audiotrivia/data/lists/non_funcitonal_lists/games.yaml index 4de795a..da1dcb6 100644 --- a/audiotrivia/data/lists/games.yaml +++ b/audiotrivia/data/lists/non_funcitonal_lists/games.yaml @@ -1,4 +1,5 @@ AUTHOR: Plab +NEEDS: New links for all songs. https://www.youtube.com/watch?v=f9O2Rjn1azc: - Transistor https://www.youtube.com/watch?v=PgUhYFkVdSY: From b27b252e6f28ef5a23218933efd0b885c3fde529 Mon Sep 17 00:00:00 2001 From: bobloy Date: Mon, 28 Sep 2020 08:44:14 -0400 Subject: [PATCH 092/121] Add new videogames trivia list --- audiotrivia/data/lists/videogames.yaml | 1762 ++++++++++++++++++++++++ 1 file changed, 1762 insertions(+) create mode 100644 audiotrivia/data/lists/videogames.yaml diff --git a/audiotrivia/data/lists/videogames.yaml b/audiotrivia/data/lists/videogames.yaml new file mode 100644 index 0000000..eec01b6 --- /dev/null +++ b/audiotrivia/data/lists/videogames.yaml @@ -0,0 +1,1762 @@ +Author: Bobloy +https://www.youtube.com/watch?v=GBPbJyxqHV0: +- Super Mario 64 +https://www.youtube.com/watch?v=0jXTBAGv9ZQ: +- Halo +https://www.youtube.com/watch?v=ZksNhHyEhE0: +- Sonic Generations +https://www.youtube.com/watch?v=4qJ-xEZhGms: +- The Legend of Zelda A Link to the Past +- A Link to the Past +https://www.youtube.com/watch?v=0J5gN1DwNDE: +- The Elder Scrolls 4 Oblivion +- Elder Scrolls 4 +- Oblivion +https://www.youtube.com/watch?v=VTsD2FjmLsw: +- Mass Effect 2 +https://www.youtube.com/watch?v=yYTY9EkFSRU: +- Mortal Kombat Deception +https://www.youtube.com/watch?v=uhscMsBhNhw: +- Super Mario Bros +https://www.youtube.com/watch?v=Jk4P10nsq4c: +- Kingdom Hearts +https://www.youtube.com/watch?v=qDnaIfiH37w: +- Super Smash Bros for Wii U +- Super Smash Bros Wii U +https://www.youtube.com/watch?v=-8wo0KBQ3oI: +- Doom +https://www.youtube.com/watch?v=jLJLyneZGKc: +- Super Street Fighter 2 +https://www.youtube.com/watch?v=T5ASJvTgpJo: +- Call Of Duty Black Ops +https://www.youtube.com/watch?v=Gb33Qnbw520: +- Super Mario Land +https://www.youtube.com/watch?v=omvdFcr0z08: +- Mortal Kombat +https://www.youtube.com/watch?v=W4VTq0sa9yg: +- GTA San Andreas +- Grand Theft Auto San Andreas +https://www.youtube.com/watch?v=7Lj1aw4J8ZA: +- Kirby's Dream Land +- Kirbys Dream Land +https://www.youtube.com/watch?v=O5FsjHRIVrc: +- Overwatch +https://www.youtube.com/watch?v=kSA4U4jNq4E: +- Hitman +https://www.youtube.com/watch?v=kzvZE4BY0hY: +- Fallout 4 +https://www.youtube.com/watch?v=5vYsVk23oxA: +- Super Metroid +https://www.youtube.com/watch?v=TVEyGntyOZQ: +- Sonic 2 +https://www.youtube.com/watch?v=EWbfixMWg4g: +- Super Smash Bros +https://www.youtube.com/watch?v=Kxp1qIYy2jQ: +- Super Smash Bros Melee +https://www.youtube.com/watch?v=zeKE0NHUtUw: +- Super Smash Bros Brawl +https://www.youtube.com/watch?v=eWSU8YOa3jU: +- Super Smash Bros 4 +https://www.youtube.com/watch?v=oC83pBZ0Z2I: +- Super Smash Bros Ultimate +https://www.youtube.com/watch?v=b8JbXCe3NJk: +- Pokémon Red/Blue/Yellow +- Pokémon Red +- Pokemon Red +- Pokémon Blue +- Pokemon Blue +- Pokémon Yellow +- Pokemon Yellow +https://www.youtube.com/watch?v=O9FeFkMyBIY: +- The Legend of Zelda Ocarina of Time +- Zelda Ocarina of Time +https://www.youtube.com/watch?v=xtJcoZ9E9ZQ: +- WOW +- World of Warcraft +https://www.youtube.com/watch?v=oDo3wlDretk: +- Street Fighter 2 +https://www.youtube.com/watch?v=l2qD1h9mGF0: +- Sonic Unleashed +https://www.youtube.com/watch?v=Oa2TCaE4MGk: +- Sonic Adventure 2 +https://www.youtube.com/watch?v=WJRoRt155mA: +- Mega Man 2 +https://www.youtube.com/watch?v=N6noiPFB3io: +- Minecraft +https://www.youtube.com/watch?v=XcHDVwL-1W8: +- Call Of Duty WWII +- Call of Duty WW2 +https://www.youtube.com/watch?v=Cgh4CFzKCps: +- Cuphead +https://www.youtube.com/watch?v=mEgE5z6Sl7Y: +- Assassin's Creed 2 +https://www.youtube.com/watch?v=KmmNYTg-wSA: +- Donkey Kong +https://www.youtube.com/watch?v=dVPF-lboK14: +- God of War 3 +https://www.youtube.com/watch?v=vf2yKCCRE50: +- New Super Mario Bros Wii +https://www.youtube.com/watch?v=BqPgeWf8vNM: +- Minecraft +https://www.youtube.com/watch?v=23hJeaLotEw: +- The Legend of Zelda Skyward Sword +- Skyward Sword +https://www.youtube.com/watch?v=hMa4hZQbrms: +- Undertale +https://www.youtube.com/watch?v=LWVjNNKDYz8: +- Ryse Son of Rome +https://www.youtube.com/watch?v=EAwWPadFsOA: +- Mortal Kombat +https://www.youtube.com/watch?v=YEZhF_98cIc: +- Uncharted 4 +https://www.youtube.com/watch?v=LRKfd5EKBtI: +- Destiny +https://www.youtube.com/watch?v=TgjOBqD_ljk: +- Super Mario Galaxy +https://www.youtube.com/watch?v=cPWBG6_jn4Y: +- The Legend of Zelda Breath of the Wild +- Breath of the Wild +- Zelda Botw +https://www.youtube.com/watch?v=M-U3sVX2G3w: +- Metroid +https://www.youtube.com/watch?v=b62RgDBmly8: +- Fire Emblem +https://www.youtube.com/watch?v=d2tdSiQAF20: +- Sonic the Hedgehog 2006 +- Sonic the Hedgehog 06 +- Sonic 06 +- Sonic 2006 +https://www.youtube.com/watch?v=qI-Takf76RY: +- Mortal Kombat +https://www.youtube.com/watch?v=GFDvcR7Ozbg: +- Kingdom hearts Birth By Sleep +https://www.youtube.com/watch?v=F-QCjTBR0Pg: +- Halo 2 Anniversary +https://www.youtube.com/watch?v=c0SuIMUoShI: +- Super Mario Bros +https://www.youtube.com/watch?v=LnSp9rgfel8: +- Borderlands +https://www.youtube.com/watch?v=cpXXfCzSmJQ: +- Red Dead Redemption +https://www.youtube.com/watch?v=CMfx3MT8ge0: +- The Elder Scrolls 5 Skyrim +- Elder Scrolls 5 +- Skyrim +https://www.youtube.com/watch?v=LtmZJwbLjb0: +- Super Mario 3D World +https://www.youtube.com/watch?v=HGXo7LExG6o: +- Tetris +https://www.youtube.com/watch?v=aUS36INQoUw: +- Fire Emblem Shadow Dragon and the Blade of Light +- Shadow Dragon and the Blade of Light +https://www.youtube.com/watch?v=s7RRgF5Ve_E: +- Undertale +https://www.youtube.com/watch?v=MuVAaU63K7k: +- Sonic Heroes +https://www.youtube.com/watch?v=7lq5rr0RTeU: +- The Legend of Zelda Twilight Princess +- Twilight Princess +https://www.youtube.com/watch?v=S62IDJBdGWA: +- Mortal Kombat X +- Mortal Komba 10 +https://www.youtube.com/watch?v=_50vhftK66I: +- Super Mario World +https://www.youtube.com/watch?v=cZjSrW4p7r8: +- Sonic Adventure 2 +https://www.youtube.com/watch?v=e-nXonpnFHI: +- Monster Hunter Frontier G6 +https://www.youtube.com/watch?v=_dWMv2hfck0: +- Halo 2 +https://www.youtube.com/watch?v=SV0IF4XVBlE: +- Battlefield 1 +https://www.youtube.com/watch?v=rqIRZHPuiqE: +- Middle Earth Shadow of War +- Shadow of War +https://www.youtube.com/watch?v=qQGh4dy1cCA: +- Killzone 3 +https://www.youtube.com/watch?v=e9r5hx47kxM: +- Super Mario Odyssey +https://www.youtube.com/watch?v=SXKrsJZWqK0: +- Batman Arkham City +https://www.youtube.com/watch?v=Wbk8WeQU0rc: +- Far Cry +https://www.youtube.com/watch?v=4i8qAZOu5-g: +- Crash Bandicoot +https://www.youtube.com/watch?v=qEpaZR2Dvlg: +- Super Mario World +https://www.youtube.com/watch?v=-uEc8_dcYUc: +- Battlefield 4 +https://www.youtube.com/watch?v=jqE8M2ZnFL8: +- Grand Theft Auto 4 +https://www.youtube.com/watch?v=_kTBbTSjZpI: +- Shadow the Hedgehog +https://www.youtube.com/watch?v=QG77HTdreh0: +- Kid Icarus Uprising +https://www.youtube.com/watch?v=Z6NaZrPQGfY: +- Sonic Adventure 2 +https://www.youtube.com/watch?v=GkQUDi0pMAE: +- Marvel Ultimate Alliance +https://www.youtube.com/watch?v=o78T9-I4OGA: +- The Legend of Zelda The Wind Waker +- Zelda Wind Waker +- Zelda The Wind Waker +https://www.youtube.com/watch?v=dLDWYSQkUbE: +- Halo Combat Evolved +https://www.youtube.com/watch?v=mp9mzmq5Oas: +- Tokyo Mirage Sessions FE +https://www.youtube.com/watch?v=T5_vKsROSU0: +- Halo 2 +https://www.youtube.com/watch?v=MC0hV3dea9g: +- Sonic R +https://www.youtube.com/watch?v=dHkyqHlQ_is: +- Marvel Ultimate Alliance +https://www.youtube.com/watch?v=VfGeeZwv4Os: +- Diablo III +- Diablo 3 +https://www.youtube.com/watch?v=E3tkgU0pQmQ: +- Super Mario Sunshine +https://www.youtube.com/watch?v=53aDI5K49F4: +- Resident Evil 2 +https://www.youtube.com/watch?v=f43swtBDkuo: +- Marvel Ultimate Alliance +https://www.youtube.com/watch?v=cn144yWiZF4: +- Silent Hill 2 +https://www.youtube.com/watch?v=JSR5rROsvL0: +- Super Mario 3D Land +https://www.youtube.com/watch?v=CXxEUEK2_cs: +- Forza Motorsport 6 +https://www.youtube.com/watch?v=r8sB8N44eBE: +- Sonic Adventure (Theme of Knuckles) +- Sonic Adventure +https://www.youtube.com/watch?v=jrB6DLpIDaA: +- Rocket League +https://www.youtube.com/watch?v=6vY7S6iwwlQ: +- GTA San Andreas +- Grand Theft Auto San Andreas +https://www.youtube.com/watch?v=ndaJphlTPY8: +- Gears of War +https://www.youtube.com/watch?v=j9JWZ0mlX94: +- Luigi's Mansion +- Luigis Mansion +https://www.youtube.com/watch?v=zh4r_sXHyjc: +- Marvel Ultimate Alliance +https://www.youtube.com/watch?v=CvERHiTfx9w: +- Sonic Adventure 2 +https://www.youtube.com/watch?v=1TxEdJaZkY4: +- Dead by Daylight +https://www.youtube.com/watch?v=rxkiOLs06Z8: +- Injustice 2 +https://www.youtube.com/watch?v=bG0xho_yoaU: +- Marvel Ultimate Alliance +https://www.youtube.com/watch?v=xsyPHkeyz6o: +- Super Mario Galaxy 2 +https://www.youtube.com/watch?v=MdQvcgBmexw: +- Battlefront 2 +https://www.youtube.com/watch?v=6lR4nexN7RE: +- DOTA 2 +https://www.youtube.com/watch?v=m0NhFUw8mpI: +- Super Smash Bros Wii U +https://www.youtube.com/watch?v=E9s1ltPGQOo: +- Mii Channel +https://www.youtube.com/watch?v=QSBco8kVuZM: +- Fallout 3 +https://www.youtube.com/watch?v=Tz5Ld2STm2M: +- Ghost Recon +https://www.youtube.com/watch?v=oY9m2sHQwLs: +- Sonic R +https://www.youtube.com/watch?v=NF8kT_6giu8: +- Halo 2 +https://www.youtube.com/watch?v=EK3q3Jb3TCQ: +- The Elder Scrolls 5 Skyrim +- Elder Scrolls 5 +- Skyrim +https://www.youtube.com/watch?v=h4fOIVlEOIY: +- Assassin’s Creed Origins +- Assasins Creed Origins +https://www.youtube.com/watch?v=mziw3FQkZYg: +- Metroid Prime +https://www.youtube.com/watch?v=CXdNLpOrwFQ: +- Pikmin 3 +https://www.youtube.com/watch?v=RZFhVs6OGAc: +- Wii Fit +https://www.youtube.com/watch?v=UW-7Em3BYiA: +- Star Fox 64 +https://www.youtube.com/watch?v=9k-XrIGobsA: +- Super Mario 3D World +https://www.youtube.com/watch?v=kohY_vaMp0c: +- Bayonetta +https://www.youtube.com/watch?v=4roOuDyLkNo: +- Fallout 3 +https://www.youtube.com/watch?v=4wzRjZh3EkM: +- DOTA 2 +https://www.youtube.com/watch?v=OiwLQAA_V8E: +- Fire Emblem Fates +https://www.youtube.com/watch?v=UigzN-4JR14: +- Kingdom Hearts +https://www.youtube.com/watch?v=bIoPaeigMJw: +- Halo 2 +https://www.youtube.com/watch?v=JIE94pn4iJs: +- Killer Instinct +https://www.youtube.com/watch?v=Y9Jt52Q6GD4: +- Deadpool +https://www.youtube.com/watch?v=uNBzfe3TAEs: +- Shadow the Hedgehog +https://www.youtube.com/watch?v=PZVNi6L7g54: +- Mario Party 8 +https://www.youtube.com/watch?v=0uom8gJxe_8: +- Overwatch +https://www.youtube.com/watch?v=OCH6nQYflwY: +- Kirby's Dream Land +- Kirbys Dream Land +https://www.youtube.com/watch?v=PDURJZp3Sv4: +- World of Warcraft +- WoW +https://www.youtube.com/watch?v=3DYqiG1VmMQ: +- Diablo 2 +https://www.youtube.com/watch?v=xWSiztHV0us: +- Mortal Kombat Armageddon +https://www.youtube.com/watch?v=D6fkkVMsrAA: +- Super Smash Bros 3DS +- Mr Game and Watch +https://www.youtube.com/watch?v=tz82xbLvK_k: +- Undertale +https://www.youtube.com/watch?v=EV6E13xODyA: +- Bayonetta +https://www.youtube.com/watch?v=bq_jS6o3OoY: +- Super Mario 64 +https://www.youtube.com/watch?v=M3CSWLFL5u8: +- The Legend of Zelda The Wind Waker +- Zelda Wind Waker +- Zelda The Wind Waker +https://www.youtube.com/watch?v=PnzjqTvHJto: +- Destiny +https://www.youtube.com/watch?v=JntQ1X46qgg: +- Sonic Adventure 2 +https://www.youtube.com/watch?v=M9Ki3br3Qec: +- Deadrising +https://www.youtube.com/watch?v=bwaFADxmoeo: +- Splatoon +https://www.youtube.com/watch?v=Tc0fDGVQtJk: +- Metal Gear Solid +https://www.youtube.com/watch?v=ZvwQNlNIcr8: +- Forza Horizon 3 +https://www.youtube.com/watch?v=B7eRmpqtL40: +- Call of Duty Ghosts +https://www.youtube.com/watch?v=qURei6svd90: +- Doom II +- Doom 2 +https://www.youtube.com/watch?v=U1sYDWzhEog: +- Mario Party 8 +https://www.youtube.com/watch?v=aKKFjNVZkHU: +- Marvel vs Capcom 3 +https://www.youtube.com/watch?v=PDM2qukzKwg: +- Team Fortress 2 +https://www.youtube.com/watch?v=0nlJuwO0GDs: +- League of Legends +https://www.youtube.com/watch?v=UuL8nnyzf14: +- Fire Emblem The Sacred Stones +https://www.youtube.com/watch?v=ARFeJ3z6wes: +- Mario Party DS +https://www.youtube.com/watch?v=pNB4Cy4mftI: +- Sonic and the Black Knight +https://www.youtube.com/watch?v=GWba_XNUxtA: +- Quantum Break +https://www.youtube.com/watch?v=7XjrTV6M84c: +- Dead Rising 3 +https://www.youtube.com/watch?v=LjYWmShOoA4: +- Overwatch +https://www.youtube.com/watch?v=1pga6OLyrAg: +- Borderlands +https://www.youtube.com/watch?v=1OEyASR5C5U: +- Uncharted +https://www.youtube.com/watch?v=K-pJ1xMF7QE: +- League of Legends +https://www.youtube.com/watch?v=9q_MEnn5w0I: +- Minecraft +https://www.youtube.com/watch?v=5WduvI0CZ4c: +- Mega Man +https://www.youtube.com/watch?v=mB7veHQPF6M: +- Halo 2 +https://www.youtube.com/watch?v=mtxlFKP0OgY: +- Call of Duty Modern Warfare 2 +- Modern Warfare 2 +https://www.youtube.com/watch?v=2Jmty_NiaXc: +- Pokémon Red/Blue/Yellow +- Pokémon Red +- Pokemon Red +- Pokémon Blue +- Pokemon Blue +- Pokémon Yellow +- Pokemon Yellow +https://www.youtube.com/watch?v=6YQm2M-v4l0: +- Metal Gear +https://www.youtube.com/watch?v=r9spthA_nAA: +- Earthbound +https://www.youtube.com/watch?v=M_hUepNd52Q: +- Fallout New Vegas +https://www.youtube.com/watch?v=_X2A7qLDuLQ: +- GTA San Andreas +- Grand Theft Auto San Andreas +https://www.youtube.com/watch?v=8SM_39H-ztc: +- Kingdom Hearts +https://www.youtube.com/watch?v=Vj3dgToY_Fg: +- Ratchet and clank +https://www.youtube.com/watch?v=jgn3GSiYtEc: +- Middle earth Shadow of Mordor +- Shadow of Mordor +https://www.youtube.com/watch?v=sZ1f6BEkn_g: +- Paper Mario The Thousand-Year Door +- Paper Mario The Thousand Year Door +https://www.youtube.com/watch?v=T7AkuzZOSWM: +- DOTA 2 +https://www.youtube.com/watch?v=ixR9kOAR3ZY: +- Dragon Quest IX Sentinels of the Starry Skies +- Dragon Quest 9 +https://www.youtube.com/watch?v=WiQ1O5of0u4: +- Street Fighter II +- Street Fighter 2 +https://www.youtube.com/watch?v=0hEYvdMoF2g: +- The Legend of Zelda Ocarina of Time +- Zelda Ocarina of Time +https://www.youtube.com/watch?v=mK4mP7grUWY: +- Final Fantasy IX +- Final Fantasy 9 +https://www.youtube.com/watch?v=YzfEjZV46wM: +- Forza Motorsport +https://www.youtube.com/watch?v=vX7G4Qq6Li0: +- Shadow the Hedgehog +https://www.youtube.com/watch?v=95jDylAOsBQ: +- Assassins Creed +https://www.youtube.com/watch?v=EjazC45Qkww: +- Castlevania II Simon's Quest +- Castlevania 2 +https://www.youtube.com/watch?v=w09rc1VfWRQ: +- Forza Motorsport 4 +https://www.youtube.com/watch?v=Nc9w4QoSdfk: +- Tomb Raider +https://www.youtube.com/watch?v=VD9XmYnml_M: +- GTA V +- GTA 5 +- Grand Theft Auto 5 +https://www.youtube.com/watch?v=D5Z95cH7iDA: +- Ghost Recon Island Thunder +https://www.youtube.com/watch?v=o9Pu92St5O0: +- Metal Gear Solid 4 +https://www.youtube.com/watch?v=Bm2U7fqtSak: +- Ninja Gaiden +https://www.youtube.com/watch?v=NfCwWu8z8zE: +- Resident Evil +https://www.youtube.com/watch?v=zWHMj6ccSwA: +- Nintendo Land +https://www.youtube.com/watch?v=L8Vbg-1kPvg: +- Far Cry 5 +https://www.youtube.com/watch?v=hOju2ThS--M: +- Sonic the Hedgehog 2006 +- Sonic the Hedgehog 06 +- Sonic 06 +- Sonic 2006 +https://www.youtube.com/watch?v=EYW7-hNXZlM: +- Sonic CD +https://www.youtube.com/watch?v=ijEOqab3StE: +- Shadow the Hedgehog +https://www.youtube.com/watch?v=nJD-Ufi1jGk: +- The Elder Scrolls 3 Morrowind +- Elder Scrolls 3 +- Morrowind +https://www.youtube.com/watch?v=Iy4iQvJo24U: +- Crysis 2 +https://www.youtube.com/watch?v=o_ayLF9vdls: +- Dragon Age Origins +https://www.youtube.com/watch?v=LFcH84oNU6s: +- Skies of Arcadia +https://www.youtube.com/watch?v=9wMjq58Fjvo: +- Castle Crashers +https://www.youtube.com/watch?v=WA2WjP6sgrc: +- Donkey Kong Country Tropical Freeze +- Donkey Kong Tropical Freeze +https://www.youtube.com/watch?v=_Uzlm2MaCWw: +- MegaMan Maverick Hunter X +https://www.youtube.com/watch?v=Vm7aNxzWTJk: +- Prince of Persia +https://www.youtube.com/watch?v=viM0-3PXef0: +- The Witcher 3 +- Witcher 3 +https://www.youtube.com/watch?v=TO7UI0WIqVw: +- Super Smash Bros Brawl +https://www.youtube.com/watch?v=Y6ljFaKRTrI: +- Portal +https://www.youtube.com/watch?v=8yj-25MOgOM: +- Star Fox Zero +https://www.youtube.com/watch?v=W7rhEKTX-sE: +- Shovel Knight +https://www.youtube.com/watch?v=dTZ8uhJ5hIE: +- Kirby's Epic Yarn Music +- Kirbys Epic Yarn Music +https://www.youtube.com/watch?v=GQZLEegUK74: +- Goldeneye 007 +https://www.youtube.com/watch?v=zz8m1oEkW5k: +- Tetris Blitz +https://www.youtube.com/watch?v=ya3yxTbkh5s: +- Okami +https://www.youtube.com/watch?v=OYc_Vosp9_Y: +- The Legend of Zelda A Link Between Worlds +- Zelda A Link Between Worlds +https://www.youtube.com/watch?v=6LB7LZZGpkw: +- Silent Hill 2 +https://www.youtube.com/watch?v=TLYimAlnxEk: +- Mario Kart 8 +https://www.youtube.com/watch?v=-_Lwf6uaYOs: +- Kid Icarus Uprising +https://www.youtube.com/watch?v=H1etAFiAPYQ: +- Pikmin 3 +https://www.youtube.com/watch?v=jrbN2y5r-vU: +- Starcraft +https://www.youtube.com/watch?v=3o_RwQysgA8: +- The Last of Us +https://www.youtube.com/watch?v=75OlLWtV8gc: +- Pokémon Diamond/Pearl/Platinum +- Pokémon Diamond +- Pokemon Diamond +- Pokémon Pearl +- Pokemon Pearl +- Pokémon Platinum +- Pokemon Platinum +https://www.youtube.com/watch?v=sWTqNGUFF0g: +- Gunman Clive +https://www.youtube.com/watch?v=D4ragdexomw: +- Apollo Justice Ace Attorney +- Ace Attorney Apollo Justice +https://www.youtube.com/watch?v=poxDJHoYxCc: +- Gravity Rush +- Gravity Daze +https://www.youtube.com/watch?v=kwmOSMttZtM: +- Luigi's Mansion Dark Moon +- Luigis Mansion Dark Moon +https://www.youtube.com/watch?v=Ib19evMJJVI: +- PAYDAY The Heist +https://www.youtube.com/watch?v=7jVPwHqXz8A: +- Metroid Fusion +https://www.youtube.com/watch?v=utOa8ZMKwSA: +- Super Smash Bros Brawl +https://www.youtube.com/watch?v=2OOYZcVIp8w: +- Super Mario Galaxy +https://www.youtube.com/watch?v=u4jlk2x3Om0: +- Banjo-Kazooie Nuts & Bolts +- Banjo-Kazooie Nuts and Bolts +- Banjo Kazooie Nuts and Bolts +https://www.youtube.com/watch?v=mbsm2YXfAzo: +- Mortal Kombat Deception +https://www.youtube.com/watch?v=WB1I3PV7Ml8: +- Crash Bandicoot +https://www.youtube.com/watch?v=gy0T8nmw5GQ: +- Hitman 2 Silent Assassin +- Hitman 2 +https://www.youtube.com/watch?v=WuUVKOwUEA0: +- Super Mario Bros 3 +https://www.youtube.com/watch?v=6hgK3XJWSOk: +- Startropics +https://www.youtube.com/watch?v=_5iRzoSLWMM: +- The Legend of Zelda A Link to the Past +- A Link to the Past +https://www.youtube.com/watch?v=bV80prS5hBk: +- Silent Hill +https://www.youtube.com/watch?v=RDeJMAsrZMU: +- Sonic Adventure 2 +https://www.youtube.com/watch?v=QEmbOL3AAEs: +- Final Fantasy VI +- Final Fantasy 6 +https://www.youtube.com/watch?v=CoOTDI2KZco: +- Mega Man 3 +https://www.youtube.com/watch?v=Xeloqt4Wkcw: +- Streets Of Rage 2 +https://www.youtube.com/watch?v=wOQ5YyAorrw: +- Pokémon X & Y +- Pokemon X +- Pokemon Y +https://www.youtube.com/watch?v=CU1goWmsTRg: +- Duck Hunt +https://www.youtube.com/watch?v=oJqlpwN9mUM: +- Mario Party 2 +https://www.youtube.com/watch?v=ye78aoUtqOY: +- Pokémon GO +- Pokemon GO +https://www.youtube.com/watch?v=hf_D3pK6LVI: +- Phoenix Write Ace Attorney +https://www.youtube.com/watch?v=7qvudKHKozc: +- Shining Force II +- Shining Force 2 +https://www.youtube.com/watch?v=xPJcOybiCKM: +- Super Mario 64 +https://www.youtube.com/watch?v=IHxFTQYAqfc: +- Halo 3 +https://www.youtube.com/watch?v=0t9TlXAnDDM: +- Call of Juarez Gunslinger +https://www.youtube.com/watch?v=LuZy6A9luhU: +- Far Cry 3 +https://www.youtube.com/watch?v=HcvX3Md4uvM: +- Rainbow Six Vegas 2 +https://www.youtube.com/watch?v=RQYh342H5ec: +- Nintendo 3DS Nintendo Video +- Nintendo Video +https://www.youtube.com/watch?v=EMlQiS372Y0: +- Wii sports resort +https://www.youtube.com/watch?v=OtXHRSRQZvU: +- DK Rap +- Super Smash Bros Melee +- Donkey Kong 64 +https://www.youtube.com/watch?v=fmHEwxYdwcA: +- Mario & Luigi Bowsers Inside Story +- Bowsers Inside Story +- Mario and Luigi Bowsers Inside Story +https://www.youtube.com/watch?v=9cXiGjjLZAI: +- Pikmin 3 +https://www.youtube.com/watch?v=Q47iqNfsA80: +- The Legend of Zelda A Link Between Worlds +- A Link Between Worlds +https://www.youtube.com/watch?v=uQjcAvGTD7M: +- Professor Layton vs. Phoenix Wright Ace Attorney +- Professor Layton vs Phoenix Wright +https://www.youtube.com/watch?v=tBTb8H6vJe8: +- Donkey Kong 64 +https://www.youtube.com/watch?v=qQFseWcceDs: +- Pilotwings +https://www.youtube.com/watch?v=OVZ7EaqplsI: +- eShop Wii U +- eShop +https://www.youtube.com/watch?v=2hVsp2ncHoA: +- Marvel Ultimate Alliance +https://www.youtube.com/watch?v=kU36OmpCK_c: +- The Legend of Zelda Skyward Sword +- Skyward Sword +https://www.youtube.com/watch?v=7TCI6rdokD4: +- Street Fighter II +- Street Fighter 2 +https://www.youtube.com/watch?v=-unUysuRmWo: +- Sonic Adventure 2 +https://www.youtube.com/watch?v=QoIE04AqDgI: +- Kirby's Epic Yarn +- Kirbys Epic Yarn +https://www.youtube.com/watch?v=lSaDibas_Sw: +- Call of Duty Black Ops 3 +- Black Ops 3 +https://www.youtube.com/watch?v=HTUq3Ik1GHM: +- Kingdom Hearts II +- Kingdom Hearts 2 +https://www.youtube.com/watch?v=6ylyRyoUyH8: +- One Piece Pirate Warriors +https://www.youtube.com/watch?v=FJd6TMOZuJQ: +- Pac-Man +- Pac Man +https://www.youtube.com/watch?v=mBwmfDh7qtA: +- Dig Dug +https://www.youtube.com/watch?v=XjPF3AwVPM4: +- Finaly Fantasy +https://www.youtube.com/watch?v=KnoXA_EAnjo: +- Sonic Heroes +https://www.youtube.com/watch?v=XHpXEfa1kzk: +- Super Smash Bros Brawl +https://www.youtube.com/watch?v=mld2IxYXgt0: +- Super Robot Wars K +- SRW K +https://www.youtube.com/watch?v=Xi31NAktYDw: +- BioShock 2 +https://www.youtube.com/watch?v=YqyhezUoDj4: +- Call of Duty World at War +- World at War +https://www.youtube.com/watch?v=hOB4q01VCVg: +- Battlefield 1943 +https://www.youtube.com/watch?v=_cZoUYndFHo: +- Cuphead +https://www.youtube.com/watch?v=ojS8lCeUgsg: +- Deadpool +https://www.youtube.com/watch?v=2HTs2n0q-RA: +- Sonic Riders Zero Gravity +https://www.youtube.com/watch?v=yykgQClYlCk: +- TitanFall +https://www.youtube.com/watch?v=I6XMQYkPczI: +- Bravely Default +https://www.youtube.com/watch?v=q_TxCygjNIM: +- Deadrising +https://www.youtube.com/watch?v=2iLl_uusujo: +- STAR WARS The Old Republic +https://www.youtube.com/watch?v=KDyTjQmIWAw: +- Check Mii Out Channel Contest Menu Wii +- Check Mii Out +https://www.youtube.com/watch?v=VhjwXsDJQ2Y: +- Wii Sports +https://www.youtube.com/watch?v=fxMW5__vaDQ: +- Super Mario 64 +https://www.youtube.com/watch?v=4DJzm9cFGkg: +- Puyopuyo Tetris +- Puyo puyo Tetris +https://www.youtube.com/watch?v=G1MfL1oGN0E: +- Call of Duty Modern Warfare 3 +- Modern Warfare 3 +https://www.youtube.com/watch?v=bRLzfoySFWc: +- Assassin's Creed 4 Black Flag +- Assasins Creed 4 +- Assasins Creed Black Flag +https://www.youtube.com/watch?v=30lnA-haJiM: +- XBOX 360 Avatar Editor +- Avatar Editor +https://www.youtube.com/watch?v=czTksCF6X8Y: +- Spider-Man 2 The Game +- Spider Man 2 +- Spider-Man 2 +https://www.youtube.com/watch?v=_FAYOrpr-Fk: +- Wii Sports +https://www.youtube.com/watch?v=UbqoImQZ-EY: +- Touhou Embodiment of Scarlet Devil +- Touhou EoSD +- Embodiment of Scarlet Devil +https://www.youtube.com/watch?v=qIHqpjVxTtY: +- Super Stardust Delta +https://www.youtube.com/watch?v=VU7qTVkjMWU: +- World of Warcraft Wrath of the Lich King +- Wrath of the Lich King +- WoW WotLK +https://www.youtube.com/watch?v=YB2hQicc13Q: +- Super Smash Bros Melee +https://www.youtube.com/watch?v=9NBu_N9paNM: +- Metro Last Light +https://www.youtube.com/watch?v=aJOovInlk-w: +- Assassin's Creed 4 Black Flag +- Assasins Creed 4 +- Assasins Creed Black Flag +https://www.youtube.com/watch?v=6OYeyGi67UI: +- Call of Duty Ghosts +https://www.youtube.com/watch?v=IuLMkfG_Xu0: +- World of Warcraft +- WoW +https://www.youtube.com/watch?v=gR03h48G9rw: +- Destiny +https://www.youtube.com/watch?v=e87BLzzcwqQ: +- Halo Combat Evolved +https://www.youtube.com/watch?v=0_FquzflYYU: +- Luigis Mansion +- Luigi's Mansion +https://www.youtube.com/watch?v=I3XPus2T58o: +- The Legend of Zelda Twilight Princess +- Twilight Princess +https://www.youtube.com/watch?v=pVFYljt05hg: +- Metal Gear Solid 3 +https://www.youtube.com/watch?v=XHXVFBKZ9i8: +- Rhythm Heaven +https://www.youtube.com/watch?v=9hUT8VsLIG4: +- Pac-Man Championship Edition 2 +- Pac Man Championship Edition 2 +- Pac-Man CE 2 +- Pac Man CE 2 +https://www.youtube.com/watch?v=OziIZxI-Am4: +- Super Mario Maker +https://www.youtube.com/watch?v=frWaTmdN1Yk: +- Saga Frontier 2 +https://www.youtube.com/watch?v=r3m8_TDltwg: +- The Simpsons Hit & Run +- The Simpsons Hit and Run +https://www.youtube.com/watch?v=6kZ5TiSl2d8: +- Metal Gear Solid 2 +https://www.youtube.com/watch?v=CvL6fzXOJyY: +- Middle Earth Shadow of War +- Shadow of War +https://www.youtube.com/watch?v=zsxwbxS6LRM: +- Pocky & Rocky +- Pocky and Rocky +https://www.youtube.com/watch?v=YEo8vfvU6t8: +- Mario Golf World Tour +https://www.youtube.com/watch?v=q38jsbJ0afg: +- Marvel Ultimate Alliance 2 +https://www.youtube.com/watch?v=jbppvtGktvA: +- Super Street Fighter IV +- Super Street Fighter 4 +https://www.youtube.com/watch?v=kEdRtzNAh0g: +- Mortal Kombat +https://www.youtube.com/watch?v=SiLykPb_14g: +- Snipperclips +https://www.youtube.com/watch?v=2LhOV_ygYlY: +- Ghostbusters The Videogame +- Ghostbusters +https://www.youtube.com/watch?v=TUA9WwTV10M: +- Back to the Future the Game +- Back to the Future +https://www.youtube.com/watch?v=krISRDj6_2w: +- DSi Shop Theme +- DSi Shop +https://www.youtube.com/watch?v=R5DJENl__ZE: +- The Elder Scrolls 3 Morrowind +- Elder Scrolls 3 +- Morrowind +https://www.youtube.com/watch?v=2MDXwJ7Skik: +- Mortal Kombat 9 +https://www.youtube.com/watch?v=NKkXYxFh9M0: +- Killzone 3 +https://www.youtube.com/watch?v=5rfpGcIWe7E: +- BioShock 2 +https://www.youtube.com/watch?v=euJNO3xzprQ: +- Call of Duty Black Ops 3 +- Black Ops 3 +https://www.youtube.com/watch?v=L8EfpcF9rRc: +- Super Smash Bros Brawl +https://www.youtube.com/watch?v=LaM9YRUsDXs: +- Dark Souls +https://www.youtube.com/watch?v=nghTrcPBp3s: +- Yoshi's Story +- Yoshis Story +https://www.youtube.com/watch?v=l7vpszYDFEo: +- Destiny 2 +https://www.youtube.com/watch?v=ukMWZymYV-E: +- Borderlands +https://www.youtube.com/watch?v=ybhjox7LhmY: +- Mario Kart Wii +https://www.youtube.com/watch?v=9vevdlA1mFE: +- Pocky & Rocky 2 +- Pocky and Rocky 2 +https://www.youtube.com/watch?v=sZZgenIEL9o: +- Call of Duty Black Ops 4 +- Black Ops 4 +https://www.youtube.com/watch?v=OTF5gn5S0zs: +- Super Mario Bros Super Smash Bros Brawl +- Super Mario Bros +- Super Smash Bros Brawl +https://www.youtube.com/watch?v=OiWIpzor_Vk: +- Far Cry 5 +https://www.youtube.com/watch?v=h-xih93YIwk: +- Super Mario Bros 3 +https://www.youtube.com/watch?v=aHW0Vn-wRzs: +- Team Sonic Racing +https://www.youtube.com/watch?v=DehK_Y0TUbE: +- Angry Birds +https://www.youtube.com/watch?v=eVKj3u8JUm0: +- Super Mario 64 +https://www.youtube.com/watch?v=vCF_mG9T6-Y: +- Super Smash Bros Ultimate +- Super Smash Bros Melee +https://www.youtube.com/watch?v=o1rXwfuffwk: +- Battle Stadium D.O.N. +- Battle Stadium DON +https://www.youtube.com/watch?v=y_qHuDjE3CQ: +- Undertale +https://www.youtube.com/watch?v=kNK3gZnd3Qc: +- LEGO Marvel Super Heroes +https://www.youtube.com/watch?v=a6t_uyg_pF8: +- Final Fantasy VI +- Final Fantasy 6 +https://www.youtube.com/watch?v=Vc1wzDWFvf8: +- Super Smash Bros Melee +https://www.youtube.com/watch?v=4QR21UOXUTk: +- J Stars Victory Vs +https://www.youtube.com/watch?v=8avMLHvLwRQ: +- Wii Shop Channel +- Wii Shop +https://www.youtube.com/watch?v=d5c4KOopwLs: +- Wii Sports +https://www.youtube.com/watch?v=K5PNe0a04_I: +- Fire Emblem Echoes Shadows of Valentia +- Fire Emblem Echoes +- Shadows of Valentia +https://www.youtube.com/watch?v=8GxAi9b0ZvM: +- Super Mario Odyssey +https://www.youtube.com/watch?v=uTcOC04eEro: +- Yu-Gi-Oh! Duel Links +- Yu-Gi-Oh Duel Links +- YuGiOh Duel Links +- Yu Gi Oh Duel Links +https://www.youtube.com/watch?v=nTSXf2YVNUs: +- Final Fantasy III +- Final Fantasy 3 +https://www.youtube.com/watch?v=96uAHbo2f0o: +- Super Smash Bros Brawl +https://www.youtube.com/watch?v=AD0SP68Z2D0: +- Tom Clancys The Division +- The Division +https://www.youtube.com/watch?v=XmK8vhe6_yI: +- Destiny +https://www.youtube.com/watch?v=OM_qluyUGtY: +- Halo 2 +https://www.youtube.com/watch?v=KCQpsPFOEyU: +- Tom Clancys Rainbow Six Siege +- Rainbow Six Siege +- R6 Siege +https://www.youtube.com/watch?v=Rvi6c8toWJM: +- Counter-Strike Global Offensive +- CS GO +- CSGO +- Counter Strike GO +- Counter Strike Global Offensive +https://www.youtube.com/watch?v=WGkBan-hrR0: +- Ninja Gaiden 2 +https://www.youtube.com/watch?v=NPKG8HOyH50: +- Call of Duty Black Ops 4 +- Black Ops 4 +https://www.youtube.com/watch?v=Pva3UHPUHns: +- Luigis Mansion +https://www.youtube.com/watch?v=Gv__pDOd9R4: +- Donkey Kong 64 +https://www.youtube.com/watch?v=IJxIR35cWFg: +- Mario Party 8 +https://www.youtube.com/watch?v=6bWqgqcaQVs: +- Batman Arkham City +https://www.youtube.com/watch?v=FL6BRBzv8Ec: +- Anthem +https://www.youtube.com/watch?v=FG9uTzsqLNc: +- Counter-Strike Global Offensive +- CS GO +- CSGO +- Counter Strike GO +- Counter Strike Global Offensive +https://www.youtube.com/watch?v=-s9hGP8EPC4: +- Crackdown +https://www.youtube.com/watch?v=p7ew-8C3G_M: +- Killzone +https://www.youtube.com/watch?v=HyoZspjzaT8: +- Dragonball Z Budokai Tenkaichi 2 +https://www.youtube.com/watch?v=KveGWSYDxKY: +- Minecraft +https://www.youtube.com/watch?v=rXGdYmigdzw: +- Conker's Bad Fur Day +- Conkers Bad Fur Day +https://www.youtube.com/watch?v=bxJaHn4OcOk: +- Nintendo 3DS Camera Music +- 3DS Camera +https://www.youtube.com/watch?v=YZ3XjVVNagU: +- Undertale +https://www.youtube.com/watch?v=Rdeuye6BfmA: +- Marvel Ultimate Alliance +https://www.youtube.com/watch?v=rVzkd6B7LCA: +- Halo 3 +https://www.youtube.com/watch?v=IhQ45MHHZGA: +- Anthem +https://www.youtube.com/watch?v=rYpqRmsA94Y: +- Spider-Man PS4 +- Spider Man +https://www.youtube.com/watch?v=dFkuCBF-bf0: +- Sonic Colors +https://www.youtube.com/watch?v=jPfcnzPixBI: +- One Piece World Seeker +https://www.youtube.com/watch?v=KamjTqi4VjI: +- Kirby Air Ride +https://www.youtube.com/watch?v=7kCXNPZscFU: +- Dance Dance Revolution Mario Mix +- DDR Mario Mix +https://www.youtube.com/watch?v=CaXapWQeeeg: +- Spongebob Battle for Bikini Bottom +https://www.youtube.com/watch?v=cSiyDP_B5vY: +- The Witcher 3 Wild Hunt +- Witcher 3 +- Witcher Wild Hunt +https://www.youtube.com/watch?v=bGTWOWeFFw8: +- Marvel vs Capcom 3 +https://www.youtube.com/watch?v=Du0r5FOKbis: +- Bayonetta +https://www.youtube.com/watch?v=07mbmJHeBhg: +- Monopoly +https://www.youtube.com/watch?v=tPbnW2HmuIY: +- Super Mario Odyssey +https://www.youtube.com/watch?v=X6SVxsouuvE: +- Call of Duty Black Ops +- Black Ops +https://www.youtube.com/watch?v=zxgJTtumaV8: +- Tetris (Game Boy) - 10. Fanfare Victory (Varation) +https://www.youtube.com/watch?v=cOlCCc-_2sg: +- Dark Castle - Kirby's Dream Land 2 +https://www.youtube.com/watch?v=UDJtsfR51To: +- Xenoblade Chronicles OST - Gaur Plain +https://www.youtube.com/watch?v=tg-yC0xcI9s: +- Triggernometry - Red Dead Redemption Soundtrack +https://www.youtube.com/watch?v=rNNQBAj10n4: +- Marvel vs Capcom 3 +https://www.youtube.com/watch?v=bwhHD92bQUk: +- Sonic CD +https://www.youtube.com/watch?v=ZlTnLsjg59s: +- GTA V +- GTA 5 +- Grand Theft Auto 5 +https://www.youtube.com/watch?v=i-x22mnsUJ8: +- Gears 5 +- Gears of War 5 +https://www.youtube.com/watch?v=mOw3pZQctJ0: +- Call of Duty Advanced Warfare +https://www.youtube.com/watch?v=cnosICv2Mjg: +- Hotel Mario +https://www.youtube.com/watch?v=MMwM6hLEyeY: +- Sonic Mania +https://www.youtube.com/watch?v=XsliycURi0Y: +- Crackdown +https://www.youtube.com/watch?v=a51CkkWec1Q: +- Half-Life 2 +https://www.youtube.com/watch?v=iKBVXX8Vh7g: +- Call of Duty WWII +- Call of Duty WW2 +https://www.youtube.com/watch?v=qEvxbJROGuQ: +- Kirby Super Star +https://www.youtube.com/watch?v=_J4R6WsSKVs: +- Cuphead +https://www.youtube.com/watch?v=vcEGFAaSpzI: +- Final Fantasy VII +- Final Fantasy 7 +https://www.youtube.com/watch?v=cOWRNLaCMJg: +- Pokémon Red/Blue/Yellow +- Pokémon Red +- Pokemon Red +- Pokémon Blue +- Pokemon Blue +- Pokémon Yellow +- Pokemon Yellow +https://www.youtube.com/watch?v=XF2fA1epvLE: +- Super Mario World +https://www.youtube.com/watch?v=-tv0ZXF7uA4: +- GTA V +- GTA 5 +- Grand Theft Auto 5 +https://www.youtube.com/watch?v=3JncraxIMXY: +- Battlefield V +- Battlefield 5 +https://www.youtube.com/watch?v=fYnw_xoZCsg: +- Mario Teaches Typing 2 +https://www.youtube.com/watch?v=FQ73YtVm3Js: +- Spider-Man 2 The Game +- Spider Man 2 +- Spider-Man 2 +https://www.youtube.com/watch?v=nC5FK864qTk: +- Mortal Kombat Armageddon +https://www.youtube.com/watch?v=SnqPJv0UqKo: +- The Elder Scrolls 5 Skyrim +- Elder Scrolls 5 +- Skyrim +https://www.youtube.com/watch?v=pTE54izXTqc: +- Arkanoid R 2000 +- Arkanoid 2000 +- Arkanoid Returns +https://www.youtube.com/watch?v=FezNgPThD3M: +- Undertale +https://www.youtube.com/watch?v=DCX6c07rcJw: +- The World Ends With You +https://www.youtube.com/watch?v=hyQ6bn1Dno4: +- Sonic & All-Stars Racing Transformed +- Sonic and All Stars Racing Transformed +- Sonic and All-Stars Racing Transformed +- Sonic and Sega All-Stars Racing Transformed +- Sonic and Sega All Stars Racing Transformed +https://www.youtube.com/watch?v=okksreh2eQ4: +- Disney Universe +https://www.youtube.com/watch?v=3L97-DxwgRU: +- Billy The Wizard +https://www.youtube.com/watch?v=kY1F5QO5VOo: +- LittleBigPlanet +- Little Big Planet +https://www.youtube.com/watch?v=QEEPfzrDpoc: +- Bust a Groove +https://www.youtube.com/watch?v=JkO05j0zzZQ: +- LA Noire +https://www.youtube.com/watch?v=TNosg0EXQlU: +- Tom Clancys Rainbow Six Siege +- Rainbow Six Siege +- R6 Siege +https://www.youtube.com/watch?v=h38zN_vF_dQ: +- Fallout +https://www.youtube.com/watch?v=P9GX6Fyxr90: +- Snipperclips +https://www.youtube.com/watch?v=aKTJNO3HLEE: +- Xenoblade Chronicles +- Super Smash Bros Ultimate +https://www.youtube.com/watch?v=0gs56c9qGKA: +- Marvel Ultimate Alliance +https://www.youtube.com/watch?v=SzDmvY2pRJI: +- Yoshi's New Island +- Yoshis New Island +https://www.youtube.com/watch?v=IVH6Gl7W0hI: +- Super Mario Galaxy +https://www.youtube.com/watch?v=tZHcVMqXGhQ: +- Fire Emblem Echoes Shadows of Valentia +- Fire Emblem Echoes +- Shadows of Valentia +https://www.youtube.com/watch?v=Ltay2kMtCeM: +- Fortune Street +https://www.youtube.com/watch?v=mi3SE5j5XrU: +- Wizard101 +- Wizard 101 +https://www.youtube.com/watch?v=_nfld1C7RuE: +- Sonic and the Secret Rings +https://www.youtube.com/watch?v=nJB0OvU_vkg: +- Animal Crossing New Leaf +https://www.youtube.com/watch?v=xi0M9SIaLb4: +- Xenoblade Chronicles +https://www.youtube.com/watch?v=qw2lEHk5kDI: +- Gears of War 5 +- Gears 5 +https://www.youtube.com/watch?v=zlUWnUzpzSA: +- Halo 5 Guardians +https://www.youtube.com/watch?v=iUZyJHDasqA: +- Dark Souls II +- Dark Souls 2 +https://www.youtube.com/watch?v=jO3BZ9b1YsE: +- Nintendogs + Cats +- Nintendogs and Cats +https://www.youtube.com/watch?v=wDgQdr8ZkTw: +- Undertale +https://www.youtube.com/watch?v=O2MrJvaSjDE: +- Paper Mario Sticker Star +https://www.youtube.com/watch?v=umSJ--aGeYo: +- Portal +https://www.youtube.com/watch?v=JJOJdWubO-o: +- Plants vs Zombies Battle For Neighborville +- PvZ Battle for Neighborville +- Plants vs Zombies +https://www.youtube.com/watch?v=hUiAmrtxits: +- The Legend of Zelda The Wind Waker +- Zelda Wind Waker +- Zelda The Wind Waker +https://www.youtube.com/watch?v=1h9bU-y49-A: +- Hyper Street Fighter II The Anniversary Edition +- Hyper Street Fighter 2 +https://www.youtube.com/watch?v=sPWnlR5PEdQ: +- Call of Duty Advanced Warfare +https://www.youtube.com/watch?v=KF32DRg9opA: +- DuckTales +https://www.youtube.com/watch?v=rWatUe5yOP8: +- Sonic Heroes +https://www.youtube.com/watch?v=OTyghyyX34I: +- Paperboy 2 +https://www.youtube.com/watch?v=bGIP3IUgOg8: +- Super Mario Odyssey +https://www.youtube.com/watch?v=M8gJxIWSxGE: +- Battletoads +https://www.youtube.com/watch?v=btgi3TPL3AE: +- Castlevania +https://www.youtube.com/watch?v=rct233J9lsE: +- Dead Rising 2 Off The Record +- Dead Rising 2 +https://www.youtube.com/watch?v=Bz-QyMQDg_8: +- Life is Strange +https://www.youtube.com/watch?v=l2ETBzOAsV0: +- Sonic Forces +https://www.youtube.com/watch?v=6xzRa1EtOaw: +- Mario Strikers Charged +https://www.youtube.com/watch?v=6QpG48UEcI4: +- Fruit Ninja +https://www.youtube.com/watch?v=4n6WP9qHyRM: +- World of Warcraft +https://www.youtube.com/watch?v=R0cia14N9no: +- League of Legends +https://www.youtube.com/watch?v=iiNyQD5Yq3E: +- Star Wars Republic Commando +https://www.youtube.com/watch?v=nJ3o3kQZWm8: +- Parappa the Rapper +https://www.youtube.com/watch?v=2aV-ej4gwJU: +- Destiny 2 +https://www.youtube.com/watch?v=eCwzdl2q6Ho: +- King of Fighters XIII +- King of Fighters 13 +https://www.youtube.com/watch?v=PLDyWLbuptQ: +- Undertale +https://www.youtube.com/watch?v=3EdeEKDxhPE: +- Halo 3 +https://www.youtube.com/watch?v=em_oGE5Ht2Y: +- Resident Evil +https://www.youtube.com/watch?v=bQYuuUN7pSY: +- Mario Sports Mix +https://www.youtube.com/watch?v=cHJLdF2WNxI: +- World of Warcraft +https://www.youtube.com/watch?v=EXAb0Df3Rpc: +- Clannad +https://www.youtube.com/watch?v=LeeD7lWh2RE: +- Super Smash Bros Ultimate +https://www.youtube.com/watch?v=nJa7hJEfdww: +- Witcher 3 +https://www.youtube.com/watch?v=-cg5QXLS4nc: +- Cuphead +https://www.youtube.com/watch?v=8eDRM-YzpIs: +- Mario Sports Mix +https://www.youtube.com/watch?v=sunu9ShZ4xs: +- Mortal Kombat Armageddon +https://www.youtube.com/watch?v=n25nqibaIDg: +- Super Smash Bros Ultimate +https://www.youtube.com/watch?v=8KT7jcB72fQ: +- Donkey Kong 64 +https://www.youtube.com/watch?v=QCQ3DpI5hHk: +- Plants Vs Zombies +https://www.youtube.com/watch?v=Ze3G5EksIQw: +- The Stanley Parable +- Stanley Parable +https://www.youtube.com/watch?v=9pPQnhhUqtU: +- Total Distortion +https://www.youtube.com/watch?v=WSig9qC9tWE: +- Pokemon Sword and Shield +- Pokemon Sword +- Pokemon Shield +https://www.youtube.com/watch?v=5_E_y1AWAfc: +- Undertale +https://www.youtube.com/watch?v=TsfRhKE5iP8: +- Tomb Raider +https://www.youtube.com/watch?v=n5mo2zPBl3k: +- Middle Earth Shadow of Modor +- Shadow of Modor +https://www.youtube.com/watch?v=Z2g-xpGOusI: +- Tom Clancys The Division +- The Division +https://www.youtube.com/watch?v=S5DFhaprlRM: +- Super Smash Bros +https://www.youtube.com/watch?v=mtfsZv_wr98: +- Tekken +https://www.youtube.com/watch?v=Y8V_MvH8y8U: +- Fallout New Vegas +https://www.youtube.com/watch?v=4eGCH6tAycE: +- Xenoblade 2 +https://www.youtube.com/watch?v=HwJgHF0xDbU: +- Street Fighter V +- Street Fighter 5 +https://www.youtube.com/watch?v=taNV00DWLq8: +- ARMS +https://www.youtube.com/watch?v=9XzAPa4ji7M: +- Punch-Out!! +- Punch Out +- Punch-Out +https://www.youtube.com/watch?v=el_r5y_AcrE: +- Super Mario Sunshine +https://www.youtube.com/watch?v=7dRVuXYmB1M: +- Deltarune +https://www.youtube.com/watch?v=E_KMghGlDY8: +- Nintendo 3DS Music StreetPass Mii Plaza +- Nintendo 3DS +- StreetPass +- Mii Plaza +https://www.youtube.com/watch?v=QJbYjj5cNqQ: +- Wolfenstein The New Order +https://www.youtube.com/watch?v=qHpKdriwcOg: +- Kingdom Hearts +https://www.youtube.com/watch?v=jxMzCya8pQM: +- Payday 2 +https://www.youtube.com/watch?v=LQdUAMrcEnw: +- Farcry 5 +https://www.youtube.com/watch?v=r5Q4FOSPSx0: +- The Witcher 2 Assasins of Kings +- The Witcher 2 +https://www.youtube.com/watch?v=hU3K2a4uXxs: +- Sonic Unleashed +https://www.youtube.com/watch?v=2UKw9aheG3Y: +- Mafia 2 +https://www.youtube.com/watch?v=YTy9v9a7Tmo: +- Undertale +https://www.youtube.com/watch?v=rU_im8hx2TI: +- Earthbound +https://www.youtube.com/watch?v=sIL2fuxlMkc: +- Super Mario Galaxy +https://www.youtube.com/watch?v=-KBLgp5_smc: +- Mortal Kombat 9 +https://www.youtube.com/watch?v=Nsps0I58yUM: +- Dark Souls +https://www.youtube.com/watch?v=RjLWGyx4zoA: +- Persona 5 +https://www.youtube.com/watch?v=XXNVnq1r3yY: +- Castlevania Harmony of Despair +https://www.youtube.com/watch?v=6qIjHtI3kGU: +- Far Cry 5 +https://www.youtube.com/watch?v=3FRJU4DGZSc: +- Red Dead Redemption 2 +https://www.youtube.com/watch?v=zKYmW19Sk8s: +- Sekiro +https://www.youtube.com/watch?v=NYoP3_p2-N8: +- Super Mario 3D Land +https://www.youtube.com/watch?v=XnV_wHQ01dU: +- Cuphead +https://www.youtube.com/watch?v=MXAHCZXaoy8: +- Pokémon Red/Blue/Yellow +- Pokémon Red +- Pokemon Red +- Pokémon Blue +- Pokemon Blue +- Pokémon Yellow +- Pokemon Yellow +https://www.youtube.com/watch?v=56xw3mryadY: +- Mega Man 11 +https://www.youtube.com/watch?v=1K1rV9kFs6I: +- Sonic 3 +https://www.youtube.com/watch?v=7RiHmQcHa84: +- Shin Megami Tensei III Nocturne +- Shin Megami Tensei 3 Nocturne +- Shin Megami Tensei 3 +https://www.youtube.com/watch?v=aPGwniiEfIk: +- Escape from Mars Starring Taz +- Taz in Escape from Mars +- Escape from Mars +https://www.youtube.com/watch?v=kZnS_HxSdvo: +- Nintendo 3DS Find Mii +- Find Mii +https://www.youtube.com/watch?v=JEj_8VkTHVw: +- Mario + Rabbids Kingdom Battle +- Mario and Rabbids Kingdom Battle +https://www.youtube.com/watch?v=n8yKpcyU0fg: +- Hot Wheels Beat That +https://www.youtube.com/watch?v=cIKpAjB5B00: +- Super Hero Squad Online +https://www.youtube.com/watch?v=dYvvKyYIc5s: +- Touhou Embodiment of Scarlet Devil +- Touhou EoSD +- Embodiment of Scarlet Devil +https://www.youtube.com/watch?v=0BkHICB4eJs: +- Half Life +https://www.youtube.com/watch?v=dqwjST3UYcc: +- Mass Effect +https://www.youtube.com/watch?v=WQJxqXo8_cY: +- Tom Clancys Rainbow Six Siege +- Rainbow Six Siege +- R6 Siege +https://www.youtube.com/watch?v=4xbXFcadgy0: +- For Honor +https://www.youtube.com/watch?v=MJrZKCZ2pqo: +- Mario Kart Wii +https://www.youtube.com/watch?v=GgbXr4rN4D4: +- Banjo-Kazooie +- Banjo Kazooi +https://www.youtube.com/watch?v=oL5fbozc3kU: +- Yoshi's Island +- Yoshis Island +https://www.youtube.com/watch?v=DMDYpRaUvIQ: +- Sonic R +https://www.youtube.com/watch?v=oOMy4AZTPLc: +- Metropolis Street Racer +https://www.youtube.com/watch?v=Zf2qOWmKiz0: +- Deltarune +https://www.youtube.com/watch?v=6jFaoLrLzd4: +- Persona 3 +https://www.youtube.com/watch?v=UKYXOobaceQ: +- Sonic 3D Blast +https://www.youtube.com/watch?v=R9awS7E0jDc: +- Donkey Konga +https://www.youtube.com/watch?v=-VhWTfHZBOI: +- Contrast +https://www.youtube.com/watch?v=6QaH6tOXHek: +- Miitomo +https://www.youtube.com/watch?v=-GK41pLUuP0: +- New Super Mario Bros +https://www.youtube.com/watch?v=xjUHMsAZti4: +- Earthbound +https://www.youtube.com/watch?v=y4y13vnGXec: +- Psycho Soldier +https://www.youtube.com/watch?v=sEhg1qAmWMg: +- Codename Kids Next Door Operation V.I.D.E.O.G.A.M.E. +- Codename KND Operation VIDEOGAME +- Codename Kids Next Door Operation VIDEOGAME +- Codename Kids Next Door +https://www.youtube.com/watch?v=TAQVE2e3u8o: +- Power Rangers +https://www.youtube.com/watch?v=NRcsLULM2ns: +- Happy Wheels +https://www.youtube.com/watch?v=pU_zIsgnb3w: +- Hitman Blood Money +https://www.youtube.com/watch?v=ZXX8f0JcSG4: +- Pokémon Black & White +- Pokemon Black and White +- Pokemon Black +- Pokemon White +https://www.youtube.com/watch?v=AWkOMVUJ7gs: +- Fire Emblem Fates Condemnation +- Fire Emblem Fates +https://www.youtube.com/watch?v=QyPR77rg1to: +- Undertale +https://www.youtube.com/watch?v=sxJ-LSBfrQ4: +- Splatoon 2 +https://www.youtube.com/watch?v=RZVyHH-voR8: +- Dark Souls +https://www.youtube.com/watch?v=nWfRO1yxaXs: +- Wario world +- Warioworld +https://www.youtube.com/watch?v=bMF_PRSXbXk: +- Half-Life Opposing Force +https://www.youtube.com/watch?v=XVpC7Tg-hH4: +- Dreams +https://www.youtube.com/watch?v=EkLvDaWNez8: +- New Super Mario Bros +https://www.youtube.com/watch?v=7LvsWVE9-es: +- Pokémon X & Y +- Pokemon X +- Pokemon Y +https://www.youtube.com/watch?v=iAWXT2HJFTI: +- The Legend of Zelda Breath of the Wild +- Breath of the Wild +- Zelda Botw +https://www.youtube.com/watch?v=JZw9wWmcXH4: +- Super Smash Bros Brawl +https://www.youtube.com/watch?v=XJsy5jJ7Dp0: +- Trauma Center Under the Knife 2 +- Trauma Center 2 +- Trauma Center Under the Knife +https://www.youtube.com/watch?v=gd22rCkvJoA: +- Assassin's Creed 4 Black Flag +- Assasins Creed 4 +- Assasins Creed Black Flag +https://www.youtube.com/watch?v=ECE7ZeDsuPk: +- Destiny 2 +https://www.youtube.com/watch?v=v0nlnUDKT1s: +- Fallout 4 +https://www.youtube.com/watch?v=zgLgSQPN0Gw: +- Super Mario Party +https://www.youtube.com/watch?v=Ib3FTf3Frbo: +- Fire Emblem Three Houses +- Super Smash Bros Ultimate +https://www.youtube.com/watch?v=LG0-UMOljBU: +- Pokémon Gold/Silver/Crystal +- Pokemon Gold +- Pokemon Silver +- Pokemon Crystal +https://www.youtube.com/watch?v=0cuhPPuTA3s: +- Saints Row The Third +https://www.youtube.com/watch?v=1scgMvQoLVg: +- Tomodachi Life +https://www.youtube.com/watch?v=D8ShsHeYkeI: +- Dark Souls 2 +https://www.youtube.com/watch?v=BG-0WXsZR-A: +- Marvel vs Capcom 2 +https://www.youtube.com/watch?v=NPjLgDF1hUg: +- The Legend of Zelda Ocarina of Time +- Zelda Ocarina of Time +https://www.youtube.com/watch?v=upPtP_qtQHM: +- Super Paper Mario +https://www.youtube.com/watch?v=5xk8-dDqu50: +- Luigi's Mansion +- Luigis Mansion +https://www.youtube.com/watch?v=zop1cFVz1So: +- Killzone +https://www.youtube.com/watch?v=WLLE_PvJ5mY: +- Metroid Prime +https://www.youtube.com/watch?v=oMed0AQt6g4: +- Mass Effect +https://www.youtube.com/watch?v=S7NuuyVIYO8: +- Hitman +https://www.youtube.com/watch?v=ENWDVA0Xz88: +- Crackdown +https://www.youtube.com/watch?v=dQNAVqW1shA: +- Banjo-Kazooie +- Banjo Kazooie +https://www.youtube.com/watch?v=tnzziAe9tYA: +- Super Mario Galaxy 2 +https://www.youtube.com/watch?v=Ma7mdIHy-kA: +- Sonic Forces +https://www.youtube.com/watch?v=avyasO9uqfo: +- Donkey Kong Country +https://www.youtube.com/watch?v=i6v6w0mbPvU: +- A Hat in Time Music +https://www.youtube.com/watch?v=Df1nqnxRe7g: +- Left 4 Dead 2 +https://www.youtube.com/watch?v=QunjkirEY_g: +- Fire Emblem Fates +https://www.youtube.com/watch?v=-9bcB4_adYo: +- Crysis +https://www.youtube.com/watch?v=QMEqfXYjyvM: +- Deadspace +https://www.youtube.com/watch?v=a75SxyVBvKU: +- Half-Life 2 +- Half Life 2 +https://www.youtube.com/watch?v=5h1t94LxGd0: +- The Witcher 2 Assasins of Kings +- The Witcher 2 +https://www.youtube.com/watch?v=Eoo5oifOsUs: +- DayZ +https://www.youtube.com/watch?v=24ijaaCSrYM: +- For Honor +https://www.youtube.com/watch?v=sGjdjdyt5s4: +- Kirby Planet Robobot +https://www.youtube.com/watch?v=oA6kuCUmBpo: +- Captain Toad Treasure Tracker +https://www.youtube.com/watch?v=x9dJrEvHk3o: +- Mario & Sonic at the London 2012 Olympic Games +- Mario and Sonic at the London 2012 Olympic Games +https://www.youtube.com/watch?v=DheEtF1G_Rs: +- Splatoon 2 +https://www.youtube.com/watch?v=TcufdwbnF0k: +- Rhythm Heaven Fever +https://www.youtube.com/watch?v=4GChwuX81o4: +- Grabbed by the Ghoulies +https://www.youtube.com/watch?v=PhWge4ro354: +- Call of Duty Black Ops 2 +- Black Ops 2 +https://www.youtube.com/watch?v=bzbWrGr_of0: +- The Legend of Zelda Majora's Mask +- Zelda Majora's Mask +- Zelda Majoras Mask +https://www.youtube.com/watch?v=2RKnfOAI1A0: +- 3D Dot Game Heroes +https://www.youtube.com/watch?v=L2f5v1w_v8g: +- Conker Live & Reloaded +- Conker Live and Reloaded +https://www.youtube.com/watch?v=nVRQJ2i59XE: +- Wii Sports +https://www.youtube.com/watch?v=JDqJa1RC3q8: +- Kid Icarus Uprising +https://www.youtube.com/watch?v=Jq6oRi6UTXY: +- Sonic the Hedgehog 3 +https://www.youtube.com/watch?v=JzzJwrIN6Mc: +- Mega Man 3 +https://www.youtube.com/watch?v=s1AkaZuRaM4: +- Sonic Dash +https://www.youtube.com/watch?v=7S7WEkopK8k: +- Dance Dance Revolution Mario Mix +- Dance Dance Revolution Mario +https://www.youtube.com/watch?v=KVwdKcwWe2k: +- Yooka-Laylee Music +- Yooka Laylee +https://www.youtube.com/watch?v=ZdBO_V-guJ4: +- Call of Duty Modern Warfare +- Modern Warfare +https://www.youtube.com/watch?v=NQIeAbFT4Kc: +- League of Legends +https://www.youtube.com/watch?v=cnAoE7tcrBI: +- Max Payne +https://www.youtube.com/watch?v=pYf0C4Yh8pE: +- Apex Legends +https://www.youtube.com/watch?v=0W-trrXXcS8: +- Far Cry New Dawn +https://www.youtube.com/watch?v=jv249KSsjcM: +- Red Dead Redemption 2 +https://www.youtube.com/watch?v=z5JhMEP0GSU: +- Mario Party The Top 100 +- Mario Party Top 100 +https://www.youtube.com/watch?v=3nxFYoGNJKc: +- Banjo-Kazooie +- Banjo Kazooie +https://www.youtube.com/watch?v=6vICKfFo6bU: +- Wii Fit +https://www.youtube.com/watch?v=EJCq_hnFZyk: +- Fire Emblem Three Houses +https://www.youtube.com/watch?v=bXM7CvKCSx0: +- GTA San Andreas +- Grand Theft Auto San Andreas +https://www.youtube.com/watch?v=5YjGmTI_fPU: +- Bayonetta +https://www.youtube.com/watch?v=5wApmRF5gyM: +- Super Mario Bros 3 +https://www.youtube.com/watch?v=kRMTqfxkCsc: +- Mortal Kombat Deadly Alliance +https://www.youtube.com/watch?v=Ljqe4Nj7nBA: +- The Legend of Zelda Ocarina of Time +- Zelda Ocarina of Time +https://www.youtube.com/watch?v=RB4nFoA63rs: +- Gravity Rush +https://www.youtube.com/watch?v=UsaoAl5EIH8: +- Punch-Out!! +- Punch Out +- Punch-Out +https://www.youtube.com/watch?v=rMudHClToL0: +- Donkey Kong Country Tropical Freeze +- Donkey Kong Tropical Freeze +https://www.youtube.com/watch?v=iT8T-dBwztg: +- FlingSmash +https://www.youtube.com/watch?v=dcp5gPf7mhI: +- Jet Set Radio Future +https://www.youtube.com/watch?v=sC0cvwnG0Ik: +- Crazy Bus +https://www.youtube.com/watch?v=KJfMS5XzmIc: +- Animal Crossing New Horizons +https://www.youtube.com/watch?v=HAYPkVeWjXM: +- Halo 5 Guardians +https://www.youtube.com/watch?v=ozLqx3EboU4: +- Shantae Half-Genie Hero +- Shantae Half Genie Hero +- 'Shantae 1/2 Genie Hero' +https://www.youtube.com/watch?v=e2Gyaqf7EoU: +- Persona 3 +https://www.youtube.com/watch?v=KC5kHf58GMI: +- The Legend of Zelda Skyward Sword +- Skyward Sword +https://www.youtube.com/watch?v=XHzlfCpmqsw: +- Super Mario Kart +https://www.youtube.com/watch?v=6l1HUDuzyrk: +- Sonic Lost World +https://www.youtube.com/watch?v=512pcIZOjm0: +- Xenoblade Chronicles +https://www.youtube.com/watch?v=7ePFxuu9lik: +- The Walking Dead Game +- The Walking Dead +https://www.youtube.com/watch?v=rgq0wF_ByO8: +- Contrast +https://www.youtube.com/watch?v=O7eGxoFLOI4: +- Mario Kart Double Dash!! +- Mario Kart Double Dash +https://www.youtube.com/watch?v=6ppBSY92rzg: +- EarthBound +https://www.youtube.com/watch?v=mmiWjG0N21w: +- Kingdom Hearts +https://www.youtube.com/watch?v=kNlVo5udnPs: +- Team Fortress 2 +https://www.youtube.com/watch?v=-t_C-CgJ-3A: +- Pokemon Sun & Moon +- Pokemon Sun +- Pokemon Moon +https://www.youtube.com/watch?v=7G_aaak-tDE: +- The Legend of Zelda Majora's Mask +- Zelda Majora's Mask +- Zelda Majoras Mask +https://www.youtube.com/watch?v=rGFic2e-uTE: +- Kirby's Epic Yarn +- Kirbys Epic Yarn +https://www.youtube.com/watch?v=1Cfin7GJUGI: +- Mario vs Donkey Kong 2 March of the Minis +- Mario vs Donkey Kong 2 +https://www.youtube.com/watch?v=gtKxgf_lETg: +- Rhythm Heaven Fever +https://www.youtube.com/watch?v=xnmxhE9usbI: +- Animal Crossing +https://www.youtube.com/watch?v=kwQ6_rwvMp8: +- ARMS +https://www.youtube.com/watch?v=2odP8HQuPgs: +- Nintendogs +https://www.youtube.com/watch?v=9ICz9BArKiM: +- Splatoon 2 +https://www.youtube.com/watch?v=IeosKeE1psE: +- Super Street Fighter IV +- Super Street Fighter 4 +https://www.youtube.com/watch?v=P9lLD_hgbU4: +- Bayonetta +https://www.youtube.com/watch?v=QTRsbrFWomM: +- Fire Emblem Fates +https://www.youtube.com/watch?v=zwOnn-ObtNo: +- Chibi-Robo! +- Chibi-Robo +- Chibi Robo +https://www.youtube.com/watch?v=5FYneJvqtMc: +- Lego Dimensions +https://www.youtube.com/watch?v=jkk6f817nNU: +- Xenoblade Chronicles +https://www.youtube.com/watch?v=N8OHSXvneOE: +- Celeste +https://www.youtube.com/watch?v=ryAv7ESrJtQ: +- Half Life 2 +https://www.youtube.com/watch?v=vkailb3xcTI: +- Super Mario RPG +https://www.youtube.com/watch?v=MOkWNuGfCD0: +- Pokémon X & Y +- Pokemon X +- Pokemon Y +https://www.youtube.com/watch?v=JUYAX3z2JYc: +- Call of Duty Black Ops 2 +- Black Ops 2 +https://www.youtube.com/watch?v=kfo7hmy-qso: +- Bomberman Hero +https://www.youtube.com/watch?v=myxM9Q72hxE: +- NES Remix Pack +https://www.youtube.com/watch?v=PbYoMpwHq5c: +- Wii Play +https://www.youtube.com/watch?v=-NuIPxOmpmc: +- Star Fox +https://www.youtube.com/watch?v=Ga9wY3CzEJQ: +- Gex Enter the Gecko +- Gex +https://www.youtube.com/watch?v=mpqct1zMqTE: +- Killer Instinct S1 +- Killer Instinct +https://www.youtube.com/watch?v=g3jCAyPai2Y: +- Yakuza +https://www.youtube.com/watch?v=4GfXKM-2nak: +- Paper Mario The Origami King +https://www.youtube.com/watch?v=6rW52ajkrZ0: +- Street Fighter V +- Street Fighter 5 +https://www.youtube.com/watch?v=zLs96hgloOA: +- Sekiro +https://www.youtube.com/watch?v=qG_lzlLH3UU: +- Titanfall +https://www.youtube.com/watch?v=U8ilLSEjlAQ: +- Destiny 2 +https://www.youtube.com/watch?v=QFaZC_R3WoE: +- Resident Evil 3 +https://www.youtube.com/watch?v=1fmhHRNzZLo: +- Gears Of War +https://www.youtube.com/watch?v=Oc44myRhNRM: +- Pokemon Omega Ruby/Alpha Sapphire +- Pokemon Omega Ruby +- Pokemon Alpha Sapphire +https://www.youtube.com/watch?v=vNVz6N-bNZs: +- Sonic Adventure +https://www.youtube.com/watch?v=WcabIuZOWYY: +- DragonBall Xenoverse 2 +https://www.youtube.com/watch?v=GmpB0btoC6M: +- Yoshi's Cookie +- Yoshis Cookie +https://www.youtube.com/watch?v=KC-B3Q0cRrY: +- Streets Of Rage 2 +https://www.youtube.com/watch?v=DekKId4f-Yk: +- Super Mario 3D World +https://www.youtube.com/watch?v=xorWgC8vf3I: +- Fallout 3 +https://www.youtube.com/watch?v=X9Q4Zg1tYno: +- Super Smash Bros Wii U +https://www.youtube.com/watch?v=llOvd2yGEio: +- Team Fortress 2 +https://www.youtube.com/watch?v=RR_ifniLcW4: +- Donkey Kong Country 2 Diddy's Kong Quest +- Donkey Kong Country 2 +- Diddys Kong Quest +https://www.youtube.com/watch?v=NoBRdekWKxI: +- The Legend of Zelda Twilight Princess +- Twilight Princess +https://www.youtube.com/watch?v=rpocm85Hauc: +- Crash Tag Team Racing +https://www.youtube.com/watch?v=-4FvqXfQYbk: +- Dragon Ball FighterZ +https://www.youtube.com/watch?v=xgcyH9I1NBE: +- Sonic Adventure +https://www.youtube.com/watch?v=gESYyh_tsGA: +- Halo Infinite +https://www.youtube.com/watch?v=fXWCotTeNgk: +- Donkey Kong 64 +https://www.youtube.com/watch?v=tWBKtLiYe5w: +- Persona 4 Dancing All Night +- Persona 4 +https://www.youtube.com/watch?v=pQ-bjZD1EnI: +- Sonic Heroes +https://www.youtube.com/watch?v=4S9qb1jZGlA: +- Ghost of Tsushima +https://www.youtube.com/watch?v=v1M8VVDFxgo: +- Celeste +https://www.youtube.com/watch?v=ROBc_MVW-Lg: +- Fall Guys Ultimate Knockout +- Fall Guys +https://www.youtube.com/watch?v=r5EjvtyLJ1g: +- Sonic the Hedgehog CD +- Sonic CD +https://www.youtube.com/watch?v=JRPXRHS4XNQ: +- Battletoads +https://www.youtube.com/watch?v=hHKmORyO8WA: +- Mario Super Sluggers +https://www.youtube.com/watch?v=oeEiOtndLB0: +- Crusader Kings 2 +https://www.youtube.com/watch?v=TKvjEQXKeec: +- Earthbound +https://www.youtube.com/watch?v=m65ns26m9IE: +- A Hat in Time +https://www.youtube.com/watch?v=feM7-QL9x38: +- Bomberman Max 2 +https://www.youtube.com/watch?v=EGeoG0KQa68: +- Mappy Arcade +- Mappy +https://www.youtube.com/watch?v=ySYwqTKRtgQ: +- Mega Man 2 +https://www.youtube.com/watch?v=VCCZe8S5miY: +- Pac-Man World +- Pac Man World +https://www.youtube.com/watch?v=g-aNgman8Wo: +- Street Fighter 5 +https://www.youtube.com/watch?v=U1_PStq_tTM: +- Watch Dogs +https://www.youtube.com/watch?v=vLljFmx7yF8: +- Counter-Strike Global Offensive +- CS GO +- CSGO +- Counter Strike GO +- Counter Strike Global Offensive +https://www.youtube.com/watch?v=gsZJg5mmsHA: +- The Angry Video Game Nerd Adventures +- Angry Video Game Nerd Adventures +https://www.youtube.com/watch?v=n3c2xbYWKY0: +- Double Dragon +https://www.youtube.com/watch?v=JNEKqu6Pe48: +- Pac Man Party +- Pac-Man Party +https://www.youtube.com/watch?v=sEFIZh_Zscc: +- Bioshock +https://www.youtube.com/watch?v=fmU9MXi9Uz0: +- Under Night In-Birth +- Under Night In Birth +https://www.youtube.com/watch?v=EzDLLILqvLU: +- Asura's Wrath +- Asuras Wrath From b9d8be397c04053d3542c2acc8e5b5f30fd9c4ca Mon Sep 17 00:00:00 2001 From: bobloy Date: Mon, 28 Sep 2020 09:50:43 -0400 Subject: [PATCH 093/121] Add workflow labeler --- .github/labeler.yml | 62 +++++++++++++++++++++++++++++++++++ .github/workflows/labeler.yml | 0 2 files changed, 62 insertions(+) create mode 100644 .github/labeler.yml create mode 100644 .github/workflows/labeler.yml diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000..dd944c8 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,62 @@ +'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 \ No newline at end of file diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 0000000..e69de29 From ad66d171d4035dc17794b346563a87d5f806d61e Mon Sep 17 00:00:00 2001 From: bobloy Date: Mon, 28 Sep 2020 09:52:17 -0400 Subject: [PATCH 094/121] Forgot the contents --- .github/workflows/labeler.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index e69de29..7c724a6 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -0,0 +1,19 @@ +# 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] + +jobs: + label: + + runs-on: ubuntu-latest + + steps: + - uses: actions/labeler@v2 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" From af41d079d336426626f7dbfb5c948579fd17781f Mon Sep 17 00:00:00 2001 From: bobloy Date: Mon, 28 Sep 2020 10:52:45 -0400 Subject: [PATCH 095/121] Add black check --- .github/workflows/black_check.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .github/workflows/black_check.yml diff --git a/.github/workflows/black_check.yml b/.github/workflows/black_check.yml new file mode 100644 index 0000000..ab2a435 --- /dev/null +++ b/.github/workflows/black_check.yml @@ -0,0 +1,20 @@ +# 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: autoblack +on: [push, pull_request] +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - 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 . \ No newline at end of file From a36a800b45d105b6f6dced7c250f0e5efeb178f9 Mon Sep 17 00:00:00 2001 From: bobloy Date: Mon, 28 Sep 2020 10:53:00 -0400 Subject: [PATCH 096/121] named black --- .github/workflows/black_check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/black_check.yml b/.github/workflows/black_check.yml index ab2a435..a36b5f2 100644 --- a/.github/workflows/black_check.yml +++ b/.github/workflows/black_check.yml @@ -3,7 +3,7 @@ # Othewrwise, Black is run and its changes are committed back to the incoming pull request. # https://github.com/cclauss/autoblack -name: autoblack +name: black on: [push, pull_request] jobs: build: From 8a42b87bd63e0c1660aadef4cb5e76b4ab12159d Mon Sep 17 00:00:00 2001 From: bobloy Date: Mon, 28 Sep 2020 10:54:47 -0400 Subject: [PATCH 097/121] black --- werewolf/builder.py | 2 +- werewolf/game.py | 5 ++--- werewolf/roles/seer.py | 10 +++++++--- werewolf/votegroups/wolfvote.py | 2 +- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/werewolf/builder.py b/werewolf/builder.py index 4c803cc..f57a669 100644 --- a/werewolf/builder.py +++ b/werewolf/builder.py @@ -27,7 +27,7 @@ log = logging.getLogger("red.fox_v3.werewolf.builder") 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'), + key=attrgetter("alignment"), ) log.debug(f"{ROLE_DICT=}") diff --git a/werewolf/game.py b/werewolf/game.py index df345af..79d8455 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -1,9 +1,8 @@ import asyncio -import inspect import logging import random from collections import deque -from typing import List, Any, Dict, Set, Union +from typing import Dict, List, Union import discord from redbot.core import commands @@ -338,7 +337,7 @@ class Game: if check(): return await self.village_channel.send( - embed=discord.Embed(title=f"**{HALF_DAY_LENGTH/60} minutes of daylight remain...**") + embed=discord.Embed(title=f"**{HALF_DAY_LENGTH / 60} minutes of daylight remain...**") ) await asyncio.sleep(HALF_DAY_LENGTH) # 4 minute days FixMe to 120 later diff --git a/werewolf/roles/seer.py b/werewolf/roles/seer.py index 32ace18..f6bd857 100644 --- a/werewolf/roles/seer.py +++ b/werewolf/roles/seer.py @@ -1,7 +1,11 @@ import logging -from werewolf.constants import ALIGNMENT_TOWN, ALIGNMENT_WEREWOLF, CATEGORY_TOWN_INVESTIGATIVE, \ - CATEGORY_TOWN_RANDOM +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 @@ -91,7 +95,7 @@ class Seer(Role): if alignment == ALIGNMENT_WEREWOLF: out = "Your insight reveals this player to be a **Werewolf!**" - else: # Don't reveal neutrals + else: # Don't reveal neutrals out = "You fail to find anything suspicious about this player..." await self.player.send_dm(out) diff --git a/werewolf/votegroups/wolfvote.py b/werewolf/votegroups/wolfvote.py index 7f6bbde..75fb01c 100644 --- a/werewolf/votegroups/wolfvote.py +++ b/werewolf/votegroups/wolfvote.py @@ -69,5 +69,5 @@ class WolfVote(VoteGroup): await self.channel.send( "{} has voted to kill {}".format(author.mention, target.member.display_name), - allowed_mentions=discord.AllowedMentions(everyone=False, users=[author]) + allowed_mentions=discord.AllowedMentions(everyone=False, users=[author]), ) From 693964183cce5e8056492a4a4374d560b75fecf3 Mon Sep 17 00:00:00 2001 From: bobloy Date: Mon, 28 Sep 2020 12:45:03 -0400 Subject: [PATCH 098/121] Update issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 26 +++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 14 ++++++++++ .../ISSUE_TEMPLATE/new-audiotrivia-list.md | 26 +++++++++++++++++++ 3 files changed, 66 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/ISSUE_TEMPLATE/new-audiotrivia-list.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..7a6e260 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,26 @@ +--- +name: Bug report +about: Create an issue to report a bug +title: '' +labels: bug +assignees: bobloy + +--- + +**Describe the bug** + + +**To Reproduce** + +1. Load cog '...' +2. Run command '....' +3. See error + +**Expected behavior** + + +**Screenshots or Error Messages** + + +**Additional context** + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..42f5500 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,14 @@ +--- +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.** + + +**Describe the solution you'd like** + diff --git a/.github/ISSUE_TEMPLATE/new-audiotrivia-list.md b/.github/ISSUE_TEMPLATE/new-audiotrivia-list.md new file mode 100644 index 0000000..b6be14e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/new-audiotrivia-list.md @@ -0,0 +1,26 @@ +--- +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?** + + +**Number of Questions** + + +**Original Content?** + + +-[ ] Yes +-[ ] No + + +**Did I test the list?** + +-[ ] Yes +-[ ] No From 94aceb32e8e37681a7d3f9b4b8fd528b639f15d1 Mon Sep 17 00:00:00 2001 From: bobloy Date: Mon, 28 Sep 2020 12:46:10 -0400 Subject: [PATCH 099/121] Update issue templates --- .github/ISSUE_TEMPLATE/new-audiotrivia-list.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/new-audiotrivia-list.md b/.github/ISSUE_TEMPLATE/new-audiotrivia-list.md index b6be14e..25bcc81 100644 --- a/.github/ISSUE_TEMPLATE/new-audiotrivia-list.md +++ b/.github/ISSUE_TEMPLATE/new-audiotrivia-list.md @@ -16,11 +16,11 @@ assignees: bobloy **Original Content?** --[ ] Yes --[ ] No +- [ ] Yes +- [ ] No **Did I test the list?** --[ ] Yes --[ ] No +- [ ] Yes +- [ ] No From 3a6d3df374e49bd0005e2cec2c7f456f3900f9ad Mon Sep 17 00:00:00 2001 From: bobloy Date: Mon, 28 Sep 2020 17:24:35 -0400 Subject: [PATCH 100/121] More WIP progress --- werewolf/game.py | 84 ++++++++++++++++++++++--------- werewolf/player.py | 3 ++ werewolf/role.py | 4 +- werewolf/roles/seer.py | 1 + werewolf/roles/vanillawerewolf.py | 15 ------ werewolf/roles/villager.py | 6 --- werewolf/votegroup.py | 1 - werewolf/werewolf.py | 22 +++++++- 8 files changed, 88 insertions(+), 48 deletions(-) diff --git a/werewolf/game.py b/werewolf/game.py index 79d8455..ee84b09 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -19,6 +19,14 @@ log = logging.getLogger("red.fox_v3.werewolf.game") HALF_DAY_LENGTH = 24 # FixMe: to 120 later for 4 minute days +async def anyone_has_role( + member_list: List[discord.Member], role: discord.Role +) -> Union[None, discord.Member]: + return await AsyncIter(member_list).find( + lambda m: AsyncIter(m.roles).find(lambda r: r.id == role.id) + ) + + class Game: """ Base class to run a single game of Werewolf @@ -129,6 +137,7 @@ class Game: self.roles = [] return False + # If there's no game role, make the role and delete it later in `self.to_delete` if self.game_role is None: try: self.game_role = await ctx.guild.create_role( @@ -144,14 +153,25 @@ class Game: ) self.roles = [] return False - try: - for player in self.players: - await player.member.add_roles(*[self.game_role]) - except discord.Forbidden: - await ctx.send( - f"Unable to add role **{self.game_role.name}**\nBot is missing `manage_roles` permissions" - ) - return False + + anyone_with_role = await anyone_has_role(self.guild.members, self.game_role) + if anyone_with_role is not None: + await ctx.maybe_send_embed( + f"{anyone_with_role.display_name} has the game role, " + f"can't continue until no one has the role" + ) + return False + + try: + for player in self.players: + await player.member.add_roles(*[self.game_role]) + except discord.Forbidden: + log.exception(f"Unable to add role **{self.game_role.name}**") + await ctx.send( + f"Unable to add role **{self.game_role.name}**\n" + f"Bot is missing `manage_roles` permissions" + ) + return False await self.assign_roles() @@ -223,9 +243,10 @@ class Game: self.started = True # Assuming everything worked so far log.debug("Pre at_game_start") - await self._at_game_start() # This will queue channels and votegroups to be made + await self._at_game_start() # This will add votegroups to self.p_channels log.debug("Post at_game_start") - for channel_id in self.p_channels: + log.debug(f"Private channels: {self.p_channels}") + for channel_id in self.p_channels.keys(): log.debug("Setup Channel id: " + channel_id) overwrite = { self.guild.default_role: discord.PermissionOverwrite(read_messages=False), @@ -251,6 +272,8 @@ class Game: self.p_channels[channel_id]["channel"] = channel + self.to_delete.add(channel) + if self.p_channels[channel_id]["votegroup"] is not None: vote_group = self.p_channels[channel_id]["votegroup"](self, channel) @@ -259,8 +282,10 @@ class Game: self.vote_groups[channel_id] = vote_group log.debug("Pre-cycle") - await asyncio.sleep(1) - await asyncio.ensure_future(self._cycle()) # Start the loop + await asyncio.sleep(0) + + asyncio.create_task(self._cycle()) # Start the loop + return True # ###########START Notify structure############ async def _cycle(self): @@ -553,13 +578,14 @@ class Game: try: await asyncio.sleep(1) # This will have multiple calls self.p_channels[channel_id]["players"].append(role.player) - if votegroup is not None: - self.p_channels[channel_id]["votegroup"] = votegroup except AttributeError: continue else: break + if votegroup is not None: + self.p_channels[channel_id]["votegroup"] = votegroup + async def join(self, member: discord.Member, channel: discord.TextChannel): """ Have a member join a game @@ -574,14 +600,15 @@ class Game: self.players.append(Player(member)) - if self.game_role is not None: - try: - await member.add_roles(*[self.game_role]) - except discord.Forbidden: - await channel.send( - f"Unable to add role **{self.game_role.name}**\n" - f"Bot is missing `manage_roles` permissions" - ) + # Add the role during setup, not before + # if self.game_role is not None: + # try: + # await member.add_roles(*[self.game_role]) + # except discord.Forbidden: + # await channel.send( + # f"Unable to add role **{self.game_role.name}**\n" + # f"Bot is missing `manage_roles` permissions" + # ) await channel.send( f"{member.display_name} has been added to the game, " @@ -908,7 +935,7 @@ class Game: # Remove game_role access for potential archiving for now reason = "(BOT) End of WW game" for obj in self.to_delete: - log.debug(f"End_game: Deleting object {obj}") + log.debug(f"End_game: Deleting object {obj.__repr__()}") await obj.delete(reason=reason) try: @@ -926,6 +953,17 @@ class Game: except (discord.HTTPException, discord.NotFound, discord.errors.NotFound): pass + for player in self.players: + try: + await player.member.remove_roles(*[self.game_role]) + except discord.Forbidden: + log.exception(f"Unable to add remove **{self.game_role.name}**") + # await ctx.send( + # f"Unable to add role **{self.game_role.name}**\n" + # f"Bot is missing `manage_roles` permissions" + # ) + pass + # Optional dynamic channels/categories def add_ww_listener(self, func, priority=0, name=None): diff --git a/werewolf/player.py b/werewolf/player.py index 7f10758..48885a8 100644 --- a/werewolf/player.py +++ b/werewolf/player.py @@ -20,6 +20,9 @@ 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 diff --git a/werewolf/role.py b/werewolf/role.py index db7b852..0997b56 100644 --- a/werewolf/role.py +++ b/werewolf/role.py @@ -73,7 +73,7 @@ class Role(WolfListener): self.properties = {} # Extra data for other roles (i.e. arsonist) def __repr__(self): - return self.__class__.__name__ + return f"{self.__class__.__name__}({self.player.__repr__()})" async def assign_player(self, player): """ @@ -84,6 +84,8 @@ class Role(WolfListener): player.role = self self.player = player + log.debug(f"Assigned {self} to {player}") + async def get_alignment(self, source=None): """ Interaction for powerful access of alignment diff --git a/werewolf/roles/seer.py b/werewolf/roles/seer.py index f6bd857..983fd14 100644 --- a/werewolf/roles/seer.py +++ b/werewolf/roles/seer.py @@ -15,6 +15,7 @@ log = logging.getLogger("red.fox_v3.werewolf.role.seer") class Seer(Role): rand_choice = True + town_balance = 4 category = [ CATEGORY_TOWN_RANDOM, CATEGORY_TOWN_INVESTIGATIVE, diff --git a/werewolf/roles/vanillawerewolf.py b/werewolf/roles/vanillawerewolf.py index 74e8d96..8abdea2 100644 --- a/werewolf/roles/vanillawerewolf.py +++ b/werewolf/roles/vanillawerewolf.py @@ -22,21 +22,6 @@ class VanillaWerewolf(Role): "Vote to kill players at night with `[p]ww vote `" ) - 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 diff --git a/werewolf/roles/villager.py b/werewolf/roles/villager.py index d669ef9..eb0b2c9 100644 --- a/werewolf/roles/villager.py +++ b/werewolf/roles/villager.py @@ -7,12 +7,9 @@ log = logging.getLogger("red.fox_v3.werewolf.role.villager") 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 channel_id = "" # Empty for no private channel @@ -23,9 +20,6 @@ class Villager(Role): "Lynch players during the day with `[p]ww vote `" ) - def __init__(self, game): - super().__init__(game) - async def see_alignment(self, source=None): """ Interaction for investigative roles attempting diff --git a/werewolf/votegroup.py b/werewolf/votegroup.py index 2f0b3a0..d8411fb 100644 --- a/werewolf/votegroup.py +++ b/werewolf/votegroup.py @@ -75,7 +75,6 @@ class VoteGroup(WolfListener): if not self.players: # TODO: Confirm deletion - self.game.to_delete.add(self) pass async def vote(self, target, author, target_id): diff --git a/werewolf/werewolf.py b/werewolf/werewolf.py index 742a890..599796c 100644 --- a/werewolf/werewolf.py +++ b/werewolf/werewolf.py @@ -1,9 +1,11 @@ import logging +from typing import List, Union import discord from redbot.core import Config, checks, commands from redbot.core.bot import Red from redbot.core.commands import Cog +from redbot.core.utils import AsyncIter from redbot.core.utils.menus import DEFAULT_CONTROLS, menu from werewolf.builder import ( @@ -18,6 +20,14 @@ from werewolf.game import Game log = logging.getLogger("red.fox_v3.werewolf") +async def anyone_has_role( + member_list: List[discord.Member], role: discord.Role +) -> Union[None, discord.Member]: + return await AsyncIter(member_list).find( + lambda m: AsyncIter(m.roles).find(lambda r: r.id == role.id) + ) + + class Werewolf(Cog): """ Base to host werewolf on a guild @@ -189,12 +199,15 @@ class Werewolf(Cog): return await game.join(ctx.author, ctx.channel) + await ctx.tick() @commands.guild_only() @ww.command(name="code") async def ww_code(self, ctx: commands.Context, code): """ - Adjust game code + Adjusts the game code. + + See `[p]buildgame` to generate a new code """ game = await self._get_game(ctx) @@ -204,6 +217,7 @@ class Werewolf(Cog): return await game.set_code(ctx, code) + await ctx.tick() @commands.guild_only() @ww.command(name="quit") @@ -215,6 +229,7 @@ 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") @@ -229,6 +244,8 @@ class Werewolf(Cog): if not await game.setup(ctx): pass # ToDo something? + await ctx.tick() + @commands.guild_only() @ww.command(name="stop") async def ww_stop(self, ctx: commands.Context): @@ -245,6 +262,7 @@ class Werewolf(Cog): game = await self._get_game(ctx) game.game_over = True + await game.current_action.cancel() await ctx.send("Game has been stopped") @commands.guild_only() @@ -358,7 +376,7 @@ class Werewolf(Cog): else: await ctx.send("Role ID not found") - async def _get_game(self, ctx: commands.Context, game_code=None): + async def _get_game(self, ctx: commands.Context, game_code=None) -> Union[Game, None]: guild: discord.Guild = getattr(ctx, "guild", None) if guild is None: From cd89bd87e9a852269aaa548ac8a0d37adb0aa572 Mon Sep 17 00:00:00 2001 From: bobloy Date: Mon, 28 Sep 2020 17:30:02 -0400 Subject: [PATCH 101/121] Only pull requests --- .github/workflows/black_check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/black_check.yml b/.github/workflows/black_check.yml index a36b5f2..076e238 100644 --- a/.github/workflows/black_check.yml +++ b/.github/workflows/black_check.yml @@ -4,7 +4,7 @@ # https://github.com/cclauss/autoblack name: black -on: [push, pull_request] +on: [pull_request] jobs: build: runs-on: ubuntu-latest From 8c0a1db06fb13087fb03158a9d5f683ff6bd7b64 Mon Sep 17 00:00:00 2001 From: bobloy Date: Mon, 28 Sep 2020 17:30:23 -0400 Subject: [PATCH 102/121] v2 checkout --- .github/workflows/black_check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/black_check.yml b/.github/workflows/black_check.yml index 076e238..5350f98 100644 --- a/.github/workflows/black_check.yml +++ b/.github/workflows/black_check.yml @@ -9,7 +9,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - name: Set up Python 3.8 uses: actions/setup-python@v2 with: From 2ab87866ddf2d4056c6d5266a4db733befe98d1c Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 29 Sep 2020 12:41:15 -0400 Subject: [PATCH 103/121] Adjust to italics, fix generate targets to be more obvious and readable, fix `night_messages` confusion with `night_results` --- werewolf/game.py | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/werewolf/game.py b/werewolf/game.py index ee84b09..c5aaad6 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -42,12 +42,12 @@ class Game: "votegroup": None, # uninitialized VoteGroup } - morning_messages = [ - "**The sun rises on day {} in the village..**", - "**Morning has arrived on day {}..**", + day_start_messages = [ + "*The sun rises on day {} in the village..*", + "*Morning has arrived on day {}..*", ] - night_messages = ["**Dawn falls on day {}..****"] + day_end_messages = ["*Dawn falls..*", "*The sun sets on the village*"] day_vote_count = 3 @@ -305,9 +305,9 @@ class Game: self.action_queue.append(self._at_day_start()) while self.action_queue and not self.game_over: - current_action = asyncio.create_task(self.action_queue.popleft()) + self.current_action = asyncio.create_task(self.action_queue.popleft()) try: - await current_action + await self.current_action except asyncio.CancelledError: log.debug("Cancelled task") # @@ -337,7 +337,7 @@ class Game: self.day_count += 1 # Print the results of who died during the night - embed = discord.Embed(title=random.choice(self.morning_messages).format(self.day_count)) + embed = discord.Embed(title=random.choice(self.day_start_messages).format(self.day_count)) for result in self.night_results: embed.add_field(name=result, value="________", inline=False) @@ -362,7 +362,7 @@ class Game: if check(): return await self.village_channel.send( - embed=discord.Embed(title=f"**{HALF_DAY_LENGTH / 60} minutes of daylight remain...**") + embed=discord.Embed(title=f"*{HALF_DAY_LENGTH / 60} minutes of daylight remain...*") ) await asyncio.sleep(HALF_DAY_LENGTH) # 4 minute days FixMe to 120 later @@ -386,7 +386,7 @@ class Game: await self.speech_perms(self.village_channel, target.member) # Only target can talk await self.village_channel.send( - f"**{target.mention} will be put to trial and has 30 seconds to defend themselves**", + f"*{target.mention} will be put to trial and has 30 seconds to defend themselves**", allowed_mentions=discord.AllowedMentions(everyone=False, users=[target]), ) @@ -480,7 +480,7 @@ class Game: await self.night_perms(self.village_channel) await self.village_channel.send( - embed=discord.Embed(title="**The sun sets on the village...**") + embed=discord.Embed(title=random.choice(self.day_end_messages)) ) await self._notify("at_day_end") @@ -546,7 +546,7 @@ class Game: # ###########END Notify structure############ async def generate_targets(self, channel, with_roles=False): - embed = discord.Embed(title="Remaining Players") + embed = discord.Embed(title="Remaining Players", description="[ID] - [Name]") for i, player in enumerate(self.players): if player.alive: status = "" @@ -554,15 +554,14 @@ class Game: status = "*[Dead]*-" if with_roles or not player.alive: embed.add_field( - name=f"ID# **{i}**", - value=f"{status}{player.member.display_name}-{player.role}", - inline=True, + name=f"{i} - {status}{player.member.display_name}", + value=f"{player.role}", + inline=False, ) else: embed.add_field( - name=f"ID# **{i}**", - value=f"{status}{player.member.display_name}", - inline=True, + name=f"{i} - {status}{player.member.display_name}", + inline=False, ) return await channel.send(embed=embed) From c7d320ccaa351c383968330faf21dda696ac4958 Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 29 Sep 2020 12:41:26 -0400 Subject: [PATCH 104/121] WIP Player converter --- werewolf/converters.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 werewolf/converters.py diff --git a/werewolf/converters.py b/werewolf/converters.py new file mode 100644 index 0000000..376749d --- /dev/null +++ b/werewolf/converters.py @@ -0,0 +1,28 @@ +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 \ No newline at end of file From 443c84ccabd38fed7c7a06a4939b24708d527c86 Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 29 Sep 2020 12:41:43 -0400 Subject: [PATCH 105/121] Fix `night_messages` to `night_results` --- werewolf/roles/blob.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/werewolf/roles/blob.py b/werewolf/roles/blob.py index bd7b598..af18983 100644 --- a/werewolf/roles/blob.py +++ b/werewolf/roles/blob.py @@ -98,4 +98,4 @@ class TheBlob(Role): if target is not None: target.role.properties["been_blobbed"] = True - self.game.night_messages.append("The Blob grows...") + self.game.night_results.append("The Blob grows...") From 211df56e1b6070196c226728b206b0f935ae529a Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 29 Sep 2020 12:41:57 -0400 Subject: [PATCH 106/121] Add repr --- werewolf/votegroup.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/werewolf/votegroup.py b/werewolf/votegroup.py index d8411fb..e651eda 100644 --- a/werewolf/votegroup.py +++ b/werewolf/votegroup.py @@ -22,6 +22,9 @@ class VoteGroup(WolfListener): self.vote_results = {} self.properties = {} # Extra data for other options + def __repr__(self): + return f"{self.__class__.__name__}({self.channel},{self.players})" + @wolflistener("at_game_start", priority=1) async def _at_game_start(self): await self.channel.send(" ".join(player.mention for player in self.players)) From e27cfba763b4d596fbed7568aeb64ed5c8389b2d Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 29 Sep 2020 12:42:05 -0400 Subject: [PATCH 107/121] Move to italics --- werewolf/votegroups/wolfvote.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/werewolf/votegroups/wolfvote.py b/werewolf/votegroups/wolfvote.py index 75fb01c..d637c87 100644 --- a/werewolf/votegroups/wolfvote.py +++ b/werewolf/votegroups/wolfvote.py @@ -55,10 +55,10 @@ class WolfVote(VoteGroup): 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) + "*{} 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 vote(self, target, author, target_id): """ From f3965b73d831bd86c36d38d270b5f38259057fac Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 29 Sep 2020 14:25:19 -0400 Subject: [PATCH 108/121] Mostly messaging adjustments, fix for failing to talley votes --- werewolf/game.py | 57 ++++++++++++++++------------- werewolf/player.py | 6 +++- werewolf/werewolf.py | 85 ++++++++++++++++++++++++++++---------------- 3 files changed, 91 insertions(+), 57 deletions(-) diff --git a/werewolf/game.py b/werewolf/game.py index c5aaad6..c0a9db4 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -10,13 +10,15 @@ from redbot.core.bot import Red from redbot.core.utils import AsyncIter from werewolf.builder import parse_code +from werewolf.constants import ALIGNMENT_NEUTRAL from werewolf.player import Player from werewolf.role import Role from werewolf.votegroup import VoteGroup log = logging.getLogger("red.fox_v3.werewolf.game") -HALF_DAY_LENGTH = 24 # FixMe: to 120 later for 4 minute days +HALF_DAY_LENGTH = 60 # FixMe: Make configurable +HALF_NIGHT_LENGTH = 60 async def anyone_has_role( @@ -167,7 +169,7 @@ class Game: await player.member.add_roles(*[self.game_role]) except discord.Forbidden: log.exception(f"Unable to add role **{self.game_role.name}**") - await ctx.send( + await ctx.maybe_send_embed( f"Unable to add role **{self.game_role.name}**\n" f"Bot is missing `manage_roles` permissions" ) @@ -210,7 +212,7 @@ class Game: category=self.channel_category, ) except discord.Forbidden: - await ctx.send( + await ctx.maybe_send_embed( "Unable to create Game Channel and none was provided\n" "Grant Bot appropriate permissions or assign a game_channel" ) @@ -225,7 +227,7 @@ class Game: ) except discord.Forbidden as e: log.exception("Unable to rename Game Channel") - await ctx.send("Unable to rename Game Channel, ignoring") + await ctx.maybe_send_embed("Unable to rename Game Channel, ignoring") try: for target, ow in overwrite.items(): @@ -235,7 +237,7 @@ class Game: target=target, overwrite=curr, reason="(BOT) New game of werewolf" ) except discord.Forbidden: - await ctx.send( + await ctx.maybe_send_embed( "Unable to edit Game Channel permissions\n" "Grant Bot appropriate permissions to manage permissions" ) @@ -406,14 +408,17 @@ class Game: await vote_message.add_reaction("👎") await asyncio.sleep(15) - reaction_list = vote_message.reactions - if True: # TODO: Allow customizable vote history deletion. - await vote_message.delete() + # Refetch for reactions + vote_message = await self.village_channel.fetch_message(id=vote_message.id) + reaction_list = vote_message.reactions raw_up_votes = sum(p for p in reaction_list if p.emoji == "👍" and not p.me) raw_down_votes = sum(p for p in reaction_list if p.emoji == "👎" and not p.me) + if True: # TODO: Allow customizable vote history deletion. + await vote_message.delete() + # TODO: Support vote count modifying roles. (Need notify and count function) voted_to_lynch = raw_down_votes > raw_up_votes @@ -492,13 +497,13 @@ class Game: return await self._notify("at_night_start") - await asyncio.sleep(12) # 2 minutes FixMe to 120 later + await asyncio.sleep(HALF_NIGHT_LENGTH) # 2 minutes FixMe to 120 later await self.village_channel.send( - embed=discord.Embed(title="**Two minutes of night remain...**") + embed=discord.Embed(title=f"**{HALF_NIGHT_LENGTH / 60} minutes of night remain...**") ) - await asyncio.sleep(9) # 1.5 minutes FixMe to 90 later + await asyncio.sleep(HALF_NIGHT_LENGTH) # 1.5 minutes FixMe to 90 later await self.village_channel.send( - embed=discord.Embed(title="**Thirty seconds until sunrise...**") + embed=discord.Embed(title=f"**{HALF_NIGHT_LENGTH / 60} minutes until sunrise...**") ) await asyncio.sleep(3) # .5 minutes FixMe to 30 Later @@ -560,8 +565,7 @@ class Game: ) else: embed.add_field( - name=f"{i} - {status}{player.member.display_name}", - inline=False, + name=f"{i} - {status}{player.member.display_name}", inline=False, value="" ) return await channel.send(embed=embed) @@ -585,16 +589,16 @@ class Game: if votegroup is not None: self.p_channels[channel_id]["votegroup"] = votegroup - async def join(self, member: discord.Member, channel: discord.TextChannel): + async def join(self, ctx, member: discord.Member): """ Have a member join a game """ if self.started: - await channel.send("**Game has already started!**") + await ctx.maybe_send_embed("**Game has already started!**") return if await self.get_player_by_member(member) is not None: - await channel.send(f"{member.display_name} is already in the game!") + await ctx.maybe_send_embed(f"{member.display_name} is already in the game!") return self.players.append(Player(member)) @@ -609,7 +613,7 @@ class Game: # f"Bot is missing `manage_roles` permissions" # ) - await channel.send( + await ctx.maybe_send_embed( f"{member.display_name} has been added to the game, " f"total players is **{len(self.players)}**" ) @@ -645,15 +649,15 @@ class Game: player = await self.get_player_by_member(ctx.author) if player is None: - await ctx.send("You're not in this game!") + await ctx.maybe_send_embed("You're not in this game!") return if not player.alive: - await ctx.send("**Corpses** can't participate...") + await ctx.maybe_send_embed("**Corpses** can't participate...") return if player.role.blocked: - await ctx.send("Something is preventing you from doing this...") + await ctx.maybe_send_embed("Something is preventing you from doing this...") return # Let role do target validation, might be alternate targets @@ -821,7 +825,7 @@ class Game: async def set_code(self, ctx: commands.Context, game_code): if game_code is not None: self.game_code = game_code - await ctx.send("Code has been set") + await ctx.maybe_send_embed("Code has been set") async def get_roles(self, ctx, game_code=None): if game_code is not None: @@ -833,10 +837,12 @@ class Game: try: self.roles = await parse_code(self.game_code, self) except ValueError as e: - await ctx.send("Invalid Code: Code contains unknown character\n{}".format(e)) + await ctx.maybe_send_embed( + "Invalid Code: Code contains unknown character\n{}".format(e) + ) return False except IndexError as e: - await ctx.send("Invalid Code: Code references unknown role\n{}".format(e)) + await ctx.maybe_send_embed("Invalid Code: Code references unknown role\n{}".format(e)) if not self.roles: return False @@ -898,7 +904,8 @@ class Game: self.game_over = True alignment1 = alive_players[0].role.alignment alignment2 = alive_players[1].role.alignment - if alignment1 == alignment2: # Same team + # Same team and not neutral + if alignment1 == alignment2 and alignment1 != ALIGNMENT_NEUTRAL: winners = alive_players else: winners = [max(alive_players, key=lambda p: p.role.alignment)] diff --git a/werewolf/player.py b/werewolf/player.py index 48885a8..7aec179 100644 --- a/werewolf/player.py +++ b/werewolf/player.py @@ -35,9 +35,13 @@ class Player: async def send_dm(self, message): try: - await self.member.send(message) # Lets do embeds later + await self.member.send(message) # Lets ToDo 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:") diff --git a/werewolf/werewolf.py b/werewolf/werewolf.py index 599796c..dc27338 100644 --- a/werewolf/werewolf.py +++ b/werewolf/werewolf.py @@ -72,9 +72,9 @@ class Werewolf(Cog): code = await gb.build_game(ctx) if code != "": - await ctx.send(f"Your game code is **{code}**") + await ctx.maybe_send_embed(f"Your game code is **{code}**") else: - await ctx.send("No code generated") + await ctx.maybe_send_embed("No code generated") @checks.guildowner() @commands.group() @@ -117,10 +117,10 @@ class Werewolf(Cog): """ if role is None: await self.config.guild(ctx.guild).role_id.set(None) - await ctx.send("Cleared Game Role") + await ctx.maybe_send_embed("Cleared Game Role") else: await self.config.guild(ctx.guild).role_id.set(role.id) - await ctx.send("Game Role has been set to **{}**".format(role.name)) + await ctx.maybe_send_embed("Game Role has been set to **{}**".format(role.name)) @commands.guild_only() @wwset.command(name="category") @@ -130,14 +130,14 @@ class Werewolf(Cog): """ if category_id is None: await self.config.guild(ctx.guild).category_id.set(None) - await ctx.send("Cleared Game Channel Category") + await ctx.maybe_send_embed("Cleared Game Channel Category") else: category = discord.utils.get(ctx.guild.categories, id=int(category_id)) if category is None: - await ctx.send("Category not found") + await ctx.maybe_send_embed("Category not found") return await self.config.guild(ctx.guild).category_id.set(category.id) - await ctx.send("Game Channel Category has been set to **{}**".format(category.name)) + await ctx.maybe_send_embed("Game Channel Category has been set to **{}**".format(category.name)) @commands.guild_only() @wwset.command(name="channel") @@ -147,10 +147,10 @@ class Werewolf(Cog): """ if channel is None: await self.config.guild(ctx.guild).channel_id.set(None) - await ctx.send("Cleared Game Channel") + await ctx.maybe_send_embed("Cleared Game Channel") else: await self.config.guild(ctx.guild).channel_id.set(channel.id) - await ctx.send("Game Channel has been set to **{}**".format(channel.mention)) + await ctx.maybe_send_embed("Game Channel has been set to **{}**".format(channel.mention)) @commands.guild_only() @wwset.command(name="logchannel") @@ -160,10 +160,10 @@ class Werewolf(Cog): """ if channel is None: await self.config.guild(ctx.guild).log_channel_id.set(None) - await ctx.send("Cleared Game Log Channel") + await ctx.maybe_send_embed("Cleared Game Log Channel") else: await self.config.guild(ctx.guild).log_channel_id.set(channel.id) - await ctx.send("Game Log Channel has been set to **{}**".format(channel.mention)) + await ctx.maybe_send_embed("Game Log Channel has been set to **{}**".format(channel.mention)) @commands.group() async def ww(self, ctx: commands.Context): @@ -181,9 +181,9 @@ class Werewolf(Cog): """ game = await self._get_game(ctx, game_code) if not game: - await ctx.send("Failed to start a new game") + await ctx.maybe_send_embed("Failed to start a new game") else: - await ctx.send("Game is ready to join! Use `[p]ww join`") + await ctx.maybe_send_embed("Game is ready to join! Use `[p]ww join`") @commands.guild_only() @ww.command(name="join") @@ -195,10 +195,27 @@ class Werewolf(Cog): game: Game = await self._get_game(ctx) if not game: - await ctx.send("No game to join!\nCreate a new one with `[p]ww new`") + await ctx.maybe_send_embed("Failed to join a game!") return - await game.join(ctx.author, ctx.channel) + 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() @commands.guild_only() @@ -213,7 +230,7 @@ class Werewolf(Cog): game = await self._get_game(ctx) if not game: - await ctx.send("No game to join!\nCreate a new one with `[p]ww new`") + await ctx.maybe_send_embed("No game to join!\nCreate a new one with `[p]ww new`") return await game.set_code(ctx, code) @@ -239,7 +256,7 @@ class Werewolf(Cog): """ game = await self._get_game(ctx) if not game: - await ctx.send("No game running, cannot start") + await ctx.maybe_send_embed("No game running, cannot start") if not await game.setup(ctx): pass # ToDo something? @@ -257,13 +274,13 @@ class Werewolf(Cog): # await ctx.send("Cannot stop game from PM!") # return if ctx.guild.id not in self.games or self.games[ctx.guild.id].game_over: - await ctx.send("No game to stop") + await ctx.maybe_send_embed("No game to stop") return game = await self._get_game(ctx) game.game_over = True await game.current_action.cancel() - await ctx.send("Game has been stopped") + await ctx.maybe_send_embed("Game has been stopped") @commands.guild_only() @ww.command(name="vote") @@ -277,7 +294,7 @@ class Werewolf(Cog): target_id = None if target_id is None: - await ctx.send("`id` must be an integer") + await ctx.maybe_send_embed("`id` must be an integer") return # if ctx.guild is None: @@ -294,7 +311,7 @@ class Werewolf(Cog): game = await self._get_game(ctx) if game is None: - await ctx.send("No game running, cannot vote") + await ctx.maybe_send_embed("No game running, cannot vote") return # Game handles response now @@ -304,7 +321,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.send("Nothing to vote for in this channel") + await ctx.maybe_send_embed("Nothing to vote for in this channel") @ww.command(name="choose") async def ww_choose(self, ctx: commands.Context, data): @@ -315,7 +332,7 @@ class Werewolf(Cog): """ if ctx.guild is not None: - await ctx.send("This action is only available in DM's") + await ctx.maybe_send_embed("This action is only available in DM's") return # DM nonsense, find their game # If multiple games, panic @@ -323,7 +340,7 @@ class Werewolf(Cog): if await game.get_player_by_member(ctx.author): break # game = game else: - await ctx.send("You're not part of any werewolf game") + await ctx.maybe_send_embed("You're not part of any werewolf game") return await game.choose(ctx, data) @@ -344,7 +361,7 @@ class Werewolf(Cog): if from_name: await menu(ctx, from_name, DEFAULT_CONTROLS) else: - await ctx.send("No roles containing that name were found") + await ctx.maybe_send_embed("No roles containing that name were found") @ww_search.command(name="alignment") async def ww_search_alignment(self, ctx: commands.Context, alignment: int): @@ -354,7 +371,7 @@ class Werewolf(Cog): if from_alignment: await menu(ctx, from_alignment, DEFAULT_CONTROLS) else: - await ctx.send("No roles with that alignment were found") + await ctx.maybe_send_embed("No roles with that alignment were found") @ww_search.command(name="category") async def ww_search_category(self, ctx: commands.Context, category: int): @@ -364,7 +381,7 @@ class Werewolf(Cog): if pages: await menu(ctx, pages, DEFAULT_CONTROLS) else: - await ctx.send("No roles in that category were found") + await ctx.maybe_send_embed("No roles in that category were found") @ww_search.command(name="index") async def ww_search_index(self, ctx: commands.Context, idx: int): @@ -374,23 +391,29 @@ class Werewolf(Cog): if idx_embed is not None: await ctx.send(embed=idx_embed) else: - await ctx.send("Role ID not found") + await ctx.maybe_send_embed("Role ID not found") async def _get_game(self, ctx: commands.Context, game_code=None) -> Union[Game, None]: guild: discord.Guild = getattr(ctx, "guild", None) if guild is None: # Private message, can't get guild - await ctx.send("Cannot start game from DM!") + await ctx.maybe_send_embed("Cannot start game from DM!") return None if guild.id not in self.games or self.games[guild.id].game_over: - await ctx.send("Starting a new game...") + await ctx.maybe_send_embed("Starting a new game...") valid, role, category, channel, log_channel = await self._get_settings(ctx) if not valid: - await ctx.send("Cannot start a new game") + await ctx.maybe_send_embed("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 ) From 9ca5d37f7e9bf318c33415e53476f89e99e1950c Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 29 Sep 2020 17:17:11 -0400 Subject: [PATCH 109/121] Fixed a variable reuse, channel naming, bot's can't play, less bold, object deletion error catching --- werewolf/game.py | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/werewolf/game.py b/werewolf/game.py index c0a9db4..bb77f02 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -221,8 +221,7 @@ class Game: self.save_perms[self.village_channel] = self.village_channel.overwrites try: await self.village_channel.edit( - name="🔵Werewolf", - category=self.channel_category, + name="🔵werewolf", reason="(BOT) New game of werewolf", ) except discord.Forbidden as e: @@ -298,7 +297,7 @@ class Game: _at_voted() _at_kill() _at_day_end() - _at_night_begin() + _at_night_start() _at_night_end() and repeat with _at_day_start() again @@ -331,6 +330,7 @@ class Game: if self.game_over: return + # await self.village_channel.edit(reason="WW Night Start", name="werewolf-🌞") self.action_queue.append(self._at_day_end()) # Get this ready in case day is cancelled def check(): @@ -413,6 +413,7 @@ class Game: vote_message = await self.village_channel.fetch_message(id=vote_message.id) reaction_list = vote_message.reactions + log.debug(f"Vote results: {[p.emoji.__repr__() for p in reaction_list]}") raw_up_votes = sum(p for p in reaction_list if p.emoji == "👍" and not p.me) raw_down_votes = sum(p for p in reaction_list if p.emoji == "👎" and not p.me) @@ -495,6 +496,9 @@ class Game: async def _at_night_start(self): # ID 6 if self.game_over: return + + # await self.village_channel.edit(reason="WW Night Start", name="werewolf-🌑") + await self._notify("at_night_start") await asyncio.sleep(HALF_NIGHT_LENGTH) # 2 minutes FixMe to 120 later @@ -502,9 +506,7 @@ class Game: embed=discord.Embed(title=f"**{HALF_NIGHT_LENGTH / 60} minutes of night remain...**") ) await asyncio.sleep(HALF_NIGHT_LENGTH) # 1.5 minutes FixMe to 90 later - await self.village_channel.send( - embed=discord.Embed(title=f"**{HALF_NIGHT_LENGTH / 60} minutes until sunrise...**") - ) + await asyncio.sleep(3) # .5 minutes FixMe to 30 Later self.action_queue.append(self._at_night_end()) @@ -522,11 +524,11 @@ class Game: return await self._notify("at_visit", target=target, source=source) - async def _notify(self, event, **kwargs): + async def _notify(self, event_name, **kwargs): for i in range(1, 7): # action guide 1-6 (0 is no action) tasks = [] - for event in self.listeners.get(event, {}).get(i, []): - tasks.append(asyncio.ensure_future(event(**kwargs), loop=self.loop)) + for event in self.listeners.get(event_name, {}).get(i, []): + tasks.append(asyncio.create_task(event(**kwargs))) # Run same-priority task simultaneously await asyncio.gather(*tasks) @@ -565,7 +567,7 @@ class Game: ) else: embed.add_field( - name=f"{i} - {status}{player.member.display_name}", inline=False, value="" + name=f"{i} - {status}{player.member.display_name}", inline=False, value="____" ) return await channel.send(embed=embed) @@ -594,7 +596,11 @@ class Game: Have a member join a game """ if self.started: - await ctx.maybe_send_embed("**Game has already started!**") + await ctx.maybe_send_embed("Game has already started!") + return + + if member.bot: + await ctx.maybe_send_embed("Bots can't play games") return if await self.get_player_by_member(member) is not None: @@ -942,10 +948,14 @@ class Game: reason = "(BOT) End of WW game" for obj in self.to_delete: log.debug(f"End_game: Deleting object {obj.__repr__()}") - await obj.delete(reason=reason) + try: + await obj.delete(reason=reason) + except discord.NotFound: + # Already deleted + pass try: - await self.village_channel.edit(reason=reason, name="Werewolf") + asyncio.create_task(self.village_channel.edit(reason=reason, name="werewolf")) async for channel, overwrites in AsyncIter(self.save_perms.items()): async for target, overwrite in AsyncIter(overwrites.items()): await channel.set_permissions(target, overwrite=overwrite, reason=reason) From 8a4893c5f54996c29c7f1a534675b5c8ffa94f21 Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 29 Sep 2020 17:17:20 -0400 Subject: [PATCH 110/121] Forgot the f in fstring --- werewolf/votegroups/wolfvote.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/werewolf/votegroups/wolfvote.py b/werewolf/votegroups/wolfvote.py index d637c87..dfb4f32 100644 --- a/werewolf/votegroups/wolfvote.py +++ b/werewolf/votegroups/wolfvote.py @@ -51,7 +51,7 @@ class WolfVote(VoteGroup): if vote_list: target_id = max(set(vote_list), key=vote_list.count) - log.debug("Target id: {target_id}\nKiller: {self.killer.member.display_name}") + log.debug(f"Target id: {target_id}\nKiller: {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( From 19104241d71391ba4eb103e90343d49c0bbddea8 Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 29 Sep 2020 17:17:29 -0400 Subject: [PATCH 111/121] Don't await task cancels --- werewolf/werewolf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/werewolf/werewolf.py b/werewolf/werewolf.py index dc27338..a9870ab 100644 --- a/werewolf/werewolf.py +++ b/werewolf/werewolf.py @@ -279,7 +279,7 @@ class Werewolf(Cog): game = await self._get_game(ctx) game.game_over = True - await game.current_action.cancel() + game.current_action.cancel() await ctx.maybe_send_embed("Game has been stopped") @commands.guild_only() From 62a70c52c63055ae88cb76ca71f6810073bf19fd Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 29 Sep 2020 17:17:49 -0400 Subject: [PATCH 112/121] Some weird error with dm-ing keeps happening, add better log to catch it --- werewolf/role.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/werewolf/role.py b/werewolf/role.py index 0997b56..e267283 100644 --- a/werewolf/role.py +++ b/werewolf/role.py @@ -120,7 +120,11 @@ class Role(WolfListener): if self.channel_name: await self.game.register_channel(self.channel_name, self) - await self.player.send_dm(self.game_start_message) # Maybe embeds eventually + try: + await self.player.send_dm(self.game_start_message) # Maybe embeds eventually + except AttributeError as e: + log.exception(self.__repr__()) + raise e async def kill(self, source): """ From c529d792e6d9416ff8f093151755e0947099e916 Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 29 Sep 2020 17:31:55 -0400 Subject: [PATCH 113/121] Fix double game_end --- werewolf/game.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/werewolf/game.py b/werewolf/game.py index bb77f02..63b922a 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -929,7 +929,7 @@ class Game: await self._announce_winners(alive_players) # If no return, cleanup and end game - await self._end_game() + # await self._end_game() async def _announce_winners(self, winnerlist): await self.village_channel.send(self.game_role.mention) From 7f8d0f13f7b0ca9866a482fd40a893d72cc33a37 Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 29 Sep 2020 17:32:35 -0400 Subject: [PATCH 114/121] Black format --- werewolf/converters.py | 2 +- werewolf/player.py | 4 +++- werewolf/werewolf.py | 12 +++++++++--- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/werewolf/converters.py b/werewolf/converters.py index 376749d..f108666 100644 --- a/werewolf/converters.py +++ b/werewolf/converters.py @@ -25,4 +25,4 @@ else: # 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 \ No newline at end of file + return target diff --git a/werewolf/player.py b/werewolf/player.py index 7aec179..c574109 100644 --- a/werewolf/player.py +++ b/werewolf/player.py @@ -44,4 +44,6 @@ class Player: ) 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( + "Someone messed up and added a bot to the game :eyes:" + ) diff --git a/werewolf/werewolf.py b/werewolf/werewolf.py index a9870ab..bd68a6f 100644 --- a/werewolf/werewolf.py +++ b/werewolf/werewolf.py @@ -137,7 +137,9 @@ class Werewolf(Cog): await ctx.maybe_send_embed("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.maybe_send_embed( + "Game Channel Category has been set to **{}**".format(category.name) + ) @commands.guild_only() @wwset.command(name="channel") @@ -150,7 +152,9 @@ class Werewolf(Cog): await ctx.maybe_send_embed("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.maybe_send_embed( + "Game Channel has been set to **{}**".format(channel.mention) + ) @commands.guild_only() @wwset.command(name="logchannel") @@ -163,7 +167,9 @@ class Werewolf(Cog): await ctx.maybe_send_embed("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.maybe_send_embed( + "Game Log Channel has been set to **{}**".format(channel.mention) + ) @commands.group() async def ww(self, ctx: commands.Context): From d0445f41c70333f403d464fff44fb427fc7c0a12 Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 29 Sep 2020 17:36:08 -0400 Subject: [PATCH 115/121] Black format update --- audiotrivia/audiotrivia.py | 8 +- ccrole/ccrole.py | 5 +- conquest/conquest.py | 7 +- fifo/timezones.py | 388 +++++++++++++++++++---------------- flag/flag.py | 2 +- forcemention/forcemention.py | 4 +- nudity/nudity.py | 4 +- rpsls/rpsls.py | 4 +- tts/tts.py | 4 +- 9 files changed, 236 insertions(+), 190 deletions(-) diff --git a/audiotrivia/audiotrivia.py b/audiotrivia/audiotrivia.py index 0bab980..c3ac1e9 100644 --- a/audiotrivia/audiotrivia.py +++ b/audiotrivia/audiotrivia.py @@ -90,7 +90,9 @@ class AudioTrivia(Trivia): 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.maybe_send_embed( + "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() @@ -110,7 +112,9 @@ class AudioTrivia(Trivia): if not ctx.author.voice.channel.permissions_for( ctx.me ).connect or self.audio.is_vc_full(ctx.author.voice.channel): - return await ctx.maybe_send_embed("I don't have permission to connect to your channel.") + return await ctx.maybe_send_embed( + "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()) diff --git a/ccrole/ccrole.py b/ccrole/ccrole.py index eb654b1..59efc55 100644 --- a/ccrole/ccrole.py +++ b/ccrole/ccrole.py @@ -143,8 +143,9 @@ 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) diff --git a/conquest/conquest.py b/conquest/conquest.py index fb8b280..fdf5e96 100644 --- a/conquest/conquest.py +++ b/conquest/conquest.py @@ -159,7 +159,12 @@ 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) diff --git a/fifo/timezones.py b/fifo/timezones.py index 5fdbdba..54d7c3e 100644 --- a/fifo/timezones.py +++ b/fifo/timezones.py @@ -15,182 +15,216 @@ 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"] = 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) return timezones diff --git a/flag/flag.py b/flag/flag.py index 035ae1d..6216f65 100644 --- a/flag/flag.py +++ b/flag/flag.py @@ -54,7 +54,7 @@ class Flag(Cog): async def flagset(self, ctx: commands.Context): """ My custom cog - + Extra information goes here """ if ctx.invoked_subcommand is None: diff --git a/forcemention/forcemention.py b/forcemention/forcemention.py index dd8948c..3df68e5 100644 --- a/forcemention/forcemention.py +++ b/forcemention/forcemention.py @@ -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)) diff --git a/nudity/nudity.py b/nudity/nudity.py index 0d46ca9..4233460 100644 --- a/nudity/nudity.py +++ b/nudity/nudity.py @@ -85,7 +85,9 @@ 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() diff --git a/rpsls/rpsls.py b/rpsls/rpsls.py index b831d1c..ed2e1dc 100644 --- a/rpsls/rpsls.py +++ b/rpsls/rpsls.py @@ -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 diff --git a/tts/tts.py b/tts/tts.py index 1291777..235d585 100644 --- a/tts/tts.py +++ b/tts/tts.py @@ -30,8 +30,8 @@ class TTS(Cog): @commands.command(aliases=["t2s", "text2"]) async def tts(self, ctx: commands.Context, *, text: str): """ - Send Text to speech messages as an mp3 - """ + Send Text to speech messages as an mp3 + """ mp3_fp = io.BytesIO() tts = gTTS(text, lang="en") tts.write_to_fp(mp3_fp) From 266b0a485d03d02f40640871d86255fa522e0760 Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 29 Sep 2020 17:39:07 -0400 Subject: [PATCH 116/121] Alpha ready changes --- werewolf/game.py | 2 +- werewolf/info.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/werewolf/game.py b/werewolf/game.py index 63b922a..668bf16 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -17,7 +17,7 @@ from werewolf.votegroup import VoteGroup log = logging.getLogger("red.fox_v3.werewolf.game") -HALF_DAY_LENGTH = 60 # FixMe: Make configurable +HALF_DAY_LENGTH = 90 # FixMe: Make configurable HALF_NIGHT_LENGTH = 60 diff --git a/werewolf/info.json b/werewolf/info.json index 99bc768..c8ef454 100644 --- a/werewolf/info.json +++ b/werewolf/info.json @@ -4,10 +4,10 @@ ], "min_bot_version": "3.3.0", "description": "Customizable Werewolf Game", - "hidden": true, + "hidden": false, "install_msg": "Thank you for installing Werewolf! Get started with `[p]load werewolf`\n Use `[p]wwset` to run inital setup", "requirements": [], - "short": "Werewolf Game", + "short": "[ALPHA] Play Werewolf (Mafia) Game in discord", "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", From da754e3cb2f6df5ca5b3ba0f85f3fd71ae03827d Mon Sep 17 00:00:00 2001 From: bobloy Date: Wed, 30 Sep 2020 10:31:53 -0400 Subject: [PATCH 117/121] Update to latest version of labeler --- .github/workflows/labeler.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 7c724a6..65e6640 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -14,6 +14,6 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/labeler@v2 + - uses: actions/labeler@2.2.0 with: repo-token: "${{ secrets.GITHUB_TOKEN }}" From b210f4a9ff4f2a637914f9cf5a4fc42efb81ab70 Mon Sep 17 00:00:00 2001 From: bobloy Date: Wed, 30 Sep 2020 12:13:40 -0400 Subject: [PATCH 118/121] Uppercase key --- audiotrivia/data/lists/videogames.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/audiotrivia/data/lists/videogames.yaml b/audiotrivia/data/lists/videogames.yaml index eec01b6..5798f0c 100644 --- a/audiotrivia/data/lists/videogames.yaml +++ b/audiotrivia/data/lists/videogames.yaml @@ -1,4 +1,4 @@ -Author: Bobloy +AUTHOR: Bobloy https://www.youtube.com/watch?v=GBPbJyxqHV0: - Super Mario 64 https://www.youtube.com/watch?v=0jXTBAGv9ZQ: From 3fceea634bb6a9e6e590a013cda8819d653f1532 Mon Sep 17 00:00:00 2001 From: bobloy Date: Thu, 1 Oct 2020 09:10:02 -0400 Subject: [PATCH 119/121] Audiotrivia updates from lessons learned attempting core --- audiotrivia/audiosession.py | 117 ++++++++----- audiotrivia/audiotrivia.py | 159 +++++++++--------- .../lists/{anime.yaml => audioanime.yaml} | 1 + ...lgoalhorns.yaml => audionhlgoalhorns.yaml} | 1 + .../{videogames.yaml => audiovideogames.yaml} | 1 + 5 files changed, 154 insertions(+), 125 deletions(-) rename audiotrivia/data/lists/{anime.yaml => audioanime.yaml} (99%) rename audiotrivia/data/lists/{nhlgoalhorns.yaml => audionhlgoalhorns.yaml} (97%) rename audiotrivia/data/lists/{videogames.yaml => audiovideogames.yaml} (99%) diff --git a/audiotrivia/audiosession.py b/audiotrivia/audiosession.py index 1bdff02..17fc998 100644 --- a/audiotrivia/audiosession.py +++ b/audiotrivia/audiosession.py @@ -2,9 +2,8 @@ import asyncio import logging -import lavalink -from lavalink.enums import LoadType 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") @@ -13,14 +12,14 @@ 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, player: lavalink.Player): + def __init__(self, ctx, question_list: dict, settings: dict, audio = None): super().__init__(ctx, question_list, settings) - self.player = player + self.audio = audio @classmethod - def start(cls, ctx, question_list, settings, player: lavalink.Player = None): - session = cls(ctx, question_list, settings, player) + def start(cls, ctx, question_list, settings, audio = None): + session = cls(ctx, question_list, settings, audio) loop = ctx.bot.loop session._task = loop.create_task(session.run()) return session @@ -34,57 +33,89 @@ class AudioSession(TriviaSession): 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"] - for question, answers in self._iter_questions(): + 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(): async with self.ctx.typing(): await asyncio.sleep(3) self.count += 1 - await self.player.stop() - - msg = bold(f"Question number {self.count}!") + "\n\nName this audio!" + 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 + + 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) + + if not player.current: + await player.play() await self.ctx.maybe_send_embed(msg) log.debug(f"Audio question: {question}") - # 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("<>") - load_result = await self.player.load_tracks(query) - log.debug(f"{load_result.load_type=}") - if load_result.has_error or load_result.load_type != LoadType.TRACK_LOADED: - await self.ctx.maybe_send_embed(f"Track has error, skipping. See logs for details") - log.info(f"Track has error: {load_result.exception_message}") - continue # Skip tracks with error - tracks = load_result.tracks - - track = tracks[0] - seconds = track.length / 1000 - - if self.settings["repeat"] and seconds < delay: - # Append it until it's longer than the delay - tot_length = seconds + 0 - while tot_length < delay: - self.player.add(self.ctx.author, track) - tot_length += seconds - else: - self.player.add(self.ctx.author, track) - - if not self.player.current: - log.debug("Pressing play") - await self.player.play() - continue_ = await self.wait_for_answer(answers, delay, timeout) + continue_ = await self.wait_for_answer( + answers, audio_delay if audio_url else 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.ctx.maybe_send_embed("There are no more questions!") await self.end_game() async def end_game(self): await super().end_game() - await self.player.disconnect() + 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 \ No newline at end of file diff --git a/audiotrivia/audiotrivia.py b/audiotrivia/audiotrivia.py index c3ac1e9..9617f32 100644 --- a/audiotrivia/audiotrivia.py +++ b/audiotrivia/audiotrivia.py @@ -1,17 +1,17 @@ import datetime import logging import pathlib -from typing import List +from typing import List, Optional +import discord 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.cogs.trivia.trivia import InvalidListError, Trivia, get_core_lists 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 box +from redbot.core.utils.chat_formatting import bold, box from .audiosession import AudioSession @@ -28,12 +28,11 @@ 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(delay=30.0, repeat=True) + self.audioconf.register_guild(audio_delay=30.0, repeat=True) @commands.group() @commands.guild_only() @@ -44,49 +43,42 @@ class AudioTrivia(Trivia): settings_dict = await audioset.all() msg = box( "**Audio settings**\n" - "Answer time limit: {delay} seconds\n" + "Answer time limit: {audio_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): + @atriviaset.command(name="timelimit") + async def atriviaset_timelimit(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)) + await settings.audo_delay.set(seconds) + await ctx.maybe_send_embed(f"Done. Maximum seconds to answer set to {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)) + await ctx.maybe_send_embed(f"Done. Repeating short audio is now set to {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. + """Start trivia session on the specified category or categories. + 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.maybe_send_embed("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: @@ -94,45 +86,9 @@ class AudioTrivia(Trivia): "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.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`" - ) - - 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.maybe_send_embed( - "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.maybe_send_embed("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.maybe_send_embed( - "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. @@ -140,19 +96,22 @@ class AudioTrivia(Trivia): dict_ = self.get_audio_list(category) except FileNotFoundError: await ctx.maybe_send_embed( - "Invalid category `{0}`. See `{1}audiotrivia list`" + f"Invalid category `{category}`. See `{ctx.prefix}audiotrivia list`" " for a list of trivia categories." - "".format(category, ctx.prefix) ) except InvalidListError: await ctx.maybe_send_embed( "There was an error parsing the trivia list for" - " the `{}` category. It may be formatted" - " incorrectly.".format(category) + f" the `{category}` category. It may be formatted" + " incorrectly." ) else: - trivia_dict.update(dict_) - authors.append(trivia_dict.pop("AUTHOR", None)) + 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 continue return if not trivia_dict: @@ -161,9 +120,35 @@ class AudioTrivia(Trivia): ) 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", None) + config = trivia_dict.pop("CONFIG", {"answer": None})["answer"] if config and settings["allow_override"]: settings.update(config) settings["lists"] = dict(zip(categories, reversed(authors))) @@ -171,25 +156,33 @@ class AudioTrivia(Trivia): # 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, + ctx, + trivia_dict, + combined_settings, + audio, ) 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 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) + """List available trivia including audio categories.""" + lists = set(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) def get_audio_list(self, category: str) -> dict: """Get the audiotrivia list corresponding to the given category. @@ -206,7 +199,7 @@ class AudioTrivia(Trivia): """ try: - path = next(p for p in self._audio_lists() if p.stem == category) + path = next(p for p in self._all_audio_lists() if p.stem == category) except StopIteration: raise FileNotFoundError("Could not find the `{}` category.".format(category)) @@ -218,13 +211,15 @@ class AudioTrivia(Trivia): else: return dict_ - def _audio_lists(self) -> List[pathlib.Path]: + def _all_audio_lists(self) -> List[pathlib.Path]: + # Custom trivia lists uploaded with audiotrivia. Not necessarily audio lists personal_lists = [p.resolve() for p in cog_data_path(self).glob("*.yaml")] - return personal_lists + get_core_lists() + # Add to that custom lists uploaded with trivia and core lists + return personal_lists + get_core_audio_lists() + self._all_lists() -def get_core_lists() -> List[pathlib.Path]: +def get_core_audio_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/anime.yaml b/audiotrivia/data/lists/audioanime.yaml similarity index 99% rename from audiotrivia/data/lists/anime.yaml rename to audiotrivia/data/lists/audioanime.yaml index 7a27a0e..8aa518d 100644 --- a/audiotrivia/data/lists/anime.yaml +++ b/audiotrivia/data/lists/audioanime.yaml @@ -1,4 +1,5 @@ AUTHOR: Plab +AUDIO: "[Audio] Identify this Anime!" https://www.youtube.com/watch?v=2uq34TeWEdQ: - 'Hagane no Renkinjutsushi (2009)' - '(2009) الخيميائي المعدني الكامل' diff --git a/audiotrivia/data/lists/nhlgoalhorns.yaml b/audiotrivia/data/lists/audionhlgoalhorns.yaml similarity index 97% rename from audiotrivia/data/lists/nhlgoalhorns.yaml rename to audiotrivia/data/lists/audionhlgoalhorns.yaml index 689f478..9e86313 100644 --- a/audiotrivia/data/lists/nhlgoalhorns.yaml +++ b/audiotrivia/data/lists/audionhlgoalhorns.yaml @@ -1,4 +1,5 @@ AUTHOR: Lazar +AUDIO: "[Audio] Identify this NHL Team by their goal horn" https://youtu.be/6OejNXrGkK0: - Anaheim Ducks - Anaheim diff --git a/audiotrivia/data/lists/videogames.yaml b/audiotrivia/data/lists/audiovideogames.yaml similarity index 99% rename from audiotrivia/data/lists/videogames.yaml rename to audiotrivia/data/lists/audiovideogames.yaml index 5798f0c..d7f594c 100644 --- a/audiotrivia/data/lists/videogames.yaml +++ b/audiotrivia/data/lists/audiovideogames.yaml @@ -1,4 +1,5 @@ AUTHOR: Bobloy +AUDIO: "[Audio] Identify this video game" https://www.youtube.com/watch?v=GBPbJyxqHV0: - Super Mario 64 https://www.youtube.com/watch?v=0jXTBAGv9ZQ: From 7c95bd4c0fa9d73fa958e53bbff3428dbf453cb7 Mon Sep 17 00:00:00 2001 From: bobloy Date: Thu, 1 Oct 2020 09:10:26 -0400 Subject: [PATCH 120/121] Black formatting --- audiotrivia/audiosession.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/audiotrivia/audiosession.py b/audiotrivia/audiosession.py index 17fc998..1f3297b 100644 --- a/audiotrivia/audiosession.py +++ b/audiotrivia/audiosession.py @@ -12,13 +12,13 @@ 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, audio=None): super().__init__(ctx, question_list, settings) self.audio = audio @classmethod - def start(cls, ctx, question_list, settings, audio = None): + def start(cls, ctx, question_list, settings, audio=None): session = cls(ctx, question_list, settings, audio) loop = ctx.bot.loop session._task = loop.create_task(session.run()) @@ -59,8 +59,8 @@ class AudioSession(TriviaSession): load_result = await player.load_tracks(audio_url) if ( - load_result.has_error - or load_result.load_type != lavalink.enums.LoadType.TRACK_LOADED + 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" @@ -118,4 +118,4 @@ class AudioSession(TriviaSession): if _audio: yield _audio, answers, question.strip("<>") else: - yield question, answers, _audio \ No newline at end of file + yield question, answers, _audio From 9440f34669f3be869bd5b9d896e6c16a26984658 Mon Sep 17 00:00:00 2001 From: bobloy Date: Thu, 1 Oct 2020 09:14:46 -0400 Subject: [PATCH 121/121] lovecalculator hotfix ssl error --- lovecalculator/lovecalculator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lovecalculator/lovecalculator.py b/lovecalculator/lovecalculator.py index 95e9f97..94b6d49 100644 --- a/lovecalculator/lovecalculator.py +++ b/lovecalculator/lovecalculator.py @@ -33,7 +33,7 @@ class LoveCalculator(Cog): x.replace(" ", "+"), y.replace(" ", "+") ) async with aiohttp.ClientSession(headers={"Connection": "keep-alive"}) as session: - async with session.get(url) as response: + async with session.get(url, ssl=False) as response: assert response.status == 200 resp = await response.text()