Merge branch 'master' into chatter_develop

pull/175/head
bobloy 4 years ago
commit 37c699eeee

@ -0,0 +1,26 @@
---
name: Bug report
about: Create an issue to report a bug
title: ''
labels: bug
assignees: bobloy
---
**Describe the bug**
<!--A clear and concise description of what the bug is.-->
**To Reproduce**
<!--Steps to reproduce the behavior:-->
1. Load cog '...'
2. Run command '....'
3. See error
**Expected behavior**
<!--A clear and concise description of what you expected to happen.-->
**Screenshots or Error Messages**
<!--If applicable, add screenshots to help explain your problem.-->
**Additional context**
<!--Add any other context about the problem here.-->

@ -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.**
<!--A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]-->
**Describe the solution you'd like**
<!--A clear and concise description of what you want to happen. Include which cog or cogs this would interact with-->

@ -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?**
<!--What's in the list? What kind of category is?-->
**Number of Questions**
<!--Rough estimate at the number of question in this list-->
**Original Content?**
<!--Did you come up with this list yourself or did you get it from some else's work?-->
<!--If no, be sure to include the source-->
- [ ] Yes
- [ ] No
**Did I test the list?**
<!--Did you already try out the list and find no bugs?-->
- [ ] Yes
- [ ] No

@ -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

@ -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: black
on: [pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.8
uses: actions/setup-python@v2
with:
python-version: '3.8'
- name: Install Black
run: pip install --upgrade --no-cache-dir black
- name: Run black --check .
run: black --check --diff -l 99 .

@ -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@2.2.0
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"

1
.gitignore vendored

@ -4,3 +4,4 @@ venv/
v-data/ v-data/
database.sqlite3 database.sqlite3
/venv3.4/ /venv3.4/
/.venv/

@ -2,9 +2,8 @@
import asyncio import asyncio
import logging import logging
import lavalink
from lavalink.enums import LoadType
from redbot.cogs.trivia import TriviaSession from redbot.cogs.trivia import TriviaSession
from redbot.cogs.trivia.session import _parse_answers
from redbot.core.utils.chat_formatting import bold from redbot.core.utils.chat_formatting import bold
log = logging.getLogger("red.fox_v3.audiotrivia.audiosession") log = logging.getLogger("red.fox_v3.audiotrivia.audiosession")
@ -13,14 +12,14 @@ log = logging.getLogger("red.fox_v3.audiotrivia.audiosession")
class AudioSession(TriviaSession): class AudioSession(TriviaSession):
"""Class to run a session of audio trivia""" """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) super().__init__(ctx, question_list, settings)
self.player = player self.audio = audio
@classmethod @classmethod
def start(cls, ctx, question_list, settings, player: lavalink.Player = None): def start(cls, ctx, question_list, settings, audio=None):
session = cls(ctx, question_list, settings, player) session = cls(ctx, question_list, settings, audio)
loop = ctx.bot.loop loop = ctx.bot.loop
session._task = loop.create_task(session.run()) session._task = loop.create_task(session.run())
return session return session
@ -34,57 +33,89 @@ class AudioSession(TriviaSession):
await self._send_startup_msg() await self._send_startup_msg()
max_score = self.settings["max_score"] max_score = self.settings["max_score"]
delay = self.settings["delay"] delay = self.settings["delay"]
audio_delay = self.settings["audio_delay"]
timeout = self.settings["timeout"] 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(): async with self.ctx.typing():
await asyncio.sleep(3) await asyncio.sleep(3)
self.count += 1 self.count += 1
await self.player.stop() msg = bold(f"Question number {self.count}!") + f"\n\n{question}"
if player:
msg = bold(f"Question number {self.count}!") + "\n\nName this audio!" 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) await self.ctx.maybe_send_embed(msg)
log.debug(f"Audio question: {question}") 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: if continue_ is False:
break break
if any(score >= max_score for score in self.scores.values()): if any(score >= max_score for score in self.scores.values()):
await self.end_game() await self.end_game()
break break
else: 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() await self.end_game()
async def end_game(self): async def end_game(self):
await super().end_game() 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

@ -1,17 +1,17 @@
import datetime import datetime
import logging import logging
import pathlib import pathlib
from typing import List from typing import List, Optional
import discord
import lavalink import lavalink
import yaml import yaml
from redbot.cogs.audio import Audio from redbot.cogs.audio import Audio
from redbot.cogs.trivia import LOG from redbot.cogs.trivia.trivia import InvalidListError, Trivia, get_core_lists
from redbot.cogs.trivia.trivia import InvalidListError, Trivia
from redbot.core import Config, checks, commands from redbot.core import Config, checks, commands
from redbot.core.bot import Red from redbot.core.bot import Red
from redbot.core.data_manager import cog_data_path 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 from .audiosession import AudioSession
@ -28,12 +28,11 @@ class AudioTrivia(Trivia):
def __init__(self, bot: Red): def __init__(self, bot: Red):
super().__init__() super().__init__()
self.bot = bot self.bot = bot
self.audio = None
self.audioconf = Config.get_conf( self.audioconf = Config.get_conf(
self, identifier=651171001051118411410511810597, force_registration=True 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.group()
@commands.guild_only() @commands.guild_only()
@ -44,91 +43,52 @@ class AudioTrivia(Trivia):
settings_dict = await audioset.all() settings_dict = await audioset.all()
msg = box( msg = box(
"**Audio settings**\n" "**Audio settings**\n"
"Answer time limit: {delay} seconds\n" "Answer time limit: {audio_delay} seconds\n"
"Repeat Short Audio: {repeat}" "Repeat Short Audio: {repeat}"
"".format(**settings_dict), "".format(**settings_dict),
lang="py", lang="py",
) )
await ctx.send(msg) await ctx.send(msg)
@atriviaset.command(name="delay") @atriviaset.command(name="timelimit")
async def atriviaset_delay(self, ctx: commands.Context, seconds: float): async def atriviaset_timelimit(self, ctx: commands.Context, seconds: float):
"""Set the maximum seconds permitted to answer a question.""" """Set the maximum seconds permitted to answer a question."""
if seconds < 4.0: if seconds < 4.0:
await ctx.send("Must be at least 4 seconds.") await ctx.send("Must be at least 4 seconds.")
return return
settings = self.audioconf.guild(ctx.guild) settings = self.audioconf.guild(ctx.guild)
await settings.delay.set(seconds) await settings.audo_delay.set(seconds)
await ctx.send("Done. Maximum seconds to answer set to {}.".format(seconds)) await ctx.maybe_send_embed(f"Done. Maximum seconds to answer set to {seconds}.")
@atriviaset.command(name="repeat") @atriviaset.command(name="repeat")
async def atriviaset_repeat(self, ctx: commands.Context, true_or_false: bool): async def atriviaset_repeat(self, ctx: commands.Context, true_or_false: bool):
"""Set whether or not short audio will be repeated""" """Set whether or not short audio will be repeated"""
settings = self.audioconf.guild(ctx.guild) settings = self.audioconf.guild(ctx.guild)
await settings.repeat.set(true_or_false) 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.group(invoke_without_command=True)
@commands.guild_only() @commands.guild_only()
async def audiotrivia(self, ctx: commands.Context, *categories: str): 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 You may list multiple categories, in which case the trivia will involve
questions from all of them. questions from all of them.
""" """
if not categories and ctx.invoked_subcommand is None: if not categories and ctx.invoked_subcommand is None:
await ctx.send_help() await ctx.send_help()
return 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] categories = [c.lower() for c in categories]
session = self._get_trivia_session(ctx.channel) session = self._get_trivia_session(ctx.channel)
if session is not None: if session is not None:
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.maybe_send_embed(
f"It is recommended to disable audio status with `{ctx.prefix}audioset status`"
)
if notify:
await ctx.maybe_send_embed( await ctx.maybe_send_embed(
f"It is recommended to disable audio notify with `{ctx.prefix}audioset notify`" "There is already an ongoing trivia session in this channel."
) )
return
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 = {} trivia_dict = {}
authors = [] authors = []
any_audio = False
for category in reversed(categories): for category in reversed(categories):
# We reverse the categories so that the first list's config takes # We reverse the categories so that the first list's config takes
# priority over the others. # priority over the others.
@ -136,19 +96,22 @@ class AudioTrivia(Trivia):
dict_ = self.get_audio_list(category) dict_ = self.get_audio_list(category)
except FileNotFoundError: except FileNotFoundError:
await ctx.maybe_send_embed( 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." " for a list of trivia categories."
"".format(category, ctx.prefix)
) )
except InvalidListError: except InvalidListError:
await ctx.maybe_send_embed( await ctx.maybe_send_embed(
"There was an error parsing the trivia list for" "There was an error parsing the trivia list for"
" the `{}` category. It may be formatted" f" the `{category}` category. It may be formatted"
" incorrectly.".format(category) " incorrectly."
) )
else: else:
trivia_dict.update(dict_) is_audio = dict_.pop("AUDIO", False)
authors.append(trivia_dict.pop("AUTHOR", None)) 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 continue
return return
if not trivia_dict: if not trivia_dict:
@ -157,9 +120,35 @@ class AudioTrivia(Trivia):
) )
return 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() settings = await self.config.guild(ctx.guild).all()
audiosettings = await self.audioconf.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"]: if config and settings["allow_override"]:
settings.update(config) settings.update(config)
settings["lists"] = dict(zip(categories, reversed(authors))) settings["lists"] = dict(zip(categories, reversed(authors)))
@ -167,25 +156,33 @@ class AudioTrivia(Trivia):
# Delay in audiosettings overwrites delay in settings # Delay in audiosettings overwrites delay in settings
combined_settings = {**settings, **audiosettings} combined_settings = {**settings, **audiosettings}
session = AudioSession.start( session = AudioSession.start(
ctx=ctx, ctx,
question_list=trivia_dict, trivia_dict,
settings=combined_settings, combined_settings,
player=lavaplayer, audio,
) )
self.trivia_sessions.append(session) 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") @audiotrivia.command(name="list")
@commands.guild_only() @commands.guild_only()
async def audiotrivia_list(self, ctx: commands.Context): async def audiotrivia_list(self, ctx: commands.Context):
"""List available trivia categories.""" """List available trivia including audio categories."""
lists = set(p.stem for p in self._audio_lists()) lists = set(p.stem for p in self._all_audio_lists())
if await ctx.embed_requested():
msg = box("**Available trivia lists**\n\n{}".format(", ".join(sorted(lists)))) await ctx.send(
if len(msg) > 1000: embed=discord.Embed(
await ctx.author.send(msg) title="Available trivia lists",
return colour=await ctx.embed_colour(),
await ctx.send(msg) 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: def get_audio_list(self, category: str) -> dict:
"""Get the audiotrivia list corresponding to the given category. """Get the audiotrivia list corresponding to the given category.
@ -202,7 +199,7 @@ class AudioTrivia(Trivia):
""" """
try: 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: except StopIteration:
raise FileNotFoundError("Could not find the `{}` category.".format(category)) raise FileNotFoundError("Could not find the `{}` category.".format(category))
@ -214,13 +211,15 @@ class AudioTrivia(Trivia):
else: else:
return dict_ 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")] 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.""" """Return a list of paths for all trivia lists packaged with the bot."""
core_lists_path = pathlib.Path(__file__).parent.resolve() / "data/lists" core_lists_path = pathlib.Path(__file__).parent.resolve() / "data/lists"
return list(core_lists_path.glob("*.yaml")) return list(core_lists_path.glob("*.yaml"))

@ -1,4 +1,5 @@
AUTHOR: Plab AUTHOR: Plab
AUDIO: "[Audio] Identify this Anime!"
https://www.youtube.com/watch?v=2uq34TeWEdQ: https://www.youtube.com/watch?v=2uq34TeWEdQ:
- 'Hagane no Renkinjutsushi (2009)' - 'Hagane no Renkinjutsushi (2009)'
- '(2009) الخيميائي المعدني الكامل' - '(2009) الخيميائي المعدني الكامل'

@ -1,4 +1,5 @@
AUTHOR: Lazar AUTHOR: Lazar
AUDIO: "[Audio] Identify this NHL Team by their goal horn"
https://youtu.be/6OejNXrGkK0: https://youtu.be/6OejNXrGkK0:
- Anaheim Ducks - Anaheim Ducks
- Anaheim - Anaheim

File diff suppressed because it is too large Load Diff

@ -1,4 +1,5 @@
AUTHOR: Plab AUTHOR: Plab
NEEDS: New links for all songs.
https://www.youtube.com/watch?v=f9O2Rjn1azc: https://www.youtube.com/watch?v=f9O2Rjn1azc:
- Transistor - Transistor
https://www.youtube.com/watch?v=PgUhYFkVdSY: https://www.youtube.com/watch?v=PgUhYFkVdSY:

@ -143,8 +143,9 @@ class CCRole(commands.Cog):
return return
# Selfrole # Selfrole
await ctx.send("Is this a targeted command?(yes/no)\n" await ctx.send(
"No will make this a selfrole command") "Is this a targeted command?(yes/no)\n" "No will make this a selfrole command"
)
try: try:
answer = await self.bot.wait_for("message", timeout=120, check=check) answer = await self.bot.wait_for("message", timeout=120, check=check)

@ -433,7 +433,7 @@ class Chatter(Cog):
else: else:
await ctx.maybe_send_embed("Error occurred :(") await ctx.maybe_send_embed("Error occurred :(")
@commands.Cog.listener() @Cog.listener()
async def on_message_without_command(self, message: discord.Message): async def on_message_without_command(self, message: discord.Message):
""" """
Credit to https://github.com/Twentysix26/26-Cogs/blob/master/cleverbot/cleverbot.py Credit to https://github.com/Twentysix26/26-Cogs/blob/master/cleverbot/cleverbot.py

@ -159,7 +159,12 @@ class Conquest(commands.Cog):
self.data_path / self.current_map / f"current.{self.ext}", x, y, zoom 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): async def _create_zoomed_map(self, map_path, x, y, zoom, **kwargs):
current_map = Image.open(map_path) current_map = Image.open(map_path)

