Compare commits

..

3 Commits

Author SHA1 Message Date
bobloy 0250a297e1 Better message
4 years ago
bobloy 711c83ddbb Enable configurable day/night lengths
4 years ago
bobloy 4523ffc98d Adjustments to setup
4 years ago

@ -53,7 +53,7 @@ Check out *Deprecated* my V2 cogs at [Fox-Cogs v2](https://github.com/bobloy/Fox
# Contact
Get support on the [Third Party Cog Server](https://discord.gg/GET4DVk)
Feel free to @ me in the #support_fox-v3 channel
Feel free to @ me in the #support_othercogs channel
Discord: Bobloy#6513

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

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

@ -3,7 +3,6 @@ import logging
import re
import discord
from discord.ext.commands import RoleConverter, Greedy, CommandError, ArgumentParsingError
from discord.ext.commands.view import StringView
from redbot.core import Config, checks, commands
from redbot.core.bot import Red
@ -14,38 +13,15 @@ log = logging.getLogger("red.fox_v3.ccrole")
async def _get_roles_from_content(ctx, content):
# greedy = Greedy[RoleConverter]
view = StringView(content)
rc = RoleConverter()
# "Borrowed" from discord.ext.commands.Command._transform_greedy_pos
result = []
while not view.eof:
# for use with a manual undo
previous = view.index
view.skip_ws()
content_list = content.split(",")
try:
argument = view.get_quoted_word()
value = await rc.convert(ctx, argument)
except (CommandError, ArgumentParsingError):
view.index = previous
break
role_list = [
discord.utils.get(ctx.guild.roles, name=role.strip(" ")).id for role in content_list
]
except (discord.HTTPException, AttributeError): # None.id is attribute error
return None
else:
result.append(value)
return [r.id for r in result]
# Old method
# content_list = content.split(",")
# try:
# role_list = [
# discord.utils.get(ctx.guild.roles, name=role.strip(" ")).id for role in content_list
# ]
# except (discord.HTTPException, AttributeError): # None.id is attribute error
# return None
# else:
# return role_list
return role_list
class CCRole(commands.Cog):
@ -72,6 +48,7 @@ class CCRole(commands.Cog):
"""Custom commands management with roles
Highly customizable custom commands with role management."""
if not ctx.invoked_subcommand:
pass
@ccrole.command(name="add")
@ -108,7 +85,7 @@ class CCRole(commands.Cog):
# Roles to add
await ctx.send(
"What roles should it add?\n"
"What roles should it add? (Must be **comma separated**)\n"
"Say `None` to skip adding roles"
)
@ -130,7 +107,7 @@ class CCRole(commands.Cog):
# Roles to remove
await ctx.send(
"What roles should it remove?\n"
"What roles should it remove? (Must be comma separated)\n"
"Say `None` to skip removing roles"
)
try:
@ -148,7 +125,7 @@ class CCRole(commands.Cog):
# Roles to use
await ctx.send(
"What roles are allowed to use this command?\n"
"What roles are allowed to use this command? (Must be comma separated)\n"
"Say `None` to allow all roles"
)
@ -251,7 +228,7 @@ class CCRole(commands.Cog):
if not role_list:
return "None"
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)
@ -275,7 +252,7 @@ class CCRole(commands.Cog):
)
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
if (
@ -348,7 +325,9 @@ class CCRole(commands.Cog):
async def eval_cc(self, cmd, message: discord.Message, ctx: commands.Context):
"""Does all the work"""
if cmd["proles"] and not {role.id for role in message.author.roles} & set(cmd["proles"]):
if cmd["proles"] and not (
set(role.id for role in message.author.roles) & set(cmd["proles"])
):
log.debug(f"{message.author} missing required role to execute {ctx.invoked_with}")
return # Not authorized, do nothing

@ -59,50 +59,63 @@ Install these on your windows machine before attempting the installation:
[Pandoc - Universal Document Converter](https://pandoc.org/installing.html)
## Methods
### Automatic
This method requires some luck to pull off.
### Windows - Manually
#### Step 1: Built-in Downloader
#### Step 1: Add repo and install cog
You need to get a copy of the requirements.txt provided with chatter, I recommend this method.
```
[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 Requirements
#### Step 2: Install additional dependencies
Make sure you have your virtual environment that you installed Red on activated before starting this step. See the Red Docs for details on how.
Here you need to decide which training models you want to have available to you.
In a terminal running as an admin, navigate to the directory containing this repo.
Shutdown the bot and run any number of these in the console:
I've used my install directory as an example.
```
python -m spacy download en_core_web_sm # ~15 MB
cd C:\Users\Bobloy\AppData\Local\Red-DiscordBot\Red-DiscordBot\data\bobbot\cogs\RepoManager\repos\Fox\chatter
pip install -r requirements.txt
pip install --no-deps "chatterbot>=1.1"
```
#### Step 3: Load Chatter
python -m spacy download en_core_web_md # ~50 MB
```
[p]repo add Fox https://github.com/bobloy/Fox-V3 # If you didn't already do this in step 1
[p]cog install Fox chatter
[p]load chatter
```
### Linux - Manually
python -m spacy download en_core_web_lg # ~750 MB (CPU Optimized)
#### Step 1: Built-in Downloader
python -m spacy download en_core_web_trf # ~500 MB (GPU Optimized)
```
[p]repo add Fox https://github.com/bobloy/Fox-V3
[p]cog install Fox chatter
```
#### Step 3: Load the cog and get started
#### Step 2: Install Requirements
In your console with your virtual environment activated:
```
[p]load chatter
pip install --no-deps "chatterbot>=1.1"
```
### Windows - Manually
Deprecated
### Step 3: Load Chatter
### Linux - Manually
Deprecated
```
[p]load chatter
```
# Configuration
Chatter works out the box without any training by learning as it goes,
Chatter works out the the box without any training by learning as it goes,
but will have very poor and repetitive responses at first.
Initial training is recommended to speed up its learning.

@ -1,10 +1,8 @@
from .chat import Chatter
async def setup(bot):
cog = Chatter(bot)
await cog.initialize()
bot.add_cog(cog)
def setup(bot):
bot.add_cog(Chatter(bot))
# __all__ = (

@ -2,10 +2,8 @@ import asyncio
import logging
import os
import pathlib
from collections import defaultdict
from datetime import datetime, timedelta
from functools import partial
from typing import Dict, List, Optional
from typing import Optional
import discord
from chatterbot import ChatBot
@ -17,9 +15,6 @@ from redbot.core.commands import Cog
from redbot.core.data_manager import cog_data_path
from redbot.core.utils.predicates import MessagePredicate
from chatter.trainers import MovieTrainer, TwitterCorpusTrainer, UbuntuCorpusTrainer2
chatterbot_log = logging.getLogger("red.fox_v3.chatterbot")
log = logging.getLogger("red.fox_v3.chatter")
@ -30,12 +25,6 @@ def my_local_get_prefix(prefixes, content):
return None
class ENG_TRF:
ISO_639_1 = "en_core_web_trf"
ISO_639 = "eng"
ENGLISH_NAME = "English"
class ENG_LG:
ISO_639_1 = "en_core_web_lg"
ISO_639 = "eng"
@ -59,77 +48,50 @@ class Chatter(Cog):
This cog trains a chatbot that will talk like members of your Guild
"""
models = [ENG_SM, ENG_MD, ENG_LG, ENG_TRF]
algos = [SpacySimilarity, JaccardSimilarity, LevenshteinDistance]
def __init__(self, bot):
super().__init__()
self.bot = bot
self.config = Config.get_conf(self, identifier=6710497116116101114)
default_global = {"learning": True, "model_number": 0, "algo_number": 0, "threshold": 0.90}
self.default_guild = {
"whitelist": None,
"days": 1,
"convo_delta": 15,
"chatchannel": None,
"reply": True,
}
default_global = {}
default_guild = {"whitelist": None, "days": 1, "convo_delta": 15, "chatchannel": None}
path: pathlib.Path = cog_data_path(self)
self.data_path = path / "database.sqlite3"
# TODO: Move training_model and similarity_algo to config
# TODO: Add an option to see current settings
self.tagger_language = ENG_SM
self.tagger_language = ENG_MD
self.similarity_algo = SpacySimilarity
self.similarity_threshold = 0.90
self.chatbot = None
self.chatbot = self._create_chatbot()
# self.chatbot.set_trainer(ListTrainer)
# self.trainer = ListTrainer(self.chatbot)
self.config.register_global(**default_global)
self.config.register_guild(**self.default_guild)
self.config.register_guild(**default_guild)
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):
"""Nothing to delete"""
return
async def initialize(self):
all_config = dict(self.config.defaults["GLOBAL"])
all_config.update(await self.config.all())
model_number = all_config["model_number"]
algo_number = all_config["algo_number"]
threshold = all_config["threshold"]
self.tagger_language = self.models[model_number]
self.similarity_algo = self.algos[algo_number]
self.similarity_threshold = threshold
self.chatbot = self._create_chatbot()
def _create_chatbot(self):
return ChatBot(
"ChatterBot",
# storage_adapter="chatterbot.storage.SQLStorageAdapter",
storage_adapter="chatter.storage_adapters.MyDumbSQLStorageAdapter",
storage_adapter="chatterbot.storage.SQLStorageAdapter",
database_uri="sqlite:///" + str(self.data_path),
statement_comparison_function=self.similarity_algo,
response_selection_method=get_random_response,
logic_adapters=["chatterbot.logic.BestMatch"],
maximum_similarity_threshold=self.similarity_threshold,
tagger_language=self.tagger_language,
logger=chatterbot_log,
logger=log,
)
async def _get_conversation(self, ctx, in_channels: List[discord.TextChannel]):
async def _get_conversation(self, ctx, in_channel: discord.TextChannel = None):
"""
Compiles all conversation in the Guild this bot can get it's hands on
Currently takes a stupid long time
@ -143,12 +105,20 @@ class Chatter(Cog):
return msg.clean_content
def new_conversation(msg, sent, out_in, delta):
# Should always be positive numbers
# if sent is None:
# 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
for channel in in_channels:
# if in_channel:
# channel = in_channel
for channel in ctx.guild.text_channels:
if in_channel:
channel = in_channel
await ctx.maybe_send_embed("Gathering {}".format(channel.mention))
user = None
i = 0
@ -183,16 +153,11 @@ class Chatter(Cog):
except discord.HTTPException:
pass
# if in_channel:
# break
if in_channel:
break
return out
def _train_twitter(self, *args, **kwargs):
trainer = TwitterCorpusTrainer(self.chatbot)
trainer.train(*args, **kwargs)
return True
def _train_ubuntu(self):
trainer = UbuntuCorpusTrainer(
self.chatbot, ubuntu_corpus_data_directory=cog_data_path(self) / "ubuntu_data"
@ -200,30 +165,6 @@ class Chatter(Cog):
trainer.train()
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):
trainer = ChatterBotCorpusTrainer(self.chatbot)
# try:
@ -235,10 +176,13 @@ class Chatter(Cog):
def _train(self, data):
trainer = ListTrainer(self.chatbot)
total = len(data)
# try:
for c, convo in enumerate(data, 1):
log.info(f"{c} / {total}")
if len(convo) > 1: # TODO: Toggleable skipping short conversations
print(f"{c} / {total}")
trainer.train(convo)
# except:
# return False
return True
@commands.group(invoke_without_command=False)
@ -246,10 +190,10 @@ class Chatter(Cog):
"""
Base command for this cog. Check help for the commands list.
"""
self._guild_cache[ctx.guild.id] = {} # Clear cache when modifying values
self._global_cache = {}
if ctx.invoked_subcommand is None:
pass
@commands.admin()
@checks.admin()
@chatter.command(name="channel")
async def chatter_channel(
self, ctx: commands.Context, channel: Optional[discord.TextChannel] = None
@ -269,55 +213,13 @@ class Chatter(Cog):
await self.config.guild(ctx.guild).chatchannel.set(channel.id)
await ctx.maybe_send_embed(f"Chat channel is now {channel.mention}")
@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()
@checks.is_owner()
@chatter.command(name="cleardata")
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
This applies to all guilds.
Use `[p]chatter cleardata True` to confirm.
Use `[p]chatter cleardata True`
"""
if not confirm:
@ -344,18 +246,21 @@ class Chatter(Cog):
await ctx.tick()
@commands.is_owner()
@checks.is_owner()
@chatter.command(name="algorithm", aliases=["algo"])
async def chatter_algorithm(
self, ctx: commands.Context, algo_number: int, threshold: float = None
):
"""
Switch the active logic algorithm to one of the three. Default is Spacy
Switch the active logic algorithm to one of the three. Default after reload is Spacy
0: Spacy
1: Jaccard
2: Levenshtein
"""
algos = [SpacySimilarity, JaccardSimilarity, LevenshteinDistance]
if algo_number < 0 or algo_number > 2:
await ctx.send_help()
return
@ -368,32 +273,31 @@ class Chatter(Cog):
return
else:
self.similarity_threshold = threshold
await self.config.threshold.set(self.similarity_threshold)
self.similarity_algo = self.algos[algo_number]
await self.config.algo_number.set(algo_number)
self.similarity_algo = algos[algo_number]
async with ctx.typing():
self.chatbot = self._create_chatbot()
await ctx.tick()
@commands.is_owner()
@checks.is_owner()
@chatter.command(name="model")
async def chatter_model(self, ctx: commands.Context, model_number: int):
"""
Switch the active model to one of the three. Default is Small
Switch the active model to one of the three. Default after reload is Medium
0: Small
1: Medium (Requires additional setup)
1: Medium
2: Large (Requires additional setup)
3. Accurate (Requires additional setup)
"""
if model_number < 0 or model_number > 3:
models = [ENG_SM, ENG_MD, ENG_LG]
if model_number < 0 or model_number > 2:
await ctx.send_help()
return
if model_number >= 0:
if model_number == 2:
await ctx.maybe_send_embed(
"Additional requirements needed. See guide before continuing.\n" "Continue?"
)
@ -406,8 +310,7 @@ class Chatter(Cog):
if not pred.result:
return
self.tagger_language = self.models[model_number]
await self.config.model_number.set(model_number)
self.tagger_language = models[model_number]
async with ctx.typing():
self.chatbot = self._create_chatbot()
@ -415,14 +318,8 @@ class Chatter(Cog):
f"Model has been switched to {self.tagger_language.ISO_639_1}"
)
@commands.is_owner()
@chatter.group(name="trainset")
async def chatter_trainset(self, ctx: commands.Context):
"""Commands for configuring training"""
pass
@commands.is_owner()
@chatter_trainset.command(name="minutes")
@checks.is_owner()
@chatter.command(name="minutes")
async def minutes(self, ctx: commands.Context, minutes: int):
"""
Sets the number of minutes the bot will consider a break in a conversation during training
@ -433,12 +330,12 @@ class Chatter(Cog):
await ctx.send_help()
return
await self.config.guild(ctx.guild).convo_delta.set(minutes)
await self.config.guild(ctx.guild).convo_length.set(minutes)
await ctx.tick()
@commands.is_owner()
@chatter_trainset.command(name="age")
@checks.is_owner()
@chatter.command(name="age")
async def age(self, ctx: commands.Context, days: int):
"""
Sets the number of days to look back
@ -452,16 +349,7 @@ class Chatter(Cog):
await self.config.guild(ctx.guild).days.set(days)
await ctx.tick()
@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()
@checks.is_owner()
@chatter.command(name="backup")
async def backup(self, ctx, backupname):
"""
@ -483,71 +371,8 @@ class Chatter(Cog):
else:
await ctx.maybe_send_embed("Error occurred :(")
@commands.is_owner()
@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")
@checks.is_owner()
@chatter.command(name="trainubuntu")
async def chatter_train_ubuntu(self, ctx: commands.Context, confirmation: bool = False):
"""
WARNING: Large Download! Trains the bot using Ubuntu Dialog Corpus data.
@ -555,7 +380,7 @@ class Chatter(Cog):
if not confirmation:
await ctx.maybe_send_embed(
"Warning: This command downloads ~500MB and is CPU intensive during training\n"
"Warning: This command downloads ~500MB then eats your CPU for training\n"
"If you're sure you want to continue, run `[p]chatter trainubuntu True`"
)
return
@ -564,11 +389,12 @@ class Chatter(Cog):
future = await self.loop.run_in_executor(None, self._train_ubuntu)
if future:
await ctx.maybe_send_embed("Training successful!")
await ctx.send("Training successful!")
else:
await ctx.maybe_send_embed("Error occurred :(")
await ctx.send("Error occurred :(")
@chatter_train.command(name="english")
@checks.is_owner()
@chatter.command(name="trainenglish")
async def chatter_train_english(self, ctx: commands.Context):
"""
Trains the bot in english
@ -581,32 +407,12 @@ class Chatter(Cog):
else:
await ctx.maybe_send_embed("Error occurred :(")
@chatter_train.command(name="list")
async def chatter_train_list(self, ctx: commands.Context):
"""Trains the bot based on an uploaded list.
Must be a file in the format of a python list: ['prompt', 'response1', 'response2']
@checks.is_owner()
@chatter.command()
async def train(self, ctx: commands.Context, channel: discord.TextChannel):
"""
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(
"Warning: The cog may use significant RAM or CPU if trained on large data sets.\n"
@ -615,7 +421,7 @@ class Chatter(Cog):
)
async with ctx.typing():
conversation = await self._get_conversation(ctx, channels)
conversation = await self._get_conversation(ctx, channel)
if not conversation:
await ctx.maybe_send_embed("Failed to gather training data")
@ -657,7 +463,7 @@ class Chatter(Cog):
guild: discord.Guild = getattr(message, "guild", None)
if guild is None or await self.bot.cog_disabled_in_guild(self, guild):
if await self.bot.cog_disabled_in_guild(self, guild):
return
ctx: commands.Context = await self.bot.get_context(message)
@ -669,18 +475,7 @@ class Chatter(Cog):
# Thank you Cog-Creators
channel: discord.TextChannel = message.channel
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"]:
if guild is not None and channel.id == await self.config.guild(guild).chatchannel():
pass # good to go
else:
when_mentionables = commands.when_mentioned(self.bot, message)
@ -695,57 +490,10 @@ class Chatter(Cog):
text = message.clean_content
async with ctx.typing():
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 (
"reply" not in self._guild_cache[guild.id] and self.default_guild["reply"]
) or self._guild_cache[guild.id]["reply"]:
if message != ctx.channel.last_message:
replying = message
async with channel.typing():
future = await self.loop.run_in_executor(None, self.chatbot.get_response, text)
if future and str(future):
self._last_message_per_channel[ctx.channel.id] = await channel.send(
str(future), reference=replying
)
await channel.send(str(future))
else:
await ctx.send(":thinking:")
async def check_for_kaggle(self):
"""Check whether Kaggle is installed and configured properly"""
# TODO: This
return False
await channel.send(":thinking:")

@ -2,15 +2,22 @@
"author": [
"Bobloy"
],
"min_bot_version": "3.4.6",
"min_bot_version": "3.4.0",
"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,
"install_msg": "Thank you for installing Chatter! Please make sure you check the install instructions at https://github.com/bobloy/Fox-V3/blob/master/chatter/README.md\nAfter that, get started ith `[p]load chatter` and `[p]help Chatter`",
"requirements": [
"git+git://github.com/bobloy/ChatterBot@fox#egg=ChatterBot>=1.1.0.dev4",
"kaggle",
"https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.1.0/en_core_web_sm-3.1.0.tar.gz#egg=en_core_web_sm",
"https://github.com/explosion/spacy-models/releases/download/en_core_web_md-3.1.0/en_core_web_md-3.1.0.tar.gz#egg=en_core_web_md"
"git+git://github.com/gunthercox/chatterbot-corpus@master#egg=chatterbot_corpus",
"mathparse>=0.1,<0.2",
"nltk>=3.2,<4.0",
"pint>=0.8.1",
"python-dateutil>=2.8,<2.9",
"pyyaml>=5.3,<5.4",
"sqlalchemy>=1.3,<1.4",
"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_md-2.3.1/en_core_web_md-2.3.1.tar.gz#egg=en_core_web_md",
"spacy>=2.3,<2.4"
],
"short": "Local Chatbot run on machine learning",
"end_user_data_statement": "This cog only stores anonymous conversations data; no End User Data is stored.",

@ -0,0 +1,12 @@
git+git://github.com/gunthercox/chatterbot-corpus@master#egg=chatterbot_corpus
mathparse>=0.1,<0.2
nltk>=3.2,<4.0
pint>=0.8.1
python-dateutil>=2.8,<2.9
pyyaml>=5.3,<5.4
sqlalchemy>=1.3,<1.4
pytz
spacy>=2.3,<2.4
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_lg-2.3.1/en_core_web_lg-2.3.1.tar.gz#egg=en_core_web_lg

@ -1,71 +0,0 @@
from chatterbot.storage import StorageAdapter, SQLStorageAdapter
class MyDumbSQLStorageAdapter(SQLStorageAdapter):
def __init__(self, **kwargs):
super(SQLStorageAdapter, self).__init__(**kwargs)
from sqlalchemy import create_engine, inspect
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, 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 inspect(self.engine).has_table("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)

@ -1,351 +0,0 @@
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,7 +58,11 @@ class CogLint(Cog):
future = await self.bot.loop.run_in_executor(None, lint.py_run, path, "return_std=True")
(pylint_stdout, pylint_stderr) = future or (None, None)
if future:
(pylint_stdout, pylint_stderr) = future
else:
(pylint_stdout, pylint_stderr) = None, None
# print(pylint_stderr)
# print(pylint_stdout)

@ -1,6 +1,5 @@
import asyncio
import json
import logging
import os
import pathlib
from abc import ABC
@ -14,8 +13,6 @@ from redbot.core import Config, commands
from redbot.core.bot import Red
from redbot.core.data_manager import bundled_data_path, cog_data_path
log = logging.getLogger("red.fox_v3.conquest")
class Conquest(commands.Cog):
"""
@ -56,27 +53,22 @@ class Conquest(commands.Cog):
self.current_map = await self.config.current_map()
if self.current_map:
if not await self.current_map_load():
await self.config.current_map.clear()
await self.current_map_load()
async def current_map_load(self):
map_data_path = self.asset_path / self.current_map / "data.json"
if not map_data_path.exists():
log.warning(f"{map_data_path} does not exist. Clearing current map")
return False
with map_data_path.open() as mapdata:
self.map_data: dict = json.load(mapdata)
self.ext = self.map_data["extension"]
self.ext_format = "JPEG" if self.ext.upper() == "JPG" else self.ext.upper()
return True
@commands.group()
async def conquest(self, ctx: commands.Context):
"""
Base command for conquest cog. Start with `[p]conquest set map` to select a map.
"""
if ctx.invoked_subcommand is None and self.current_map is not None:
if ctx.invoked_subcommand is None:
if self.current_map is not None:
await self._conquest_current(ctx)
@conquest.command(name="list")
@ -88,12 +80,13 @@ class Conquest(commands.Cog):
with maps_json.open() as maps:
maps_json = json.load(maps)
map_list = "\n".join(maps_json["maps"])
map_list = "\n".join(map_name for map_name in maps_json["maps"])
await ctx.maybe_send_embed(f"Current maps:\n{map_list}")
@conquest.group(name="set")
async def conquest_set(self, ctx: commands.Context):
"""Base command for admin actions like selecting a map"""
if ctx.invoked_subcommand is None:
pass
@conquest_set.command(name="resetzoom")

@ -1,7 +1,7 @@
{
"maps": [
"simple",
"ck2",
"HoI"
"simple_blank_map",
"test",
"test2"
]
}

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

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

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

@ -68,7 +68,10 @@ class CapturePrint:
self.string = None
def write(self, string):
self.string = string if self.string is None else self.string + "\n" + string
if self.string is None:
self.string = string
else:
self.string = self.string + "\n" + string
class FIFO(commands.Cog):
@ -227,6 +230,7 @@ class FIFO(commands.Cog):
"""
Base command for handling scheduling of tasks
"""
if ctx.invoked_subcommand is None:
pass
@fifo.command(name="wakeup")
@ -441,7 +445,6 @@ class FIFO(commands.Cog):
self.scheduler.print_jobs(out=cp)
out = cp.string
out=out.replace("*","\*")
if out:
if len(out) > 2000:
@ -519,6 +522,7 @@ class FIFO(commands.Cog):
"""
Add a new trigger for a task from the current guild.
"""
if ctx.invoked_subcommand is None:
pass
@fifo_trigger.command(name="interval")

@ -90,7 +90,6 @@ things_for_fakemessage_to_steal = [
"content",
"nonce",
"reference",
"_edited_timestamp" # New 7/23/21
]
things_fakemessage_sets_by_default = {
@ -143,7 +142,7 @@ class FakeMessage(discord.Message):
self._update(
{
"mention_roles": self.raw_role_mentions,
"mentions": [{"id": _id} for _id in self.raw_mentions],
"mentions": self.raw_mentions,
}
)

@ -55,6 +55,7 @@ class Flag(Cog):
"""
Commands for managing Flag settings
"""
if ctx.invoked_subcommand is None:
pass
@flagset.command(name="expire")

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

@ -28,12 +28,9 @@ async def get_channel_counts(category, guild):
online_num = members - offline_num
# Gets count of actual users
human_num = members - bot_num
# count amount of premium subs/nitro subs.
boosters = guild.premium_subscription_count
return {
"members": members,
"humans": human_num,
"boosters": boosters,
"bots": bot_num,
"roles": roles_num,
"channels": channels_num,
@ -61,7 +58,6 @@ class InfoChannel(Cog):
self.default_channel_names = {
"members": "Members: {count}",
"humans": "Humans: {count}",
"boosters": "Boosters: {count}",
"bots": "Bots: {count}",
"roles": "Roles: {count}",
"channels": "Channels: {count}",
@ -69,9 +65,9 @@ class InfoChannel(Cog):
"offline": "Offline: {count}",
}
default_channel_ids = {k: None for k in self.default_channel_names}
default_channel_ids = {k: None for k in self.default_channel_names.keys()}
# Only members is enabled by default
default_enabled_counts = {k: k == "members" for k in self.default_channel_names}
default_enabled_counts = {k: k == "members" for k in self.default_channel_names.keys()}
default_guild = {
"category_id": None,
@ -163,6 +159,7 @@ class InfoChannel(Cog):
"""
Toggle different types of infochannels
"""
if not ctx.invoked_subcommand:
pass
@infochannelset.command(name="togglechannel")
@ -174,7 +171,6 @@ class InfoChannel(Cog):
Valid Types are:
- `members`: Total members on the server
- `humans`: Total members that aren't bots
- `boosters`: Total amount of boosters
- `bots`: Total bots
- `roles`: Total number of roles
- `channels`: Total number of channels excluding infochannels,
@ -229,7 +225,6 @@ class InfoChannel(Cog):
Valid Types are:
- `members`: Total members on the server
- `humans`: Total members that aren't bots
- `boosters`: Total amount of boosters
- `bots`: Total bots
- `roles`: Total number of roles
- `channels`: Total number of channels excluding infochannels
@ -447,7 +442,6 @@ class InfoChannel(Cog):
guild,
members=True,
humans=True,
boosters=True,
bots=True,
roles=True,
channels=True,
@ -504,16 +498,14 @@ class InfoChannel(Cog):
guild_data = await self.config.guild(guild).all()
to_update = (
kwargs.keys() & [key for key, value in guild_data["enabled_channels"].items() if value]
kwargs.keys() & guild_data["enabled_channels"].keys()
) # Value in kwargs doesn't matter
if to_update or extra_roles:
log.debug(f"{to_update=}\n"
f"{extra_roles=}")
log.debug(f"{to_update=}")
if to_update or extra_roles:
category = guild.get_channel(guild_data["category_id"])
if category is None:
log.debug('Channel category is missing, updating must be off')
return # Nothing to update, must be off
channel_data = await get_channel_counts(category, guild)

@ -36,6 +36,58 @@ class LaunchLib(commands.Cog):
async def _embed_launch_data(self, launch: ll.AsyncLaunch):
if False:
example_launch = ll.AsyncLaunch(
id="9279744e-46b2-4eca-adea-f1379672ec81",
name="Atlas LV-3A | Samos 2",
tbddate=False,
tbdtime=False,
status={"id": 3, "name": "Success"},
inhold=False,
windowstart="1961-01-31 20:21:19+00:00",
windowend="1961-01-31 20:21:19+00:00",
net="1961-01-31 20:21:19+00:00",
info_urls=[],
vid_urls=[],
holdreason=None,
failreason=None,
probability=0,
hashtag=None,
agency=None,
changed=None,
pad=ll.Pad(
id=93,
name="Space Launch Complex 3W",
latitude=34.644,
longitude=-120.593,
map_url="http://maps.google.com/maps?q=34.644+N,+120.593+W",
retired=None,
total_launch_count=3,
agency_id=161,
wiki_url=None,
info_url=None,
location=ll.Location(
id=11,
name="Vandenberg AFB, CA, USA",
country_code="USA",
total_launch_count=83,
total_landing_count=3,
pads=None,
),
map_image="https://spacelaunchnow-prod-east.nyc3.digitaloceanspaces.com/media/launch_images/pad_93_20200803143225.jpg",
),
rocket=ll.Rocket(
id=2362,
name=None,
default_pads=None,
family=None,
wiki_url=None,
info_url=None,
image_url=None,
),
missions=None,
)
# status: ll.AsyncLaunchStatus = await launch.get_status()
status = launch.status
@ -50,7 +102,11 @@ class LaunchLib(commands.Cog):
if launch.pad:
urls += [launch.pad.info_url, launch.pad.wiki_url]
url = next((url for url in urls if urls is not None), None) if urls else None
if urls:
url = next((url for url in urls if urls is not None), None)
else:
url = 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)
@ -96,9 +152,7 @@ class LaunchLib(commands.Cog):
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
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)
@ -115,6 +169,7 @@ class LaunchLib(commands.Cog):
@commands.group()
async def launchlib(self, ctx: commands.Context):
"""Base command for getting launches"""
if ctx.invoked_subcommand is None:
pass
@launchlib.command()

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

@ -40,20 +40,16 @@ class LoveCalculator(Cog):
log.debug(f"{resp=}")
soup_object = BeautifulSoup(resp, "html.parser")
description = soup_object.find("div", class_="result__score")
description = soup_object.find("div", class_="result__score").get_text()
if description is None:
description = "Dr. Love is busy right now"
else:
description = description.get_text().strip()
description = description.strip()
result_image = soup_object.find("img", class_="result__image").get("src")
result_text = soup_object.find("div", class_="result-text")
if result_text is None:
result_text = f"{x} and {y} aren't compatible 😔"
else:
result_text = result_text.get_text()
result_text = soup_object.find("div", class_="result-text").get_text()
result_text = " ".join(result_text.split())
try:

@ -45,11 +45,13 @@ class LastSeen(Cog):
@staticmethod
def get_date_time(s):
return dateutil.parser.parse(s)
d = dateutil.parser.parse(s)
return d
@commands.group(aliases=["setlseen"], name="lseenset")
async def lset(self, ctx: commands.Context):
"""Change settings for lseen"""
if ctx.invoked_subcommand is None:
pass
@lset.command(name="toggle")
@ -77,13 +79,11 @@ class LastSeen(Cog):
return
last_seen = self.get_date_time(last_seen)
embed = discord.Embed(
description="{} was last seen at this date and time".format(member.display_name),
timestamp=last_seen,
color=await self.bot.get_embed_color(ctx),
)
# embed = discord.Embed(
# description="{} was last seen at this date and time".format(member.display_name),
# timestamp=last_seen)
# embed = discord.Embed(timestamp=last_seen, color=await self.bot.get_embed_color(ctx))
embed = discord.Embed(timestamp=last_seen, color=await self.bot.get_embed_color(ctx))
await ctx.send(embed=embed)
@commands.Cog.listener()

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

@ -67,10 +67,8 @@ class QRInvite(Cog):
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)
image_path = path / f"{save_as_name}.{extension}"
image_path = path / (ctx.guild.icon + "." + extension)
async with aiohttp.ClientSession() as session:
async with session.get(image_url) as response:
image = await response.read()
@ -79,29 +77,27 @@ class QRInvite(Cog):
file.write(image)
if extension == "webp":
new_image_path = convert_webp_to_png(str(image_path))
new_path = convert_webp_to_png(str(image_path))
elif extension == "gif":
await ctx.maybe_send_embed("gif is not supported yet, stay tuned")
return
elif extension == "png":
new_image_path = str(image_path)
elif extension == "jpg":
new_image_path = convert_jpg_to_png(str(image_path))
new_path = str(image_path)
else:
await ctx.maybe_send_embed(f"{extension} is not supported yet, stay tuned")
return
myqr.run(
invite,
picture=new_image_path,
save_name=f"{save_as_name}_qrcode.png",
picture=new_path,
save_name=ctx.guild.icon + "_qrcode.png",
save_dir=str(cog_data_path(self)),
colorized=colorized,
)
png_path: pathlib.Path = path / f"{save_as_name}_qrcode.png"
# with png_path.open("rb") as png_fp:
await ctx.send(file=discord.File(png_path, "qrcode.png"))
png_path: pathlib.Path = path / (ctx.guild.icon + "_qrcode.png")
with png_path.open("rb") as png_fp:
await ctx.send(file=discord.File(png_fp.read(), "qrcode.png"))
def convert_webp_to_png(path):
@ -114,10 +110,3 @@ def convert_webp_to_png(path):
new_path = path.replace(".webp", ".png")
im.save(new_path, transparency=255)
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,7 +97,9 @@ class ReactRestrict(Cog):
"""
current_combos = await self.combo_list()
to_keep = [c for c in current_combos if c.message_id != message_id or c.role_id != role.id]
to_keep = [
c for c in current_combos if not (c.message_id == message_id and c.role_id == role.id)
]
if to_keep != current_combos:
await self.set_combo_list(to_keep)
@ -208,6 +210,7 @@ class ReactRestrict(Cog):
"""
Base command for this cog. Check help for the commands list.
"""
if ctx.invoked_subcommand is None:
pass
@reactrestrict.command()

@ -32,7 +32,6 @@ class RecyclingPlant(Cog):
x = 0
reward = 0
timeoutcount = 0
await ctx.send(
"{0} has signed up for a shift at the Recycling Plant! Type ``exit`` to terminate it early.".format(
ctx.author.display_name
@ -54,25 +53,14 @@ class RecyclingPlant(Cog):
return m.author == ctx.author and m.channel == ctx.channel
try:
answer = await self.bot.wait_for("message", timeout=20, check=check)
answer = await self.bot.wait_for("message", timeout=120, check=check)
except asyncio.TimeoutError:
answer = None
if answer is None:
if timeoutcount == 2:
await ctx.send(
"{} slacked off at work, so they were sacked with no pay.".format(
ctx.author.display_name
)
)
break
else:
await ctx.send(
"{} is slacking, and if they carry on not working, they'll be fired.".format(
ctx.author.display_name
)
"``{}`` fell down the conveyor belt to be sorted again!".format(used["object"])
)
timeoutcount += 1
elif answer.content.lower().strip() == used["action"]:
await ctx.send(
"Congratulations! You put ``{}`` down the correct chute! (**+50**)".format(

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

@ -177,3 +177,7 @@ class SCP(Cog):
msg = "http://www.scp-wiki.net/log-of-unexplained-locations"
await ctx.maybe_send_embed(msg)
def setup(bot):
bot.add_cog(SCP(bot))

@ -6,7 +6,6 @@ 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.chat_formatting import pagify
log = logging.getLogger("red.fox_v3.stealemoji")
# Replaced with discord.Asset.read()
@ -70,6 +69,7 @@ class StealEmoji(Cog):
"""
Base command for this cog. Check help for the commands list.
"""
if ctx.invoked_subcommand is None:
pass
@checks.is_owner()
@ -100,8 +100,7 @@ class StealEmoji(Cog):
await ctx.maybe_send_embed("No stolen emojis yet")
return
for page in pagify(emoj, delims=[" "]):
await ctx.maybe_send_embed(page)
await ctx.maybe_send_embed(emoj)
@checks.is_owner()
@stealemoji.command(name="notify")
@ -269,9 +268,7 @@ class StealEmoji(Cog):
break
if guildbank is None:
if not await self.config.autobank():
return
if await self.config.autobank():
try:
guildbank: discord.Guild = await self.bot.create_guild(
"StealEmoji Guildbank", code="S93bqTqKQ9rM"
@ -299,6 +296,9 @@ class StealEmoji(Cog):
await self.bot.send_to_owners(invite)
log.info(f"Guild created id {guildbank.id}. Invite: {invite}")
else:
return
# Next, have I saved this emoji before (because uploaded emoji != orignal emoji)
if str(emoji.id) in await self.config.stolemoji():

@ -37,7 +37,7 @@ class Timerole(Cog):
self.bot = bot
self.config = Config.get_conf(self, identifier=9811198108111121, force_registration=True)
default_global = {}
default_guild = {"announce": None, "reapply": True, "roles": {}, "skipbots": True}
default_guild = {"announce": None, "reapply": True, "roles": {}}
default_rolemember = {"had_role": False, "check_again_time": None}
self.config.register_global(**default_global)
@ -77,6 +77,7 @@ class Timerole(Cog):
@commands.guild_only()
async def timerole(self, ctx):
"""Adjust timerole settings"""
if ctx.invoked_subcommand is None:
pass
@timerole.command()
@ -92,9 +93,6 @@ class Timerole(Cog):
await ctx.maybe_send_embed("Error: Invalid time string.")
return
if parsed_time is None:
return await ctx.maybe_send_embed("Error: Invalid time string.")
days = parsed_time.days
hours = parsed_time.seconds // 60 // 60
@ -154,14 +152,6 @@ class Timerole(Cog):
await self.config.guild(guild).reapply.set(not current_setting)
await ctx.maybe_send_embed(f"Reapplying roles is now set to: {not current_setting}")
@timerole.command()
async def skipbots(self, ctx: commands.Context):
"""Toggle skipping bots when adding/removing roles. Defaults to True"""
guild = ctx.guild
current_setting = await self.config.guild(guild).skipbots()
await self.config.guild(guild).skipbots.set(not current_setting)
await ctx.maybe_send_embed(f"Skipping bots is now set to: {not current_setting}")
@timerole.command()
async def delrole(self, ctx: commands.Context, role: discord.Role):
"""Deletes a role from being added/removed after specified time"""
@ -210,9 +200,8 @@ class Timerole(Cog):
remove_results = ""
reapply = all_guilds[guild_id]["reapply"]
role_dict = all_guilds[guild_id]["roles"]
skipbots = all_guilds[guild_id]["skipbots"]
if not any(role_dict.values()): # No roles
if not any(role_data for role_data in role_dict.values()): # No roles
log.debug(f"No roles are configured for guild: {guild}")
continue
@ -220,10 +209,6 @@ class Timerole(Cog):
# log.debug(f"{all_mr=}")
async for member in AsyncIter(guild.members, steps=10):
if member.bot and skipbots:
continue
addlist = []
removelist = []
@ -247,7 +232,7 @@ class Timerole(Cog):
log.debug(f"{member.display_name} - Not time to check again yet")
continue
member: discord.Member
has_roles = {r.id for r in member.roles}
has_roles = set(r.id for r in member.roles)
# Stop if they currently have or don't have the role, and mark had_role
if (int(role_id) in has_roles and not role_data["remove"]) or (
@ -311,12 +296,9 @@ class Timerole(Cog):
log.exception("Failed Adding Roles")
add_results += f"{member.display_name} : **(Failed Adding Roles)**\n"
else:
add_results += (
" \n".join(
add_results += " \n".join(
f"{member.display_name} : {role.name}" for role in add_roles
)
+ "\n"
)
for role_id in addlist:
await self.config.custom(
"RoleMember", role_id, member.id
@ -329,12 +311,9 @@ class Timerole(Cog):
log.exception("Failed Removing Roles")
remove_results += f"{member.display_name} : **(Failed Removing Roles)**\n"
else:
remove_results += (
" \n".join(
remove_results += " \n".join(
f"{member.display_name} : {role.name}" for role in remove_roles
)
+ "\n"
)
for role_id in removelist:
await self.config.custom(
"RoleMember", role_id, member.id

@ -1,35 +1,11 @@
import io
import logging
from typing import Optional, TYPE_CHECKING
import discord
from discord.ext.commands import BadArgument, Converter
from gtts import gTTS
from gtts.lang import _fallback_deprecated_lang, tts_langs
from redbot.core import Config, commands
from redbot.core.bot import Red
from redbot.core.commands import Cog
log = logging.getLogger("red.fox_v3.tts")
if TYPE_CHECKING:
ISO639Converter = str
else:
class ISO639Converter(Converter):
async def convert(self, ctx, argument) -> str:
lang = _fallback_deprecated_lang(argument)
try:
langs = tts_langs()
if lang not in langs:
raise BadArgument("Language not supported: %s" % lang)
except RuntimeError as e:
log.debug(str(e), exc_info=True)
log.warning(str(e))
return lang
class TTS(Cog):
"""
@ -42,7 +18,7 @@ class TTS(Cog):
self.config = Config.get_conf(self, identifier=9811198108111121, force_registration=True)
default_global = {}
default_guild = {"language": "en"}
default_guild = {}
self.config.register_global(**default_global)
self.config.register_guild(**default_guild)
@ -51,29 +27,13 @@ class TTS(Cog):
"""Nothing to delete"""
return
@commands.mod()
@commands.command()
async def ttslang(self, ctx: commands.Context, lang: ISO639Converter):
"""
Sets the default language for TTS in this guild.
Default is `en` for English
"""
await self.config.guild(ctx.guild).language.set(lang)
await ctx.send(f"Default tts language set to {lang}")
@commands.command(aliases=["t2s", "text2"])
async def tts(
self, ctx: commands.Context, lang: Optional[ISO639Converter] = None, *, text: str
):
async def tts(self, ctx: commands.Context, *, text: str):
"""
Send Text to speech messages as an mp3
"""
if lang is None:
lang = await self.config.guild(ctx.guild).language()
mp3_fp = io.BytesIO()
tts = gTTS(text, lang=lang)
tts = gTTS(text, lang="en")
tts.write_to_fp(mp3_fp)
mp3_fp.seek(0)
await ctx.send(file=discord.File(mp3_fp, "text.mp3"))

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

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

@ -62,6 +62,8 @@ class Game:
village: discord.TextChannel = None,
log_channel: discord.TextChannel = None,
game_code=None,
day_length=HALF_DAY_LENGTH * 2,
night_length=HALF_NIGHT_LENGTH * 2,
):
self.bot = bot
self.guild = guild
@ -73,6 +75,9 @@ class Game:
self.day_vote = {} # author: target
self.vote_totals = {} # id: total_votes
self.half_day_length = day_length // 2
self.half_night_length = night_length // 2
self.started = False
self.game_over = False
self.any_votes_remaining = False
@ -97,6 +102,7 @@ class Game:
self.loop = asyncio.get_event_loop()
self.cycle_task = None
self.action_queue = deque()
self.current_action = None
self.listeners = {}
@ -116,6 +122,13 @@ class Game:
# for c_data in self.p_channels.values():
# asyncio.ensure_future(c_data["channel"].delete("Werewolf game-over"))
def _prestart_status(self, ctx):
return (
f"Currently **{len(self.players)} / {len(self.roles)}**\n"
f"Use `{ctx.prefix}ww code` to pick a game setup\n"
f"Use `{ctx.prefix}buildgame` to generate a new game"
)
async def setup(self, ctx: commands.Context):
"""
Runs the initial setup
@ -127,15 +140,20 @@ class Game:
4. Start game
"""
if self.game_code:
# Turn random roles into real roles
await self.get_roles(ctx)
else:
await ctx.maybe_send_embed(
f"No game code has been assigned, cannot start\n{self._prestart_status(ctx)}"
)
return False
if len(self.players) != len(self.roles):
await ctx.maybe_send_embed(
f"Player count does not match role count, cannot start\n"
f"Currently **{len(self.players)} / {len(self.roles)}**\n"
f"Use `{ctx.prefix}ww code` to pick a game setup\n"
f"Use `{ctx.prefix}buildgame` to generate a new game"
f"{self._prestart_status(ctx)}"
)
# Clear the roles to be randomly generated again
self.roles = []
return False
@ -156,6 +174,7 @@ class Game:
self.roles = []
return False
# Check if the role is already in use. If so, cannot continue until removed
anyone_with_role = await anyone_has_role(self.guild.members, self.game_role)
if anyone_with_role is not None:
await ctx.maybe_send_embed(
@ -164,6 +183,7 @@ class Game:
)
return False
# Add the game role to those who are playing
try:
for player in self.players:
await player.member.add_roles(*[self.game_role])
@ -175,6 +195,7 @@ class Game:
)
return False
# Randomly assign the roles in the game to the players
await self.assign_roles()
# Create category and channel with individual overwrites
@ -219,14 +240,16 @@ class Game:
return False
else:
self.save_perms[self.village_channel] = self.village_channel.overwrites
try:
await self.village_channel.edit(
name="🔵werewolf",
reason="(BOT) New game of werewolf",
)
except discord.Forbidden as e:
log.exception("Unable to rename Game Channel")
await ctx.maybe_send_embed("Unable to rename Game Channel, ignoring")
# Disable renaming channels, too easy to get rate limited.
# try:
# await self.village_channel.edit(
# name="🔵werewolf",
# reason="(BOT) New game of werewolf",
# )
# except discord.Forbidden as e:
# log.exception("Unable to rename Game Channel")
# await ctx.maybe_send_embed("Unable to rename Game Channel, ignoring")
try:
for target, ow in overwrite.items():
@ -283,9 +306,9 @@ class Game:
self.vote_groups[channel_id] = vote_group
log.debug("Pre-cycle")
await asyncio.sleep(0)
await asyncio.sleep(0) # Pass back to controller to avoid heartbeat issues
asyncio.create_task(self._cycle()) # Start the loop
self.cycle_task = asyncio.create_task(self._cycle()) # Start the loop
return True
# ###########START Notify structure############
@ -360,13 +383,15 @@ class Game:
self.any_votes_remaining = True
# Now we sleep and let the day happen. Print the remaining daylight half way through
await asyncio.sleep(HALF_DAY_LENGTH) # 4 minute days FixMe to 120 later
await asyncio.sleep(self.half_day_length)
if check():
return
await self.village_channel.send(
embed=discord.Embed(title=f"*{HALF_DAY_LENGTH / 60} minutes of daylight remain...*")
embed=discord.Embed(
title=f"*{self.half_day_length/ 60} minutes of daylight remain...*"
)
)
await asyncio.sleep(HALF_DAY_LENGTH) # 4 minute days FixMe to 120 later
await asyncio.sleep(self.half_day_length)
# Need a loop here to wait for trial to end
while self.ongoing_vote:
@ -501,13 +526,13 @@ class Game:
await self._notify("at_night_start")
await asyncio.sleep(HALF_NIGHT_LENGTH) # 2 minutes FixMe to 120 later
await asyncio.sleep(self.half_night_length)
await self.village_channel.send(
embed=discord.Embed(title=f"**{HALF_NIGHT_LENGTH / 60} minutes of night remain...**")
embed=discord.Embed(
title=f"**{self.half_night_length / 60} minutes of night remain...**"
)
await asyncio.sleep(HALF_NIGHT_LENGTH) # 1.5 minutes FixMe to 90 later
await asyncio.sleep(3) # .5 minutes FixMe to 30 Later
)
await asyncio.sleep(self.half_night_length)
self.action_queue.append(self._at_night_end())
@ -526,10 +551,9 @@ class Game:
async def _notify(self, event_name, **kwargs):
for i in range(1, 7): # action guide 1-6 (0 is no action)
tasks = [
asyncio.create_task(event(**kwargs))
for event in self.listeners.get(event_name, {}).get(i, [])
]
tasks = []
for event in self.listeners.get(event_name, {}).get(i, []):
tasks.append(asyncio.create_task(event(**kwargs)))
# Run same-priority task simultaneously
await asyncio.gather(*tasks)
@ -554,18 +578,23 @@ class Game:
# ###########END Notify structure############
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):
status = "" if player.alive else "*[Dead]*-"
if player.alive:
status = ""
else:
status = "*[Dead]*-"
if with_roles or not player.alive:
embed.add_field(
name=f"{i} - {status}{player.member.display_name}",
name=f"{i} || {status}{player.member.display_name}",
value=f"{player.role}",
inline=False,
)
else:
embed.add_field(
name=f"{i} - {status}{player.member.display_name}", inline=False, value="____"
name=f"{i} || {status}{player.member.display_name}",
inline=False,
value="\N{Zero Width Space}",
)
return await channel.send(embed=embed)
@ -577,7 +606,7 @@ class Game:
if channel_id not in self.p_channels:
self.p_channels[channel_id] = self.default_secret_channel.copy()
for _ in range(10): # Retry 10 times
for x in range(10): # Retry 10 times
try:
await asyncio.sleep(1) # This will have multiple calls
self.p_channels[channel_id]["players"].append(role.player)
@ -704,7 +733,9 @@ class Game:
if not self.any_votes_remaining:
await channel.send("Voting is not allowed right now")
return
elif channel.name not in self.p_channels:
elif channel.name in self.p_channels:
pass
else:
# Not part of the game
await channel.send("Cannot vote in this channel")
return
@ -753,14 +784,14 @@ class Game:
await self._at_voted(target)
async def eval_results(self, target, source=None, method=None):
if method is None:
if method is not 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(
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):
"""
Have player quit the game
@ -855,7 +886,7 @@ class Game:
self.players.sort(key=lambda pl: pl.member.display_name.lower())
if len(self.roles) != len(self.players):
await self.village_channel.send("Unhandled error - roles!=players")
await self.village_channel.send("Unhandled error - roles != # players")
return False
for idx, role in enumerate(self.roles):

@ -36,6 +36,7 @@ class Werewolf(Cog):
"category_id": None,
"channel_id": None,
"log_channel_id": None,
"default_game": {"daytime": 60 * 5, "nighttime": 60 * 5},
}
self.config.register_global(**default_global)
@ -75,6 +76,7 @@ class Werewolf(Cog):
"""
Base command to adjust settings. Check help for command list.
"""
if ctx.invoked_subcommand is None:
pass
@commands.guild_only()
@ -165,6 +167,7 @@ class Werewolf(Cog):
"""
Base command for this cog. Check help for the commands list.
"""
if ctx.invoked_subcommand is None:
pass
@commands.guild_only()
@ -346,6 +349,7 @@ class Werewolf(Cog):
"""
Find custom roles by name, alignment, category, or ID
"""
if ctx.invoked_subcommand is None or ctx.invoked_subcommand == self.ww_search:
pass
@ww_search.command(name="name")
@ -395,12 +399,13 @@ class Werewolf(Cog):
# Private message, can't get guild
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.maybe_send_embed("Starting a new game...")
valid, role, category, channel, log_channel = await self._get_settings(ctx)
if not valid:
await ctx.maybe_send_embed("Cannot start a new game")
await ctx.maybe_send_embed("Cannot start a new game, check server settings.")
return None
who_has_the_role = await anyone_has_role(guild.members, role)
@ -410,7 +415,15 @@ class Werewolf(Cog):
)
return None
self.games[guild.id] = Game(
self.bot, guild, role, category, channel, log_channel, game_code
bot=self.bot,
guild=guild,
role=role,
category=category,
village=channel,
log_channel=log_channel,
game_code=game_code,
day_length=await self.config.guild(guild).default_game.day_length(),
night_length=await self.config.guild(guild).default_game.night_length(),
)
return self.games[guild.id]

Loading…
Cancel
Save