Merge branch 'master' into ccrole_develop

ccrole_develop
bobloy 4 years ago
commit ad7c2d9cf6

@ -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,21 +1,25 @@
"""Module to manage audio trivia sessions."""
import asyncio
import logging
import lavalink
from redbot.cogs.trivia import TriviaSession
from redbot.cogs.trivia.session import _parse_answers
from redbot.core.utils.chat_formatting import bold
log = logging.getLogger("red.fox_v3.audiotrivia.audiosession")
class AudioSession(TriviaSession):
"""Class to run a session of audio trivia"""
def __init__(self, ctx, question_list: dict, settings: dict, player: lavalink.Player):
def __init__(self, ctx, question_list: dict, settings: dict, audio=None):
super().__init__(ctx, question_list, settings)
self.player = player
self.audio = audio
@classmethod
def start(cls, ctx, question_list, settings, player: lavalink.Player = None):
session = cls(ctx, question_list, settings, player)
def start(cls, ctx, question_list, settings, audio=None):
session = cls(ctx, question_list, settings, audio)
loop = ctx.bot.loop
session._task = loop.create_task(session.run())
return session
@ -29,46 +33,89 @@ class AudioSession(TriviaSession):
await self._send_startup_msg()
max_score = self.settings["max_score"]
delay = self.settings["delay"]
audio_delay = self.settings["audio_delay"]
timeout = self.settings["timeout"]
for question, answers in self._iter_questions():
if self.audio is not None:
import lavalink
player = lavalink.get_player(self.ctx.guild.id)
player.store("channel", self.ctx.channel.id) # What's this for? I dunno
await self.audio.set_player_settings(self.ctx)
else:
lavalink = None
player = False
for question, answers, audio_url in self._iter_questions():
async with self.ctx.typing():
await asyncio.sleep(3)
self.count += 1
await self.player.stop()
msg = "**Question number {}!**\n\nName this audio!".format(self.count)
await self.ctx.send(msg)
# print("Audio question: {}".format(question))
# await self.ctx.invoke(self.audio.play(ctx=self.ctx, query=question))
# ctx_copy = copy(self.ctx)
msg = bold(f"Question number {self.count}!") + f"\n\n{question}"
if player:
await player.stop()
if audio_url:
if not player:
log.debug("Got an audio question in a non-audio trivia session")
continue
# await self.ctx.invoke(self.player.play, query=question)
query = question.strip("<>")
tracks = await self.player.get_tracks(query)
seconds = tracks[0].length / 1000
if self.settings["repeat"] and seconds < delay:
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 < delay:
self.player.add(self.ctx.author, tracks[0])
while tot_length < audio_delay:
player.add(self.ctx.author, track)
tot_length += seconds
else:
self.player.add(self.ctx.author, tracks[0])
player.add(self.ctx.author, track)
if not self.player.current:
await self.player.play()
if not player.current:
await player.play()
await self.ctx.maybe_send_embed(msg)
log.debug(f"Audio question: {question}")
continue_ = await self.wait_for_answer(answers, delay, timeout)
continue_ = await self.wait_for_answer(
answers, audio_delay if audio_url else delay, timeout
)
if continue_ is False:
break
if any(score >= max_score for score in self.scores.values()):
await self.end_game()
break
else:
await self.ctx.send("There are no more questions!")
await self.ctx.maybe_send_embed("There are no more questions!")
await self.end_game()
async def end_game(self):
await super().end_game()
await self.player.disconnect()
if self.audio is not None:
await self.ctx.invoke(self.audio.command_disconnect)
def _iter_questions(self):
"""Iterate over questions and answers for this session.
Yields
------
`tuple`
A tuple containing the question (`str`) and the answers (`tuple` of
`str`).
"""
for question, q_data in self.question_list:
answers = _parse_answers(q_data["answers"])
_audio = q_data["audio"]
if _audio:
yield _audio, answers, question.strip("<>")
else:
yield question, answers, _audio