@ -15,182 +15,216 @@ def assemble_timezones():
""" """
timezones = {} timezones = {}
timezones['ACDT'] = timezone('Australia/Darwin') # Australian Central Daylight Savings Time (UTC+10:30) timezones["ACDT"] = timezone(
timezones['ACST'] = timezone('Australia/Darwin') # Australian Central Standard Time (UTC+09:30) "Australia/Darwin"
timezones['ACT'] = timezone('Brazil/Acre') # Acre Time (UTC05) ) # Australian Central Daylight Savings Time (UTC+10:30)
timezones['ADT'] = timezone('America/Halifax') # Atlantic Daylight Time (UTC03) timezones["ACST"] = timezone(
timezones['AEDT'] = timezone('Australia/Sydney') # Australian Eastern Daylight Savings Time (UTC+11) "Australia/Darwin"
timezones['AEST'] = timezone('Australia/Sydney') # Australian Eastern Standard Time (UTC+10) ) # Australian Central Standard Time (UTC+09:30)
timezones['AFT'] = timezone('Asia/Kabul') # Afghanistan Time (UTC+04:30) timezones["ACT"] = timezone("Brazil/Acre") # Acre Time (UTC05)
timezones['AKDT'] = timezone('America/Juneau') # Alaska Daylight Time (UTC08) timezones["ADT"] = timezone("America/Halifax") # Atlantic Daylight Time (UTC03)
timezones['AKST'] = timezone('America/Juneau') # Alaska Standard Time (UTC09) timezones["AEDT"] = timezone(
timezones['AMST'] = timezone('America/Manaus') # Amazon Summer Time (Brazil)[1] (UTC03) "Australia/Sydney"
timezones['AMT'] = timezone('America/Manaus') # Amazon Time (Brazil)[2] (UTC04) ) # Australian Eastern Daylight Savings Time (UTC+11)
timezones['ART'] = timezone('America/Cordoba') # Argentina Time (UTC03) timezones["AEST"] = timezone("Australia/Sydney") # Australian Eastern Standard Time (UTC+10)
timezones['AST'] = timezone('Asia/Riyadh') # Arabia Standard Time (UTC+03) timezones["AFT"] = timezone("Asia/Kabul") # Afghanistan Time (UTC+04:30)
timezones['AWST'] = timezone('Australia/Perth') # Australian Western Standard Time (UTC+08) timezones["AKDT"] = timezone("America/Juneau") # Alaska Daylight Time (UTC08)
timezones['AZOST'] = timezone('Atlantic/Azores') # Azores Summer Time (UTC±00) timezones["AKST"] = timezone("America/Juneau") # Alaska Standard Time (UTC09)
timezones['AZOT'] = timezone('Atlantic/Azores') # Azores Standard Time (UTC01) timezones["AMST"] = timezone("America/Manaus") # Amazon Summer Time (Brazil)[1] (UTC03)
timezones['AZT'] = timezone('Asia/Baku') # Azerbaijan Time (UTC+04) timezones["AMT"] = timezone("America/Manaus") # Amazon Time (Brazil)[2] (UTC04)
timezones['BDT'] = timezone('Asia/Brunei') # Brunei Time (UTC+08) timezones["ART"] = timezone("America/Cordoba") # Argentina Time (UTC03)
timezones['BIOT'] = timezone('Etc/GMT+6') # British Indian Ocean Time (UTC+06) timezones["AST"] = timezone("Asia/Riyadh") # Arabia Standard Time (UTC+03)
timezones['BIT'] = timezone('Pacific/Funafuti') # Baker Island Time (UTC12) timezones["AWST"] = timezone("Australia/Perth") # Australian Western Standard Time (UTC+08)
timezones['BOT'] = timezone('America/La_Paz') # Bolivia Time (UTC04) timezones["AZOST"] = timezone("Atlantic/Azores") # Azores Summer Time (UTC±00)
timezones['BRST'] = timezone('America/Sao_Paulo') # Brasilia Summer Time (UTC02) timezones["AZOT"] = timezone("Atlantic/Azores") # Azores Standard Time (UTC01)
timezones['BRT'] = timezone('America/Sao_Paulo') # Brasilia Time (UTC03) timezones["AZT"] = timezone("Asia/Baku") # Azerbaijan Time (UTC+04)
timezones['BST'] = timezone('Asia/Dhaka') # Bangladesh Standard Time (UTC+06) timezones["BDT"] = timezone("Asia/Brunei") # Brunei Time (UTC+08)
timezones['BTT'] = timezone('Asia/Thimphu') # Bhutan Time (UTC+06) timezones["BIOT"] = timezone("Etc/GMT+6") # British Indian Ocean Time (UTC+06)
timezones['CAT'] = timezone('Africa/Harare') # Central Africa Time (UTC+02) timezones["BIT"] = timezone("Pacific/Funafuti") # Baker Island Time (UTC12)
timezones['CCT'] = timezone('Indian/Cocos') # Cocos Islands Time (UTC+06:30) timezones["BOT"] = timezone("America/La_Paz") # Bolivia Time (UTC04)
timezones['CDT'] = timezone('America/Chicago') # Central Daylight Time (North America) (UTC05) timezones["BRST"] = timezone("America/Sao_Paulo") # Brasilia Summer Time (UTC02)
timezones['CEST'] = timezone('Europe/Berlin') # Central European Summer Time (Cf. HAEC) (UTC+02) timezones["BRT"] = timezone("America/Sao_Paulo") # Brasilia Time (UTC03)
timezones['CET'] = timezone('Europe/Berlin') # Central European Time (UTC+01) timezones["BST"] = timezone("Asia/Dhaka") # Bangladesh Standard Time (UTC+06)
timezones['CHADT'] = timezone('Pacific/Chatham') # Chatham Daylight Time (UTC+13:45) timezones["BTT"] = timezone("Asia/Thimphu") # Bhutan Time (UTC+06)
timezones['CHAST'] = timezone('Pacific/Chatham') # Chatham Standard Time (UTC+12:45) timezones["CAT"] = timezone("Africa/Harare") # Central Africa Time (UTC+02)
timezones['CHOST'] = timezone('Asia/Choibalsan') # Choibalsan Summer Time (UTC+09) timezones["CCT"] = timezone("Indian/Cocos") # Cocos Islands Time (UTC+06:30)
timezones['CHOT'] = timezone('Asia/Choibalsan') # Choibalsan Standard Time (UTC+08) timezones["CDT"] = timezone(
timezones['CHST'] = timezone('Pacific/Guam') # Chamorro Standard Time (UTC+10) "America/Chicago"
timezones['CHUT'] = timezone('Pacific/Chuuk') # Chuuk Time (UTC+10) ) # Central Daylight Time (North America) (UTC05)
timezones['CIST'] = timezone('Etc/GMT-8') # Clipperton Island Standard Time (UTC08) timezones["CEST"] = timezone(
timezones['CIT'] = timezone('Asia/Makassar') # Central Indonesia Time (UTC+08) "Europe/Berlin"
timezones['CKT'] = timezone('Pacific/Rarotonga') # Cook Island Time (UTC10) ) # Central European Summer Time (Cf. HAEC) (UTC+02)
timezones['CLST'] = timezone('America/Santiago') # Chile Summer Time (UTC03) timezones["CET"] = timezone("Europe/Berlin") # Central European Time (UTC+01)
timezones['CLT'] = timezone('America/Santiago') # Chile Standard Time (UTC04) timezones["CHADT"] = timezone("Pacific/Chatham") # Chatham Daylight Time (UTC+13:45)
timezones['COST'] = timezone('America/Bogota') # Colombia Summer Time (UTC04) timezones["CHAST"] = timezone("Pacific/Chatham") # Chatham Standard Time (UTC+12:45)
timezones['COT'] = timezone('America/Bogota') # Colombia Time (UTC05) timezones["CHOST"] = timezone("Asia/Choibalsan") # Choibalsan Summer Time (UTC+09)
timezones['CST'] = timezone('America/Chicago') # Central Standard Time (North America) (UTC06) timezones["CHOT"] = timezone("Asia/Choibalsan") # Choibalsan Standard Time (UTC+08)
timezones['CT'] = timezone('Asia/Chongqing') # China time (UTC+08) timezones["CHST"] = timezone("Pacific/Guam") # Chamorro Standard Time (UTC+10)
timezones['CVT'] = timezone('Atlantic/Cape_Verde') # Cape Verde Time (UTC01) timezones["CHUT"] = timezone("Pacific/Chuuk") # Chuuk Time (UTC+10)
timezones['CXT'] = timezone('Indian/Christmas') # Christmas Island Time (UTC+07) timezones["CIST"] = timezone("Etc/GMT-8") # Clipperton Island Standard Time (UTC08)
timezones['DAVT'] = timezone('Antarctica/Davis') # Davis Time (UTC+07) timezones["CIT"] = timezone("Asia/Makassar") # Central Indonesia Time (UTC+08)
timezones['DDUT'] = timezone('Antarctica/DumontDUrville') # Dumont d'Urville Time (UTC+10) timezones["CKT"] = timezone("Pacific/Rarotonga") # Cook Island Time (UTC10)
timezones['DFT'] = timezone('Europe/Berlin') # AIX equivalent of Central European Time (UTC+01) timezones["CLST"] = timezone("America/Santiago") # Chile Summer Time (UTC03)
timezones['EASST'] = timezone('Chile/EasterIsland') # Easter Island Summer Time (UTC05) timezones["CLT"] = timezone("America/Santiago") # Chile Standard Time (UTC04)
timezones['EAST'] = timezone('Chile/EasterIsland') # Easter Island Standard Time (UTC06) timezones["COST"] = timezone("America/Bogota") # Colombia Summer Time (UTC04)
timezones['EAT'] = timezone('Africa/Mogadishu') # East Africa Time (UTC+03) timezones["COT"] = timezone("America/Bogota") # Colombia Time (UTC05)
timezones['ECT'] = timezone('America/Guayaquil') # Ecuador Time (UTC05) timezones["CST"] = timezone(
timezones['EDT'] = timezone('America/New_York') # Eastern Daylight Time (North America) (UTC04) "America/Chicago"
timezones['EEST'] = timezone('Europe/Bucharest') # Eastern European Summer Time (UTC+03) ) # Central Standard Time (North America) (UTC06)
timezones['EET'] = timezone('Europe/Bucharest') # Eastern European Time (UTC+02) timezones["CT"] = timezone("Asia/Chongqing") # China time (UTC+08)
timezones['EGST'] = timezone('America/Scoresbysund') # Eastern Greenland Summer Time (UTC±00) timezones["CVT"] = timezone("Atlantic/Cape_Verde") # Cape Verde Time (UTC01)
timezones['EGT'] = timezone('America/Scoresbysund') # Eastern Greenland Time (UTC01) timezones["CXT"] = timezone("Indian/Christmas") # Christmas Island Time (UTC+07)
timezones['EIT'] = timezone('Asia/Jayapura') # Eastern Indonesian Time (UTC+09) timezones["DAVT"] = timezone("Antarctica/Davis") # Davis Time (UTC+07)
timezones['EST'] = timezone('America/New_York') # Eastern Standard Time (North America) (UTC05) timezones["DDUT"] = timezone("Antarctica/DumontDUrville") # Dumont d'Urville Time (UTC+10)
timezones['FET'] = timezone('Europe/Minsk') # Further-eastern European Time (UTC+03) timezones["DFT"] = timezone(
timezones['FJT'] = timezone('Pacific/Fiji') # Fiji Time (UTC+12) "Europe/Berlin"
timezones['FKST'] = timezone('Atlantic/Stanley') # Falkland Islands Summer Time (UTC03) ) # AIX equivalent of Central European Time (UTC+01)
timezones['FKT'] = timezone('Atlantic/Stanley') # Falkland Islands Time (UTC04) timezones["EASST"] = timezone("Chile/EasterIsland") # Easter Island Summer Time (UTC05)
timezones['FNT'] = timezone('Brazil/DeNoronha') # Fernando de Noronha Time (UTC02) timezones["EAST"] = timezone("Chile/EasterIsland") # Easter Island Standard Time (UTC06)
timezones['GALT'] = timezone('Pacific/Galapagos') # Galapagos Time (UTC06) timezones["EAT"] = timezone("Africa/Mogadishu") # East Africa Time (UTC+03)
timezones['GAMT'] = timezone('Pacific/Gambier') # Gambier Islands (UTC09) timezones["ECT"] = timezone("America/Guayaquil") # Ecuador Time (UTC05)
timezones['GET'] = timezone('Asia/Tbilisi') # Georgia Standard Time (UTC+04) timezones["EDT"] = timezone(
timezones['GFT'] = timezone('America/Cayenne') # French Guiana Time (UTC03) "America/New_York"
timezones['GILT'] = timezone('Pacific/Tarawa') # Gilbert Island Time (UTC+12) ) # Eastern Daylight Time (North America) (UTC04)
timezones['GIT'] = timezone('Pacific/Gambier') # Gambier Island Time (UTC09) timezones["EEST"] = timezone("Europe/Bucharest") # Eastern European Summer Time (UTC+03)
timezones['GMT'] = timezone('GMT') # Greenwich Mean Time (UTC±00) timezones["EET"] = timezone("Europe/Bucharest") # Eastern European Time (UTC+02)
timezones['GST'] = timezone('Asia/Muscat') # Gulf Standard Time (UTC+04) timezones["EGST"] = timezone("America/Scoresbysund") # Eastern Greenland Summer Time (UTC±00)
timezones['GYT'] = timezone('America/Guyana') # Guyana Time (UTC04) timezones["EGT"] = timezone("America/Scoresbysund") # Eastern Greenland Time (UTC01)
timezones['HADT'] = timezone('Pacific/Honolulu') # Hawaii-Aleutian Daylight Time (UTC09) timezones["EIT"] = timezone("Asia/Jayapura") # Eastern Indonesian Time (UTC+09)
timezones['HAEC'] = timezone('Europe/Paris') # Heure Avancée d'Europe Centrale (CEST) (UTC+02) timezones["EST"] = timezone(
timezones['HAST'] = timezone('Pacific/Honolulu') # Hawaii-Aleutian Standard Time (UTC10) "America/New_York"
timezones['HKT'] = timezone('Asia/Hong_Kong') # Hong Kong Time (UTC+08) ) # Eastern Standard Time (North America) (UTC05)
timezones['HMT'] = timezone('Indian/Kerguelen') # Heard and McDonald Islands Time (UTC+05) timezones["FET"] = timezone("Europe/Minsk") # Further-eastern European Time (UTC+03)
timezones['HOVST'] = timezone('Asia/Hovd') # Khovd Summer Time (UTC+08) timezones["FJT"] = timezone("Pacific/Fiji") # Fiji Time (UTC+12)
timezones['HOVT'] = timezone('Asia/Hovd') # Khovd Standard Time (UTC+07) timezones["FKST"] = timezone("Atlantic/Stanley") # Falkland Islands Summer Time (UTC03)
timezones['ICT'] = timezone('Asia/Ho_Chi_Minh') # Indochina Time (UTC+07) timezones["FKT"] = timezone("Atlantic/Stanley") # Falkland Islands Time (UTC04)
timezones['IDT'] = timezone('Asia/Jerusalem') # Israel Daylight Time (UTC+03) timezones["FNT"] = timezone("Brazil/DeNoronha") # Fernando de Noronha Time (UTC02)
timezones['IOT'] = timezone('Etc/GMT+3') # Indian Ocean Time (UTC+03) timezones["GALT"] = timezone("Pacific/Galapagos") # Galapagos Time (UTC06)
timezones['IRDT'] = timezone('Asia/Tehran') # Iran Daylight Time (UTC+04:30) timezones["GAMT"] = timezone("Pacific/Gambier") # Gambier Islands (UTC09)
timezones['IRKT'] = timezone('Asia/Irkutsk') # Irkutsk Time (UTC+08) timezones["GET"] = timezone("Asia/Tbilisi") # Georgia Standard Time (UTC+04)
timezones['IRST'] = timezone('Asia/Tehran') # Iran Standard Time (UTC+03:30) timezones["GFT"] = timezone("America/Cayenne") # French Guiana Time (UTC03)
timezones['IST'] = timezone('Asia/Kolkata') # Indian Standard Time (UTC+05:30) timezones["GILT"] = timezone("Pacific/Tarawa") # Gilbert Island Time (UTC+12)
timezones['JST'] = timezone('Asia/Tokyo') # Japan Standard Time (UTC+09) timezones["GIT"] = timezone("Pacific/Gambier") # Gambier Island Time (UTC09)
timezones['KGT'] = timezone('Asia/Bishkek') # Kyrgyzstan time (UTC+06) timezones["GMT"] = timezone("GMT") # Greenwich Mean Time (UTC±00)
timezones['KOST'] = timezone('Pacific/Kosrae') # Kosrae Time (UTC+11) timezones["GST"] = timezone("Asia/Muscat") # Gulf Standard Time (UTC+04)
timezones['KRAT'] = timezone('Asia/Krasnoyarsk') # Krasnoyarsk Time (UTC+07) timezones["GYT"] = timezone("America/Guyana") # Guyana Time (UTC04)
timezones['KST'] = timezone('Asia/Seoul') # Korea Standard Time (UTC+09) timezones["HADT"] = timezone("Pacific/Honolulu") # Hawaii-Aleutian Daylight Time (UTC09)
timezones['LHST'] = timezone('Australia/Lord_Howe') # Lord Howe Standard Time (UTC+10:30) timezones["HAEC"] = timezone("Europe/Paris") # Heure Avancée d'Europe Centrale (CEST) (UTC+02)
timezones['LINT'] = timezone('Pacific/Kiritimati') # Line Islands Time (UTC+14) timezones["HAST"] = timezone("Pacific/Honolulu") # Hawaii-Aleutian Standard Time (UTC10)
timezones['MAGT'] = timezone('Asia/Magadan') # Magadan Time (UTC+12) timezones["HKT"] = timezone("Asia/Hong_Kong") # Hong Kong Time (UTC+08)
timezones['MART'] = timezone('Pacific/Marquesas') # Marquesas Islands Time (UTC09:30) timezones["HMT"] = timezone("Indian/Kerguelen") # Heard and McDonald Islands Time (UTC+05)
timezones['MAWT'] = timezone('Antarctica/Mawson') # Mawson Station Time (UTC+05) timezones["HOVST"] = timezone("Asia/Hovd") # Khovd Summer Time (UTC+08)
timezones['MDT'] = timezone('America/Denver') # Mountain Daylight Time (North America) (UTC06) timezones["HOVT"] = timezone("Asia/Hovd") # Khovd Standard Time (UTC+07)
timezones['MEST'] = timezone('Europe/Paris') # Middle European Summer Time Same zone as CEST (UTC+02) timezones["ICT"] = timezone("Asia/Ho_Chi_Minh") # Indochina Time (UTC+07)
timezones['MET'] = timezone('Europe/Berlin') # Middle European Time Same zone as CET (UTC+01) timezones["IDT"] = timezone("Asia/Jerusalem") # Israel Daylight Time (UTC+03)
timezones['MHT'] = timezone('Pacific/Kwajalein') # Marshall Islands (UTC+12) timezones["IOT"] = timezone("Etc/GMT+3") # Indian Ocean Time (UTC+03)
timezones['MIST'] = timezone('Antarctica/Macquarie') # Macquarie Island Station Time (UTC+11) timezones["IRDT"] = timezone("Asia/Tehran") # Iran Daylight Time (UTC+04:30)
timezones['MIT'] = timezone('Pacific/Marquesas') # Marquesas Islands Time (UTC09:30) timezones["IRKT"] = timezone("Asia/Irkutsk") # Irkutsk Time (UTC+08)
timezones['MMT'] = timezone('Asia/Rangoon') # Myanmar Standard Time (UTC+06:30) timezones["IRST"] = timezone("Asia/Tehran") # Iran Standard Time (UTC+03:30)
timezones['MSK'] = timezone('Europe/Moscow') # Moscow Time (UTC+03) timezones["IST"] = timezone("Asia/Kolkata") # Indian Standard Time (UTC+05:30)
timezones['MST'] = timezone('America/Denver') # Mountain Standard Time (North America) (UTC07) timezones["JST"] = timezone("Asia/Tokyo") # Japan Standard Time (UTC+09)
timezones['MUT'] = timezone('Indian/Mauritius') # Mauritius Time (UTC+04) timezones["KGT"] = timezone("Asia/Bishkek") # Kyrgyzstan time (UTC+06)
timezones['MVT'] = timezone('Indian/Maldives') # Maldives Time (UTC+05) timezones["KOST"] = timezone("Pacific/Kosrae") # Kosrae Time (UTC+11)
timezones['MYT'] = timezone('Asia/Kuching') # Malaysia Time (UTC+08) timezones["KRAT"] = timezone("Asia/Krasnoyarsk") # Krasnoyarsk Time (UTC+07)
timezones['NCT'] = timezone('Pacific/Noumea') # New Caledonia Time (UTC+11) timezones["KST"] = timezone("Asia/Seoul") # Korea Standard Time (UTC+09)
timezones['NDT'] = timezone('Canada/Newfoundland') # Newfoundland Daylight Time (UTC02:30) timezones["LHST"] = timezone("Australia/Lord_Howe") # Lord Howe Standard Time (UTC+10:30)
timezones['NFT'] = timezone('Pacific/Norfolk') # Norfolk Time (UTC+11) timezones["LINT"] = timezone("Pacific/Kiritimati") # Line Islands Time (UTC+14)
timezones['NPT'] = timezone('Asia/Kathmandu') # Nepal Time (UTC+05:45) timezones["MAGT"] = timezone("Asia/Magadan") # Magadan Time (UTC+12)
timezones['NST'] = timezone('Canada/Newfoundland') # Newfoundland Standard Time (UTC03:30) timezones["MART"] = timezone("Pacific/Marquesas") # Marquesas Islands Time (UTC09:30)
timezones['NT'] = timezone('Canada/Newfoundland') # Newfoundland Time (UTC03:30) timezones["MAWT"] = timezone("Antarctica/Mawson") # Mawson Station Time (UTC+05)
timezones['NUT'] = timezone('Pacific/Niue') # Niue Time (UTC11) timezones["MDT"] = timezone(
timezones['NZDT'] = timezone('Pacific/Auckland') # New Zealand Daylight Time (UTC+13) "America/Denver"
timezones['NZST'] = timezone('Pacific/Auckland') # New Zealand Standard Time (UTC+12) ) # Mountain Daylight Time (North America) (UTC06)
timezones['OMST'] = timezone('Asia/Omsk') # Omsk Time (UTC+06) timezones["MEST"] = timezone(
timezones['ORAT'] = timezone('Asia/Oral') # Oral Time (UTC+05) "Europe/Paris"
timezones['PDT'] = timezone('America/Los_Angeles') # Pacific Daylight Time (North America) (UTC07) ) # Middle European Summer Time Same zone as CEST (UTC+02)
timezones['PET'] = timezone('America/Lima') # Peru Time (UTC05) timezones["MET"] = timezone("Europe/Berlin") # Middle European Time Same zone as CET (UTC+01)
timezones['PETT'] = timezone('Asia/Kamchatka') # Kamchatka Time (UTC+12) timezones["MHT"] = timezone("Pacific/Kwajalein") # Marshall Islands (UTC+12)
timezones['PGT'] = timezone('Pacific/Port_Moresby') # Papua New Guinea Time (UTC+10) timezones["MIST"] = timezone("Antarctica/Macquarie") # Macquarie Island Station Time (UTC+11)
timezones['PHOT'] = timezone('Pacific/Enderbury') # Phoenix Island Time (UTC+13) timezones["MIT"] = timezone("Pacific/Marquesas") # Marquesas Islands Time (UTC09:30)
timezones['PKT'] = timezone('Asia/Karachi') # Pakistan Standard Time (UTC+05) timezones["MMT"] = timezone("Asia/Rangoon") # Myanmar Standard Time (UTC+06:30)
timezones['PMDT'] = timezone('America/Miquelon') # Saint Pierre and Miquelon Daylight time (UTC02) timezones["MSK"] = timezone("Europe/Moscow") # Moscow Time (UTC+03)
timezones['PMST'] = timezone('America/Miquelon') # Saint Pierre and Miquelon Standard Time (UTC03) timezones["MST"] = timezone(
timezones['PONT'] = timezone('Pacific/Pohnpei') # Pohnpei Standard Time (UTC+11) "America/Denver"
timezones['PST'] = timezone('America/Los_Angeles') # Pacific Standard Time (North America) (UTC08) ) # Mountain Standard Time (North America) (UTC07)
timezones['PYST'] = timezone('America/Asuncion') # Paraguay Summer Time (South America)[7] (UTC03) timezones["MUT"] = timezone("Indian/Mauritius") # Mauritius Time (UTC+04)
timezones['PYT'] = timezone('America/Asuncion') # Paraguay Time (South America)[8] (UTC04) timezones["MVT"] = timezone("Indian/Maldives") # Maldives Time (UTC+05)
timezones['RET'] = timezone('Indian/Reunion') # Réunion Time (UTC+04) timezones["MYT"] = timezone("Asia/Kuching") # Malaysia Time (UTC+08)
timezones['ROTT'] = timezone('Antarctica/Rothera') # Rothera Research Station Time (UTC03) timezones["NCT"] = timezone("Pacific/Noumea") # New Caledonia Time (UTC+11)
timezones['SAKT'] = timezone('Asia/Vladivostok') # Sakhalin Island time (UTC+11) timezones["NDT"] = timezone("Canada/Newfoundland") # Newfoundland Daylight Time (UTC02:30)
timezones['SAMT'] = timezone('Europe/Samara') # Samara Time (UTC+04) timezones["NFT"] = timezone("Pacific/Norfolk") # Norfolk Time (UTC+11)
timezones['SAST'] = timezone('Africa/Johannesburg') # South African Standard Time (UTC+02) timezones["NPT"] = timezone("Asia/Kathmandu") # Nepal Time (UTC+05:45)
timezones['SBT'] = timezone('Pacific/Guadalcanal') # Solomon Islands Time (UTC+11) timezones["NST"] = timezone("Canada/Newfoundland") # Newfoundland Standard Time (UTC03:30)
timezones['SCT'] = timezone('Indian/Mahe') # Seychelles Time (UTC+04) timezones["NT"] = timezone("Canada/Newfoundland") # Newfoundland Time (UTC03:30)
timezones['SGT'] = timezone('Asia/Singapore') # Singapore Time (UTC+08) timezones["NUT"] = timezone("Pacific/Niue") # Niue Time (UTC11)
timezones['SLST'] = timezone('Asia/Colombo') # Sri Lanka Standard Time (UTC+05:30) timezones["NZDT"] = timezone("Pacific/Auckland") # New Zealand Daylight Time (UTC+13)
timezones['SRET'] = timezone('Asia/Srednekolymsk') # Srednekolymsk Time (UTC+11) timezones["NZST"] = timezone("Pacific/Auckland") # New Zealand Standard Time (UTC+12)
timezones['SRT'] = timezone('America/Paramaribo') # Suriname Time (UTC03) timezones["OMST"] = timezone("Asia/Omsk") # Omsk Time (UTC+06)
timezones['SST'] = timezone('Asia/Singapore') # Singapore Standard Time (UTC+08) timezones["ORAT"] = timezone("Asia/Oral") # Oral Time (UTC+05)
timezones['SYOT'] = timezone('Antarctica/Syowa') # Showa Station Time (UTC+03) timezones["PDT"] = timezone(
timezones['TAHT'] = timezone('Pacific/Tahiti') # Tahiti Time (UTC10) "America/Los_Angeles"
timezones['TFT'] = timezone('Indian/Kerguelen') # Indian/Kerguelen (UTC+05) ) # Pacific Daylight Time (North America) (UTC07)
timezones['THA'] = timezone('Asia/Bangkok') # Thailand Standard Time (UTC+07) timezones["PET"] = timezone("America/Lima") # Peru Time (UTC05)
timezones['TJT'] = timezone('Asia/Dushanbe') # Tajikistan Time (UTC+05) timezones["PETT"] = timezone("Asia/Kamchatka") # Kamchatka Time (UTC+12)
timezones['TKT'] = timezone('Pacific/Fakaofo') # Tokelau Time (UTC+13) timezones["PGT"] = timezone("Pacific/Port_Moresby") # Papua New Guinea Time (UTC+10)
timezones['TLT'] = timezone('Asia/Dili') # Timor Leste Time (UTC+09) timezones["PHOT"] = timezone("Pacific/Enderbury") # Phoenix Island Time (UTC+13)
timezones['TMT'] = timezone('Asia/Ashgabat') # Turkmenistan Time (UTC+05) timezones["PKT"] = timezone("Asia/Karachi") # Pakistan Standard Time (UTC+05)
timezones['TOT'] = timezone('Pacific/Tongatapu') # Tonga Time (UTC+13) timezones["PMDT"] = timezone(
timezones['TVT'] = timezone('Pacific/Funafuti') # Tuvalu Time (UTC+12) "America/Miquelon"
timezones['ULAST'] = timezone('Asia/Ulan_Bator') # Ulaanbaatar Summer Time (UTC+09) ) # Saint Pierre and Miquelon Daylight time (UTC02)
timezones['ULAT'] = timezone('Asia/Ulan_Bator') # Ulaanbaatar Standard Time (UTC+08) timezones["PMST"] = timezone(
timezones['USZ1'] = timezone('Europe/Kaliningrad') # Kaliningrad Time (UTC+02) "America/Miquelon"
timezones['UTC'] = timezone('UTC') # Coordinated Universal Time (UTC±00) ) # Saint Pierre and Miquelon Standard Time (UTC03)
timezones['UYST'] = timezone('America/Montevideo') # Uruguay Summer Time (UTC02) timezones["PONT"] = timezone("Pacific/Pohnpei") # Pohnpei Standard Time (UTC+11)
timezones['UYT'] = timezone('America/Montevideo') # Uruguay Standard Time (UTC03) timezones["PST"] = timezone(
timezones['UZT'] = timezone('Asia/Tashkent') # Uzbekistan Time (UTC+05) "America/Los_Angeles"
timezones['VET'] = timezone('America/Caracas') # Venezuelan Standard Time (UTC04) ) # Pacific Standard Time (North America) (UTC08)
timezones['VLAT'] = timezone('Asia/Vladivostok') # Vladivostok Time (UTC+10) timezones["PYST"] = timezone(
timezones['VOLT'] = timezone('Europe/Volgograd') # Volgograd Time (UTC+04) "America/Asuncion"
timezones['VOST'] = timezone('Antarctica/Vostok') # Vostok Station Time (UTC+06) ) # Paraguay Summer Time (South America)[7] (UTC03)
timezones['VUT'] = timezone('Pacific/Efate') # Vanuatu Time (UTC+11) timezones["PYT"] = timezone("America/Asuncion") # Paraguay Time (South America)[8] (UTC04)
timezones['WAKT'] = timezone('Pacific/Wake') # Wake Island Time (UTC+12) timezones["RET"] = timezone("Indian/Reunion") # Réunion Time (UTC+04)
timezones['WAST'] = timezone('Africa/Lagos') # West Africa Summer Time (UTC+02) timezones["ROTT"] = timezone("Antarctica/Rothera") # Rothera Research Station Time (UTC03)
timezones['WAT'] = timezone('Africa/Lagos') # West Africa Time (UTC+01) timezones["SAKT"] = timezone("Asia/Vladivostok") # Sakhalin Island time (UTC+11)
timezones['WEST'] = timezone('Europe/London') # Western European Summer Time (UTC+01) timezones["SAMT"] = timezone("Europe/Samara") # Samara Time (UTC+04)
timezones['WET'] = timezone('Europe/London') # Western European Time (UTC±00) timezones["SAST"] = timezone("Africa/Johannesburg") # South African Standard Time (UTC+02)
timezones['WIT'] = timezone('Asia/Jakarta') # Western Indonesian Time (UTC+07) timezones["SBT"] = timezone("Pacific/Guadalcanal") # Solomon Islands Time (UTC+11)
timezones['WST'] = timezone('Australia/Perth') # Western Standard Time (UTC+08) timezones["SCT"] = timezone("Indian/Mahe") # Seychelles Time (UTC+04)
timezones['YAKT'] = timezone('Asia/Yakutsk') # Yakutsk Time (UTC+09) timezones["SGT"] = timezone("Asia/Singapore") # Singapore Time (UTC+08)
timezones['YEKT'] = timezone('Asia/Yekaterinburg') # Yekaterinburg Time (UTC+05) 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 (UTC03)
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 (UTC10)
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 (UTC02)
timezones["UYT"] = timezone("America/Montevideo") # Uruguay Standard Time (UTC03)
timezones["UZT"] = timezone("Asia/Tashkent") # Uzbekistan Time (UTC+05)
timezones["VET"] = timezone("America/Caracas") # Venezuelan Standard Time (UTC04)
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 return timezones

