Merge branch 'master' into cogguide_develop

cogguide_develop
bobloy 4 years ago
commit bd45f353a8

@ -54,8 +54,7 @@ class AnnounceDaily(Cog):
Do `[p]help annd <subcommand>` for more details Do `[p]help annd <subcommand>` for more details
""" """
if ctx.invoked_subcommand is None: pass
pass
@commands.command() @commands.command()
@checks.guildowner() @checks.guildowner()

@ -168,7 +168,7 @@ class AudioTrivia(Trivia):
@commands.guild_only() @commands.guild_only()
async def audiotrivia_list(self, ctx: commands.Context): async def audiotrivia_list(self, ctx: commands.Context):
"""List available trivia including audio categories.""" """List available trivia including audio categories."""
lists = set(p.stem for p in self._all_audio_lists()) lists = {p.stem for p in self._all_audio_lists()}
if await ctx.embed_requested(): if await ctx.embed_requested():
await ctx.send( await ctx.send(
embed=discord.Embed( embed=discord.Embed(

@ -48,8 +48,7 @@ class CCRole(commands.Cog):
"""Custom commands management with roles """Custom commands management with roles
Highly customizable custom commands with role management.""" Highly customizable custom commands with role management."""
if not ctx.invoked_subcommand: pass
pass
@ccrole.command(name="add") @ccrole.command(name="add")
@checks.mod_or_permissions(administrator=True) @checks.mod_or_permissions(administrator=True)
@ -228,7 +227,7 @@ class CCRole(commands.Cog):
if not role_list: if not role_list:
return "None" return "None"
return ", ".join( return ", ".join(
[discord.utils.get(ctx.guild.roles, id=roleid).name for roleid in role_list] discord.utils.get(ctx.guild.roles, id=roleid).name for roleid in role_list
) )
embed.add_field(name="Text", value="```{}```".format(cmd["text"]), inline=False) embed.add_field(name="Text", value="```{}```".format(cmd["text"]), inline=False)
@ -252,7 +251,7 @@ class CCRole(commands.Cog):
) )
return return
cmd_list = ", ".join([ctx.prefix + c for c in sorted(cmd_list.keys())]) cmd_list = ", ".join(ctx.prefix + c for c in sorted(cmd_list.keys()))
cmd_list = "Custom commands:\n\n" + cmd_list cmd_list = "Custom commands:\n\n" + cmd_list
if ( if (
@ -325,9 +324,7 @@ class CCRole(commands.Cog):
async def eval_cc(self, cmd, message: discord.Message, ctx: commands.Context): async def eval_cc(self, cmd, message: discord.Message, ctx: commands.Context):
"""Does all the work""" """Does all the work"""
if cmd["proles"] and not ( if cmd["proles"] and not {role.id for role in message.author.roles} & set(cmd["proles"]):
set(role.id for role in message.author.roles) & set(cmd["proles"])
):
log.debug(f"{message.author} missing required role to execute {ctx.invoked_with}") log.debug(f"{message.author} missing required role to execute {ctx.invoked_with}")
return # Not authorized, do nothing return # Not authorized, do nothing

@ -59,6 +59,35 @@ Install these on your windows machine before attempting the installation:
[Pandoc - Universal Document Converter](https://pandoc.org/installing.html) [Pandoc - Universal Document Converter](https://pandoc.org/installing.html)
## Methods ## Methods
### Automatic
This method requires some luck to pull off.
#### Step 1: Add repo and install cog
```
[p]repo add Fox https://github.com/bobloy/Fox-V3
[p]cog install Fox chatter
```
If you get an error at this step, stop and skip to one of the manual methods below.
#### Step 2: Install additional dependencies
Assuming the previous commands had no error, you can now use `pipinstall` to add the remaining dependencies.
NOTE: This method is not the intended use case for `pipinstall` and may stop working in the future.
```
[p]pipinstall --no-deps chatterbot>=1.1
```
#### Step 3: Load the cog and get started
```
[p]load chatter
```
### Windows - Manually ### Windows - Manually
#### Step 1: Built-in Downloader #### Step 1: Built-in Downloader

@ -2,8 +2,10 @@ import asyncio
import logging import logging
import os import os
import pathlib import pathlib
from collections import defaultdict
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Optional from functools import partial
from typing import Dict, List, Optional
import discord import discord
from chatterbot import ChatBot from chatterbot import ChatBot
@ -15,6 +17,8 @@ from redbot.core.commands import Cog
from redbot.core.data_manager import cog_data_path from redbot.core.data_manager import cog_data_path
from redbot.core.utils.predicates import MessagePredicate from redbot.core.utils.predicates import MessagePredicate
from chatter.trainers import MovieTrainer, TwitterCorpusTrainer, UbuntuCorpusTrainer2
log = logging.getLogger("red.fox_v3.chatter") log = logging.getLogger("red.fox_v3.chatter")
@ -52,8 +56,14 @@ class Chatter(Cog):
super().__init__() super().__init__()
self.bot = bot self.bot = bot
self.config = Config.get_conf(self, identifier=6710497116116101114) self.config = Config.get_conf(self, identifier=6710497116116101114)
default_global = {} default_global = {"learning": True}
default_guild = {"whitelist": None, "days": 1, "convo_delta": 15, "chatchannel": None} default_guild = {
"whitelist": None,
"days": 1,
"convo_delta": 15,
"chatchannel": None,
"reply": True,
}
path: pathlib.Path = cog_data_path(self) path: pathlib.Path = cog_data_path(self)
self.data_path = path / "database.sqlite3" self.data_path = path / "database.sqlite3"
@ -73,6 +83,11 @@ class Chatter(Cog):
self.loop = asyncio.get_event_loop() self.loop = asyncio.get_event_loop()
self._guild_cache = defaultdict(dict)
self._global_cache = {}
self._last_message_per_channel: Dict[Optional[discord.Message]] = defaultdict(lambda: None)
async def red_delete_data_for_user(self, **kwargs): async def red_delete_data_for_user(self, **kwargs):
"""Nothing to delete""" """Nothing to delete"""
return return
@ -81,7 +96,8 @@ class Chatter(Cog):
return ChatBot( return ChatBot(
"ChatterBot", "ChatterBot",
storage_adapter="chatterbot.storage.SQLStorageAdapter", # storage_adapter="chatterbot.storage.SQLStorageAdapter",
storage_adapter="chatter.storage_adapters.MyDumbSQLStorageAdapter",
database_uri="sqlite:///" + str(self.data_path), database_uri="sqlite:///" + str(self.data_path),
statement_comparison_function=self.similarity_algo, statement_comparison_function=self.similarity_algo,
response_selection_method=get_random_response, response_selection_method=get_random_response,
@ -91,7 +107,7 @@ class Chatter(Cog):
logger=log, logger=log,
) )
async def _get_conversation(self, ctx, in_channel: discord.TextChannel = None): async def _get_conversation(self, ctx, in_channels: List[discord.TextChannel]):
""" """
Compiles all conversation in the Guild this bot can get it's hands on Compiles all conversation in the Guild this bot can get it's hands on
Currently takes a stupid long time Currently takes a stupid long time
@ -105,20 +121,12 @@ class Chatter(Cog):
return msg.clean_content return msg.clean_content
def new_conversation(msg, sent, out_in, delta): def new_conversation(msg, sent, out_in, delta):
# if sent is None: # Should always be positive numbers
# return False
# Don't do "too short" processing here. Sometimes people don't respond.
# if len(out_in) < 2:
# return False
# print(msg.created_at - sent)
return msg.created_at - sent >= delta return msg.created_at - sent >= delta
for channel in ctx.guild.text_channels: for channel in in_channels:
if in_channel: # if in_channel:
channel = in_channel # channel = in_channel
await ctx.maybe_send_embed("Gathering {}".format(channel.mention)) await ctx.maybe_send_embed("Gathering {}".format(channel.mention))
user = None user = None
i = 0 i = 0
@ -153,11 +161,16 @@ class Chatter(Cog):
except discord.HTTPException: except discord.HTTPException:
pass pass
if in_channel: # if in_channel:
break # break
return out return out
def _train_twitter(self, *args, **kwargs):
trainer = TwitterCorpusTrainer(self.chatbot)
trainer.train(*args, **kwargs)
return True
def _train_ubuntu(self): def _train_ubuntu(self):
trainer = UbuntuCorpusTrainer( trainer = UbuntuCorpusTrainer(
self.chatbot, ubuntu_corpus_data_directory=cog_data_path(self) / "ubuntu_data" self.chatbot, ubuntu_corpus_data_directory=cog_data_path(self) / "ubuntu_data"
@ -165,6 +178,30 @@ class Chatter(Cog):
trainer.train() trainer.train()
return True return True
async def _train_movies(self):
trainer = MovieTrainer(self.chatbot, cog_data_path(self))
return await trainer.asynctrain()
async def _train_ubuntu2(self, intensity):
train_kwarg = {}
if intensity == 196:
train_kwarg["train_dialogue"] = False
train_kwarg["train_196"] = True
elif intensity == 301:
train_kwarg["train_dialogue"] = False
train_kwarg["train_301"] = True
elif intensity == 497:
train_kwarg["train_dialogue"] = False
train_kwarg["train_196"] = True
train_kwarg["train_301"] = True
elif intensity >= 9000: # NOT 9000!
train_kwarg["train_dialogue"] = True
train_kwarg["train_196"] = True
train_kwarg["train_301"] = True
trainer = UbuntuCorpusTrainer2(self.chatbot, cog_data_path(self))
return await trainer.asynctrain(**train_kwarg)
def _train_english(self): def _train_english(self):
trainer = ChatterBotCorpusTrainer(self.chatbot) trainer = ChatterBotCorpusTrainer(self.chatbot)
# try: # try:
@ -176,13 +213,10 @@ class Chatter(Cog):
def _train(self, data): def _train(self, data):
trainer = ListTrainer(self.chatbot) trainer = ListTrainer(self.chatbot)
total = len(data) total = len(data)
# try:
for c, convo in enumerate(data, 1): for c, convo in enumerate(data, 1):
log.info(f"{c} / {total}")
if len(convo) > 1: # TODO: Toggleable skipping short conversations if len(convo) > 1: # TODO: Toggleable skipping short conversations
print(f"{c} / {total}")
trainer.train(convo) trainer.train(convo)
# except:
# return False
return True return True
@commands.group(invoke_without_command=False) @commands.group(invoke_without_command=False)
@ -190,10 +224,10 @@ class Chatter(Cog):
""" """
Base command for this cog. Check help for the commands list. Base command for this cog. Check help for the commands list.
""" """
if ctx.invoked_subcommand is None: self._guild_cache[ctx.guild.id] = {} # Clear cache when modifying values
pass self._global_cache = {}
@checks.admin() @commands.admin()
@chatter.command(name="channel") @chatter.command(name="channel")
async def chatter_channel( async def chatter_channel(
self, ctx: commands.Context, channel: Optional[discord.TextChannel] = None self, ctx: commands.Context, channel: Optional[discord.TextChannel] = None
@ -213,13 +247,55 @@ class Chatter(Cog):
await self.config.guild(ctx.guild).chatchannel.set(channel.id) await self.config.guild(ctx.guild).chatchannel.set(channel.id)
await ctx.maybe_send_embed(f"Chat channel is now {channel.mention}") await ctx.maybe_send_embed(f"Chat channel is now {channel.mention}")
@checks.is_owner() @commands.admin()
@chatter.command(name="reply")
async def chatter_reply(self, ctx: commands.Context, toggle: Optional[bool] = None):
"""
Toggle bot reply to messages if conversation continuity is not present
"""
reply = await self.config.guild(ctx.guild).reply()
if toggle is None:
toggle = not reply
await self.config.guild(ctx.guild).reply.set(toggle)
if toggle:
await ctx.maybe_send_embed(
"I will now respond to you if conversation continuity is not present"
)
else:
await ctx.maybe_send_embed(
"I will not reply to your message if conversation continuity is not present, anymore"
)
@commands.is_owner()
@chatter.command(name="learning")
async def chatter_learning(self, ctx: commands.Context, toggle: Optional[bool] = None):
"""
Toggle the bot learning from its conversations.
This is a global setting.
This is on by default.
"""
learning = await self.config.learning()
if toggle is None:
toggle = not learning
await self.config.learning.set(toggle)
if toggle:
await ctx.maybe_send_embed("I will now learn from conversations.")
else:
await ctx.maybe_send_embed("I will no longer learn from conversations.")
@commands.is_owner()
@chatter.command(name="cleardata") @chatter.command(name="cleardata")
async def chatter_cleardata(self, ctx: commands.Context, confirm: bool = False): async def chatter_cleardata(self, ctx: commands.Context, confirm: bool = False):
""" """
This command will erase all training data and reset your configuration settings This command will erase all training data and reset your configuration settings.
Use `[p]chatter cleardata True` This applies to all guilds.
Use `[p]chatter cleardata True` to confirm.
""" """
if not confirm: if not confirm:
@ -246,7 +322,7 @@ class Chatter(Cog):
await ctx.tick() await ctx.tick()
@checks.is_owner() @commands.is_owner()
@chatter.command(name="algorithm", aliases=["algo"]) @chatter.command(name="algorithm", aliases=["algo"])
async def chatter_algorithm( async def chatter_algorithm(
self, ctx: commands.Context, algo_number: int, threshold: float = None self, ctx: commands.Context, algo_number: int, threshold: float = None
@ -280,7 +356,7 @@ class Chatter(Cog):
await ctx.tick() await ctx.tick()
@checks.is_owner() @commands.is_owner()
@chatter.command(name="model") @chatter.command(name="model")
async def chatter_model(self, ctx: commands.Context, model_number: int): async def chatter_model(self, ctx: commands.Context, model_number: int):
""" """
@ -318,7 +394,7 @@ class Chatter(Cog):
f"Model has been switched to {self.tagger_language.ISO_639_1}" f"Model has been switched to {self.tagger_language.ISO_639_1}"
) )
@checks.is_owner() @commands.is_owner()
@chatter.command(name="minutes") @chatter.command(name="minutes")
async def minutes(self, ctx: commands.Context, minutes: int): async def minutes(self, ctx: commands.Context, minutes: int):
""" """
@ -330,11 +406,11 @@ class Chatter(Cog):
await ctx.send_help() await ctx.send_help()
return return
await self.config.guild(ctx.guild).convo_length.set(minutes) await self.config.guild(ctx.guild).convo_delta.set(minutes)
await ctx.tick() await ctx.tick()
@checks.is_owner() @commands.is_owner()
@chatter.command(name="age") @chatter.command(name="age")
async def age(self, ctx: commands.Context, days: int): async def age(self, ctx: commands.Context, days: int):
""" """
@ -349,7 +425,16 @@ class Chatter(Cog):
await self.config.guild(ctx.guild).days.set(days) await self.config.guild(ctx.guild).days.set(days)
await ctx.tick() await ctx.tick()
@checks.is_owner() @commands.is_owner()
@chatter.command(name="kaggle")
async def chatter_kaggle(self, ctx: commands.Context):
"""Register with the kaggle API to download additional datasets for training"""
if not await self.check_for_kaggle():
await ctx.maybe_send_embed(
"[Click here for instructions to setup the kaggle api](https://github.com/Kaggle/kaggle-api#api-credentials)"
)
@commands.is_owner()
@chatter.command(name="backup") @chatter.command(name="backup")
async def backup(self, ctx, backupname): async def backup(self, ctx, backupname):
""" """
@ -371,8 +456,71 @@ class Chatter(Cog):
else: else:
await ctx.maybe_send_embed("Error occurred :(") await ctx.maybe_send_embed("Error occurred :(")
@checks.is_owner() @commands.is_owner()
@chatter.command(name="trainubuntu") @chatter.group(name="train")
async def chatter_train(self, ctx: commands.Context):
"""Commands for training the bot"""
pass
@chatter_train.group(name="kaggle")
async def chatter_train_kaggle(self, ctx: commands.Context):
"""
Base command for kaggle training sets.
See `[p]chatter kaggle` for details on how to enable this option
"""
pass
@chatter_train_kaggle.command(name="ubuntu")
async def chatter_train_kaggle_ubuntu(
self, ctx: commands.Context, confirmation: bool = False, intensity=0
):
"""
WARNING: Large Download! Trains the bot using *NEW* Ubuntu Dialog Corpus data.
"""
if not confirmation:
await ctx.maybe_send_embed(
"Warning: This command downloads ~800MB and is CPU intensive during training\n"
"If you're sure you want to continue, run `[p]chatter train kaggle ubuntu True`"
)
return
async with ctx.typing():
future = await self._train_ubuntu2(intensity)
if future:
await ctx.maybe_send_embed("Training successful!")
else:
await ctx.maybe_send_embed("Error occurred :(")
@chatter_train_kaggle.command(name="movies")
async def chatter_train_kaggle_movies(self, ctx: commands.Context, confirmation: bool = False):
"""
WARNING: Language! Trains the bot using Cornell University's "Movie Dialog Corpus".
This training set contains dialog from a spread of movies with different MPAA.
This dialog includes racism, sexism, and any number of sensitive topics.
Use at your own risk.
"""
if not confirmation:
await ctx.maybe_send_embed(
"Warning: This command downloads ~29MB and is CPU intensive during training\n"
"If you're sure you want to continue, run `[p]chatter train kaggle movies True`"
)
return
async with ctx.typing():
future = await self._train_movies()
if future:
await ctx.maybe_send_embed("Training successful!")
else:
await ctx.maybe_send_embed("Error occurred :(")
@chatter_train.command(name="ubuntu")
async def chatter_train_ubuntu(self, ctx: commands.Context, confirmation: bool = False): async def chatter_train_ubuntu(self, ctx: commands.Context, confirmation: bool = False):
""" """
WARNING: Large Download! Trains the bot using Ubuntu Dialog Corpus data. WARNING: Large Download! Trains the bot using Ubuntu Dialog Corpus data.
@ -380,8 +528,8 @@ class Chatter(Cog):
if not confirmation: if not confirmation:
await ctx.maybe_send_embed( await ctx.maybe_send_embed(
"Warning: This command downloads ~500MB then eats your CPU for training\n" "Warning: This command downloads ~500MB and is CPU intensive during training\n"
"If you're sure you want to continue, run `[p]chatter trainubuntu True`" "If you're sure you want to continue, run `[p]chatter train ubuntu True`"
) )
return return
@ -389,12 +537,11 @@ class Chatter(Cog):
future = await self.loop.run_in_executor(None, self._train_ubuntu) future = await self.loop.run_in_executor(None, self._train_ubuntu)
if future: if future:
await ctx.send("Training successful!") await ctx.maybe_send_embed("Training successful!")
else: else:
await ctx.send("Error occurred :(") await ctx.maybe_send_embed("Error occurred :(")
@checks.is_owner() @chatter_train.command(name="english")
@chatter.command(name="trainenglish")
async def chatter_train_english(self, ctx: commands.Context): async def chatter_train_english(self, ctx: commands.Context):
""" """
Trains the bot in english Trains the bot in english
@ -407,12 +554,32 @@ class Chatter(Cog):
else: else:
await ctx.maybe_send_embed("Error occurred :(") await ctx.maybe_send_embed("Error occurred :(")
@checks.is_owner() @chatter_train.command(name="list")
@chatter.command() async def chatter_train_list(self, ctx: commands.Context):
async def train(self, ctx: commands.Context, channel: discord.TextChannel): """Trains the bot based on an uploaded list.
Must be a file in the format of a python list: ['prompt', 'response1', 'response2']
"""
if not ctx.message.attachments:
await ctx.maybe_send_embed("You must upload a file when using this command")
return
attachment: discord.Attachment = ctx.message.attachments[0]
a_bytes = await attachment.read()
await ctx.send("Not yet implemented")
@chatter_train.command(name="channel")
async def chatter_train_channel(
self, ctx: commands.Context, channels: commands.Greedy[discord.TextChannel]
):
""" """
Trains the bot based on language in this guild Trains the bot based on language in this guild.
""" """
if not channels:
await ctx.send_help()
return
await ctx.maybe_send_embed( await ctx.maybe_send_embed(
"Warning: The cog may use significant RAM or CPU if trained on large data sets.\n" "Warning: The cog may use significant RAM or CPU if trained on large data sets.\n"
@ -421,7 +588,7 @@ class Chatter(Cog):
) )
async with ctx.typing(): async with ctx.typing():
conversation = await self._get_conversation(ctx, channel) conversation = await self._get_conversation(ctx, channels)
if not conversation: if not conversation:
await ctx.maybe_send_embed("Failed to gather training data") await ctx.maybe_send_embed("Failed to gather training data")
@ -475,7 +642,18 @@ class Chatter(Cog):
# Thank you Cog-Creators # Thank you Cog-Creators
channel: discord.TextChannel = message.channel channel: discord.TextChannel = message.channel
if guild is not None and channel.id == await self.config.guild(guild).chatchannel(): if not self._guild_cache[guild.id]:
self._guild_cache[guild.id] = await self.config.guild(guild).all()
is_reply = False # this is only useful with in_response_to
if (
message.reference is not None
and isinstance(message.reference.resolved, discord.Message)
and message.reference.resolved.author.id == self.bot.user.id
):
is_reply = True # this is only useful with in_response_to
pass # this is a reply to the bot, good to go
elif guild is not None and channel.id == self._guild_cache[guild.id]["chatchannel"]:
pass # good to go pass # good to go
else: else:
when_mentionables = commands.when_mentioned(self.bot, message) when_mentionables = commands.when_mentioned(self.bot, message)
@ -490,10 +668,55 @@ class Chatter(Cog):
text = message.clean_content text = message.clean_content
async with channel.typing(): async with ctx.typing():
future = await self.loop.run_in_executor(None, self.chatbot.get_response, text)
if is_reply:
in_response_to = message.reference.resolved.content
elif self._last_message_per_channel[ctx.channel.id] is not None:
last_m: discord.Message = self._last_message_per_channel[ctx.channel.id]
minutes = self._guild_cache[ctx.guild.id]["convo_delta"]
if (datetime.utcnow() - last_m.created_at).seconds > minutes * 60:
in_response_to = None
else:
in_response_to = last_m.content
else:
in_response_to = None
# Always use generate reponse
# Chatterbot tries to learn based on the result it comes up with, which is dumb
log.debug("Generating response")
Statement = self.chatbot.storage.get_object("statement")
future = await self.loop.run_in_executor(
None, self.chatbot.generate_response, Statement(text)
)
if not self._global_cache:
self._global_cache = await self.config.all()
if in_response_to is not None and self._global_cache["learning"]:
log.debug("learning response")
await self.loop.run_in_executor(
None,
partial(
self.chatbot.learn_response,
Statement(text),
previous_statement=in_response_to,
),
)
replying = None
if self._guild_cache[guild.id]["reply"]:
if message != ctx.channel.last_message:
replying = message
if future and str(future): if future and str(future):
await channel.send(str(future)) self._last_message_per_channel[ctx.channel.id] = await channel.send(
str(future), reference=replying
)
else: else:
await channel.send(":thinking:") await ctx.send(":thinking:")
async def check_for_kaggle(self):
"""Check whether Kaggle is installed and configured properly"""
# TODO: This
return False

@ -2,7 +2,7 @@
"author": [ "author": [
"Bobloy" "Bobloy"
], ],
"min_bot_version": "3.4.0", "min_bot_version": "3.4.6",
"description": "Create an offline chatbot that talks like your average member using Machine Learning. See setup instructions at https://github.com/bobloy/Fox-V3/tree/master/chatter", "description": "Create an offline chatbot that talks like your average member using Machine Learning. See setup instructions at https://github.com/bobloy/Fox-V3/tree/master/chatter",
"hidden": false, "hidden": false,
"install_msg": "Thank you for installing Chatter! Please make sure you check the install instructions at https://github.com/bobloy/Fox-V3/blob/master/chatter/README.md\nAfter that, get started ith `[p]load chatter` and `[p]help Chatter`", "install_msg": "Thank you for installing Chatter! Please make sure you check the install instructions at https://github.com/bobloy/Fox-V3/blob/master/chatter/README.md\nAfter that, get started ith `[p]load chatter` and `[p]help Chatter`",
@ -17,7 +17,8 @@
"pytz", "pytz",
"https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-2.3.1/en_core_web_sm-2.3.1.tar.gz#egg=en_core_web_sm", "https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-2.3.1/en_core_web_sm-2.3.1.tar.gz#egg=en_core_web_sm",
"https://github.com/explosion/spacy-models/releases/download/en_core_web_md-2.3.1/en_core_web_md-2.3.1.tar.gz#egg=en_core_web_md", "https://github.com/explosion/spacy-models/releases/download/en_core_web_md-2.3.1/en_core_web_md-2.3.1.tar.gz#egg=en_core_web_md",
"spacy>=2.3,<2.4" "spacy>=2.3,<2.4",
"kaggle"
], ],
"short": "Local Chatbot run on machine learning", "short": "Local Chatbot run on machine learning",
"end_user_data_statement": "This cog only stores anonymous conversations data; no End User Data is stored.", "end_user_data_statement": "This cog only stores anonymous conversations data; no End User Data is stored.",

@ -0,0 +1,73 @@
from chatterbot.storage import StorageAdapter, SQLStorageAdapter
class MyDumbSQLStorageAdapter(SQLStorageAdapter):
def __init__(self, **kwargs):
super(SQLStorageAdapter, self).__init__(**kwargs)
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
self.database_uri = kwargs.get("database_uri", False)
# None results in a sqlite in-memory database as the default
if self.database_uri is None:
self.database_uri = "sqlite://"
# Create a file database if the database is not a connection string
if not self.database_uri:
self.database_uri = "sqlite:///db.sqlite3"
self.engine = create_engine(
self.database_uri, convert_unicode=True, connect_args={"check_same_thread": False}
)
if self.database_uri.startswith("sqlite://"):
from sqlalchemy.engine import Engine
from sqlalchemy import event
@event.listens_for(Engine, "connect")
def set_sqlite_pragma(dbapi_connection, connection_record):
dbapi_connection.execute("PRAGMA journal_mode=WAL")
dbapi_connection.execute("PRAGMA synchronous=NORMAL")
if not self.engine.dialect.has_table(self.engine, "Statement"):
self.create_database()
self.Session = sessionmaker(bind=self.engine, expire_on_commit=True)
class AsyncSQLStorageAdapter(SQLStorageAdapter):
def __init__(self, **kwargs):
super(SQLStorageAdapter, self).__init__(**kwargs)
self.database_uri = kwargs.get("database_uri", False)
# None results in a sqlite in-memory database as the default
if self.database_uri is None:
self.database_uri = "sqlite://"
# Create a file database if the database is not a connection string
if not self.database_uri:
self.database_uri = "sqlite:///db.sqlite3"
async def initialize(self):
# from sqlalchemy import create_engine
from aiomysql.sa import create_engine
from sqlalchemy.orm import sessionmaker
self.engine = await create_engine(self.database_uri, convert_unicode=True)
if self.database_uri.startswith("sqlite://"):
from sqlalchemy.engine import Engine
from sqlalchemy import event
@event.listens_for(Engine, "connect")
def set_sqlite_pragma(dbapi_connection, connection_record):
dbapi_connection.execute("PRAGMA journal_mode=WAL")
dbapi_connection.execute("PRAGMA synchronous=NORMAL")
if not self.engine.dialect.has_table(self.engine, "Statement"):
self.create_database()
self.Session = sessionmaker(bind=self.engine, expire_on_commit=True)

@ -0,0 +1,351 @@
import asyncio
import csv
import html
import logging
import os
import pathlib
import time
from functools import partial
from chatterbot import utils
from chatterbot.conversation import Statement
from chatterbot.tagging import PosLemmaTagger
from chatterbot.trainers import Trainer
from redbot.core.bot import Red
from dateutil import parser as date_parser
from redbot.core.utils import AsyncIter
log = logging.getLogger("red.fox_v3.chatter.trainers")
class KaggleTrainer(Trainer):
def __init__(self, chatbot, datapath: pathlib.Path, **kwargs):
super().__init__(chatbot, **kwargs)
self.data_directory = datapath / kwargs.get("downloadpath", "kaggle_download")
self.kaggle_dataset = kwargs.get(
"kaggle_dataset",
"Cornell-University/movie-dialog-corpus",
)
# Create the data directory if it does not already exist
if not os.path.exists(self.data_directory):
os.makedirs(self.data_directory)
def is_downloaded(self, file_path):
"""
Check if the data file is already downloaded.
"""
if os.path.exists(file_path):
self.chatbot.logger.info("File is already downloaded")
return True
return False
async def download(self, dataset):
import kaggle # This triggers the API token check
future = await asyncio.get_event_loop().run_in_executor(
None,
partial(
kaggle.api.dataset_download_files,
dataset=dataset,
path=self.data_directory,
quiet=False,
unzip=True,
),
)
def train(self, *args, **kwargs):
log.error("See asynctrain instead")
def asynctrain(self, *args, **kwargs):
raise self.TrainerInitializationException()
class SouthParkTrainer(KaggleTrainer):
def __init__(self, chatbot, datapath: pathlib.Path, **kwargs):
super().__init__(
chatbot,
datapath,
downloadpath="ubuntu_data_v2",
kaggle_dataset="tovarischsukhov/southparklines",
**kwargs,
)
class MovieTrainer(KaggleTrainer):
def __init__(self, chatbot, datapath: pathlib.Path, **kwargs):
super().__init__(
chatbot,
datapath,
downloadpath="kaggle_movies",
kaggle_dataset="Cornell-University/movie-dialog-corpus",
**kwargs,
)
async def run_movie_training(self):
dialogue_file = "movie_lines.tsv"
conversation_file = "movie_conversations.tsv"
log.info(f"Beginning dialogue training on {dialogue_file}")
start_time = time.time()
tagger = PosLemmaTagger(language=self.chatbot.storage.tagger.language)
# [lineID, characterID, movieID, character name, text of utterance]
# File parsing from https://www.kaggle.com/mushaya/conversation-chatbot
with open(self.data_directory / conversation_file, "r", encoding="utf-8-sig") as conv_tsv:
conv_lines = conv_tsv.readlines()
with open(self.data_directory / dialogue_file, "r", encoding="utf-8-sig") as lines_tsv:
dialog_lines = lines_tsv.readlines()
# trans_dict = str.maketrans({"<u>": "__", "</u>": "__", '""': '"'})
lines_dict = {}
for line in dialog_lines:
_line = line[:-1].strip('"').split("\t")
if len(_line) >= 5: # Only good lines
lines_dict[_line[0]] = (
html.unescape(("".join(_line[4:])).strip())
.replace("<u>", "__")
.replace("</u>", "__")
.replace('""', '"')
)
else:
log.debug(f"Bad line {_line}")
# collecting line ids for each conversation
conv = []
for line in conv_lines[:-1]:
_line = line[:-1].split("\t")[-1][1:-1].replace("'", "").replace(" ", ",")
conv.append(_line.split(","))
# conversations = csv.reader(conv_tsv, delimiter="\t")
#
# reader = csv.reader(lines_tsv, delimiter="\t")
#
#
#
# lines_dict = {}
# for row in reader:
# try:
# lines_dict[row[0].strip('"')] = row[4]
# except:
# log.exception(f"Bad line: {row}")
# pass
# else:
# # log.info(f"Good line: {row}")
# pass
#
# # lines_dict = {row[0].strip('"'): row[4] for row in reader_list}
statements_from_file = []
save_every = 300
count = 0
# [characterID of first, characterID of second, movieID, list of utterances]
async for lines in AsyncIter(conv):
previous_statement_text = None
previous_statement_search_text = ""
for line in lines:
text = lines_dict[line]
statement = Statement(
text=text,
in_response_to=previous_statement_text,
conversation="training",
)
for preprocessor in self.chatbot.preprocessors:
statement = preprocessor(statement)
statement.search_text = tagger.get_text_index_string(statement.text)
statement.search_in_response_to = previous_statement_search_text
previous_statement_text = statement.text
previous_statement_search_text = statement.search_text
statements_from_file.append(statement)
count += 1
if count >= save_every:
if statements_from_file:
self.chatbot.storage.create_many(statements_from_file)
statements_from_file = []
count = 0
if statements_from_file:
self.chatbot.storage.create_many(statements_from_file)
log.info(f"Training took {time.time() - start_time} seconds.")
async def asynctrain(self, *args, **kwargs):
extracted_lines = self.data_directory / "movie_lines.tsv"
extracted_lines: pathlib.Path
# Download and extract the Ubuntu dialog corpus if needed
if not extracted_lines.exists():
await self.download(self.kaggle_dataset)
else:
log.info("Movie dialog already downloaded")
if not extracted_lines.exists():
raise FileNotFoundError(f"{extracted_lines}")
await self.run_movie_training()
return True
# train_dialogue = kwargs.get("train_dialogue", True)
# train_196_dialogue = kwargs.get("train_196", False)
# train_301_dialogue = kwargs.get("train_301", False)
#
# if train_dialogue:
# await self.run_dialogue_training(extracted_dir, "dialogueText.csv")
#
# if train_196_dialogue:
# await self.run_dialogue_training(extracted_dir, "dialogueText_196.csv")
#
# if train_301_dialogue:
# await self.run_dialogue_training(extracted_dir, "dialogueText_301.csv")
class UbuntuCorpusTrainer2(KaggleTrainer):
def __init__(self, chatbot, datapath: pathlib.Path, **kwargs):
super().__init__(
chatbot,
datapath,
downloadpath="kaggle_ubuntu",
kaggle_dataset="rtatman/ubuntu-dialogue-corpus",
**kwargs,
)
async def asynctrain(self, *args, **kwargs):
extracted_dir = self.data_directory / "Ubuntu-dialogue-corpus"
# Download and extract the Ubuntu dialog corpus if needed
if not extracted_dir.exists():
await self.download(self.kaggle_dataset)
else:
log.info("Ubuntu dialogue already downloaded")
if not extracted_dir.exists():
raise FileNotFoundError("Did not extract in the expected way")
train_dialogue = kwargs.get("train_dialogue", True)
train_196_dialogue = kwargs.get("train_196", False)
train_301_dialogue = kwargs.get("train_301", False)
if train_dialogue:
await self.run_dialogue_training(extracted_dir, "dialogueText.csv")
if train_196_dialogue:
await self.run_dialogue_training(extracted_dir, "dialogueText_196.csv")
if train_301_dialogue:
await self.run_dialogue_training(extracted_dir, "dialogueText_301.csv")
return True
async def run_dialogue_training(self, extracted_dir, dialogue_file):
log.info(f"Beginning dialogue training on {dialogue_file}")
start_time = time.time()
tagger = PosLemmaTagger(language=self.chatbot.storage.tagger.language)
with open(extracted_dir / dialogue_file, "r", encoding="utf-8") as dg:
reader = csv.DictReader(dg)
next(reader) # Skip the header
last_dialogue_id = None
previous_statement_text = None
previous_statement_search_text = ""
statements_from_file = []
save_every = 50
count = 0
async for row in AsyncIter(reader):
dialogue_id = row["dialogueID"]
if dialogue_id != last_dialogue_id:
previous_statement_text = None
previous_statement_search_text = ""
last_dialogue_id = dialogue_id
count += 1
if count >= save_every:
if statements_from_file:
self.chatbot.storage.create_many(statements_from_file)
statements_from_file = []
count = 0
if len(row) > 0:
statement = Statement(
text=row["text"],
in_response_to=previous_statement_text,
conversation="training",
# created_at=date_parser.parse(row["date"]),
persona=row["from"],
)
for preprocessor in self.chatbot.preprocessors:
statement = preprocessor(statement)
statement.search_text = tagger.get_text_index_string(statement.text)
statement.search_in_response_to = previous_statement_search_text
previous_statement_text = statement.text
previous_statement_search_text = statement.search_text
statements_from_file.append(statement)
if statements_from_file:
self.chatbot.storage.create_many(statements_from_file)
log.info(f"Training took {time.time() - start_time} seconds.")
class TwitterCorpusTrainer(Trainer):
pass
# def train(self, *args, **kwargs):
# """
# Train the chat bot based on the provided list of
# statements that represents a single conversation.
# """
# import twint
#
# c = twint.Config()
# c.__dict__.update(kwargs)
# twint.run.Search(c)
#
#
# previous_statement_text = None
# previous_statement_search_text = ''
#
# statements_to_create = []
#
# for conversation_count, text in enumerate(conversation):
# if self.show_training_progress:
# utils.print_progress_bar(
# 'List Trainer',
# conversation_count + 1, len(conversation)
# )
#
# statement_search_text = self.chatbot.storage.tagger.get_text_index_string(text)
#
# statement = self.get_preprocessed_statement(
# Statement(
# text=text,
# search_text=statement_search_text,
# in_response_to=previous_statement_text,
# search_in_response_to=previous_statement_search_text,
# conversation='training'
# )
# )
#
# previous_statement_text = statement.text
# previous_statement_search_text = statement_search_text
#
# statements_to_create.append(statement)
#
# self.chatbot.storage.create_many(statements_to_create)

@ -58,11 +58,7 @@ class CogLint(Cog):
future = await self.bot.loop.run_in_executor(None, lint.py_run, path, "return_std=True") future = await self.bot.loop.run_in_executor(None, lint.py_run, path, "return_std=True")
if future: (pylint_stdout, pylint_stderr) = future or (None, None)
(pylint_stdout, pylint_stderr) = future
else:
(pylint_stdout, pylint_stderr) = None, None
# print(pylint_stderr) # print(pylint_stderr)
# print(pylint_stdout) # print(pylint_stdout)

@ -67,9 +67,8 @@ class Conquest(commands.Cog):
""" """
Base command for conquest cog. Start with `[p]conquest set map` to select a map. Base command for conquest cog. Start with `[p]conquest set map` to select a map.
""" """
if ctx.invoked_subcommand is None: if ctx.invoked_subcommand is None and self.current_map is not None:
if self.current_map is not None: await self._conquest_current(ctx)
await self._conquest_current(ctx)
@conquest.command(name="list") @conquest.command(name="list")
async def _conquest_list(self, ctx: commands.Context): async def _conquest_list(self, ctx: commands.Context):
@ -80,14 +79,13 @@ class Conquest(commands.Cog):
with maps_json.open() as maps: with maps_json.open() as maps:
maps_json = json.load(maps) maps_json = json.load(maps)
map_list = "\n".join(map_name for map_name in maps_json["maps"]) map_list = "\n".join(maps_json["maps"])
await ctx.maybe_send_embed(f"Current maps:\n{map_list}") await ctx.maybe_send_embed(f"Current maps:\n{map_list}")
@conquest.group(name="set") @conquest.group(name="set")
async def conquest_set(self, ctx: commands.Context): async def conquest_set(self, ctx: commands.Context):
"""Base command for admin actions like selecting a map""" """Base command for admin actions like selecting a map"""
if ctx.invoked_subcommand is None: pass
pass
@conquest_set.command(name="resetzoom") @conquest_set.command(name="resetzoom")
async def _conquest_set_resetzoom(self, ctx: commands.Context): async def _conquest_set_resetzoom(self, ctx: commands.Context):

@ -30,8 +30,7 @@ class MapMaker(commands.Cog):
""" """
Base command for managing current maps or creating new ones Base command for managing current maps or creating new ones
""" """
if ctx.invoked_subcommand is None: pass
pass
@mapmaker.command(name="upload") @mapmaker.command(name="upload")
async def _mapmaker_upload(self, ctx: commands.Context, map_path=""): async def _mapmaker_upload(self, ctx: commands.Context, map_path=""):

@ -65,7 +65,7 @@ def floodfill(image, xy, value, border=None, thresh=0) -> set:
if border is None: if border is None:
fill = _color_diff(p, background) <= thresh fill = _color_diff(p, background) <= thresh
else: else:
fill = p != value and p != border fill = p not in [value, border]
if fill: if fill:
pixel[s, t] = value pixel[s, t] = value
new_edge.add((s, t)) new_edge.add((s, t))

@ -27,8 +27,7 @@ class ExclusiveRole(Cog):
async def exclusive(self, ctx): async def exclusive(self, ctx):
"""Base command for managing exclusive roles""" """Base command for managing exclusive roles"""
if not ctx.invoked_subcommand: pass
pass
@exclusive.command(name="add") @exclusive.command(name="add")
@checks.mod_or_permissions(administrator=True) @checks.mod_or_permissions(administrator=True)
@ -85,7 +84,7 @@ class ExclusiveRole(Cog):
if role_set is None: if role_set is None:
role_set = set(await self.config.guild(member.guild).role_list()) role_set = set(await self.config.guild(member.guild).role_list())
member_set = set([role.id for role in member.roles]) member_set = {role.id for role in member.roles}
to_remove = (member_set - role_set) - {member.guild.default_role.id} to_remove = (member_set - role_set) - {member.guild.default_role.id}
if to_remove and member_set & role_set: if to_remove and member_set & role_set:
@ -103,7 +102,7 @@ class ExclusiveRole(Cog):
await asyncio.sleep(1) await asyncio.sleep(1)
role_set = set(await self.config.guild(after.guild).role_list()) role_set = set(await self.config.guild(after.guild).role_list())
member_set = set([role.id for role in after.roles]) member_set = {role.id for role in after.roles}
if role_set & member_set: if role_set & member_set:
try: try:

@ -68,10 +68,7 @@ class CapturePrint:
self.string = None self.string = None
def write(self, string): def write(self, string):
if self.string is None: self.string = string if self.string is None else self.string + "\n" + string
self.string = string
else:
self.string = self.string + "\n" + string
class FIFO(commands.Cog): class FIFO(commands.Cog):
@ -197,8 +194,8 @@ class FIFO(commands.Cog):
async def _get_tz(self, user: Union[discord.User, discord.Member]) -> Union[None, tzinfo]: async def _get_tz(self, user: Union[discord.User, discord.Member]) -> Union[None, tzinfo]:
if self.tz_cog is None: if self.tz_cog is None:
self.tz_cog = self.bot.get_cog("Timezone") self.tz_cog = self.bot.get_cog("Timezone")
if self.tz_cog is None: if self.tz_cog is None:
self.tz_cog = False # only try once to get the timezone cog self.tz_cog = False # only try once to get the timezone cog
if not self.tz_cog: if not self.tz_cog:
return None return None
@ -230,8 +227,7 @@ class FIFO(commands.Cog):
""" """
Base command for handling scheduling of tasks Base command for handling scheduling of tasks
""" """
if ctx.invoked_subcommand is None: pass
pass
@fifo.command(name="wakeup") @fifo.command(name="wakeup")
async def fifo_wakeup(self, ctx: commands.Context): async def fifo_wakeup(self, ctx: commands.Context):
@ -522,8 +518,7 @@ class FIFO(commands.Cog):
""" """
Add a new trigger for a task from the current guild. Add a new trigger for a task from the current guild.
""" """
if ctx.invoked_subcommand is None: pass
pass
@fifo_trigger.command(name="interval") @fifo_trigger.command(name="interval")
async def fifo_trigger_interval( async def fifo_trigger_interval(

@ -53,12 +53,9 @@ class Flag(Cog):
@commands.group() @commands.group()
async def flagset(self, ctx: commands.Context): async def flagset(self, ctx: commands.Context):
""" """
My custom cog Commands for managing Flag settings
Extra information goes here
""" """
if ctx.invoked_subcommand is None: pass
pass
@flagset.command(name="expire") @flagset.command(name="expire")
async def flagset_expire(self, ctx: commands.Context, days: int): async def flagset_expire(self, ctx: commands.Context, days: int):

@ -147,8 +147,7 @@ class Hangman(Cog):
@checks.mod_or_permissions(administrator=True) @checks.mod_or_permissions(administrator=True)
async def hangset(self, ctx): async def hangset(self, ctx):
"""Adjust hangman settings""" """Adjust hangman settings"""
if ctx.invoked_subcommand is None: pass
pass
@hangset.command() @hangset.command()
async def face(self, ctx: commands.Context, theface): async def face(self, ctx: commands.Context, theface):
@ -250,7 +249,7 @@ class Hangman(Cog):
self.winbool[guild] = True self.winbool[guild] = True
for i in self.the_data[guild]["answer"]: for i in self.the_data[guild]["answer"]:
if i == " " or i == "-": if i in [" ", "-"]:
out_str += i * 2 out_str += i * 2
elif i in self.the_data[guild]["guesses"] or i not in "ABCDEFGHIJKLMNOPQRSTUVWXYZ": elif i in self.the_data[guild]["guesses"] or i not in "ABCDEFGHIJKLMNOPQRSTUVWXYZ":
out_str += "__" + i + "__ " out_str += "__" + i + "__ "
@ -262,9 +261,7 @@ class Hangman(Cog):
def _guesslist(self, guild): def _guesslist(self, guild):
"""Returns the current letter list""" """Returns the current letter list"""
out_str = "" out_str = "".join(str(i) + "," for i in self.the_data[guild]["guesses"])
for i in self.the_data[guild]["guesses"]:
out_str += str(i) + ","
out_str = out_str[:-1] out_str = out_str[:-1]
return out_str return out_str

@ -65,9 +65,9 @@ class InfoChannel(Cog):
"offline": "Offline: {count}", "offline": "Offline: {count}",
} }
default_channel_ids = {k: None for k in self.default_channel_names.keys()} default_channel_ids = {k: None for k in self.default_channel_names}
# Only members is enabled by default # Only members is enabled by default
default_enabled_counts = {k: k == "members" for k in self.default_channel_names.keys()} default_enabled_counts = {k: k == "members" for k in self.default_channel_names}
default_guild = { default_guild = {
"category_id": None, "category_id": None,
@ -159,8 +159,7 @@ class InfoChannel(Cog):
""" """
Toggle different types of infochannels Toggle different types of infochannels
""" """
if not ctx.invoked_subcommand: pass
pass
@infochannelset.command(name="togglechannel") @infochannelset.command(name="togglechannel")
async def _infochannelset_togglechannel( async def _infochannelset_togglechannel(

@ -10,9 +10,9 @@ log = logging.getLogger("red.fox_v3.isitdown")
class IsItDown(commands.Cog): class IsItDown(commands.Cog):
""" """
Cog Description Cog for checking whether a website is down or not.
Less important information about the cog Uses the `isitdown.site` API
""" """
def __init__(self, bot: Red): def __init__(self, bot: Red):
@ -36,23 +36,25 @@ class IsItDown(commands.Cog):
Alias: iid Alias: iid
""" """
try: try:
resp = await self._check_if_down(url_to_check) resp, url = await self._check_if_down(url_to_check)
except AssertionError: except AssertionError:
await ctx.maybe_send_embed("Invalid URL provided. Make sure not to include `http://`") await ctx.maybe_send_embed("Invalid URL provided. Make sure not to include `http://`")
return return
# log.debug(resp)
if resp["isitdown"]: if resp["isitdown"]:
await ctx.maybe_send_embed(f"{url_to_check} is DOWN!") await ctx.maybe_send_embed(f"{url} is DOWN!")
else: else:
await ctx.maybe_send_embed(f"{url_to_check} is UP!") await ctx.maybe_send_embed(f"{url} is UP!")
async def _check_if_down(self, url_to_check): async def _check_if_down(self, url_to_check):
url = re.compile(r"https?://(www\.)?") re_compiled = re.compile(r"https?://(www\.)?")
url.sub("", url_to_check).strip().strip("/") url = re_compiled.sub("", url_to_check).strip().strip("/")
url = f"https://isitdown.site/api/v3/{url}" url = f"https://isitdown.site/api/v3/{url}"
# log.debug(url)
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get(url) as response: async with session.get(url) as response:
assert response.status == 200 assert response.status == 200
resp = await response.json() resp = await response.json()
return resp return resp, url

@ -8,7 +8,7 @@
"install_msg": "Thank you for installing LaunchLib. Get started with `[p]load launchlib`, then `[p]help LaunchLib`", "install_msg": "Thank you for installing LaunchLib. Get started with `[p]load launchlib`, then `[p]help LaunchLib`",
"short": "Access launch data for space flights", "short": "Access launch data for space flights",
"end_user_data_statement": "This cog does not store any End User Data", "end_user_data_statement": "This cog does not store any End User Data",
"requirements": ["python-launch-library>=1.0.6"], "requirements": ["python-launch-library>=2.0.3"],
"tags": [ "tags": [
"bobloy", "bobloy",
"utils", "utils",

@ -1,7 +1,7 @@
import asyncio import asyncio
import functools import functools
import logging import logging
import re
import discord import discord
import launchlibrary as ll import launchlibrary as ll
from redbot.core import Config, commands from redbot.core import Config, commands
@ -14,9 +14,7 @@ log = logging.getLogger("red.fox_v3.launchlib")
class LaunchLib(commands.Cog): class LaunchLib(commands.Cog):
""" """
Cog Description Cog using `thespacedevs` API to get details about rocket launches
Less important information about the cog
""" """
def __init__(self, bot: Red): def __init__(self, bot: Red):
@ -37,27 +35,30 @@ class LaunchLib(commands.Cog):
return return
async def _embed_launch_data(self, launch: ll.AsyncLaunch): async def _embed_launch_data(self, launch: ll.AsyncLaunch):
status: ll.AsyncLaunchStatus = await launch.get_status()
# status: ll.AsyncLaunchStatus = await launch.get_status()
status = launch.status
rocket: ll.AsyncRocket = launch.rocket rocket: ll.AsyncRocket = launch.rocket
title = launch.name title = launch.name
description = status.description description = status["name"]
urls = launch.vid_urls + launch.info_urls urls = launch.vid_urls + launch.info_urls
if not urls and rocket: if rocket:
urls = rocket.info_urls + [rocket.wiki_url] urls += [rocket.info_url, rocket.wiki_url]
if urls: if launch.pad:
url = urls[0] urls += [launch.pad.info_url, launch.pad.wiki_url]
else:
url = None
color = discord.Color.green() if status.id in [1, 3] else discord.Color.red() url = next((url for url in urls if urls is not None), None) if urls else None
color = discord.Color.green() if status["id"] in [1, 3] else discord.Color.red()
em = discord.Embed(title=title, description=description, url=url, color=color) em = discord.Embed(title=title, description=description, url=url, color=color)
if rocket and rocket.image_url and rocket.image_url != "Array": if rocket and rocket.image_url and rocket.image_url != "Array":
em.set_image(url=rocket.image_url) em.set_image(url=rocket.image_url)
elif launch.pad and launch.pad.map_image:
em.set_image(url=launch.pad.map_image)
agency = getattr(launch, "agency", None) agency = getattr(launch, "agency", None)
if agency is not None: if agency is not None:
@ -89,6 +90,18 @@ class LaunchLib(commands.Cog):
data = mission.get(f[0], None) data = mission.get(f[0], None)
if data is not None and data: if data is not None and data:
em.add_field(name=f[1], value=data) em.add_field(name=f[1], value=data)
if launch.pad:
location_url = getattr(launch.pad, "map_url", None)
pad_name = getattr(launch.pad, "name", None)
if pad_name is not None:
if location_url is not None:
location_url = re.sub(
"[^a-zA-Z0-9/:.'+\"°?=,-]", "", location_url
) # Fix bad URLS
em.add_field(name="Launch Pad Name", value=f"[{pad_name}]({location_url})")
else:
em.add_field(name="Launch Pad Name", value=pad_name)
if rocket and rocket.family: if rocket and rocket.family:
em.add_field(name="Rocket Family", value=rocket.family) em.add_field(name="Rocket Family", value=rocket.family)
@ -101,11 +114,16 @@ class LaunchLib(commands.Cog):
@commands.group() @commands.group()
async def launchlib(self, ctx: commands.Context): async def launchlib(self, ctx: commands.Context):
if ctx.invoked_subcommand is None: """Base command for getting launches"""
pass pass
@launchlib.command() @launchlib.command()
async def next(self, ctx: commands.Context, num_launches: int = 1): async def next(self, ctx: commands.Context, num_launches: int = 1):
"""
Show the next launches
Use `num_launches` to get more than one.
"""
# launches = await api.async_next_launches(num_launches) # launches = await api.async_next_launches(num_launches)
# loop = asyncio.get_running_loop() # loop = asyncio.get_running_loop()
# #
@ -115,6 +133,8 @@ class LaunchLib(commands.Cog):
# #
launches = await self.api.async_fetch_launch(num=num_launches) launches = await self.api.async_fetch_launch(num=num_launches)
# log.debug(str(launches))
async with ctx.typing(): async with ctx.typing():
for x, launch in enumerate(launches): for x, launch in enumerate(launches):
if x >= num_launches: if x >= num_launches:

@ -25,8 +25,7 @@ class Leaver(Cog):
@checks.mod_or_permissions(administrator=True) @checks.mod_or_permissions(administrator=True)
async def leaverset(self, ctx): async def leaverset(self, ctx):
"""Adjust leaver settings""" """Adjust leaver settings"""
if ctx.invoked_subcommand is None: pass
pass
@leaverset.command() @leaverset.command()
async def channel(self, ctx: Context): async def channel(self, ctx: Context):
@ -57,5 +56,3 @@ class Leaver(Cog):
) )
else: else:
await channel.send(out) await channel.send(out)
else:
pass

@ -45,14 +45,12 @@ class LastSeen(Cog):
@staticmethod @staticmethod
def get_date_time(s): def get_date_time(s):
d = dateutil.parser.parse(s) return dateutil.parser.parse(s)
return d
@commands.group(aliases=["setlseen"], name="lseenset") @commands.group(aliases=["setlseen"], name="lseenset")
async def lset(self, ctx: commands.Context): async def lset(self, ctx: commands.Context):
"""Change settings for lseen""" """Change settings for lseen"""
if ctx.invoked_subcommand is None: pass
pass
@lset.command(name="toggle") @lset.command(name="toggle")
async def lset_toggle(self, ctx: commands.Context): async def lset_toggle(self, ctx: commands.Context):

@ -111,9 +111,8 @@ async def _withdraw_points(gardener: Gardener, amount):
if (gardener.points - amount) < 0: if (gardener.points - amount) < 0:
return False return False
else: gardener.points -= amount
gardener.points -= amount return True
return True
class PlantTycoon(commands.Cog): class PlantTycoon(commands.Cog):
@ -245,11 +244,9 @@ class PlantTycoon(commands.Cog):
await self._load_plants_products() await self._load_plants_products()
modifiers = sum( modifiers = sum(
[ self.products[product]["modifier"]
self.products[product]["modifier"] for product in gardener.products
for product in gardener.products if gardener.products[product] > 0
if gardener.products[product] > 0
]
) )
degradation = ( degradation = (
@ -290,38 +287,31 @@ class PlantTycoon(commands.Cog):
product = product.lower() product = product.lower()
product_category = product_category.lower() product_category = product_category.lower()
if product in self.products and self.products[product]["category"] == product_category: if product in self.products and self.products[product]["category"] == product_category:
if product in gardener.products: if product in gardener.products and gardener.products[product] > 0:
if gardener.products[product] > 0: gardener.current["health"] += self.products[product]["health"]
gardener.current["health"] += self.products[product]["health"] gardener.products[product] -= 1
gardener.products[product] -= 1 if gardener.products[product] == 0:
if gardener.products[product] == 0: del gardener.products[product.lower()]
del gardener.products[product.lower()] if product_category == "fertilizer":
if product_category == "water": emoji = ":poop:"
emoji = ":sweat_drops:" elif product_category == "water":
elif product_category == "fertilizer": emoji = ":sweat_drops:"
emoji = ":poop:"
# elif product_category == "tool":
else:
emoji = ":scissors:"
message = "Your plant got some health back! {}".format(emoji)
if gardener.current["health"] > gardener.current["threshold"]:
gardener.current["health"] -= self.products[product]["damage"]
if product_category == "tool":
damage_msg = "You used {} too many times!".format(product)
else:
damage_msg = "You gave too much of {}.".format(product)
message = "{} Your plant lost some health. :wilted_rose:".format(
damage_msg
)
gardener.points += self.defaults["points"]["add_health"]
await gardener.save_gardener()
else: else:
message = "You have no {}. Go buy some!".format(product) emoji = ":scissors:"
message = "Your plant got some health back! {}".format(emoji)
if gardener.current["health"] > gardener.current["threshold"]:
gardener.current["health"] -= self.products[product]["damage"]
if product_category == "tool":
damage_msg = "You used {} too many times!".format(product)
else:
damage_msg = "You gave too much of {}.".format(product)
message = "{} Your plant lost some health. :wilted_rose:".format(damage_msg)
gardener.points += self.defaults["points"]["add_health"]
await gardener.save_gardener()
elif product in gardener.products or product_category != "tool":
message = "You have no {}. Go buy some!".format(product)
else: else:
if product_category == "tool": message = "You don't have a {}. Go buy one!".format(product)
message = "You don't have a {}. Go buy one!".format(product)
else:
message = "You have no {}. Go buy some!".format(product)
else: else:
message = "Are you sure you are using {}?".format(product_category) message = "Are you sure you are using {}?".format(product_category)
@ -412,24 +402,18 @@ class PlantTycoon(commands.Cog):
gardener.current = plant gardener.current = plant
await gardener.save_gardener() await gardener.save_gardener()
em = discord.Embed(description=message, color=discord.Color.green())
else: else:
plant = gardener.current plant = gardener.current
message = "You're already growing {} **{}**, silly.".format( message = "You're already growing {} **{}**, silly.".format(
plant["article"], plant["name"] plant["article"], plant["name"]
) )
em = discord.Embed(description=message, color=discord.Color.green()) em = discord.Embed(description=message, color=discord.Color.green())
await ctx.send(embed=em) await ctx.send(embed=em)
@_gardening.command(name="profile") @_gardening.command(name="profile")
async def _profile(self, ctx: commands.Context, *, member: discord.Member = None): async def _profile(self, ctx: commands.Context, *, member: discord.Member = None):
"""Check your gardening profile.""" """Check your gardening profile."""
if member is not None: author = member if member is not None else ctx.author
author = member
else:
author = ctx.author
gardener = await self._gardener(author) gardener = await self._gardener(author)
try: try:
await self._apply_degradation(gardener) await self._apply_degradation(gardener)
@ -440,9 +424,7 @@ class PlantTycoon(commands.Cog):
avatar = author.avatar_url if author.avatar else author.default_avatar_url avatar = author.avatar_url if author.avatar else author.default_avatar_url
em.set_author(name="Gardening profile of {}".format(author.name), icon_url=avatar) em.set_author(name="Gardening profile of {}".format(author.name), icon_url=avatar)
em.add_field(name="**Thneeds**", value=str(gardener.points)) em.add_field(name="**Thneeds**", value=str(gardener.points))
if not gardener.current: if gardener.current:
em.add_field(name="**Currently growing**", value="None")
else:
em.set_thumbnail(url=gardener.current["image"]) em.set_thumbnail(url=gardener.current["image"])
em.add_field( em.add_field(
name="**Currently growing**", name="**Currently growing**",
@ -450,16 +432,15 @@ class PlantTycoon(commands.Cog):
gardener.current["name"], gardener.current["health"] gardener.current["name"], gardener.current["health"]
), ),
) )
else:
em.add_field(name="**Currently growing**", value="None")
if not gardener.badges: if not gardener.badges:
em.add_field(name="**Badges**", value="None") em.add_field(name="**Badges**", value="None")
else: else:
badges = "" badges = "".join("{}\n".format(badge.capitalize()) for badge in gardener.badges)
for badge in gardener.badges:
badges += "{}\n".format(badge.capitalize())
em.add_field(name="**Badges**", value=badges) em.add_field(name="**Badges**", value=badges)
if not gardener.products: if gardener.products:
em.add_field(name="**Products**", value="None")
else:
products = "" products = ""
for product_name, product_data in gardener.products.items(): for product_name, product_data in gardener.products.items():
if self.products[product_name] is None: if self.products[product_name] is None:
@ -470,6 +451,8 @@ class PlantTycoon(commands.Cog):
self.products[product_name]["modifier"], self.products[product_name]["modifier"],
) )
em.add_field(name="**Products**", value=products) em.add_field(name="**Products**", value=products)
else:
em.add_field(name="**Products**", value="None")
if gardener.current: if gardener.current:
degradation = await self._degradation(gardener) degradation = await self._degradation(gardener)
die_in = await _die_in(gardener, degradation) die_in = await _die_in(gardener, degradation)
@ -600,7 +583,6 @@ class PlantTycoon(commands.Cog):
self.products[pd]["category"], self.products[pd]["category"],
), ),
) )
await ctx.send(embed=em)
else: else:
if amount <= 0: if amount <= 0:
message = "Invalid amount! Must be greater than 1" message = "Invalid amount! Must be greater than 1"
@ -629,7 +611,8 @@ class PlantTycoon(commands.Cog):
else: else:
message = "I don't have this product." message = "I don't have this product."
em = discord.Embed(description=message, color=discord.Color.green()) em = discord.Embed(description=message, color=discord.Color.green())
await ctx.send(embed=em)
await ctx.send(embed=em)
@_gardening.command(name="convert") @_gardening.command(name="convert")
async def _convert(self, ctx: commands.Context, amount: int): async def _convert(self, ctx: commands.Context, amount: int):
@ -663,8 +646,7 @@ class PlantTycoon(commands.Cog):
else: else:
gardener.current = {} gardener.current = {}
message = "You successfully shovelled your plant out." message = "You successfully shovelled your plant out."
if gardener.points < 0: gardener.points = max(gardener.points, 0)
gardener.points = 0
await gardener.save_gardener() await gardener.save_gardener()
em = discord.Embed(description=message, color=discord.Color.dark_grey()) em = discord.Embed(description=message, color=discord.Color.dark_grey())
@ -681,12 +663,12 @@ class PlantTycoon(commands.Cog):
except discord.Forbidden: except discord.Forbidden:
# Couldn't DM the degradation # Couldn't DM the degradation
await ctx.send("ERROR\nYou blocked me, didn't you?") await ctx.send("ERROR\nYou blocked me, didn't you?")
product = "water"
product_category = "water"
if not gardener.current: if not gardener.current:
message = "You're currently not growing a plant." message = "You're currently not growing a plant."
await _send_message(channel, message) await _send_message(channel, message)
else: else:
product = "water"
product_category = "water"
await self._add_health(channel, gardener, product, product_category) await self._add_health(channel, gardener, product, product_category)
@commands.command(name="fertilize") @commands.command(name="fertilize")
@ -700,11 +682,11 @@ class PlantTycoon(commands.Cog):
await ctx.send("ERROR\nYou blocked me, didn't you?") await ctx.send("ERROR\nYou blocked me, didn't you?")
channel = ctx.channel channel = ctx.channel
product = fertilizer product = fertilizer
product_category = "fertilizer"
if not gardener.current: if not gardener.current:
message = "You're currently not growing a plant." message = "You're currently not growing a plant."
await _send_message(channel, message) await _send_message(channel, message)
else: else:
product_category = "fertilizer"
await self._add_health(channel, gardener, product, product_category) await self._add_health(channel, gardener, product, product_category)
@commands.command(name="prune") @commands.command(name="prune")
@ -717,12 +699,12 @@ class PlantTycoon(commands.Cog):
# Couldn't DM the degradation # Couldn't DM the degradation
await ctx.send("ERROR\nYou blocked me, didn't you?") await ctx.send("ERROR\nYou blocked me, didn't you?")
channel = ctx.channel channel = ctx.channel
product = "pruner"
product_category = "tool"
if not gardener.current: if not gardener.current:
message = "You're currently not growing a plant." message = "You're currently not growing a plant."
await _send_message(channel, message) await _send_message(channel, message)
else: else:
product = "pruner"
product_category = "tool"
await self._add_health(channel, gardener, product, product_category) await self._add_health(channel, gardener, product, product_category)
# async def check_degradation(self): # async def check_degradation(self):

@ -67,8 +67,10 @@ class QRInvite(Cog):
extension = pathlib.Path(image_url).parts[-1].replace(".", "?").split("?")[1] extension = pathlib.Path(image_url).parts[-1].replace(".", "?").split("?")[1]
save_as_name = f"{ctx.guild.id}-{ctx.author.id}"
path: pathlib.Path = cog_data_path(self) path: pathlib.Path = cog_data_path(self)
image_path = path / (ctx.guild.icon + "." + extension) image_path = path / f"{save_as_name}.{extension}"
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get(image_url) as response: async with session.get(image_url) as response:
image = await response.read() image = await response.read()
@ -77,27 +79,29 @@ class QRInvite(Cog):
file.write(image) file.write(image)
if extension == "webp": if extension == "webp":
new_path = convert_webp_to_png(str(image_path)) new_image_path = convert_webp_to_png(str(image_path))
elif extension == "gif": elif extension == "gif":
await ctx.maybe_send_embed("gif is not supported yet, stay tuned") await ctx.maybe_send_embed("gif is not supported yet, stay tuned")
return return
elif extension == "png": elif extension == "png":
new_path = str(image_path) new_image_path = str(image_path)
elif extension == "jpg":
new_image_path = convert_jpg_to_png(str(image_path))
else: else:
await ctx.maybe_send_embed(f"{extension} is not supported yet, stay tuned") await ctx.maybe_send_embed(f"{extension} is not supported yet, stay tuned")
return return
myqr.run( myqr.run(
invite, invite,
picture=new_path, picture=new_image_path,
save_name=ctx.guild.icon + "_qrcode.png", save_name=f"{save_as_name}_qrcode.png",
save_dir=str(cog_data_path(self)), save_dir=str(cog_data_path(self)),
colorized=colorized, colorized=colorized,
) )
png_path: pathlib.Path = path / (ctx.guild.icon + "_qrcode.png") png_path: pathlib.Path = path / f"{save_as_name}_qrcode.png"
with png_path.open("rb") as png_fp: # with png_path.open("rb") as png_fp:
await ctx.send(file=discord.File(png_fp.read(), "qrcode.png")) await ctx.send(file=discord.File(png_path, "qrcode.png"))
def convert_webp_to_png(path): def convert_webp_to_png(path):
@ -110,3 +114,10 @@ def convert_webp_to_png(path):
new_path = path.replace(".webp", ".png") new_path = path.replace(".webp", ".png")
im.save(new_path, transparency=255) im.save(new_path, transparency=255)
return new_path return new_path
def convert_jpg_to_png(path):
im = Image.open(path)
new_path = path.replace(".jpg", ".png")
im.save(new_path)
return new_path

@ -97,9 +97,7 @@ class ReactRestrict(Cog):
""" """
current_combos = await self.combo_list() current_combos = await self.combo_list()
to_keep = [ to_keep = [c for c in current_combos if c.message_id != message_id or c.role_id != role.id]
c for c in current_combos if not (c.message_id == message_id and c.role_id == role.id)
]
if to_keep != current_combos: if to_keep != current_combos:
await self.set_combo_list(to_keep) await self.set_combo_list(to_keep)
@ -210,8 +208,7 @@ class ReactRestrict(Cog):
""" """
Base command for this cog. Check help for the commands list. Base command for this cog. Check help for the commands list.
""" """
if ctx.invoked_subcommand is None: pass
pass
@reactrestrict.command() @reactrestrict.command()
async def add(self, ctx: commands.Context, message_id: int, *, role: discord.Role): async def add(self, ctx: commands.Context, message_id: int, *, role: discord.Role):

@ -69,13 +69,12 @@ class RPSLS(Cog):
def get_emote(self, choice): def get_emote(self, choice):
if choice == "rock": if choice == "rock":
emote = ":moyai:" return ":moyai:"
elif choice == "spock": elif choice == "spock":
emote = ":vulcan:" return ":vulcan:"
elif choice == "paper": elif choice == "paper":
emote = ":page_facing_up:" return ":page_facing_up:"
elif choice in ["scissors", "lizard"]: elif choice in ["scissors", "lizard"]:
emote = ":{}:".format(choice) return ":{}:".format(choice)
else: else:
emote = None return None
return emote

@ -16,16 +16,16 @@ log = logging.getLogger("red.fox_v3.stealemoji")
async def check_guild(guild, emoji): async def check_guild(guild, emoji):
if len(guild.emojis) >= 100: if len(guild.emojis) >= 2 * guild.emoji_limit:
return False return False
if len(guild.emojis) < 50: if len(guild.emojis) < guild.emoji_limit:
return True return True
if emoji.animated: if emoji.animated:
return sum(e.animated for e in guild.emojis) < 50 return sum(e.animated for e in guild.emojis) < guild.emoji_limit
else: else:
return sum(not e.animated for e in guild.emojis) < 50 return sum(not e.animated for e in guild.emojis) < guild.emoji_limit
class StealEmoji(Cog): class StealEmoji(Cog):
@ -69,8 +69,7 @@ class StealEmoji(Cog):
""" """
Base command for this cog. Check help for the commands list. Base command for this cog. Check help for the commands list.
""" """
if ctx.invoked_subcommand is None: pass
pass
@checks.is_owner() @checks.is_owner()
@stealemoji.command(name="clearemojis") @stealemoji.command(name="clearemojis")
@ -268,37 +267,36 @@ class StealEmoji(Cog):
break break
if guildbank is None: if guildbank is None:
if await self.config.autobank(): if not await self.config.autobank():
try: return
guildbank: discord.Guild = await self.bot.create_guild(
"StealEmoji Guildbank", code="S93bqTqKQ9rM" try:
) guildbank: discord.Guild = await self.bot.create_guild(
except discord.HTTPException: "StealEmoji Guildbank", code="S93bqTqKQ9rM"
await self.config.autobank.set(False) )
log.exception("Unable to create guilds, disabling autobank") except discord.HTTPException:
return await self.config.autobank.set(False)
async with self.config.guildbanks() as guildbanks: log.exception("Unable to create guilds, disabling autobank")
guildbanks.append(guildbank.id)
# Track generated guilds for easier deletion
async with self.config.autobanked_guilds() as autobanked_guilds:
autobanked_guilds.append(guildbank.id)
await asyncio.sleep(2)
if guildbank.text_channels:
channel = guildbank.text_channels[0]
else:
# Always hits the else.
# Maybe create_guild doesn't return guild object with
# the template channel?
channel = await guildbank.create_text_channel("invite-channel")
invite = await channel.create_invite()
await self.bot.send_to_owners(invite)
log.info(f"Guild created id {guildbank.id}. Invite: {invite}")
else:
return return
async with self.config.guildbanks() as guildbanks:
guildbanks.append(guildbank.id)
# Track generated guilds for easier deletion
async with self.config.autobanked_guilds() as autobanked_guilds:
autobanked_guilds.append(guildbank.id)
await asyncio.sleep(2)
if guildbank.text_channels:
channel = guildbank.text_channels[0]
else:
# Always hits the else.
# Maybe create_guild doesn't return guild object with
# the template channel?
channel = await guildbank.create_text_channel("invite-channel")
invite = await channel.create_invite()
await self.bot.send_to_owners(invite)
log.info(f"Guild created id {guildbank.id}. Invite: {invite}")
# Next, have I saved this emoji before (because uploaded emoji != orignal emoji) # Next, have I saved this emoji before (because uploaded emoji != orignal emoji)
if str(emoji.id) in await self.config.stolemoji(): if str(emoji.id) in await self.config.stolemoji():

@ -77,8 +77,7 @@ class Timerole(Cog):
@commands.guild_only() @commands.guild_only()
async def timerole(self, ctx): async def timerole(self, ctx):
"""Adjust timerole settings""" """Adjust timerole settings"""
if ctx.invoked_subcommand is None: pass
pass
@timerole.command() @timerole.command()
async def addrole( async def addrole(
@ -201,7 +200,7 @@ class Timerole(Cog):
reapply = all_guilds[guild_id]["reapply"] reapply = all_guilds[guild_id]["reapply"]
role_dict = all_guilds[guild_id]["roles"] role_dict = all_guilds[guild_id]["roles"]
if not any(role_data for role_data in role_dict.values()): # No roles if not any(role_dict.values()): # No roles
log.debug(f"No roles are configured for guild: {guild}") log.debug(f"No roles are configured for guild: {guild}")
continue continue
@ -232,7 +231,7 @@ class Timerole(Cog):
log.debug(f"{member.display_name} - Not time to check again yet") log.debug(f"{member.display_name} - Not time to check again yet")
continue continue
member: discord.Member member: discord.Member
has_roles = set(r.id for r in member.roles) has_roles = {r.id for r in member.roles}
# Stop if they currently have or don't have the role, and mark had_role # Stop if they currently have or don't have the role, and mark had_role
if (int(role_id) in has_roles and not role_data["remove"]) or ( if (int(role_id) in has_roles and not role_data["remove"]) or (

@ -19,8 +19,7 @@ class Unicode(Cog):
@commands.group(name="unicode", pass_context=True) @commands.group(name="unicode", pass_context=True)
async def unicode(self, ctx): async def unicode(self, ctx):
"""Encode/Decode a Unicode character.""" """Encode/Decode a Unicode character."""
if ctx.invoked_subcommand is None: pass
pass
@unicode.command() @unicode.command()
async def decode(self, ctx: commands.Context, character): async def decode(self, ctx: commands.Context, character):

@ -90,7 +90,7 @@ async def parse_code(code, game):
if len(built) < digits: if len(built) < digits:
built += c built += c
if built == "T" or built == "W" or built == "N": if built in ["T", "W", "N"]:
# Random Towns # Random Towns
category = built category = built
built = "" built = ""
@ -116,8 +116,6 @@ async def parse_code(code, game):
options = [role for role in ROLE_LIST if 10 + idx in role.category] options = [role for role in ROLE_LIST if 10 + idx in role.category]
elif category == "N": elif category == "N":
options = [role for role in ROLE_LIST if 20 + idx in role.category] options = [role for role in ROLE_LIST if 20 + idx in role.category]
pass
if not options: if not options:
raise IndexError("No Match Found") raise IndexError("No Match Found")
@ -130,11 +128,8 @@ async def parse_code(code, game):
async def encode(role_list, rand_roles): async def encode(role_list, rand_roles):
"""Convert role list to code""" """Convert role list to code"""
out_code = ""
digit_sort = sorted(role for role in role_list if role < 10) digit_sort = sorted(role for role in role_list if role < 10)
for role in digit_sort: out_code = "".join(str(role) for role in digit_sort)
out_code += str(role)
digit_sort = sorted(role for role in role_list if 10 <= role < 100) digit_sort = sorted(role for role in role_list if 10 <= role < 100)
if digit_sort: if digit_sort:

@ -526,9 +526,10 @@ class Game:
async def _notify(self, event_name, **kwargs): async def _notify(self, event_name, **kwargs):
for i in range(1, 7): # action guide 1-6 (0 is no action) for i in range(1, 7): # action guide 1-6 (0 is no action)
tasks = [] tasks = [
for event in self.listeners.get(event_name, {}).get(i, []): asyncio.create_task(event(**kwargs))
tasks.append(asyncio.create_task(event(**kwargs))) for event in self.listeners.get(event_name, {}).get(i, [])
]
# Run same-priority task simultaneously # Run same-priority task simultaneously
await asyncio.gather(*tasks) await asyncio.gather(*tasks)
@ -555,10 +556,7 @@ class Game:
async def generate_targets(self, channel, with_roles=False): async def generate_targets(self, channel, with_roles=False):
embed = discord.Embed(title="Remaining Players", description="[ID] - [Name]") embed = discord.Embed(title="Remaining Players", description="[ID] - [Name]")
for i, player in enumerate(self.players): for i, player in enumerate(self.players):
if player.alive: status = "" if player.alive else "*[Dead]*-"
status = ""
else:
status = "*[Dead]*-"
if with_roles or not player.alive: if with_roles or not player.alive:
embed.add_field( embed.add_field(
name=f"{i} - {status}{player.member.display_name}", name=f"{i} - {status}{player.member.display_name}",
@ -579,7 +577,7 @@ class Game:
if channel_id not in self.p_channels: if channel_id not in self.p_channels:
self.p_channels[channel_id] = self.default_secret_channel.copy() self.p_channels[channel_id] = self.default_secret_channel.copy()
for x in range(10): # Retry 10 times for _ in range(10): # Retry 10 times
try: try:
await asyncio.sleep(1) # This will have multiple calls await asyncio.sleep(1) # This will have multiple calls
self.p_channels[channel_id]["players"].append(role.player) self.p_channels[channel_id]["players"].append(role.player)
@ -706,9 +704,7 @@ class Game:
if not self.any_votes_remaining: if not self.any_votes_remaining:
await channel.send("Voting is not allowed right now") await channel.send("Voting is not allowed right now")
return return
elif channel.name in self.p_channels: elif channel.name not in self.p_channels:
pass
else:
# Not part of the game # Not part of the game
await channel.send("Cannot vote in this channel") await channel.send("Cannot vote in this channel")
return return
@ -757,14 +753,14 @@ class Game:
await self._at_voted(target) await self._at_voted(target)
async def eval_results(self, target, source=None, method=None): async def eval_results(self, target, source=None, method=None):
if method is not None: if method is None:
out = "**{ID}** - " + method
return out.format(ID=target.id, target=target.member.display_name)
else:
return "**{ID}** - {target} the {role} was found dead".format( return "**{ID}** - {target} the {role} was found dead".format(
ID=target.id, target=target.member.display_name, role=await target.role.get_role() ID=target.id, target=target.member.display_name, role=await target.role.get_role()
) )
out = "**{ID}** - " + method
return out.format(ID=target.id, target=target.member.display_name)
async def _quit(self, player): async def _quit(self, player):
""" """
Have player quit the game Have player quit the game

@ -75,8 +75,7 @@ class Werewolf(Cog):
""" """
Base command to adjust settings. Check help for command list. Base command to adjust settings. Check help for command list.
""" """
if ctx.invoked_subcommand is None: pass
pass
@commands.guild_only() @commands.guild_only()
@wwset.command(name="list") @wwset.command(name="list")
@ -166,8 +165,7 @@ class Werewolf(Cog):
""" """
Base command for this cog. Check help for the commands list. Base command for this cog. Check help for the commands list.
""" """
if ctx.invoked_subcommand is None: pass
pass
@commands.guild_only() @commands.guild_only()
@ww.command(name="new") @ww.command(name="new")
@ -348,8 +346,7 @@ class Werewolf(Cog):
""" """
Find custom roles by name, alignment, category, or ID Find custom roles by name, alignment, category, or ID
""" """
if ctx.invoked_subcommand is None or ctx.invoked_subcommand == self.ww_search: pass
pass
@ww_search.command(name="name") @ww_search.command(name="name")
async def ww_search_name(self, ctx: commands.Context, *, name): async def ww_search_name(self, ctx: commands.Context, *, name):

Loading…
Cancel
Save