@ -1,21 +1,22 @@
import datetime
import logging
import pathlib
from typing import List
from typing import List, Optional
import discord
import lavalink
import yaml
from redbot.cogs.audio import Audio
from redbot.cogs.trivia import LOG
from redbot.cogs.trivia.trivia import InvalidListError, Trivia
from redbot.cogs.trivia.trivia import InvalidListError, Trivia, get_core_lists
from redbot.core import Config, checks, commands
from redbot.core.bot import Red
from redbot.core.data_manager import cog_data_path
from redbot.core.utils.chat_formatting import box
from redbot.core.utils.chat_formatting import bold, box
from .audiosession import AudioSession
# from redbot.cogs.audio.utils import userlimit
log = logging.getLogger("red.fox_v3.audiotrivia")
class AudioTrivia(Trivia):
@ -27,12 +28,11 @@ class AudioTrivia(Trivia):
def __init__(self, bot: Red):
super().__init__()
self.bot = bot
self.audio = None
self.audioconf = Config.get_conf(
self, identifier=651171001051118411410511810597, force_registration=True
)
self.audioconf.register_guild(delay=30.0, repeat=True)
self.audioconf.register_guild(audio_delay=30.0, repeat=True)
@commands.group()
@commands.guild_only()
@ -43,122 +43,112 @@ class AudioTrivia(Trivia):
settings_dict = await audioset.all()
msg = box(
"**Audio settings**\n"
"Answer time limit: {delay} seconds\n"
"Answer time limit: {audio_delay} seconds\n"
"Repeat Short Audio: {repeat}"
"".format(**settings_dict),
lang="py",
)
await ctx.send(msg)
@atriviaset.command(name="delay")
async def atriviaset_delay(self, ctx: commands.Context, seconds: float):
@atriviaset.command(name="timelimit")
async def atriviaset_timelimit(self, ctx: commands.Context, seconds: float):
"""Set the maximum seconds permitted to answer a question."""
if seconds < 4.0:
await ctx.send("Must be at least 4 seconds.")
return
settings = self.audioconf.guild(ctx.guild)
await settings.delay.set(seconds)
await ctx.send("Done. Maximum seconds to answer set to {}.".format(seconds))
await settings.audo_delay.set(seconds)
await ctx.maybe_send_embed(f"Done. Maximum seconds to answer set to {seconds}.")
@atriviaset.command(name="repeat")
async def atriviaset_repeat(self, ctx: commands.Context, true_or_false: bool):
"""Set whether or not short audio will be repeated"""
settings = self.audioconf.guild(ctx.guild)
await settings.repeat.set(true_or_false)
await ctx.send("Done. Repeating short audio is now set to {}.".format(true_or_false))
await ctx.maybe_send_embed(f"Done. Repeating short audio is now set to {true_or_false}.")
@commands.group(invoke_without_command=True)
@commands.guild_only()
async def audiotrivia(self, ctx: commands.Context, *categories: str):
"""Start trivia session on the specified category.
"""Start trivia session on the specified category or categories.
Includes Audio categories.
You may list multiple categories, in which case the trivia will involve
questions from all of them.
"""
if not categories and ctx.invoked_subcommand is None:
await ctx.send_help()
return
if self.audio is None:
self.audio: Audio = self.bot.get_cog("Audio")
if self.audio is None:
await ctx.send("Audio is not loaded. Load it and try again")
return
categories = [c.lower() for c in categories]
session = self._get_trivia_session(ctx.channel)
if session is not None:
await ctx.send("There is already an ongoing trivia session in this channel.")
return
status = await self.audio.config.status()
notify = await self.audio.config.guild(ctx.guild).notify()
if status:
await ctx.send(
f"It is recommended to disable audio status with `{ctx.prefix}audioset status`"
)
if notify:
await ctx.send(
f"It is recommended to disable audio notify with `{ctx.prefix}audioset notify`"
await ctx.maybe_send_embed(
"There is already an ongoing trivia session in this channel."
)
if not self.audio._player_check(ctx):
try:
if not ctx.author.voice.channel.permissions_for(
ctx.me
).connect or self.audio.is_vc_full(ctx.author.voice.channel):
return await ctx.send("I don't have permission to connect to your channel.")
await lavalink.connect(ctx.author.voice.channel)
lavaplayer = lavalink.get_player(ctx.guild.id)
lavaplayer.store("connect", datetime.datetime.utcnow())
except AttributeError:
return await ctx.send("Connect to a voice channel first.")
lavaplayer = lavalink.get_player(ctx.guild.id)
lavaplayer.store("channel", ctx.channel.id) # What's this for? I dunno
await self.audio.set_player_settings(ctx)
if not ctx.author.voice or ctx.author.voice.channel != lavaplayer.channel:
return await ctx.send(
"You must be in the voice channel to use the audiotrivia command."
)
return
trivia_dict = {}
authors = []
any_audio = False
for category in reversed(categories):
# We reverse the categories so that the first list's config takes
# priority over the others.
try:
dict_ = self.get_audio_list(category)
except FileNotFoundError:
await ctx.send(
"Invalid category `{0}`. See `{1}audiotrivia list`"
await ctx.maybe_send_embed(
f"Invalid category `{category}`. See `{ctx.prefix}audiotrivia list`"
" for a list of trivia categories."
"".format(category, ctx.prefix)
)
except InvalidListError:
await ctx.send(
await ctx.maybe_send_embed(
"There was an error parsing the trivia list for"
" the `{}` category. It may be formatted"
" incorrectly.".format(category)
f" the `{category}` category. It may be formatted"
" incorrectly."
)
else:
trivia_dict.update(dict_)
authors.append(trivia_dict.pop("AUTHOR", None))
is_audio = dict_.pop("AUDIO", False)
authors.append(dict_.pop("AUTHOR", None))
trivia_dict.update(
{_q: {"audio": is_audio, "answers": _a} for _q, _a in dict_.items()}
)
any_audio = any_audio or is_audio
continue
return
if not trivia_dict:
await ctx.send(
await ctx.maybe_send_embed(
"The trivia list was parsed successfully, however it appears to be empty!"
)
return
if not any_audio:
audio = None
else:
audio: Optional["Audio"] = self.bot.get_cog("Audio")
if audio is None:
await ctx.send("Audio lists were parsed but Audio is not loaded!")
return
status = await audio.config.status()
notify = await audio.config.guild(ctx.guild).notify()
if status:
await ctx.maybe_send_embed(
f"It is recommended to disable audio status with `{ctx.prefix}audioset status`"
)
if notify:
await ctx.maybe_send_embed(
f"It is recommended to disable audio notify with `{ctx.prefix}audioset notify`"
)
failed = await ctx.invoke(audio.command_summon)
if failed:
return
lavaplayer = lavalink.get_player(ctx.guild.id)
lavaplayer.store("channel", ctx.channel.id) # What's this for? I dunno
settings = await self.config.guild(ctx.guild).all()
audiosettings = await self.audioconf.guild(ctx.guild).all()
config = trivia_dict.pop("CONFIG", None)
config = trivia_dict.pop("CONFIG", {"answer": None})["answer"]
if config and settings["allow_override"]:
settings.update(config)
settings["lists"] = dict(zip(categories, reversed(authors)))
@ -166,21 +156,32 @@ class AudioTrivia(Trivia):
# Delay in audiosettings overwrites delay in settings
combined_settings = {**settings, **audiosettings}
session = AudioSession.start(
ctx=ctx, question_list=trivia_dict, settings=combined_settings, player=lavaplayer,
ctx,
trivia_dict,
combined_settings,
audio,
)
self.trivia_sessions.append(session)
LOG.debug("New audio trivia session; #%s in %d", ctx.channel, ctx.guild.id)
log.debug("New audio trivia session; #%s in %d", ctx.channel, ctx.guild.id)
@audiotrivia.command(name="list")
@commands.guild_only()
async def audiotrivia_list(self, ctx: commands.Context):
"""List available trivia categories."""
lists = set(p.stem for p in self._audio_lists())
msg = box("**Available trivia lists**\n\n{}".format(", ".join(sorted(lists))))
"""List available trivia including audio categories."""
lists = set(p.stem for p in self._all_audio_lists())
if await ctx.embed_requested():
await ctx.send(
embed=discord.Embed(
title="Available trivia lists",
colour=await ctx.embed_colour(),
description=", ".join(sorted(lists)),
)
)
else:
msg = box(bold("Available trivia lists") + "\n\n" + ", ".join(sorted(lists)))
if len(msg) > 1000:
await ctx.author.send(msg)
return
else:
await ctx.send(msg)
def get_audio_list(self, category: str) -> dict:
@ -198,7 +199,7 @@ class AudioTrivia(Trivia):
"""
try:
path = next(p for p in self._audio_lists() if p.stem == category)
path = next(p for p in self._all_audio_lists() if p.stem == category)
except StopIteration:
raise FileNotFoundError("Could not find the `{}` category.".format(category))
@ -210,13 +211,15 @@ class AudioTrivia(Trivia):
else:
return dict_
def _audio_lists(self) -> List[pathlib.Path]:
def _all_audio_lists(self) -> List[pathlib.Path]:
# Custom trivia lists uploaded with audiotrivia. Not necessarily audio lists
personal_lists = [p.resolve() for p in cog_data_path(self).glob("*.yaml")]
return personal_lists + get_core_lists()
# Add to that custom lists uploaded with trivia and core lists
return personal_lists + get_core_audio_lists() + self._all_lists()
def get_core_lists() -> List[pathlib.Path]:
def get_core_audio_lists() -> List[pathlib.Path]:
"""Return a list of paths for all trivia lists packaged with the bot."""
core_lists_path = pathlib.Path(__file__).parent.resolve() / "data/lists"
return list(core_lists_path.glob("*.yaml"))

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

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

File diff suppressed because it is too large Load Diff

@ -1,13 +1,14 @@
AUTHOR: Plab
https://www.youtube.com/watch?v=--bWm9hhoZo:
NEEDS: New links for all songs.
https://www.youtube.com/watch?v=f9O2Rjn1azc:
- Transistor
https://www.youtube.com/watch?v=-4nCbgayZNE:
https://www.youtube.com/watch?v=PgUhYFkVdSY:
- Dark Cloud 2
- Dark Cloud II
https://www.youtube.com/watch?v=-64NlME4lJU:
https://www.youtube.com/watch?v=1T1RZttyMwU:
- Mega Man 7
- Mega Man VII
https://www.youtube.com/watch?v=-AesqnudNuw:
https://www.youtube.com/watch?v=AdDbbzuq1vY:
- Mega Man 9
- Mega Man IX
https://www.youtube.com/watch?v=-BmGDtP2t7M:

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

@ -159,7 +159,12 @@ class Conquest(commands.Cog):
self.data_path / self.current_map / f"current.{self.ext}", x, y, zoom
)
await ctx.send(file=discord.File(fp=zoomed_path, filename=f"current_zoomed.{self.ext}",))
await ctx.send(
file=discord.File(
fp=zoomed_path,
filename=f"current_zoomed.{self.ext}",
)
)
async def _create_zoomed_map(self, map_path, x, y, zoom, **kwargs):
current_map = Image.open(map_path)

@ -85,6 +85,8 @@ class Dad(Cog):
@commands.Cog.listener()
async def on_message_without_command(self, message: discord.Message):
if message.author.bot:
return
guild: discord.Guild = getattr(message, "guild", None)
if guild is None:
return