@ -54,7 +54,7 @@ class Flag(Cog):
async def flagset(self, ctx: commands.Context): async def flagset(self, ctx: commands.Context):
""" """
My custom cog My custom cog
Extra information goes here Extra information goes here
""" """
if ctx.invoked_subcommand is None: if ctx.invoked_subcommand is None:

@ -30,8 +30,8 @@ class ForceMention(Cog):
@commands.command() @commands.command()
async def forcemention(self, ctx: commands.Context, role: str, *, message=""): 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) role_obj = get(ctx.guild.roles, name=role)
if role_obj is None: if role_obj is None:
await ctx.maybe_send_embed("Couldn't find role named {}".format(role)) await ctx.maybe_send_embed("Couldn't find role named {}".format(role))

@ -33,7 +33,7 @@ class LoveCalculator(Cog):
x.replace(" ", "+"), y.replace(" ", "+") x.replace(" ", "+"), y.replace(" ", "+")
) )
async with aiohttp.ClientSession(headers={"Connection": "keep-alive"}) as session: 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 assert response.status == 200
resp = await response.text() resp = await response.text()
@ -60,14 +60,11 @@ class LoveCalculator(Cog):
else: else:
emoji = "💔" emoji = "💔"
title = f"Dr. Love says that the love percentage for {x} and {y} is: {emoji} {description} {emoji}" title = f"Dr. Love says that the love percentage for {x} and {y} is: {emoji} {description} {emoji}"
except: except (TypeError, ValueError):
title = "Dr. Love has left a note for you." title = "Dr. Love has left a note for you."
em = discord.Embed( em = discord.Embed(
title=title, title=title, description=result_text, color=discord.Color.red(), url=url
description=result_text,
color=discord.Color.red(),
url=f"https://www.lovecalculator.com/{result_image}",
) )
em.set_image(url=f"https://www.lovecalculator.com/{result_image}")
await ctx.send(embed=em) await ctx.send(embed=em)