@ -1,9 +1,10 @@
from datetime import datetime
from datetime import datetime, tzinfo
from typing import TYPE_CHECKING
from apscheduler.triggers.cron import CronTrigger
from dateutil import parser
from discord.ext.commands import BadArgument, Converter
from pytz import timezone
from fifo.timezones import assemble_timezones
@ -12,6 +13,18 @@ if TYPE_CHECKING:
CronConverter = str
else:
class TimezoneConverter(Converter):
async def convert(self, ctx, argument) -> tzinfo:
tzinfos = assemble_timezones()
if argument.upper() in tzinfos:
return tzinfos[argument.upper()]
timez = timezone(argument)
if timez is not None:
return timez
raise BadArgument()
class DatetimeConverter(Converter):
async def convert(self, ctx, argument) -> datetime:
dt = parser.parse(argument, fuzzy=True, tzinfos=assemble_timezones())

@ -1,5 +1,5 @@
import logging
from datetime import datetime, timedelta
from datetime import datetime, timedelta, tzinfo
from typing import Optional, Union
import discord
@ -10,8 +10,9 @@ from apscheduler.schedulers.base import STATE_PAUSED, STATE_RUNNING
from redbot.core import Config, checks, commands
from redbot.core.bot import Red
from redbot.core.commands import TimedeltaConverter
from redbot.core.utils.chat_formatting import pagify
from .datetime_cron_converters import CronConverter, DatetimeConverter
from .datetime_cron_converters import CronConverter, DatetimeConverter, TimezoneConverter
from .task import Task
schedule_log = logging.getLogger("red.fox_v3.fifo.scheduler")
@ -57,6 +58,8 @@ class FIFO(commands.Cog):
self.scheduler = None
self.jobstore = None
self.tz_cog = None
async def red_delete_data_for_user(self, **kwargs):
"""Nothing to delete"""
return
@ -131,6 +134,24 @@ class FIFO(commands.Cog):
async def _remove_job(self, task: Task):
return self.scheduler.remove_job(job_id=_assemble_job_id(task.name, task.guild_id))
async def _get_tz(self, user: Union[discord.User, discord.Member]) -> Union[None, tzinfo]:
if self.tz_cog is None:
self.tz_cog = self.bot.get_cog("Timezone")
if self.tz_cog is None:
self.tz_cog = False # only try once to get the timezone cog
if not self.tz_cog:
return None
try:
usertime = await self.tz_cog.config.user(user).usertime()
except AttributeError:
return None
if usertime:
return await TimezoneConverter().convert(None, usertime)
else:
return None
@checks.is_owner()
@commands.guild_only()
@commands.command()
@ -139,7 +160,7 @@ class FIFO(commands.Cog):
self.scheduler.remove_all_jobs()
await self.config.guild(ctx.guild).tasks.clear()
await self.config.jobs.clear()
await self.config.jobs_index.clear()
# await self.config.jobs_index.clear()
await ctx.tick()
@checks.is_owner() # Will be reduced when I figure out permissions later
@ -306,6 +327,10 @@ class FIFO(commands.Cog):
out += f"{task_name}: {task_data}\n"
if out:
if len(out) > 2000:
for page in pagify(out):
await ctx.maybe_send_embed(page)
else:
await ctx.maybe_send_embed(out)
else:
await ctx.maybe_send_embed("No tasks to list")
@ -406,7 +431,7 @@ class FIFO(commands.Cog):
job: Job = await self._process_task(task)
delta_from_now: timedelta = job.next_run_time - datetime.now(job.next_run_time.tzinfo)
await ctx.maybe_send_embed(
f"Task `{task_name}` added interval of {interval_str} to its scheduled runtimes\n"
f"Task `{task_name}` added interval of {interval_str} to its scheduled runtimes\n\n"
f"Next run time: {job.next_run_time} ({delta_from_now.total_seconds()} seconds)"
)
@ -427,7 +452,9 @@ class FIFO(commands.Cog):
)
return
result = await task.add_trigger("date", datetime_str)
maybe_tz = await self._get_tz(ctx.author)
result = await task.add_trigger("date", datetime_str, maybe_tz)
if not result:
await ctx.maybe_send_embed(
"Failed to add a date trigger to this task, see console for logs"
@ -444,7 +471,12 @@ class FIFO(commands.Cog):
@fifo_trigger.command(name="cron")
async def fifo_trigger_cron(
self, ctx: commands.Context, task_name: str, *, cron_str: CronConverter
self,
ctx: commands.Context,
task_name: str,
optional_tz: Optional[TimezoneConverter] = None,
*,
cron_str: CronConverter,
):
"""
Add a cron "time of day" trigger to the specified task
@ -460,7 +492,10 @@ class FIFO(commands.Cog):
)
return
result = await task.add_trigger("cron", cron_str)
if optional_tz is None:
optional_tz = await self._get_tz(ctx.author) # might still be None
result = await task.add_trigger("cron", cron_str, optional_tz)
if not result:
await ctx.maybe_send_embed(
"Failed to add a cron trigger to this task, see console for logs"

@ -3,14 +3,14 @@
"Bobloy"
],
"min_bot_version": "3.4.0",
"description": "[ALPHA] Schedule commands to be run at certain times or intervals",
"description": "[BETA] Schedule commands to be run at certain times or intervals",
"hidden": false,
"install_msg": "Thank you for installing FIFO.\nGet started with `[p]load fifo`, then `[p]help FIFO`",
"short": "[ALPHA] Schedule commands to be run at certain times or intervals",
"short": "[BETA] Schedule commands to be run at certain times or intervals",
"end_user_data_statement": "This cog does not store any End User Data",
"requirements": [
"apscheduler",
"python-dateutil"
"pytz"
],
"tags": [
"bobloy",
@ -24,6 +24,7 @@
"date",
"datetime",
"time",
"calendar"
"calendar",
"timezone"
]
}

@ -9,6 +9,7 @@ from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.date import DateTrigger
from apscheduler.triggers.interval import IntervalTrigger
from discord.utils import time_snowflake
from pytz import timezone
from redbot.core import Config, commands
from redbot.core.bot import Red
@ -25,10 +26,10 @@ def get_trigger(data):
return IntervalTrigger(days=parsed_time.days, seconds=parsed_time.seconds)
if data["type"] == "date":
return DateTrigger(data["time_data"])
return DateTrigger(data["time_data"], timezone=data["tzinfo"])
if data["type"] == "cron":
return CronTrigger.from_crontab(data["time_data"])
return CronTrigger.from_crontab(data["time_data"], timezone=data["tzinfo"])
return False
@ -70,6 +71,7 @@ class Task:
default_trigger = {
"type": "",
"time_data": None, # Used for Interval and Date Triggers
"tzinfo": None,
}
def __init__(
@ -99,7 +101,13 @@ class Task:
if t["type"] == "date": # Convert into datetime
dt: datetime = t["time_data"]
triggers.append({"type": t["type"], "time_data": dt.isoformat()})
triggers.append(
{
"type": t["type"],
"time_data": dt.isoformat(),
"tzinfo": getattr(t["tzinfo"], "zone", None),
}
)
# triggers.append(
# {
# "type": t["type"],
@ -117,9 +125,18 @@ class Task:
continue
if t["type"] == "cron":
if t["tzinfo"] is None:
triggers.append(t) # already a string, nothing to do
else:
triggers.append(
{
"type": t["type"],
"time_data": t["time_data"],
"tzinfo": getattr(t["tzinfo"], "zone", None),
}
)
continue
raise NotImplemented
return triggers
@ -128,18 +145,27 @@ class Task:
if not self.data or not self.data.get("triggers", None):
return
for n, t in enumerate(self.data["triggers"]):
for t in self.data["triggers"]:
# Backwards compatibility
if "tzinfo" not in t:
t["tzinfo"] = None
# First decode timezone if there is one
if t["tzinfo"] is not None:
t["tzinfo"] = timezone(t["tzinfo"])
if t["type"] == "interval": # Convert into timedelta
self.data["triggers"][n]["time_data"] = timedelta(**t["time_data"])
t["time_data"] = timedelta(**t["time_data"])
continue
if t["type"] == "date": # Convert into datetime
# self.data["triggers"][n]["time_data"] = datetime(**t["time_data"])
self.data["triggers"][n]["time_data"] = datetime.fromisoformat(t["time_data"])
t["time_data"] = datetime.fromisoformat(t["time_data"])
continue
if t["type"] == "cron":
continue # already a string
raise NotImplemented
# async def load_from_data(self, data: Dict):
@ -300,8 +326,16 @@ class Task:
self.data["command_str"] = command_str
return True
async def add_trigger(self, param, parsed_time: Union[timedelta, datetime, str]):
trigger_data = {"type": param, "time_data": parsed_time}
async def add_trigger(
self, param, parsed_time: Union[timedelta, datetime, str], timezone=None
):
# TODO: Save timezone separately for cron and date triggers
trigger_data = self.default_trigger.copy()
trigger_data["type"] = param
trigger_data["time_data"] = parsed_time
if timezone is not None:
trigger_data["tzinfo"] = timezone
if not get_trigger(trigger_data):
return False

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

@ -6,4 +6,3 @@ def setup(bot):
n = Hangman(bot)
data_manager.bundled_data_path(n)
bot.add_cog(n)
bot.add_listener(n.on_react, "on_reaction_add")

@ -50,27 +50,27 @@ class Hangman(Cog):
theface = await self.config.guild(guild).theface()
self.hanglist[guild] = (
""">
\_________
\\_________
|/
|
|
|
|
|
|\___
|\\___
""",
""">
\_________
\\_________
|/ |
|
|
|
|
|
|\___
|\\___
H""",
""">
\_________
\\_________
|/ |
| """
+ theface
@ -79,10 +79,10 @@ class Hangman(Cog):
|
|
|
|\___
|\\___
HA""",
""">
\________
\\________
|/ |
| """
+ theface
@ -91,10 +91,10 @@ class Hangman(Cog):
| |
|
|
|\___
|\\___
HAN""",
""">
\_________
\\_________
|/ |
| """
+ theface
@ -103,43 +103,43 @@ class Hangman(Cog):
| |
|
|
|\___
|\\___
HANG""",
""">
\_________
\\_________
|/ |
| """
+ theface
+ """
| /|\
| /|\\
| |
|
|
|\___
|\\___
HANGM""",
""">
\________
\\________
|/ |
| """
+ theface
+ """
| /|\
| /|\\
| |
| /
|
|\___
|\\___
HANGMA""",
""">
\________
\\________
|/ |
| """
+ theface
+ """
| /|\
| /|\\
| |
| / \
| / \\
|
|\___
|\\___
HANGMAN""",
)
@ -255,7 +255,7 @@ class Hangman(Cog):
elif i in self.the_data[guild]["guesses"] or i not in "ABCDEFGHIJKLMNOPQRSTUVWXYZ":
out_str += "__" + i + "__ "
else:
out_str += "**\_** "
out_str += "**\\_** "
self.winbool[guild] = False
return out_str
@ -286,9 +286,9 @@ class Hangman(Cog):
await self._reprintgame(message)
@commands.Cog.listener()
@commands.Cog.listener("on_reaction_add")
async def on_react(self, reaction, user: Union[discord.User, discord.Member]):
""" Thanks to flapjack reactpoll for guidelines
"""Thanks to flapjack reactpoll for guidelines
https://github.com/flapjax/FlapJack-Cogs/blob/master/reactpoll/reactpoll.py"""
guild: discord.Guild = getattr(user, "guild", None)
if guild is None:

@ -33,7 +33,7 @@ class LoveCalculator(Cog):
x.replace(" ", "+"), y.replace(" ", "+")
)
async with aiohttp.ClientSession(headers={"Connection": "keep-alive"}) as session:
async with session.get(url) as response:
async with session.get(url, ssl=False) as response:
assert response.status == 200
resp = await response.text()
@ -60,14 +60,11 @@ class LoveCalculator(Cog):
else:
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."
em = discord.Embed(
title=title,
description=result_text,
color=discord.Color.red(),
url=f"https://www.lovecalculator.com/{result_image}",
title=title, description=result_text, color=discord.Color.red(), url=url
)
em.set_image(url=f"https://www.lovecalculator.com/{result_image}")
await ctx.send(embed=em)

@ -75,9 +75,7 @@ class LastSeen(Cog):
else:
last_seen = await self.config.member(member).seen()
if last_seen is None:
await ctx.maybe_send_embed(
embed=discord.Embed(description="I've never seen this user")
)
await ctx.maybe_send_embed("I've never seen this user")
return
last_seen = self.get_date_time(last_seen)

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

@ -360,7 +360,9 @@ class PlantTycoon(commands.Cog):
``{0}prune``: Prune your plant.\n"""
em = discord.Embed(
title=title, description=description.format(prefix), color=discord.Color.green(),
title=title,
description=description.format(prefix),
color=discord.Color.green(),
)
em.set_thumbnail(url="https://image.prntscr.com/image/AW7GuFIBSeyEgkR2W3SeiQ.png")
em.set_footer(
@ -525,7 +527,8 @@ class PlantTycoon(commands.Cog):
if t:
em = discord.Embed(
title="Plant statistics of {}".format(plant["name"]), color=discord.Color.green(),
title="Plant statistics of {}".format(plant["name"]),
color=discord.Color.green(),
)
em.set_thumbnail(url=plant["image"])
em.add_field(name="**Name**", value=plant["name"])
@ -583,7 +586,8 @@ class PlantTycoon(commands.Cog):
author = ctx.author
if product is None:
em = discord.Embed(
title="All gardening supplies that you can buy:", color=discord.Color.green(),
title="All gardening supplies that you can buy:",
color=discord.Color.green(),
)
for pd in self.products:
em.add_field(
@ -616,8 +620,11 @@ class PlantTycoon(commands.Cog):
await gardener.save_gardener()
message = "You bought {}.".format(product.lower())
else:
message = "You don't have enough Thneeds. You have {}, but need {}.".format(
gardener.points, self.products[product.lower()]["cost"] * amount,
message = (
"You don't have enough Thneeds. You have {}, but need {}.".format(
gardener.points,
self.products[product.lower()]["cost"] * amount,
)
)
else:
message = "I don't have this product."

@ -157,7 +157,7 @@ class Timerole(Cog):
await ctx.maybe_send_embed(out)
async def timerole_update(self):
for guild in self.bot.guilds:
async for guild in AsyncIter(self.bot.guilds):
addlist = []
removelist = []
@ -200,7 +200,7 @@ class Timerole(Cog):
async def announce_roles(self, title, role_list, channel, guild, to_add: True):
results = ""
for member, role_id in role_list:
async for member, role_id in AsyncIter(role_list):
role = discord.utils.get(guild.roles, id=role_id)
try:
if to_add:
@ -219,7 +219,7 @@ class Timerole(Cog):
log.info(results)
async def check_required_and_date(self, role_list, check_roles, has_roles, member, role_dict):
for role_id in check_roles:
async for role_id in AsyncIter(check_roles):
# Check for required role
if "required" in role_dict[str(role_id)]:
if not set(role_dict[str(role_id)]["required"]) & set(has_roles):
@ -242,6 +242,3 @@ class Timerole(Cog):
while self is self.bot.get_cog("Timerole"):
await self.timerole_update()
await sleep_till_next_hour()

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

@ -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",
"description": "Customizable Werewolf Game",
"hidden": true,
"hidden": false,
"install_msg": "Thank you for installing Werewolf! Get started with `[p]load werewolf`\n Use `[p]wwset` to run inital setup",
"requirements": [],
"short": "Werewolf Game",
"short": "[ALPHA] Play Werewolf (Mafia) Game in discord",
"end_user_data_statement": "This stores user IDs in memory while they're actively using the cog, and stores no persistent End User Data.",
"tags": [
"mafia",

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

@ -1,5 +1,9 @@
import logging
import discord
log = logging.getLogger("red.fox_v3.werewolf.player")
class Player:
"""
@ -16,6 +20,9 @@ class Player:
self.muted = False
self.protected = False
def __repr__(self):
return f"{self.__class__.__name__}({self.member})"
async def assign_role(self, role):
"""
Give this player a role
@ -28,6 +35,15 @@ class Player:
async def send_dm(self, message):
try:
await self.member.send(message) # Lets do embeds later
await self.member.send(message) # Lets ToDo embeds later
except discord.Forbidden:
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,4 +1,12 @@
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
@ -18,9 +26,11 @@ class Role:
category = [1, 5, 6] Could be Veteran
category = [1, 5] Could be Bodyguard
category = [11, 16] Could be Werewolf Silencer
category = [22] Could be Blob (non-killing)
category = [22, 23] Could be Serial-Killer
Action guide as follows (on_event function):
Action priority guide as follows (on_event function):
_at_night_start
0. No Action
1. Detain actions (Jailer/Kidnapper)
@ -33,13 +43,15 @@ class Role:
3. Protection / Preempt actions (bodyguard/framer)
4. Non-disruptive actions (seer/silencer)
5. Disruptive actions (Killing)
6. Role altering actions (Cult / Mason)
6. Role altering actions (Cult / Mason / Shifter)
"""
rand_choice = False # Determines if it can be picked as a random role (False for unusually disruptive roles)
# Determines if it can be picked as a random role (False for unusually disruptive roles)
rand_choice = False # TODO: Rework random with categories
town_balance = 0 # Guess at power level and it's balance on the town
category = [0] # List of enrolled categories (listed above)
alignment = 0 # 1: Town, 2: Werewolf, 3: Neutral
channel_id = "" # Empty for no private channel
channel_name = "" # Empty for no private channel
unique = False # Only one of this role per game
game_start_message = (
"Your role is **Default**\n"
@ -54,32 +66,14 @@ class Role:
icon_url = None # Adding a URL here will enable a thumbnail of the role
def __init__(self, game):
super().__init__(game)
self.game = game
self.player = None
self.blocked = False
self.properties = {} # Extra data for other roles (i.e. arsonist)
self.action_list = [
(self._at_game_start, 1), # (Action, Priority)
(self._at_day_start, 0),
(self._at_voted, 0),
(self._at_kill, 0),
(self._at_hang, 0),
(self._at_day_end, 0),
(self._at_night_start, 0),
(self._at_night_end, 0),
(self._at_visit, 0)
]
def __repr__(self):
return self.__class__.__name__
async def on_event(self, event, data):
"""
See Game class for event guide
"""
await self.action_list[event][0](data)
return f"{self.__class__.__name__}({self.player.__repr__()})"
async def assign_player(self, player):
"""
@ -90,6 +84,8 @@ class Role:
player.role = self
self.player = player
log.debug(f"Assigned {self} to {player}")
async def get_alignment(self, source=None):
"""
Interaction for powerful access of alignment
@ -101,7 +97,7 @@ class Role:
async def see_alignment(self, source=None):
"""
Interaction for investigative roles attempting
to see alignment (Village, Werewolf Other)
to see alignment (Village, Werewolf, Other)
"""
return "Other"
@ -119,35 +115,16 @@ class Role:
"""
return "Default"
async def _at_game_start(self, data=None):
if self.channel_id:
await self.game.register_channel(self.channel_id, self)
@wolflistener("at_game_start", priority=2)
async def _at_game_start(self):
if self.channel_name:
await self.game.register_channel(self.channel_name, self)
try:
await self.player.send_dm(self.game_start_message) # Maybe embeds eventually
async def _at_day_start(self, data=None):
pass
async def _at_voted(self, data=None):
pass
async def _at_kill(self, data=None):
pass
async def _at_hang(self, data=None):
pass
async def _at_day_end(self, data=None):
pass
async def _at_night_start(self, data=None):
pass
async def _at_night_end(self, data=None):
pass
async def _at_visit(self, data=None):
pass
except AttributeError as e:
log.exception(self.__repr__())
raise e
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
from ..role import Role
import logging
from werewolf.constants import (
ALIGNMENT_TOWN,
ALIGNMENT_WEREWOLF,
CATEGORY_TOWN_INVESTIGATIVE,
CATEGORY_TOWN_RANDOM,
)
from werewolf.listener import wolflistener
from werewolf.night_powers import pick_target
from werewolf.role import Role
log = logging.getLogger("red.fox_v3.werewolf.role.seer")
class Seer(Role):
rand_choice = True # Determines if it can be picked as a random role (False for unusually disruptive roles)
category = [1, 2] # List of enrolled categories (listed above)
alignment = 1 # 1: Town, 2: Werewolf, 3: Neutral
rand_choice = True
town_balance = 4
category = [
CATEGORY_TOWN_RANDOM,
CATEGORY_TOWN_INVESTIGATIVE,
] # List of enrolled categories (listed above)
alignment = ALIGNMENT_TOWN # 1: Town, 2: Werewolf, 3: Neutral
channel_id = "" # Empty for no private channel
unique = False # Only one of this role per game
game_start_message = (
@ -14,8 +29,10 @@ class Seer(Role):
"Lynch players during the day with `[p]ww vote <ID>`\n"
"Check for werewolves at night with `[p]ww choose <ID>`"
)
description = "A mystic in search of answers in a chaotic town.\n" \
description = (
"A mystic in search of answers in a chaotic town.\n"
"Calls upon the cosmos to discern those of Lycan blood"
)
def __init__(self, game):
super().__init__(game)
@ -24,47 +41,49 @@ class Seer(Role):
# self.blocked = False
# self.properties = {} # Extra data for other roles (i.e. arsonist)
self.see_target = None
self.action_list = [
(self._at_game_start, 1), # (Action, Priority)
(self._at_day_start, 0),
(self._at_voted, 0),
(self._at_kill, 0),
(self._at_hang, 0),
(self._at_day_end, 0),
(self._at_night_start, 2),
(self._at_night_end, 4),
(self._at_visit, 0)
]
# self.action_list = [
# (self._at_game_start, 1), # (Action, Priority)
# (self._at_day_start, 0),
# (self._at_voted, 0),
# (self._at_kill, 0),
# (self._at_hang, 0),
# (self._at_day_end, 0),
# (self._at_night_start, 2),
# (self._at_night_end, 4),
# (self._at_visit, 0),
# ]
async def see_alignment(self, source=None):
"""
Interaction for investigative roles attempting
to see team (Village, Werewolf Other)
to see team (Village, Werewolf, Other)
"""
return "Village"
return ALIGNMENT_TOWN
async def get_role(self, source=None):
"""
Interaction for powerful access of role
Unlikely to be able to deceive this
"""
return "Villager"
return "Seer"
async def see_role(self, source=None):
"""
Interaction for investigative roles.
More common to be able to deceive these roles
"""
return "Villager"
return "Seer"
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:
return
self.see_target = None
await self.game.generate_targets(self.player.member)
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.player.alive:
await self.player.send_dm("You will not use your powers tonight...")
@ -75,9 +94,9 @@ class Seer(Role):
if target:
alignment = await target.role.see_alignment(self.player)
if alignment == "Werewolf":
if alignment == ALIGNMENT_WEREWOLF:
out = "Your insight reveals this player to be a **Werewolf!**"
else:
else: # Don't reveal neutrals
out = "You fail to find anything suspicious about this player..."
await self.player.send_dm(out)
@ -87,4 +106,6 @@ class Seer(Role):
await super().choose(ctx, data)
self.see_target, target = await pick_target(self, ctx, data)
await ctx.send("**You will attempt to see the role of {} tonight...**".format(target.member.display_name))
await ctx.send(
f"**You will attempt to see the role of {target.member.display_name} tonight...**"
)

@ -1,5 +1,11 @@
from ..night_powers import pick_target
from ..role import Role
import logging
from werewolf.constants import ALIGNMENT_NEUTRAL, CATEGORY_NEUTRAL_BENIGN
from werewolf.listener import wolflistener
from werewolf.night_powers import pick_target
from werewolf.role import Role
log = logging.getLogger("red.fox_v3.werewolf.role.shifter")
class Shifter(Role):
@ -37,12 +43,13 @@ class Shifter(Role):
3. Protection / Preempt actions (bodyguard/framer)
4. Non-disruptive actions (seer/silencer)
5. Disruptive actions (Killing)
6. Role altering actions (Cult / Mason)
6. Role altering actions (Cult / Mason / Shifter)
"""
rand_choice = False # Determines if it can be picked as a random role (False for unusually disruptive roles)
category = [22] # List of enrolled categories (listed above)
alignment = 3 # 1: Town, 2: Werewolf, 3: Neutral
town_balance = -3
category = [CATEGORY_NEUTRAL_BENIGN] # List of enrolled categories (listed above)
alignment = ALIGNMENT_NEUTRAL # 1: Town, 2: Werewolf, 3: Neutral
channel_id = "" # Empty for no private channel
unique = False # Only one of this role per game
game_start_message = (
@ -61,22 +68,22 @@ class Shifter(Role):
super().__init__(game)
self.shift_target = None
self.action_list = [
(self._at_game_start, 1), # (Action, Priority)
(self._at_day_start, 0),
(self._at_voted, 0),
(self._at_kill, 0),
(self._at_hang, 0),
(self._at_day_end, 0),
(self._at_night_start, 2), # Chooses targets
(self._at_night_end, 6), # Role Swap
(self._at_visit, 0)
]
# self.action_list = [
# (self._at_game_start, 1), # (Action, Priority)
# (self._at_day_start, 0),
# (self._at_voted, 0),
# (self._at_kill, 0),
# (self._at_hang, 0),
# (self._at_day_end, 0),
# (self._at_night_start, 2), # Chooses targets
# (self._at_night_end, 6), # Role Swap
# (self._at_visit, 0),
# ]
async def see_alignment(self, source=None):
"""
Interaction for investigative roles attempting
to see alignment (Village, Werewolf, Other)
to see alignment (Village, Werewolf,, Other)
"""
return "Other"
@ -94,14 +101,14 @@ class Shifter(Role):
"""
return "Shifter"
async def _at_night_start(self, data=None):
await super()._at_night_start(data)
@wolflistener("at_night_start", priority=2)
async def _at_night_start(self):
self.shift_target = None
await self.game.generate_targets(self.player.member)
await self.player.send_dm("**Pick a target to shift into**")
async def _at_night_end(self, data=None):
await super()._at_night_end(data)
@wolflistener("at_night_end", priority=6)
async def _at_night_end(self):
if self.shift_target is None:
if self.player.alive:
await self.player.send_dm("You will not use your powers tonight...")
@ -114,16 +121,20 @@ class Shifter(Role):
# Roles have now been swapped
await self.player.send_dm("Your role has been stolen...\n"
"You are now a **Shifter**.")
await self.player.send_dm(
"Your role has been stolen...\n" "You are now a **Shifter**."
)
await self.player.send_dm(self.game_start_message)
await target.send_dm(target.role.game_start_message)
else:
await self.player.send_dm("**Your shift failed...**")
async def choose(self, ctx, data):
"""Handle night actions"""
await super().choose(ctx, data)
self.shift_target, target = await pick_target(self, ctx, data)
await ctx.send("**You will attempt to see the role of {} tonight...**".format(target.member.display_name))
await ctx.send(
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):
rand_choice = True
category = [11, 15]
alignment = 2 # 1: Town, 2: Werewolf, 3: Neutral
channel_id = "werewolves"
town_balance = -6
category = [CATEGORY_WW_RANDOM, CATEGORY_WW_KILLING]
alignment = ALIGNMENT_WEREWOLF # 1: Town, 2: Werewolf, 3: Neutral
channel_name = "werewolves"
unique = False
game_start_message = (
"Your role is **Werewolf**\n"
@ -16,34 +22,19 @@ class VanillaWerewolf(Role):
"Vote to kill players at night with `[p]ww vote <ID>`"
)
def __init__(self, game):
super().__init__(game)
self.action_list = [
(self._at_game_start, 1), # (Action, Priority)
(self._at_day_start, 0),
(self._at_voted, 0),
(self._at_kill, 0),
(self._at_hang, 0),
(self._at_day_end, 0),
(self._at_night_start, 0),
(self._at_night_end, 0),
(self._at_visit, 0)
]
async def see_alignment(self, source=None):
"""
Interaction for investigative roles attempting
to see team (Village, Werewolf Other)
"""
return "Werewolf"
return ALIGNMENT_WEREWOLF
async def get_role(self, source=None):
"""
Interaction for powerful access of role
Unlikely to be able to deceive this
"""
return "Werewolf"
return "VanillaWerewolf"
async def see_role(self, source=None):
"""
@ -52,10 +43,13 @@ class VanillaWerewolf(Role):
"""
return "Werewolf"
async def _at_game_start(self, data=None):
if self.channel_id:
print("Wolf has channel_id: " + self.channel_id)
await self.game.register_channel(self.channel_id, self, WolfVote) # Add VoteGroup WolfVote
@wolflistener("at_game_start", priority=2)
async def _at_game_start(self):
if self.channel_name:
log.debug("Wolf has channel_name: " + self.channel_name)
await self.game.register_channel(
self.channel_name, self, WolfVote
) # Add VoteGroup WolfVote
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):
rand_choice = True # Determines if it can be picked as a random role (False for unusually disruptive roles)
category = [1] # List of enrolled categories (listed above)
alignment = 1 # 1: Town, 2: Werewolf, 3: Neutral
# Determines if it can be picked as a random role (False for unusually disruptive roles)
rand_choice = True
town_balance = 1
category = [CATEGORY_TOWN_RANDOM] # List of enrolled categories (listed above)
alignment = ALIGNMENT_TOWN # 1: Town, 2: Werewolf, 3: Neutral
channel_id = "" # Empty for no private channel
unique = False # Only one of this role per game
game_start_message = (
@ -13,15 +20,12 @@ class Villager(Role):
"Lynch players during the day with `[p]ww vote <ID>`"
)
def __init__(self, game):
super().__init__(game)
async def see_alignment(self, source=None):
"""
Interaction for investigative roles attempting
to see team (Village, Werewolf Other)
to see team (Village, Werewolf, Other)
"""
return "Village"
return ALIGNMENT_TOWN
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
Handles secret channels and group decisions
@ -8,58 +15,41 @@ class VoteGroup:
channel_id = ""
def __init__(self, game, channel):
super().__init__(game)
self.game = game
self.channel = channel
self.players = []
self.vote_results = {}
self.properties = {} # Extra data for other options
self.action_list = [
(self._at_game_start, 1), # (Action, Priority)
(self._at_day_start, 0),
(self._at_voted, 0),
(self._at_kill, 1),
(self._at_hang, 1),
(self._at_day_end, 0),
(self._at_night_start, 2),
(self._at_night_end, 0),
(self._at_visit, 0)
]
async def on_event(self, event, data):
"""
See Game class for event guide
"""
await self.action_list[event][0](data)
def __repr__(self):
return f"{self.__class__.__name__}({self.channel},{self.players})"
async def _at_game_start(self, data=None):
@wolflistener("at_game_start", priority=1)
async def _at_game_start(self):
await self.channel.send(" ".join(player.mention for player in self.players))
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.remove(data["player"])
async def _at_hang(self, data=None):
if data["player"] in self.players:
self.players.remove(data["player"])
@wolflistener("at_kill", priority=1)
async def _at_kill(self, player):
if player in self.players:
self.players.remove(player)
async def _at_day_end(self, data=None):
pass
@wolflistener("at_hang", priority=1)
async def _at_hang(self, player):
if player in self.players:
self.players.remove(player)
async def _at_night_start(self, data=None):
@wolflistener("at_night_start", priority=2)
async def _at_night_start(self):
if self.channel is None:
return
self.vote_results = {}
await self.game.generate_targets(self.channel)
async def _at_night_end(self, data=None):
@wolflistener("at_night_end", priority=5)
async def _at_night_end(self):
if self.channel is None:
return
@ -70,11 +60,8 @@ class VoteGroup:
target = max(set(vote_list), key=vote_list.count)
if target:
# Do what you voted on
pass
async def _at_visit(self, data=None):
pass
# Do what the votegroup votes on
raise NotImplementedError
async def register_players(self, *players):
"""
@ -90,7 +77,7 @@ class VoteGroup:
self.players.remove(player)
if not self.players:
# ToDo: Trigger deletion of votegroup
# TODO: Confirm deletion
pass
async def vote(self, target, author, target_id):

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

@ -1,6 +1,12 @@
import logging
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):
@ -13,71 +19,29 @@ class WolfVote(VoteGroup):
kill_messages = [
"**{ID}** - {target} was mauled by wolves",
"**{ID}** - {target} was found torn to shreds"]
"**{ID}** - {target} was found torn to shreds",
]
def __init__(self, game, channel):
super().__init__(game, channel)
# self.game = game
# self.channel = channel
# self.players = []
# self.vote_results = {}
# self.properties = {} # Extra data for other options
self.killer = None # Added killer
self.action_list = [
(self._at_game_start, 1), # (Action, Priority)
(self._at_day_start, 0),
(self._at_voted, 0),
(self._at_kill, 1),
(self._at_hang, 1),
(self._at_day_end, 0),
(self._at_night_start, 2),
(self._at_night_end, 5), # Kill priority
(self._at_visit, 0)
]
# async def on_event(self, event, data):
# """
# See Game class for event guide
# """
#
# await action_list[event][0](data)
#
# async def _at_game_start(self, data=None):
# await self.channel.send(" ".join(player.mention for player in self.players))
#
# async def _at_day_start(self, data=None):
# pass
#
# async def _at_voted(self, data=None):
# pass
#
# async def _at_kill(self, data=None):
# if data["player"] in self.players:
# self.players.pop(data["player"])
#
# async def _at_hang(self, data=None):
# if data["player"] in self.players:
# self.players.pop(data["player"])
#
# async def _at_day_end(self, data=None):
# pass
async def _at_night_start(self, data=None):
if self.channel is None:
return
@wolflistener("at_night_start", priority=2)
async def _at_night_start(self):
await super()._at_night_start()
await self.game.generate_targets(self.channel)
mention_list = " ".join(player.mention for player in self.players)
if mention_list != "":
await self.channel.send(mention_list)
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:
return
@ -87,34 +51,23 @@ class WolfVote(VoteGroup):
if vote_list:
target_id = max(set(vote_list), key=vote_list.count)
print("Target id: {}\nKiller: {}".format(target_id, self.killer.member.display_name))
log.debug(f"Target id: {target_id}\nKiller: {self.killer.member.display_name}")
if target_id is not None and self.killer:
await self.game.kill(target_id, self.killer, random.choice(self.kill_messages))
await self.channel.send("**{} has left to complete the kill...**".format(self.killer.member.display_name))
await self.channel.send(
"*{} has left to complete the kill...*".format(self.killer.member.display_name)
)
else:
await self.channel.send("**No kill will be attempted tonight...**")
# 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)
await self.channel.send("*No kill will be attempted tonight...*")
async def vote(self, target, author, target_id):
"""
Receive vote from game
"""
self.vote_results[author.id] = target_id
await super().vote(target, author, target_id)
await self.channel.send("{} has voted to kill {}".format(author.mention, target.member.display_name))
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
from redbot.core import Config, checks, commands
from redbot.core.bot import Red
from redbot.core.commands import Cog
from redbot.core.utils import AsyncIter
from redbot.core.utils.menus import DEFAULT_CONTROLS, menu
from .builder import (
from werewolf.builder import (
GameBuilder,
role_from_alignment,
role_from_category,
role_from_id,
role_from_name,
)
from .game import Game
from werewolf.game import Game
log = logging.getLogger("red.fox_v3.werewolf")
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):
@ -43,7 +57,7 @@ class Werewolf(Cog):
return
def __unload(self):
print("Unload called")
log.debug("Unload called")
for game in self.games.values():
del game
@ -58,9 +72,9 @@ class Werewolf(Cog):
code = await gb.build_game(ctx)
if code != "":
await ctx.send("Your game code is **{}**".format(code))
await ctx.maybe_send_embed(f"Your game code is **{code}**")
else:
await ctx.send("No code generated")
await ctx.maybe_send_embed("No code generated")
@checks.guildowner()
@commands.group()
@ -77,31 +91,36 @@ class Werewolf(Cog):
"""
Lists current guild settings
"""
success, role, category, channel, log_channel = await self._get_settings(ctx)
if not success:
await ctx.send("Failed to get settings")
return None
valid, role, category, channel, log_channel = await self._get_settings(ctx)
# if not valid:
# await ctx.send("Failed to get settings")
# return None
embed = discord.Embed(title="Current Guild Settings")
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="Category", value=str(category))
embed.add_field(name="Channel", value=str(channel))
embed.add_field(name="Log Channel", value=str(log_channel))
await ctx.send(embed=embed)
@commands.guild_only()
@wwset.command(name="role")
async def wwset_role(self, ctx: commands.Context, role: discord.Role = None):
"""
Assign the game role
Set the game role
This role should not be manually assigned
"""
if role is None:
await self.config.guild(ctx.guild).role_id.set(None)
await ctx.send("Cleared Game Role")
await ctx.maybe_send_embed("Cleared Game Role")
else:
await self.config.guild(ctx.guild).role_id.set(role.id)
await ctx.send("Game Role has been set to **{}**".format(role.name))
await ctx.maybe_send_embed("Game Role has been set to **{}**".format(role.name))
@commands.guild_only()
@wwset.command(name="category")
@ -111,14 +130,16 @@ class Werewolf(Cog):
"""
if category_id is None:
await self.config.guild(ctx.guild).category_id.set(None)
await ctx.send("Cleared Game Channel Category")
await ctx.maybe_send_embed("Cleared Game Channel Category")
else:
category = discord.utils.get(ctx.guild.categories, id=int(category_id))
if category is None:
await ctx.send("Category not found")
await ctx.maybe_send_embed("Category not found")
return
await self.config.guild(ctx.guild).category_id.set(category.id)
await ctx.send("Game Channel Category has been set to **{}**".format(category.name))
await ctx.maybe_send_embed(
"Game Channel Category has been set to **{}**".format(category.name)
)
@commands.guild_only()
@wwset.command(name="channel")
@ -128,10 +149,12 @@ class Werewolf(Cog):
"""
if channel is None:
await self.config.guild(ctx.guild).channel_id.set(None)
await ctx.send("Cleared Game Channel")
await ctx.maybe_send_embed("Cleared Game Channel")
else:
await self.config.guild(ctx.guild).channel_id.set(channel.id)
await ctx.send("Game Channel has been set to **{}**".format(channel.mention))
await ctx.maybe_send_embed(
"Game Channel has been set to **{}**".format(channel.mention)
)
@commands.guild_only()
@wwset.command(name="logchannel")
@ -141,10 +164,12 @@ class Werewolf(Cog):
"""
if channel is None:
await self.config.guild(ctx.guild).log_channel_id.set(None)
await ctx.send("Cleared Game Log Channel")
await ctx.maybe_send_embed("Cleared Game Log Channel")
else:
await self.config.guild(ctx.guild).log_channel_id.set(channel.id)
await ctx.send("Game Log Channel has been set to **{}**".format(channel.mention))
await ctx.maybe_send_embed(
"Game Log Channel has been set to **{}**".format(channel.mention)
)
@commands.group()
async def ww(self, ctx: commands.Context):
@ -162,9 +187,9 @@ class Werewolf(Cog):
"""
game = await self._get_game(ctx, game_code)
if not game:
await ctx.send("Failed to start a new game")
await ctx.maybe_send_embed("Failed to start a new game")
else:
await ctx.send("Game is ready to join! Use `[p]ww join`")
await ctx.maybe_send_embed("Game is ready to join! Use `[p]ww join`")
@commands.guild_only()
@ww.command(name="join")
@ -173,28 +198,49 @@ class Werewolf(Cog):
Joins a game of Werewolf
"""
game = await self._get_game(ctx)
game: Game = await self._get_game(ctx)
if not game:
await ctx.maybe_send_embed("Failed to join a game!")
return
await game.join(ctx, ctx.author)
await ctx.tick()
@commands.guild_only()
@commands.admin()
@ww.command(name="forcejoin")
async def ww_forcejoin(self, ctx: commands.Context, target: discord.Member):
"""
Force someone to join a game of Werewolf
"""
game: Game = await self._get_game(ctx)
if not game:
await ctx.send("No game to join!\nCreate a new one with `[p]ww new`")
await ctx.maybe_send_embed("Failed to join a game!")
return
await game.join(ctx.author, ctx.channel)
await game.join(ctx, target)
await ctx.tick()
@commands.guild_only()
@ww.command(name="code")
async def ww_code(self, ctx: commands.Context, code):
"""
Adjust game code
Adjusts the game code.
See `[p]buildgame` to generate a new code
"""
game = await self._get_game(ctx)
if not game:
await ctx.send("No game to join!\nCreate a new one with `[p]ww new`")
await ctx.maybe_send_embed("No game to join!\nCreate a new one with `[p]ww new`")
return
await game.set_code(ctx, code)
await ctx.tick()
@commands.guild_only()
@ww.command(name="quit")
@ -206,6 +252,7 @@ class Werewolf(Cog):
game = await self._get_game(ctx)
await game.quit(ctx.author, ctx.channel)
await ctx.tick()
@commands.guild_only()
@ww.command(name="start")
@ -215,10 +262,12 @@ class Werewolf(Cog):
"""
game = await self._get_game(ctx)
if not game:
await ctx.send("No game running, cannot start")
await ctx.maybe_send_embed("No game running, cannot start")
if not await game.setup(ctx):
pass # Do something?
pass # ToDo something?
await ctx.tick()
@commands.guild_only()
@ww.command(name="stop")
@ -226,17 +275,18 @@ class Werewolf(Cog):
"""
Stops the current game
"""
if ctx.guild is None:
# Private message, can't get guild
await ctx.send("Cannot start game from PM!")
return
# if ctx.guild is None:
# # Private message, can't get guild
# await ctx.send("Cannot stop game from PM!")
# return
if ctx.guild.id not in self.games or self.games[ctx.guild.id].game_over:
await ctx.send("No game to stop")
await ctx.maybe_send_embed("No game to stop")
return
game = await self._get_game(ctx)
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()
@ww.command(name="vote")
@ -250,7 +300,7 @@ class Werewolf(Cog):
target_id = None
if target_id is None:
await ctx.send("`id` must be an integer")
await ctx.maybe_send_embed("`id` must be an integer")
return
# if ctx.guild is None:
@ -267,7 +317,7 @@ class Werewolf(Cog):
game = await self._get_game(ctx)
if game is None:
await ctx.send("No game running, cannot vote")
await ctx.maybe_send_embed("No game running, cannot vote")
return
# Game handles response now
@ -277,7 +327,7 @@ class Werewolf(Cog):
elif channel in (c["channel"] for c in game.p_channels.values()):
await game.vote(ctx.author, target_id, channel)
else:
await ctx.send("Nothing to vote for in this channel")
await ctx.maybe_send_embed("Nothing to vote for in this channel")
@ww.command(name="choose")
async def ww_choose(self, ctx: commands.Context, data):
@ -288,7 +338,7 @@ class Werewolf(Cog):
"""
if ctx.guild is not None:
await ctx.send("This action is only available in DM's")
await ctx.maybe_send_embed("This action is only available in DM's")
return
# DM nonsense, find their game
# If multiple games, panic
@ -296,7 +346,7 @@ class Werewolf(Cog):
if await game.get_player_by_member(ctx.author):
break # game = game
else:
await ctx.send("You're not part of any werewolf game")
await ctx.maybe_send_embed("You're not part of any werewolf game")
return
await game.choose(ctx, data)
@ -317,7 +367,7 @@ class Werewolf(Cog):
if from_name:
await menu(ctx, from_name, DEFAULT_CONTROLS)
else:
await ctx.send("No roles containing that name were found")
await ctx.maybe_send_embed("No roles containing that name were found")
@ww_search.command(name="alignment")
async def ww_search_alignment(self, ctx: commands.Context, alignment: int):
@ -327,7 +377,7 @@ class Werewolf(Cog):
if from_alignment:
await menu(ctx, from_alignment, DEFAULT_CONTROLS)
else:
await ctx.send("No roles with that alignment were found")
await ctx.maybe_send_embed("No roles with that alignment were found")
@ww_search.command(name="category")
async def ww_search_category(self, ctx: commands.Context, category: int):
@ -337,7 +387,7 @@ class Werewolf(Cog):
if pages:
await menu(ctx, pages, DEFAULT_CONTROLS)
else:
await ctx.send("No roles in that category were found")
await ctx.maybe_send_embed("No roles in that category were found")
@ww_search.command(name="index")
async def ww_search_index(self, ctx: commands.Context, idx: int):
@ -347,24 +397,32 @@ class Werewolf(Cog):
if idx_embed is not None:
await ctx.send(embed=idx_embed)
else:
await ctx.send("Role ID not found")
await ctx.maybe_send_embed("Role ID not found")
async def _get_game(self, ctx: commands.Context, game_code=None):
guild: discord.Guild = ctx.guild
async def _get_game(self, ctx: commands.Context, game_code=None) -> Union[Game, None]:
guild: discord.Guild = getattr(ctx, "guild", None)
if guild is None:
# Private message, can't get guild
await ctx.send("Cannot start game from PM!")
await ctx.maybe_send_embed("Cannot start game from DM!")
return None
if guild.id not in self.games or self.games[guild.id].game_over:
await ctx.send("Starting a new game...")
success, role, category, channel, log_channel = await self._get_settings(ctx)
await ctx.maybe_send_embed("Starting a new game...")
valid, role, category, channel, log_channel = await self._get_settings(ctx)
if not success:
await ctx.send("Cannot start a new game")
if not valid:
await ctx.maybe_send_embed("Cannot start a new game")
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]
@ -385,23 +443,30 @@ class Werewolf(Cog):
if role_id is not None:
role = discord.utils.get(guild.roles, id=role_id)
if role is None:
await ctx.send("Game Role is invalid")
return False, None, None, None, None
# if role is None:
# # await ctx.send("Game Role is invalid")
# return False, None, None, None, None
if category_id is not None:
category = discord.utils.get(guild.categories, id=category_id)
if category is None:
await ctx.send("Game Category is invalid")
return False, None, None, None, None
# if category is None:
# # await ctx.send("Game Category is invalid")
# return False, role, None, None, None
if channel_id is not None:
channel = discord.utils.get(guild.text_channels, id=channel_id)
if channel is None:
await ctx.send("Village Channel is invalid")
return False, None, None, None, None
# if channel is None:
# # await ctx.send("Village Channel is invalid")
# return False, role, category, None, None
if log_channel_id is not None:
log_channel = discord.utils.get(guild.text_channels, id=log_channel_id)
if log_channel is None:
await ctx.send("Log Channel is invalid")
return False, None, None, None, None
return True, role, category, channel, log_channel
# if log_channel is None:
# # await ctx.send("Log Channel is invalid")
# return False, None, None, None, None
return (
role is not None and category is not None and channel is not None,
role,
category,
channel,
log_channel,
)

Loading…
Cancel
Save