@ -83,7 +83,7 @@ class LastSeen(Cog):
# description="{} was last seen at this date and time".format(member.display_name), # description="{} was last seen at this date and time".format(member.display_name),
# timestamp=last_seen) # timestamp=last_seen)
embed = discord.Embed(timestamp=last_seen) embed = discord.Embed(timestamp=last_seen, color=await self.bot.get_embed_color(ctx))
await ctx.send(embed=embed) await ctx.send(embed=embed)
@commands.Cog.listener() @commands.Cog.listener()

@ -85,7 +85,9 @@ class Nudity(commands.Cog):
if r["unsafe"] > 0.7: if r["unsafe"] > 0.7:
await nsfw_channel.send( await nsfw_channel.send(
"NSFW Image from {}".format(message.channel.mention), "NSFW Image from {}".format(message.channel.mention),
file=discord.File(image,), file=discord.File(
image,
),
) )
@commands.Cog.listener() @commands.Cog.listener()

@ -28,8 +28,8 @@ class RPSLS(Cog):
@commands.command() @commands.command()
async def rpsls(self, ctx: commands.Context, choice: str): 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: Rules:
Scissors cuts Paper Scissors cuts Paper
Paper covers Rock Paper covers Rock

@ -50,6 +50,7 @@ class StealEmoji(Cog):
default_global = { default_global = {
"stolemoji": {}, "stolemoji": {},
"guildbanks": [], "guildbanks": [],
"autobanked_guilds": [],
"on": False, "on": False,
"notify": 0, "notify": 0,
"autobank": False, "autobank": False,
@ -145,11 +146,54 @@ class StealEmoji(Cog):
await ctx.maybe_send_embed("AutoBanking is now " + str(not curr_setting)) await ctx.maybe_send_embed("AutoBanking is now " + str(not curr_setting))
@checks.is_owner()
@commands.guild_only()
@stealemoji.command(name="deleteserver", aliases=["deleteguild"])
async def se_deleteserver(self, ctx: commands.Context, guild_id=None):
"""Delete servers the bot is the owner of.
Useful for auto-generated guildbanks."""
if guild_id is None:
guild = ctx.guild
else:
guild = await self.bot.get_guild(guild_id)
if guild is None:
await ctx.maybe_send_embed("Failed to get guild, cancelling")
return
guild: discord.Guild
await ctx.maybe_send_embed(
f"Will attempt to delete {guild.name} ({guild.id})\n" f"Okay to continue? (yes/no)"
)
def check(m):
return m.author == ctx.author and m.channel == ctx.channel
try:
answer = await self.bot.wait_for("message", timeout=120, check=check)
except asyncio.TimeoutError:
await ctx.send("Timed out, canceling")
return
if answer.content.upper() not in ["Y", "YES"]:
await ctx.maybe_send_embed("Cancelling")
return
try:
await guild.delete()
except discord.Forbidden:
log.exception("No permission to delete. I'm probably not the guild owner")
await ctx.maybe_send_embed("No permission to delete. I'm probably not the guild owner")
except discord.HTTPException:
log.exception("Unexpected error when deleting guild")
await ctx.maybe_send_embed("Unexpected error when deleting guild")
else:
await self.bot.send_to_owners(f"Guild {guild.name} deleted")
@checks.is_owner() @checks.is_owner()
@commands.guild_only() @commands.guild_only()
@stealemoji.command(name="bank") @stealemoji.command(name="bank")
async def se_bank(self, ctx): async def se_bank(self, ctx):
"""Add current server as emoji bank""" """Add or remove current server as emoji bank"""
def check(m): def check(m):
return ( return (
@ -235,6 +279,9 @@ class StealEmoji(Cog):
return return
async with self.config.guildbanks() as guildbanks: async with self.config.guildbanks() as guildbanks:
guildbanks.append(guildbank.id) guildbanks.append(guildbank.id)
# Track generated guilds for easier deletion
async with self.config.autobanked_guilds() as autobanked_guilds:
autobanked_guilds.append(guildbank.id)
await asyncio.sleep(2) await asyncio.sleep(2)

@ -1,6 +1,7 @@
import asyncio import asyncio
import logging import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Optional
import discord import discord
from redbot.core import Config, checks, commands from redbot.core import Config, checks, commands
@ -19,6 +20,15 @@ async def sleep_till_next_hour():
await asyncio.sleep((next_hour - datetime.utcnow()).seconds) await asyncio.sleep((next_hour - datetime.utcnow()).seconds)
async def announce_to_channel(channel, results, title):
if channel is not None and results:
await channel.send(title)
for page in pagify(results, shorten_by=50):
await channel.send(page)
elif results: # Channel is None, log the results
log.info(results)
class Timerole(Cog): class Timerole(Cog):
"""Add roles to users based on time on server""" """Add roles to users based on time on server"""
@ -27,10 +37,15 @@ class Timerole(Cog):
self.bot = bot self.bot = bot
self.config = Config.get_conf(self, identifier=9811198108111121, force_registration=True) self.config = Config.get_conf(self, identifier=9811198108111121, force_registration=True)
default_global = {} default_global = {}
default_guild = {"announce": None, "roles": {}} default_guild = {"announce": None, "reapply": True, "roles": {}}
default_rolemember = {"had_role": False, "check_again_time": None}
self.config.register_global(**default_global) self.config.register_global(**default_global)
self.config.register_guild(**default_guild) self.config.register_guild(**default_guild)
self.config.init_custom("RoleMember", 2)
self.config.register_custom("RoleMember", **default_rolemember)
self.updating = asyncio.create_task(self.check_hour()) self.updating = asyncio.create_task(self.check_hour())
async def red_delete_data_for_user(self, **kwargs): async def red_delete_data_for_user(self, **kwargs):
@ -49,11 +64,14 @@ class Timerole(Cog):
Useful for troubleshooting the initial setup Useful for troubleshooting the initial setup
""" """
async with ctx.typing(): async with ctx.typing():
pre_run = datetime.utcnow()
await self.timerole_update() await self.timerole_update()
after_run = datetime.utcnow()
await ctx.tick() await ctx.tick()
await ctx.maybe_send_embed(f"Took {after_run-pre_run} seconds")
@commands.group() @commands.group()
@checks.mod_or_permissions(administrator=True) @checks.mod_or_permissions(administrator=True)
@commands.guild_only() @commands.guild_only()
@ -84,9 +102,7 @@ class Timerole(Cog):
await self.config.guild(guild).roles.set_raw(role.id, value=to_set) await self.config.guild(guild).roles.set_raw(role.id, value=to_set)
await ctx.maybe_send_embed( await ctx.maybe_send_embed(
"Time Role for {0} set to {1} days and {2} hours until added".format( f"Time Role for {role.name} set to {days} days and {hours} hours until added"
role.name, days, hours
)
) )
@timerole.command() @timerole.command()
@ -114,18 +130,27 @@ class Timerole(Cog):
await self.config.guild(guild).roles.set_raw(role.id, value=to_set) await self.config.guild(guild).roles.set_raw(role.id, value=to_set)
await ctx.maybe_send_embed( await ctx.maybe_send_embed(
"Time Role for {0} set to {1} days and {2} hours until removed".format( f"Time Role for {role.name} set to {days} days and {hours} hours until removed"
role.name, days, hours
)
) )
@timerole.command() @timerole.command()
async def channel(self, ctx: commands.Context, channel: discord.TextChannel): async def channel(self, ctx: commands.Context, channel: Optional[discord.TextChannel] = None):
"""Sets the announce channel for role adds""" """Sets the announce channel for role adds"""
guild = ctx.guild guild = ctx.guild
if channel is None:
await self.config.guild(guild).announce.clear()
await ctx.maybe_send_embed(f"Announce channel has been cleared")
else:
await self.config.guild(guild).announce.set(channel.id)
await ctx.send(f"Announce channel set to {channel.mention}")
await self.config.guild(guild).announce.set(channel.id) @timerole.command()
await ctx.send("Announce channel set to {0}".format(channel.mention)) async def reapply(self, ctx: commands.Context):
"""Toggle reapplying roles if the member loses it somehow. Defaults to True"""
guild = ctx.guild
current_setting = await self.config.guild(guild).reapply()
await self.config.guild(guild).reapply.set(not current_setting)
await ctx.maybe_send_embed(f"Reapplying roles is now set to: {not current_setting}")
@timerole.command() @timerole.command()
async def delrole(self, ctx: commands.Context, role: discord.Role): async def delrole(self, ctx: commands.Context, role: discord.Role):
@ -133,7 +158,8 @@ class Timerole(Cog):
guild = ctx.guild guild = ctx.guild
await self.config.guild(guild).roles.set_raw(role.id, value=None) await self.config.guild(guild).roles.set_raw(role.id, value=None)
await ctx.send("{0} will no longer be applied".format(role.name)) await self.config.custom("RoleMember", role.id).clear()
await ctx.maybe_send_embed(f"{role.name} will no longer be applied")
@timerole.command() @timerole.command()
async def list(self, ctx: commands.Context): async def list(self, ctx: commands.Context):
@ -153,95 +179,200 @@ class Timerole(Cog):
str(discord.utils.get(guild.roles, id=int(new_id))) str(discord.utils.get(guild.roles, id=int(new_id)))
for new_id in r_data["required"] for new_id in r_data["required"]
] ]
out += "{} | {} days | requires: {}\n".format(str(role), r_data["days"], r_roles) out += f"{role} | {r_data['days']} days | requires: {r_roles}\n"
await ctx.maybe_send_embed(out) await ctx.maybe_send_embed(out)
async def timerole_update(self): async def timerole_update(self):
utcnow = datetime.utcnow()
all_guilds = await self.config.all_guilds()
# all_mrs = await self.config.custom("RoleMember").all()
# log.debug(f"Begin timerole update")
for guild in self.bot.guilds: for guild in self.bot.guilds:
addlist = [] guild_id = guild.id
removelist = [] if guild_id not in all_guilds:
log.debug(f"Guild has no configured settings: {guild}")
continue
add_results = ""
remove_results = ""
reapply = all_guilds[guild_id]["reapply"]
role_dict = all_guilds[guild_id]["roles"]
role_dict = await self.config.guild(guild).roles()
if not any(role_data for role_data in role_dict.values()): # No roles if not any(role_data for role_data in role_dict.values()): # No roles
log.debug(f"No roles are configured for guild: {guild}")
continue continue
async for member in AsyncIter(guild.members): # all_mr = await self.config.all_custom("RoleMember")
has_roles = [r.id for r in member.roles] # log.debug(f"{all_mr=}")
async for member in AsyncIter(guild.members, steps=10):
addlist = []
removelist = []
for role_id, role_data in role_dict.items():
# Skip non-configured roles
if not role_data:
continue
mr_dict = await self.config.custom("RoleMember", role_id, member.id).all()
# Stop if they've had the role and reapplying is disabled
if not reapply and mr_dict["had_role"]:
log.debug(f"{member.display_name} - Not reapplying")
continue
# Stop if the check_again_time hasn't passed yet
if (
mr_dict["check_again_time"] is not None
and datetime.fromisoformat(mr_dict["check_again_time"]) >= utcnow
):
log.debug(f"{member.display_name} - Not time to check again yet")
continue
member: discord.Member
has_roles = set(r.id for r in member.roles)
# Stop if they currently have or don't have the role, and mark had_role
if (int(role_id) in has_roles and not role_data["remove"]) or (
int(role_id) not in has_roles and role_data["remove"]
):
if not mr_dict["had_role"]:
await self.config.custom(
"RoleMember", role_id, member.id
).had_role.set(True)
log.debug(f"{member.display_name} - applying had_role")
continue
# Stop if they don't have all the required roles
if role_data is None or (
"required" in role_data and not set(role_data["required"]) & has_roles
):
continue
check_time = member.joined_at + timedelta(
days=role_data["days"],
hours=role_data.get("hours", 0),
)
# Check if enough time has passed to get the role and save the check_again_time
if check_time >= utcnow:
await self.config.custom(
"RoleMember", role_id, member.id
).check_again_time.set(check_time.isoformat())
log.debug(
f"{member.display_name} - Not enough time has passed to qualify for the role\n"
f"Waiting until {check_time}"
)
continue
if role_data["remove"]:
removelist.append(role_id)
else:
addlist.append(role_id)
# Done iterating through roles, now add or remove the roles
if not addlist and not removelist:
continue
# log.debug(f"{addlist=}\n{removelist=}")
add_roles = [ add_roles = [
int(rID) discord.utils.get(guild.roles, id=int(role_id)) for role_id in addlist
for rID, r_data in role_dict.items()
if r_data is not None and not r_data["remove"]
] ]
remove_roles = [ remove_roles = [
int(rID) discord.utils.get(guild.roles, id=int(role_id)) for role_id in removelist
for rID, r_data in role_dict.items()
if r_data is not None and r_data["remove"]
] ]
check_add_roles = set(add_roles) - set(has_roles) if None in add_roles or None in remove_roles:
check_remove_roles = set(remove_roles) & set(has_roles) log.info(
f"Timerole ran into an error with the roles in: {add_roles + remove_roles}"
await self.check_required_and_date( )
addlist, check_add_roles, has_roles, member, role_dict
) if addlist:
await self.check_required_and_date( try:
removelist, check_remove_roles, has_roles, member, role_dict await member.add_roles(*add_roles, reason="Timerole", atomic=False)
) except (discord.Forbidden, discord.NotFound) as e:
log.exception("Failed Adding Roles")
add_results += f"{member.display_name} : **(Failed Adding Roles)**\n"
else:
add_results += " \n".join(
f"{member.display_name} : {role.name}" for role in add_roles
)
for role_id in addlist:
await self.config.custom(
"RoleMember", role_id, member.id
).had_role.set(True)
if removelist:
try:
await member.remove_roles(*remove_roles, reason="Timerole", atomic=False)
except (discord.Forbidden, discord.NotFound) as e:
log.exception("Failed Removing Roles")
remove_results += f"{member.display_name} : **(Failed Removing Roles)**\n"
else:
remove_results += " \n".join(
f"{member.display_name} : {role.name}" for role in remove_roles
)
for role_id in removelist:
await self.config.custom(
"RoleMember", role_id, member.id
).had_role.set(True)
# Done iterating through members, now maybe announce to the guild
channel = await self.config.guild(guild).announce() channel = await self.config.guild(guild).announce()
if channel is not None: if channel is not None:
channel = guild.get_channel(channel) channel = guild.get_channel(channel)
title = "**These members have received the following roles**\n" if add_results:
await self.announce_roles(title, addlist, channel, guild, to_add=True) title = "**These members have received the following roles**\n"
title = "**These members have lost the following roles**\n" await announce_to_channel(channel, add_results, title)
await self.announce_roles(title, removelist, channel, guild, to_add=False) if remove_results:
title = "**These members have lost the following roles**\n"
async def announce_roles(self, title, role_list, channel, guild, to_add: True): await announce_to_channel(channel, remove_results, title)
results = "" # End
for member, role_id in role_list:
role = discord.utils.get(guild.roles, id=role_id) # async def announce_roles(self, title, role_list, channel, guild, to_add: True):
try: # results = ""
if to_add: # async for member, role_id in AsyncIter(role_list):
await member.add_roles(role, reason="Timerole") # role = discord.utils.get(guild.roles, id=role_id)
else: # try:
await member.remove_roles(role, reason="Timerole") # if to_add:
except (discord.Forbidden, discord.NotFound) as e: # await member.add_roles(role, reason="Timerole")
results += "{} : {} **(Failed)**\n".format(member.display_name, role.name) # else:
else: # await member.remove_roles(role, reason="Timerole")
results += "{} : {}\n".format(member.display_name, role.name) # except (discord.Forbidden, discord.NotFound) as e:
if channel is not None and results: # results += f"{member.display_name} : {role.name} **(Failed)**\n"
await channel.send(title) # else:
for page in pagify(results, shorten_by=50): # results += f"{member.display_name} : {role.name}\n"
await channel.send(page) # if channel is not None and results:
elif results: # Channel is None, log the results # await channel.send(title)
log.info(results) # for page in pagify(results, shorten_by=50):
# await channel.send(page)
async def check_required_and_date(self, role_list, check_roles, has_roles, member, role_dict): # elif results: # Channel is None, log the results
for role_id in check_roles: # log.info(results)
# Check for required role
if "required" in role_dict[str(role_id)]: # async def check_required_and_date(self, role_list, check_roles, has_roles, member, role_dict):
if not set(role_dict[str(role_id)]["required"]) & set(has_roles): # async for role_id in AsyncIter(check_roles):
# Doesn't have required role # # Check for required role
continue # if "required" in role_dict[str(role_id)]:
# if not set(role_dict[str(role_id)]["required"]) & set(has_roles):
if ( # # Doesn't have required role
member.joined_at # continue
+ timedelta( #
days=role_dict[str(role_id)]["days"], # if (
hours=role_dict[str(role_id)].get("hours", 0), # member.joined_at
) # + timedelta(
<= datetime.today() # days=role_dict[str(role_id)]["days"],
): # hours=role_dict[str(role_id)].get("hours", 0),
# Qualifies # )
role_list.append((member, role_id)) # <= datetime.utcnow()
# ):
# # Qualifies
# role_list.append((member, role_id))
async def check_hour(self): async def check_hour(self):
await sleep_till_next_hour() await sleep_till_next_hour()
while self is self.bot.get_cog("Timerole"): while self is self.bot.get_cog("Timerole"):
await self.timerole_update() await self.timerole_update()
await sleep_till_next_hour() await sleep_till_next_hour()

@ -30,8 +30,8 @@ class TTS(Cog):
@commands.command(aliases=["t2s", "text2"]) @commands.command(aliases=["t2s", "text2"])
async def tts(self, ctx: commands.Context, *, text: str): 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() mp3_fp = io.BytesIO()
tts = gTTS(text, lang="en") tts = gTTS(text, lang="en")
tts.write_to_fp(mp3_fp) tts.write_to_fp(mp3_fp)

@ -1,5 +1,7 @@
import bisect import bisect
import logging
from collections import defaultdict from collections import defaultdict
from operator import attrgetter
from random import choice from random import choice
import discord import discord
@ -8,77 +10,55 @@ import discord
# Import all roles here # Import all roles here
from redbot.core import commands from redbot.core import commands
from .roles.seer import Seer # from .roles.seer import Seer
from .roles.vanillawerewolf import VanillaWerewolf # from .roles.vanillawerewolf import VanillaWerewolf
from .roles.villager import Villager # from .roles.villager import Villager
from redbot.core.utils.menus import menu, prev_page, next_page, close_menu
# All roles in this list for iterating from werewolf import roles
from redbot.core.utils.menus import menu, prev_page, next_page, close_menu
ROLE_LIST = sorted([Villager, Seer, VanillaWerewolf], key=lambda x: x.alignment) from werewolf.constants import ROLE_CATEGORY_DESCRIPTIONS
from werewolf.role import Role
ALIGNMENT_COLORS = [0x008000, 0xff0000, 0xc0c0c0] log = logging.getLogger("red.fox_v3.werewolf.builder")
TOWN_ROLES = [(idx, role) for idx, role in enumerate(ROLE_LIST) if role.alignment == 1]
WW_ROLES = [(idx, role) for idx, role in enumerate(ROLE_LIST) if role.alignment == 2]
OTHER_ROLES = [(idx, role) for idx, role in enumerate(ROLE_LIST) if role.alignment not in [0, 1]]
ROLE_PAGES = [] # All roles in this list for iterating
PAGE_GROUPS = [0]
ROLE_CATEGORIES = { ROLE_DICT = {name: cls for name, cls in roles.__dict__.items() if isinstance(cls, type)}
1: "Random", 2: "Investigative", 3: "Protective", 4: "Government", ROLE_LIST = sorted(
5: "Killing", 6: "Power (Special night action)", [cls for cls in ROLE_DICT.values()],
11: "Random", 12: "Deception", 15: "Killing", 16: "Support", key=attrgetter("alignment"),
21: "Benign", 22: "Evil", 23: "Killing"} )
CATEGORY_COUNT = [] log.debug(f"{ROLE_DICT=}")
# Town, Werewolf, Neutral
ALIGNMENT_COLORS = [0x008000, 0xFF0000, 0xC0C0C0]
def role_embed(idx, role, color): ROLE_PAGES = []
embed = discord.Embed(title="**{}** - {}".format(idx, str(role.__name__)), description=role.game_start_message,
color=color)
embed.add_field(name='Alignment', value=['Town', 'Werewolf', 'Neutral'][role.alignment - 1], inline=True)
embed.add_field(name='Multiples Allowed', value=str(not role.unique), inline=True)
embed.add_field(name='Role Type', value=", ".join(ROLE_CATEGORIES[x] for x in role.category), inline=True)
embed.add_field(name='Random Option', value=str(role.rand_choice), inline=True)
return embed
def role_embed(idx, role: Role, color):
embed = discord.Embed(
title=f"**{idx}** - {role.__name__}",
description=role.game_start_message,
color=color,
)
if role.icon_url is not None:
embed.set_thumbnail(url=role.icon_url)
embed.add_field(
name="Alignment", value=["Town", "Werewolf", "Neutral"][role.alignment - 1], inline=False
)
embed.add_field(name="Multiples Allowed", value=str(not role.unique), inline=False)
embed.add_field(
name="Role Types",
value=", ".join(ROLE_CATEGORY_DESCRIPTIONS[x] for x in role.category),
inline=False,
)
embed.add_field(name="Random Option", value=str(role.rand_choice), inline=False)
def setup(): return embed
# Roles
last_alignment = ROLE_LIST[0].alignment
for idx, role in enumerate(ROLE_LIST):
if role.alignment != last_alignment and len(ROLE_PAGES) - 1 not in PAGE_GROUPS:
PAGE_GROUPS.append(len(ROLE_PAGES) - 1)
last_alignment = role.alignment
ROLE_PAGES.append(role_embed(idx, role, ALIGNMENT_COLORS[role.alignment - 1]))
# Random Town Roles
if len(ROLE_PAGES) - 1 not in PAGE_GROUPS:
PAGE_GROUPS.append(len(ROLE_PAGES) - 1)
for k, v in ROLE_CATEGORIES.items():
if 0 < k <= 6:
ROLE_PAGES.append(discord.Embed(title="RANDOM:Town Role", description="Town {}".format(v), color=0x008000))
CATEGORY_COUNT.append(k)
# Random WW Roles
if len(ROLE_PAGES) - 1 not in PAGE_GROUPS:
PAGE_GROUPS.append(len(ROLE_PAGES) - 1)
for k, v in ROLE_CATEGORIES.items():
if 10 < k <= 16:
ROLE_PAGES.append(
discord.Embed(title="RANDOM:Werewolf Role", description="Werewolf {}".format(v), color=0xff0000))
CATEGORY_COUNT.append(k)
# Random Neutral Roles
if len(ROLE_PAGES) - 1 not in PAGE_GROUPS:
PAGE_GROUPS.append(len(ROLE_PAGES) - 1)
for k, v in ROLE_CATEGORIES.items():
if 20 < k <= 26:
ROLE_PAGES.append(
discord.Embed(title="RANDOM:Neutral Role", description="Neutral {}".format(v), color=0xc0c0c0))
CATEGORY_COUNT.append(k)
""" """
@ -147,15 +127,15 @@ async def parse_code(code, game):
return decode return decode
async def encode(roles, rand_roles): async def encode(role_list, rand_roles):
"""Convert role list to code""" """Convert role list to code"""
out_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: for role in digit_sort:
out_code += str(role) 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: if digit_sort:
out_code += "-" out_code += "-"
for role in digit_sort: for role in digit_sort:
@ -187,49 +167,20 @@ async def encode(roles, rand_roles):
return out_code return out_code
async def next_group(ctx: commands.Context, pages: list,
controls: dict, message: discord.Message, page: int,
timeout: float, emoji: str):
perms = message.channel.permissions_for(ctx.guild.me)
if perms.manage_messages: # Can manage messages, so remove react
try:
await message.remove_reaction(emoji, ctx.author)
except discord.NotFound:
pass
page = bisect.bisect_right(PAGE_GROUPS, page)
if page == len(PAGE_GROUPS):
page = PAGE_GROUPS[0]
else:
page = PAGE_GROUPS[page]
return await menu(ctx, pages, controls, message=message,
page=page, timeout=timeout)
async def prev_group(ctx: commands.Context, pages: list,
controls: dict, message: discord.Message, page: int,
timeout: float, emoji: str):
perms = message.channel.permissions_for(ctx.guild.me)
if perms.manage_messages: # Can manage messages, so remove react
try:
await message.remove_reaction(emoji, ctx.author)
except discord.NotFound:
pass
page = PAGE_GROUPS[bisect.bisect_left(PAGE_GROUPS, page) - 1]
return await menu(ctx, pages, controls, message=message,
page=page, timeout=timeout)
def role_from_alignment(alignment): def role_from_alignment(alignment):
return [role_embed(idx, role, ALIGNMENT_COLORS[role.alignment - 1]) return [
for idx, role in enumerate(ROLE_LIST) if alignment == role.alignment] 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): def role_from_category(category):
return [role_embed(idx, role, ALIGNMENT_COLORS[role.alignment - 1]) return [
for idx, role in enumerate(ROLE_LIST) if category in role.category] 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): def role_from_id(idx):
@ -242,8 +193,11 @@ def role_from_id(idx):
def role_from_name(name: str): def role_from_name(name: str):
return [role_embed(idx, role, ALIGNMENT_COLORS[role.alignment - 1]) return [
for idx, role in enumerate(ROLE_LIST) if name in role.__name__] 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): def say_role_list(code_list, rand_roles):
@ -255,34 +209,87 @@ def say_role_list(code_list, rand_roles):
for role in rand_roles: for role in rand_roles:
if 0 < role <= 6: if 0 < role <= 6:
role_dict["Town {}".format(ROLE_CATEGORIES[role])] += 1 role_dict[f"Town {ROLE_CATEGORY_DESCRIPTIONS[role]}"] += 1
if 10 < role <= 16: if 10 < role <= 16:
role_dict["Werewolf {}".format(ROLE_CATEGORIES[role])] += 1 role_dict[f"Werewolf {ROLE_CATEGORY_DESCRIPTIONS[role]}"] += 1
if 20 < role <= 26: if 20 < role <= 26:
role_dict["Neutral {}".format(ROLE_CATEGORIES[role])] += 1 role_dict[f"Neutral {ROLE_CATEGORY_DESCRIPTIONS[role]}"] += 1
for k, v in role_dict.items(): 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 return embed
class GameBuilder: class GameBuilder:
def __init__(self): def __init__(self):
self.code = [] self.code = []
self.rand_roles = [] 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): async def build_game(self, ctx: commands.Context):
new_controls = { new_controls = {
'': prev_group, "": self.prev_group,
"": prev_page, "": prev_page,
'': self.select_page, "": self.select_page,
"": next_page, "": next_page,
'': next_group, "": self.next_group,
'📇': self.list_roles, "📇": self.list_roles,
"": close_menu "": close_menu,
} }
await ctx.send("Browse through roles and add the ones you want using the check mark") await ctx.send("Browse through roles and add the ones you want using the check mark")
@ -292,10 +299,17 @@ class GameBuilder:
out = await encode(self.code, self.rand_roles) out = await encode(self.code, self.rand_roles)
return out return out
async def list_roles(self, ctx: commands.Context, pages: list, async def list_roles(
controls: dict, message: discord.Message, page: int, self,
timeout: float, emoji: str): ctx: commands.Context,
perms = message.channel.permissions_for(ctx.guild.me) 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 if perms.manage_messages: # Can manage messages, so remove react
try: try:
await message.remove_reaction(emoji, ctx.author) await message.remove_reaction(emoji, ctx.author)
@ -304,13 +318,19 @@ class GameBuilder:
await ctx.send(embed=say_role_list(self.code, self.rand_roles)) await ctx.send(embed=say_role_list(self.code, self.rand_roles))
return await menu(ctx, pages, controls, message=message, return await menu(ctx, pages, controls, message=message, page=page, timeout=timeout)
page=page, timeout=timeout)
async def select_page(
async def select_page(self, ctx: commands.Context, pages: list, self,
controls: dict, message: discord.Message, page: int, ctx: commands.Context,
timeout: float, emoji: str): pages: list,
perms = message.channel.permissions_for(ctx.guild.me) 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 if perms.manage_messages: # Can manage messages, so remove react
try: try:
await message.remove_reaction(emoji, ctx.author) await message.remove_reaction(emoji, ctx.author)
@ -318,9 +338,53 @@ class GameBuilder:
pass pass
if page >= len(ROLE_LIST): 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: else:
self.code.append(page) self.code.append(page)
return await menu(ctx, pages, controls, message=message, return await menu(ctx, pages, controls, message=message, page=page, timeout=timeout)
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)

@ -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)
"""

@ -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

File diff suppressed because it is too large Load Diff

@ -4,10 +4,10 @@
], ],
"min_bot_version": "3.3.0", "min_bot_version": "3.3.0",
"description": "Customizable Werewolf Game", "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", "install_msg": "Thank you for installing Werewolf! Get started with `[p]load werewolf`\n Use `[p]wwset` to run inital setup",
"requirements": [], "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.", "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": [ "tags": [
"mafia", "mafia",

@ -0,0 +1,106 @@
import inspect
def wolflistener(name=None, priority=0):
"""A decorator that marks a function as a listener.
This is the werewolf.Game equivalent of :meth:`.Cog.listener`.
Parameters
------------
name: :class:`str`
The name of the event being listened to. If not provided, it
defaults to the function's name.
priority: :class:`int`
The priority of the listener.
Priority guide as follows:
_at_night_start
0. No Action
1. Detain actions (Jailer/Kidnapper)
2. Group discussions and choose targets
_at_night_end
0. No Action
1. Self actions (Veteran)
2. Target switching and role blocks (bus driver, witch, escort)
3. Protection / Preempt actions (bodyguard/framer)
4. Non-disruptive actions (seer/silencer)
5. Disruptive actions (Killing)
6. Role altering actions (Cult / Mason / Shifter)
Raises
--------
TypeError
The function is not a coroutine function or a string was not passed as
the name.
"""
if name is not None and not isinstance(name, str):
raise TypeError(
"Game.listener expected str but received {0.__class__.__name__!r} instead.".format(
name
)
)
def decorator(func):
actual = func
if isinstance(actual, staticmethod):
actual = actual.__func__
if not inspect.iscoroutinefunction(actual):
raise TypeError("Listener function must be a coroutine function.")
actual.__wolf_listener__ = priority
to_assign = name or actual.__name__
try:
actual.__wolf_listener_names__.append((priority, to_assign))
except AttributeError:
actual.__wolf_listener_names__ = [(priority, to_assign)]
# we have to return `func` instead of `actual` because
# we need the type to be `staticmethod` for the metaclass
# to pick it up but the metaclass unfurls the function and
# thus the assignments need to be on the actual function
return func
return decorator
class WolfListenerMeta(type):
def __new__(mcs, *args, **kwargs):
name, bases, attrs = args
listeners = {}
need_at_msg = "Listeners must start with at_ (in method {0.__name__}.{1})"
new_cls = super().__new__(mcs, name, bases, attrs, **kwargs)
for base in reversed(new_cls.__mro__):
for elem, value in base.__dict__.items():
if elem in listeners:
del listeners[elem]
is_static_method = isinstance(value, staticmethod)
if is_static_method:
value = value.__func__
if inspect.iscoroutinefunction(value):
try:
is_listener = getattr(value, "__wolf_listener__")
except AttributeError:
continue
else:
# if not elem.startswith("at_"):
# raise TypeError(need_at_msg.format(base, elem))
listeners[elem] = value
listeners_as_list = []
for listener in listeners.values():
for priority, listener_name in listener.__wolf_listener_names__:
# I use __name__ instead of just storing the value so I can inject
# the self attribute when the time comes to add them to the bot
listeners_as_list.append((priority, listener_name, listener.__name__))
new_cls.__wolf_listeners__ = listeners_as_list
return new_cls
class WolfListener(metaclass=WolfListenerMeta):
def __init__(self, game):
for priority, name, method_name in self.__wolf_listeners__:
game.add_ww_listener(getattr(self, method_name), priority, name)

@ -1,4 +1,8 @@
from .role import Role import logging
from werewolf.role import Role
log = logging.getLogger("red.fox_v3.werewolf.night_powers")
def night_immune(role: Role): def night_immune(role: Role):

@ -1,5 +1,9 @@
import logging
import discord import discord
log = logging.getLogger("red.fox_v3.werewolf.player")
class Player: class Player:
""" """
@ -16,6 +20,9 @@ class Player:
self.muted = False self.muted = False
self.protected = False self.protected = False
def __repr__(self):
return f"{self.__class__.__name__}({self.member})"
async def assign_role(self, role): async def assign_role(self, role):
""" """
Give this player a role Give this player a role
@ -28,6 +35,15 @@ class Player:
async def send_dm(self, message): async def send_dm(self, message):
try: try:
await self.member.send(message) # Lets do embeds later await self.member.send(message) # Lets ToDo embeds later
except discord.Forbidden: except discord.Forbidden:
await self.role.game.village_channel.send("Couldn't DM {}, uh oh".format(self.mention)) 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:"
)

@ -1,31 +1,41 @@
class Role: import inspect
import logging
from werewolf.listener import WolfListener, wolflistener
log = logging.getLogger("red.fox_v3.werewolf.role")
class Role(WolfListener):
""" """
Base Role class for werewolf game Base Role class for werewolf game
Category enrollment guide as follows (category property): Category enrollment guide as follows (category property):
Town: Town:
1: Random, 2: Investigative, 3: Protective, 4: Government, 1: Random, 2: Investigative, 3: Protective, 4: Government,
5: Killing, 6: Power (Special night action) 5: Killing, 6: Power (Special night action)
Werewolf: Werewolf:
11: Random, 12: Deception, 15: Killing, 16: Support 11: Random, 12: Deception, 15: Killing, 16: Support
Neutral: Neutral:
21: Benign, 22: Evil, 23: Killing 21: Benign, 22: Evil, 23: Killing
Example category: Example category:
category = [1, 5, 6] Could be Veteran category = [1, 5, 6] Could be Veteran
category = [1, 5] Could be Bodyguard category = [1, 5] Could be Bodyguard
category = [11, 16] Could be Werewolf Silencer category = [11, 16] Could be Werewolf Silencer
category = [22] Could be Blob (non-killing)
category = [22, 23] Could be Serial-Killer
Action guide as follows (on_event function):
Action priority guide as follows (on_event function):
_at_night_start _at_night_start
0. No Action 0. No Action
1. Detain actions (Jailer/Kidnapper) 1. Detain actions (Jailer/Kidnapper)
2. Group discussions and choose targets 2. Group discussions and choose targets
_at_night_end _at_night_end
0. No Action 0. No Action
1. Self actions (Veteran) 1. Self actions (Veteran)
@ -33,13 +43,15 @@ class Role:
3. Protection / Preempt actions (bodyguard/framer) 3. Protection / Preempt actions (bodyguard/framer)
4. Non-disruptive actions (seer/silencer) 4. Non-disruptive actions (seer/silencer)
5. Disruptive actions (Killing) 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) # 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) category = [0] # List of enrolled categories (listed above)
alignment = 0 # 1: Town, 2: Werewolf, 3: Neutral 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 unique = False # Only one of this role per game
game_start_message = ( game_start_message = (
"Your role is **Default**\n" "Your role is **Default**\n"
@ -54,32 +66,14 @@ class Role:
icon_url = None # Adding a URL here will enable a thumbnail of the role icon_url = None # Adding a URL here will enable a thumbnail of the role
def __init__(self, game): def __init__(self, game):
super().__init__(game)
self.game = game self.game = game
self.player = None self.player = None
self.blocked = False self.blocked = False
self.properties = {} # Extra data for other roles (i.e. arsonist) 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): def __repr__(self):
return self.__class__.__name__ return f"{self.__class__.__name__}({self.player.__repr__()})"
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): async def assign_player(self, player):
""" """
@ -90,6 +84,8 @@ class Role:
player.role = self player.role = self
self.player = player self.player = player
log.debug(f"Assigned {self} to {player}")
async def get_alignment(self, source=None): async def get_alignment(self, source=None):
""" """
Interaction for powerful access of alignment Interaction for powerful access of alignment
@ -101,7 +97,7 @@ class Role:
async def see_alignment(self, source=None): async def see_alignment(self, source=None):
""" """
Interaction for investigative roles attempting Interaction for investigative roles attempting
to see alignment (Village, Werewolf Other) to see alignment (Village, Werewolf, Other)
""" """
return "Other" return "Other"
@ -119,35 +115,16 @@ class Role:
""" """
return "Default" return "Default"
async def _at_game_start(self, data=None): @wolflistener("at_game_start", priority=2)
if self.channel_id: async def _at_game_start(self):
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): try:
pass await self.player.send_dm(self.game_start_message) # Maybe embeds eventually
except AttributeError as e:
async def _at_voted(self, data=None): log.exception(self.__repr__())
pass raise e
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): async def kill(self, source):
""" """

@ -0,0 +1,11 @@
from .villager import Villager
from .seer import Seer
from .vanillawerewolf import VanillaWerewolf
from .shifter import Shifter
# Don't sort these imports. They are unstably in order
# TODO: Replace with unique IDs for roles in the future
__all__ = ["Seer", "Shifter", "VanillaWerewolf", "Villager"]

@ -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 <ID>`\n"
"Each night you will absorb an adjacent player"
)
description = (
"A mysterious green blob of jelly, slowly growing in size.\n"
"The Blob fears no evil, must be dealt with in town"
)
def __init__(self, game):
super().__init__(game)
self.blob_target = None
async def see_alignment(self, source=None):
"""
Interaction for investigative roles attempting
to see team (Village, Werewolf, Other)
"""
return ALIGNMENT_NEUTRAL
async def get_role(self, source=None):
"""
Interaction for powerful access of role
Unlikely to be able to deceive this
"""
return "The Blob"
async def see_role(self, source=None):
"""
Interaction for investigative roles.
More common to be able to deceive these roles
"""
return "The Blob"
async def kill(self, source):
"""
Called when someone is trying to kill you!
Can you do anything about it?
self.player.alive is now set to False, set to True to stay alive
"""
# Blob cannot simply be killed
self.player.alive = True
@wolflistener("at_night_start", priority=2)
async def _at_night_start(self):
if not self.player.alive:
return
self.blob_target = None
idx = self.player.id
left_or_right = random.choice((-1, 1))
while self.blob_target is None:
idx += left_or_right
if idx >= len(self.game.players):
idx = 0
player = self.game.players[idx]
# you went full circle, everyone is a blob or something else is wrong
if player == self.player:
break
if player.role.properties.get("been_blobbed", False):
self.blob_target = player
if self.blob_target is not None:
await self.player.send_dm(f"**You will attempt to absorb {self.blob_target} tonight**")
else:
await self.player.send_dm(f"**No player will be absorbed tonight**")
@wolflistener("at_night_end", priority=4)
async def _at_night_end(self):
if self.blob_target is None or not self.player.alive:
return
target: "Player" = await self.game.visit(self.blob_target, self.player)
if target is not None:
target.role.properties["been_blobbed"] = True
self.game.night_results.append("The Blob grows...")

@ -1,11 +1,26 @@
from ..night_powers import pick_target import logging
from ..role import Role
from werewolf.constants import (
ALIGNMENT_TOWN,
ALIGNMENT_WEREWOLF,
CATEGORY_TOWN_INVESTIGATIVE,
CATEGORY_TOWN_RANDOM,
)
from werewolf.listener import wolflistener
from werewolf.night_powers import pick_target
from werewolf.role import Role
log = logging.getLogger("red.fox_v3.werewolf.role.seer")
class Seer(Role): class Seer(Role):
rand_choice = True # Determines if it can be picked as a random role (False for unusually disruptive roles) rand_choice = True
category = [1, 2] # List of enrolled categories (listed above) town_balance = 4
alignment = 1 # 1: Town, 2: Werewolf, 3: Neutral 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 channel_id = "" # Empty for no private channel
unique = False # Only one of this role per game unique = False # Only one of this role per game
game_start_message = ( game_start_message = (
@ -14,8 +29,10 @@ class Seer(Role):
"Lynch players during the day with `[p]ww vote <ID>`\n" "Lynch players during the day with `[p]ww vote <ID>`\n"
"Check for werewolves at night with `[p]ww choose <ID>`" "Check for werewolves at night with `[p]ww choose <ID>`"
) )
description = "A mystic in search of answers in a chaotic town.\n" \ description = (
"Calls upon the cosmos to discern those of Lycan blood" "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): def __init__(self, game):
super().__init__(game) super().__init__(game)
@ -24,47 +41,49 @@ class Seer(Role):
# self.blocked = False # self.blocked = False
# self.properties = {} # Extra data for other roles (i.e. arsonist) # self.properties = {} # Extra data for other roles (i.e. arsonist)
self.see_target = None self.see_target = None
self.action_list = [ # self.action_list = [
(self._at_game_start, 1), # (Action, Priority) # (self._at_game_start, 1), # (Action, Priority)
(self._at_day_start, 0), # (self._at_day_start, 0),
(self._at_voted, 0), # (self._at_voted, 0),
(self._at_kill, 0), # (self._at_kill, 0),
(self._at_hang, 0), # (self._at_hang, 0),
(self._at_day_end, 0), # (self._at_day_end, 0),
(self._at_night_start, 2), # (self._at_night_start, 2),
(self._at_night_end, 4), # (self._at_night_end, 4),
(self._at_visit, 0) # (self._at_visit, 0),
] # ]
async def see_alignment(self, source=None): async def see_alignment(self, source=None):
""" """
Interaction for investigative roles attempting 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): async def get_role(self, source=None):
""" """
Interaction for powerful access of role Interaction for powerful access of role
Unlikely to be able to deceive this Unlikely to be able to deceive this
""" """
return "Villager" return "Seer"
async def see_role(self, source=None): async def see_role(self, source=None):
""" """
Interaction for investigative roles. Interaction for investigative roles.
More common to be able to deceive these roles More common to be able to deceive these roles
""" """
return "Villager" return "Seer"
async def _at_night_start(self, data=None): @wolflistener("at_night_start", priority=2)
async def _at_night_start(self):
if not self.player.alive: if not self.player.alive:
return return
self.see_target = None self.see_target = None
await self.game.generate_targets(self.player.member) await self.game.generate_targets(self.player.member)
await self.player.send_dm("**Pick a target to see tonight**") await self.player.send_dm("**Pick a target to see tonight**")
async def _at_night_end(self, data=None): @wolflistener("at_night_end", priority=4)
async def _at_night_end(self):
if self.see_target is None: if self.see_target is None:
if self.player.alive: if self.player.alive:
await self.player.send_dm("You will not use your powers tonight...") await self.player.send_dm("You will not use your powers tonight...")
@ -75,9 +94,9 @@ class Seer(Role):
if target: if target:
alignment = await target.role.see_alignment(self.player) 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!**" 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..." out = "You fail to find anything suspicious about this player..."
await self.player.send_dm(out) await self.player.send_dm(out)
@ -87,4 +106,6 @@ class Seer(Role):
await super().choose(ctx, data) await super().choose(ctx, data)
self.see_target, target = await pick_target(self, 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(
f"**You will attempt to see the role of {target.member.display_name} tonight...**"
)

@ -1,35 +1,41 @@
from ..night_powers import pick_target import logging
from ..role import Role
from werewolf.constants import ALIGNMENT_NEUTRAL, CATEGORY_NEUTRAL_BENIGN
from werewolf.listener import wolflistener
from werewolf.night_powers import pick_target
from werewolf.role import Role
log = logging.getLogger("red.fox_v3.werewolf.role.shifter")
class Shifter(Role): class Shifter(Role):
""" """
Base Role class for werewolf game Base Role class for werewolf game
Category enrollment guide as follows (category property): Category enrollment guide as follows (category property):
Town: Town:
1: Random, 2: Investigative, 3: Protective, 4: Government, 1: Random, 2: Investigative, 3: Protective, 4: Government,
5: Killing, 6: Power (Special night action) 5: Killing, 6: Power (Special night action)
Werewolf: Werewolf:
11: Random, 12: Deception, 15: Killing, 16: Support 11: Random, 12: Deception, 15: Killing, 16: Support
Neutral: Neutral:
21: Benign, 22: Evil, 23: Killing 21: Benign, 22: Evil, 23: Killing
Example category: Example category:
category = [1, 5, 6] Could be Veteran category = [1, 5, 6] Could be Veteran
category = [1, 5] Could be Bodyguard category = [1, 5] Could be Bodyguard
category = [11, 16] Could be Werewolf Silencer category = [11, 16] Could be Werewolf Silencer
Action guide as follows (on_event function): Action guide as follows (on_event function):
_at_night_start _at_night_start
0. No Action 0. No Action
1. Detain actions (Jailer/Kidnapper) 1. Detain actions (Jailer/Kidnapper)
2. Group discussions and choose targets 2. Group discussions and choose targets
_at_night_end _at_night_end
0. No Action 0. No Action
1. Self actions (Veteran) 1. Self actions (Veteran)
@ -37,12 +43,13 @@ class Shifter(Role):
3. Protection / Preempt actions (bodyguard/framer) 3. Protection / Preempt actions (bodyguard/framer)
4. Non-disruptive actions (seer/silencer) 4. Non-disruptive actions (seer/silencer)
5. Disruptive actions (Killing) 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) 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) town_balance = -3
alignment = 3 # 1: Town, 2: Werewolf, 3: Neutral 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 channel_id = "" # Empty for no private channel
unique = False # Only one of this role per game unique = False # Only one of this role per game
game_start_message = ( game_start_message = (
@ -61,22 +68,22 @@ class Shifter(Role):
super().__init__(game) super().__init__(game)
self.shift_target = None self.shift_target = None
self.action_list = [ # self.action_list = [
(self._at_game_start, 1), # (Action, Priority) # (self._at_game_start, 1), # (Action, Priority)
(self._at_day_start, 0), # (self._at_day_start, 0),
(self._at_voted, 0), # (self._at_voted, 0),
(self._at_kill, 0), # (self._at_kill, 0),
(self._at_hang, 0), # (self._at_hang, 0),
(self._at_day_end, 0), # (self._at_day_end, 0),
(self._at_night_start, 2), # Chooses targets # (self._at_night_start, 2), # Chooses targets
(self._at_night_end, 6), # Role Swap # (self._at_night_end, 6), # Role Swap
(self._at_visit, 0) # (self._at_visit, 0),
] # ]
async def see_alignment(self, source=None): async def see_alignment(self, source=None):
""" """
Interaction for investigative roles attempting Interaction for investigative roles attempting
to see alignment (Village, Werewolf, Other) to see alignment (Village, Werewolf,, Other)
""" """
return "Other" return "Other"
@ -94,14 +101,14 @@ class Shifter(Role):
""" """
return "Shifter" return "Shifter"
async def _at_night_start(self, data=None): @wolflistener("at_night_start", priority=2)
await super()._at_night_start(data) async def _at_night_start(self):
self.shift_target = None self.shift_target = None
await self.game.generate_targets(self.player.member) await self.game.generate_targets(self.player.member)
await self.player.send_dm("**Pick a target to shift into**") await self.player.send_dm("**Pick a target to shift into**")
async def _at_night_end(self, data=None): @wolflistener("at_night_end", priority=6)
await super()._at_night_end(data) async def _at_night_end(self):
if self.shift_target is None: if self.shift_target is None:
if self.player.alive: if self.player.alive:
await self.player.send_dm("You will not use your powers tonight...") await self.player.send_dm("You will not use your powers tonight...")
@ -114,16 +121,20 @@ class Shifter(Role):
# Roles have now been swapped # Roles have now been swapped
await self.player.send_dm("Your role has been stolen...\n" await self.player.send_dm(
"You are now a **Shifter**.") "Your role has been stolen...\n" "You are now a **Shifter**."
)
await self.player.send_dm(self.game_start_message) await self.player.send_dm(self.game_start_message)
await target.send_dm(target.role.game_start_message) await target.send_dm(target.role.game_start_message)
else: else:
await self.player.send_dm("**Your shift failed...**") await self.player.send_dm("**Your shift failed...**")
async def choose(self, ctx, data): async def choose(self, ctx, data):
"""Handle night actions""" """Handle night actions"""
await super().choose(ctx, data) await super().choose(ctx, data)
self.shift_target, target = await pick_target(self, 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(
f"**You will attempt to see the role of {target.member.display_name} tonight...**"
)

@ -1,13 +1,19 @@
from ..role import Role import logging
from ..votegroups.wolfvote import WolfVote from werewolf.constants import ALIGNMENT_WEREWOLF, CATEGORY_WW_KILLING, CATEGORY_WW_RANDOM
from werewolf.listener import wolflistener
from werewolf.role import Role
from werewolf.votegroups.wolfvote import WolfVote
log = logging.getLogger("red.fox_v3.werewolf.role.vanillawerewolf")
class VanillaWerewolf(Role): class VanillaWerewolf(Role):
rand_choice = True rand_choice = True
category = [11, 15] town_balance = -6
alignment = 2 # 1: Town, 2: Werewolf, 3: Neutral category = [CATEGORY_WW_RANDOM, CATEGORY_WW_KILLING]
channel_id = "werewolves" alignment = ALIGNMENT_WEREWOLF # 1: Town, 2: Werewolf, 3: Neutral
channel_name = "werewolves"
unique = False unique = False
game_start_message = ( game_start_message = (
"Your role is **Werewolf**\n" "Your role is **Werewolf**\n"
@ -16,34 +22,19 @@ class VanillaWerewolf(Role):
"Vote to kill players at night with `[p]ww vote <ID>`" "Vote to kill players at night with `[p]ww vote <ID>`"
) )
def __init__(self, game):
super().__init__(game)
self.action_list = [
(self._at_game_start, 1), # (Action, Priority)
(self._at_day_start, 0),
(self._at_voted, 0),
(self._at_kill, 0),
(self._at_hang, 0),
(self._at_day_end, 0),
(self._at_night_start, 0),
(self._at_night_end, 0),
(self._at_visit, 0)
]
async def see_alignment(self, source=None): async def see_alignment(self, source=None):
""" """
Interaction for investigative roles attempting Interaction for investigative roles attempting
to see team (Village, Werewolf Other) to see team (Village, Werewolf Other)
""" """
return "Werewolf" return ALIGNMENT_WEREWOLF
async def get_role(self, source=None): async def get_role(self, source=None):
""" """
Interaction for powerful access of role Interaction for powerful access of role
Unlikely to be able to deceive this Unlikely to be able to deceive this
""" """
return "Werewolf" return "VanillaWerewolf"
async def see_role(self, source=None): async def see_role(self, source=None):
""" """
@ -52,10 +43,13 @@ class VanillaWerewolf(Role):
""" """
return "Werewolf" return "Werewolf"
async def _at_game_start(self, data=None): @wolflistener("at_game_start", priority=2)
if self.channel_id: async def _at_game_start(self):
print("Wolf has channel_id: " + self.channel_id) if self.channel_name:
await self.game.register_channel(self.channel_id, self, WolfVote) # Add VoteGroup WolfVote log.debug("Wolf has channel_name: " + self.channel_name)
await self.game.register_channel(
self.channel_name, self, WolfVote
) # Add VoteGroup WolfVote
await self.player.send_dm(self.game_start_message) await self.player.send_dm(self.game_start_message)

@ -1,10 +1,17 @@
from ..role import Role 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): class Villager(Role):
rand_choice = True # 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)
category = [1] # List of enrolled categories (listed above) rand_choice = True
alignment = 1 # 1: Town, 2: Werewolf, 3: Neutral 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 channel_id = "" # Empty for no private channel
unique = False # Only one of this role per game unique = False # Only one of this role per game
game_start_message = ( game_start_message = (
@ -13,15 +20,12 @@ class Villager(Role):
"Lynch players during the day with `[p]ww vote <ID>`" "Lynch players during the day with `[p]ww vote <ID>`"
) )
def __init__(self, game):
super().__init__(game)
async def see_alignment(self, source=None): async def see_alignment(self, source=None):
""" """
Interaction for investigative roles attempting 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): async def get_role(self, source=None):
""" """

@ -1,4 +1,11 @@
class VoteGroup: import logging
from werewolf.listener import WolfListener, wolflistener
log = logging.getLogger("red.fox_v3.werewolf.votegroup")
class VoteGroup(WolfListener):
""" """
Base VoteGroup class for werewolf game Base VoteGroup class for werewolf game
Handles secret channels and group decisions Handles secret channels and group decisions
@ -8,58 +15,41 @@ class VoteGroup:
channel_id = "" channel_id = ""
def __init__(self, game, channel): def __init__(self, game, channel):
super().__init__(game)
self.game = game self.game = game
self.channel = channel self.channel = channel
self.players = [] self.players = []
self.vote_results = {} self.vote_results = {}
self.properties = {} # Extra data for other options self.properties = {} # Extra data for other options
self.action_list = [ def __repr__(self):
(self._at_game_start, 1), # (Action, Priority) return f"{self.__class__.__name__}({self.channel},{self.players})"
(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", priority=1)
async def _at_game_start(self):
async def _at_game_start(self, data=None):
await self.channel.send(" ".join(player.mention for player in self.players)) await self.channel.send(" ".join(player.mention for player in self.players))
async def _at_day_start(self, data=None): @wolflistener("at_kill", priority=1)
pass async def _at_kill(self, player):
if player in self.players:
async def _at_voted(self, data=None): self.players.remove(player)
pass
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): @wolflistener("at_hang", priority=1)
pass async def _at_hang(self, player):
if player in self.players:
self.players.remove(player)
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: if self.channel is None:
return return
self.vote_results = {}
await self.game.generate_targets(self.channel) await self.game.generate_targets(self.channel)
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: if self.channel is None:
return return
@ -70,11 +60,8 @@ class VoteGroup:
target = max(set(vote_list), key=vote_list.count) target = max(set(vote_list), key=vote_list.count)
if target: if target:
# Do what you voted on # Do what the votegroup votes on
pass raise NotImplementedError
async def _at_visit(self, data=None):
pass
async def register_players(self, *players): async def register_players(self, *players):
""" """
@ -90,7 +77,7 @@ class VoteGroup:
self.players.remove(player) self.players.remove(player)
if not self.players: if not self.players:
# ToDo: Trigger deletion of votegroup # TODO: Confirm deletion
pass pass
async def vote(self, target, author, target_id): async def vote(self, target, author, target_id):

@ -0,0 +1 @@
from .wolfvote import WolfVote

@ -1,6 +1,12 @@
import logging
import random import random
from ..votegroup import VoteGroup import discord
from werewolf.listener import wolflistener
from werewolf.votegroup import VoteGroup
log = logging.getLogger("red.fox_v3.werewolf.votegroup.wolfvote")
class WolfVote(VoteGroup): class WolfVote(VoteGroup):
@ -13,71 +19,29 @@ class WolfVote(VoteGroup):
kill_messages = [ kill_messages = [
"**{ID}** - {target} was mauled by wolves", "**{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): def __init__(self, game, channel):
super().__init__(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.killer = None # Added killer
self.action_list = [ @wolflistener("at_night_start", priority=2)
(self._at_game_start, 1), # (Action, Priority) async def _at_night_start(self):
(self._at_day_start, 0), await super()._at_night_start()
(self._at_voted, 0),
(self._at_kill, 1),
(self._at_hang, 1),
(self._at_day_end, 0),
(self._at_night_start, 2),
(self._at_night_end, 5), # Kill priority
(self._at_visit, 0)
]
# async def on_event(self, event, data):
# """
# See Game class for event guide
# """
#
# await action_list[event][0](data)
#
# async def _at_game_start(self, data=None):
# await self.channel.send(" ".join(player.mention for player in self.players))
#
# async def _at_day_start(self, data=None):
# pass
#
# async def _at_voted(self, data=None):
# pass
#
# async def _at_kill(self, data=None):
# if data["player"] in self.players:
# self.players.pop(data["player"])
#
# async def _at_hang(self, data=None):
# if data["player"] in self.players:
# self.players.pop(data["player"])
#
# async def _at_day_end(self, data=None):
# pass
async def _at_night_start(self, data=None):
if self.channel is None:
return
await self.game.generate_targets(self.channel)
mention_list = " ".join(player.mention for player in self.players) mention_list = " ".join(player.mention for player in self.players)
if mention_list != "": if mention_list != "":
await self.channel.send(mention_list) await self.channel.send(mention_list)
self.killer = random.choice(self.players) 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(
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: if self.channel is None:
return return
@ -87,34 +51,23 @@ class WolfVote(VoteGroup):
if vote_list: if vote_list:
target_id = max(set(vote_list), key=vote_list.count) target_id = max(set(vote_list), key=vote_list.count)
print("Target id: {}\nKiller: {}".format(target_id, 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: if target_id is not None and self.killer:
await self.game.kill(target_id, self.killer, random.choice(self.kill_messages)) 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: else:
await self.channel.send("**No kill will be attempted tonight...**") await self.channel.send("*No kill will be attempted tonight...*")
# async def _at_visit(self, data=None):
# pass
#
# async def register_players(self, *players):
# """
# Extend players by passed list
# """
# self.players.extend(players)
#
# async def remove_player(self, player):
# """
# Remove a player from player list
# """
# if player.id in self.players:
# self.players.remove(player)
async def vote(self, target, author, target_id): async def vote(self, target, author, target_id):
""" """
Receive vote from game 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)) await self.channel.send(
"{} has voted to kill {}".format(author.mention, target.member.display_name),
allowed_mentions=discord.AllowedMentions(everyone=False, users=[author]),
)

@ -1,17 +1,31 @@
import logging
from typing import List, Union
import discord import discord
from redbot.core import Config, checks, commands from redbot.core import Config, checks, commands
from redbot.core.bot import Red from redbot.core.bot import Red
from redbot.core.commands import Cog from redbot.core.commands import Cog
from redbot.core.utils import AsyncIter
from redbot.core.utils.menus import DEFAULT_CONTROLS, menu from redbot.core.utils.menus import DEFAULT_CONTROLS, menu
from .builder import ( from werewolf.builder import (
GameBuilder, GameBuilder,
role_from_alignment, role_from_alignment,
role_from_category, role_from_category,
role_from_id, role_from_id,
role_from_name, role_from_name,
) )
from .game import Game 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): class Werewolf(Cog):
@ -43,7 +57,7 @@ class Werewolf(Cog):
return return
def __unload(self): def __unload(self):
print("Unload called") log.debug("Unload called")
for game in self.games.values(): for game in self.games.values():
del game del game
@ -58,9 +72,9 @@ class Werewolf(Cog):
code = await gb.build_game(ctx) code = await gb.build_game(ctx)
if code != "": if code != "":
await ctx.send("Your game code is **{}**".format(code)) await ctx.maybe_send_embed(f"Your game code is **{code}**")
else: else:
await ctx.send("No code generated") await ctx.maybe_send_embed("No code generated")
@checks.guildowner() @checks.guildowner()
@commands.group() @commands.group()
@ -77,31 +91,36 @@ class Werewolf(Cog):
""" """
Lists current guild settings Lists current guild settings
""" """
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("Failed to get settings") # await ctx.send("Failed to get settings")
return None # return None
embed = discord.Embed(title="Current Guild Settings") embed = discord.Embed(
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="Role", value=str(role))
embed.add_field(name="Category", value=str(category)) embed.add_field(name="Category", value=str(category))
embed.add_field(name="Channel", value=str(channel)) embed.add_field(name="Channel", value=str(channel))
embed.add_field(name="Log Channel", value=str(log_channel)) embed.add_field(name="Log Channel", value=str(log_channel))
await ctx.send(embed=embed) await ctx.send(embed=embed)
@commands.guild_only() @commands.guild_only()
@wwset.command(name="role") @wwset.command(name="role")
async def wwset_role(self, ctx: commands.Context, role: discord.Role = None): 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 This role should not be manually assigned
""" """
if role is None: if role is None:
await self.config.guild(ctx.guild).role_id.set(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: else:
await self.config.guild(ctx.guild).role_id.set(role.id) 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() @commands.guild_only()
@wwset.command(name="category") @wwset.command(name="category")
@ -111,14 +130,16 @@ class Werewolf(Cog):
""" """
if category_id is None: if category_id is None:
await self.config.guild(ctx.guild).category_id.set(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: else:
category = discord.utils.get(ctx.guild.categories, id=int(category_id)) category = discord.utils.get(ctx.guild.categories, id=int(category_id))
if category is None: if category is None:
await ctx.send("Category not found") await ctx.maybe_send_embed("Category not found")
return return
await self.config.guild(ctx.guild).category_id.set(category.id) 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() @commands.guild_only()
@wwset.command(name="channel") @wwset.command(name="channel")
@ -128,10 +149,12 @@ class Werewolf(Cog):
""" """
if channel is None: if channel is None:
await self.config.guild(ctx.guild).channel_id.set(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: else:
await self.config.guild(ctx.guild).channel_id.set(channel.id) 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() @commands.guild_only()
@wwset.command(name="logchannel") @wwset.command(name="logchannel")
@ -141,10 +164,12 @@ class Werewolf(Cog):
""" """
if channel is None: if channel is None:
await self.config.guild(ctx.guild).log_channel_id.set(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: else:
await self.config.guild(ctx.guild).log_channel_id.set(channel.id) 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() @commands.group()
async def ww(self, ctx: commands.Context): async def ww(self, ctx: commands.Context):
@ -162,9 +187,9 @@ class Werewolf(Cog):
""" """
game = await self._get_game(ctx, game_code) game = await self._get_game(ctx, game_code)
if not game: if not game:
await ctx.send("Failed to start a new game") await ctx.maybe_send_embed("Failed to start a new game")
else: 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() @commands.guild_only()
@ww.command(name="join") @ww.command(name="join")
@ -173,28 +198,49 @@ class Werewolf(Cog):
Joins a game of Werewolf Joins a game of Werewolf
""" """
game = await self._get_game(ctx) game: Game = await self._get_game(ctx)
if not game: 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 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() @commands.guild_only()
@ww.command(name="code") @ww.command(name="code")
async def ww_code(self, ctx: commands.Context, 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) game = await self._get_game(ctx)
if not game: 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 return
await game.set_code(ctx, code) await game.set_code(ctx, code)
await ctx.tick()
@commands.guild_only() @commands.guild_only()
@ww.command(name="quit") @ww.command(name="quit")
@ -206,6 +252,7 @@ class Werewolf(Cog):
game = await self._get_game(ctx) game = await self._get_game(ctx)
await game.quit(ctx.author, ctx.channel) await game.quit(ctx.author, ctx.channel)
await ctx.tick()
@commands.guild_only() @commands.guild_only()
@ww.command(name="start") @ww.command(name="start")
@ -215,10 +262,12 @@ class Werewolf(Cog):
""" """
game = await self._get_game(ctx) game = await self._get_game(ctx)
if not game: 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): if not await game.setup(ctx):
pass # Do something? pass # ToDo something?
await ctx.tick()
@commands.guild_only() @commands.guild_only()
@ww.command(name="stop") @ww.command(name="stop")
@ -226,17 +275,18 @@ class Werewolf(Cog):
""" """
Stops the current game Stops the current game
""" """
if ctx.guild is None: # if ctx.guild is None:
# Private message, can't get guild # # Private message, can't get guild
await ctx.send("Cannot start game from PM!") # await ctx.send("Cannot stop game from PM!")
return # return
if ctx.guild.id not in self.games or self.games[ctx.guild.id].game_over: 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 return
game = await self._get_game(ctx) game = await self._get_game(ctx)
game.game_over = True game.game_over = True
await ctx.send("Game has been stopped") game.current_action.cancel()
await ctx.maybe_send_embed("Game has been stopped")
@commands.guild_only() @commands.guild_only()
@ww.command(name="vote") @ww.command(name="vote")
@ -250,7 +300,7 @@ class Werewolf(Cog):
target_id = None target_id = None
if target_id is 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 return
# if ctx.guild is None: # if ctx.guild is None:
@ -267,7 +317,7 @@ class Werewolf(Cog):
game = await self._get_game(ctx) game = await self._get_game(ctx)
if game is None: if game is None:
await ctx.send("No game running, cannot vote") await ctx.maybe_send_embed("No game running, cannot vote")
return return
# Game handles response now # Game handles response now
@ -277,7 +327,7 @@ class Werewolf(Cog):
elif channel in (c["channel"] for c in game.p_channels.values()): elif channel in (c["channel"] for c in game.p_channels.values()):
await game.vote(ctx.author, target_id, channel) await game.vote(ctx.author, target_id, channel)
else: 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") @ww.command(name="choose")
async def ww_choose(self, ctx: commands.Context, data): async def ww_choose(self, ctx: commands.Context, data):
@ -288,7 +338,7 @@ class Werewolf(Cog):
""" """
if ctx.guild is not None: 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 return
# DM nonsense, find their game # DM nonsense, find their game
# If multiple games, panic # If multiple games, panic
@ -296,7 +346,7 @@ class Werewolf(Cog):
if await game.get_player_by_member(ctx.author): if await game.get_player_by_member(ctx.author):
break # game = game break # game = game
else: 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 return
await game.choose(ctx, data) await game.choose(ctx, data)
@ -317,7 +367,7 @@ class Werewolf(Cog):
if from_name: if from_name:
await menu(ctx, from_name, DEFAULT_CONTROLS) await menu(ctx, from_name, DEFAULT_CONTROLS)
else: 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") @ww_search.command(name="alignment")
async def ww_search_alignment(self, ctx: commands.Context, alignment: int): async def ww_search_alignment(self, ctx: commands.Context, alignment: int):
@ -327,7 +377,7 @@ class Werewolf(Cog):
if from_alignment: if from_alignment:
await menu(ctx, from_alignment, DEFAULT_CONTROLS) await menu(ctx, from_alignment, DEFAULT_CONTROLS)
else: 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") @ww_search.command(name="category")
async def ww_search_category(self, ctx: commands.Context, category: int): async def ww_search_category(self, ctx: commands.Context, category: int):
@ -337,7 +387,7 @@ class Werewolf(Cog):
if pages: if pages:
await menu(ctx, pages, DEFAULT_CONTROLS) await menu(ctx, pages, DEFAULT_CONTROLS)
else: 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") @ww_search.command(name="index")
async def ww_search_index(self, ctx: commands.Context, idx: int): async def ww_search_index(self, ctx: commands.Context, idx: int):
@ -347,24 +397,32 @@ class Werewolf(Cog):
if idx_embed is not None: if idx_embed is not None:
await ctx.send(embed=idx_embed) await ctx.send(embed=idx_embed)
else: 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): async def _get_game(self, ctx: commands.Context, game_code=None) -> Union[Game, None]:
guild: discord.Guild = ctx.guild guild: discord.Guild = getattr(ctx, "guild", None)
if guild is None: if guild is None:
# Private message, can't get guild # Private message, can't get guild
await ctx.send("Cannot start game from PM!") await ctx.maybe_send_embed("Cannot start game from DM!")
return None return None
if guild.id not in self.games or self.games[guild.id].game_over: 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...")
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") await ctx.maybe_send_embed("Cannot start a new game")
return None return None
self.games[guild.id] = Game(guild, role, category, channel, log_channel, game_code) 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
)
return self.games[guild.id] return self.games[guild.id]
@ -385,23 +443,30 @@ class Werewolf(Cog):
if role_id is not None: if role_id is not None:
role = discord.utils.get(guild.roles, id=role_id) role = discord.utils.get(guild.roles, id=role_id)
if role is None: # if role is None:
await ctx.send("Game Role is invalid") # # await ctx.send("Game Role is invalid")
return False, None, None, None, None # return False, None, None, None, None
if category_id is not None: if category_id is not None:
category = discord.utils.get(guild.categories, id=category_id) category = discord.utils.get(guild.categories, id=category_id)
if category is None: # if category is None:
await ctx.send("Game Category is invalid") # # await ctx.send("Game Category is invalid")
return False, None, None, None, None # return False, role, None, None, None
if channel_id is not None: if channel_id is not None:
channel = discord.utils.get(guild.text_channels, id=channel_id) channel = discord.utils.get(guild.text_channels, id=channel_id)
if channel is None: # if channel is None:
await ctx.send("Village Channel is invalid") # # await ctx.send("Village Channel is invalid")
return False, None, None, None, None # return False, role, category, None, None
if log_channel_id is not None: if log_channel_id is not None:
log_channel = discord.utils.get(guild.text_channels, id=log_channel_id) log_channel = discord.utils.get(guild.text_channels, id=log_channel_id)
if log_channel is None: # if log_channel is None:
await ctx.send("Log Channel is invalid") # # await ctx.send("Log Channel is invalid")
return False, None, None, None, None # return False, None, None, None, None
return True, role, category, channel, log_channel return (
role is not None and category is not None and channel is not None,
role,
category,
channel,
log_channel,
)

Loading…
Cancel
Save