Compare commits

..

2 Commits

Author SHA1 Message Date
bobloy 1594df5f85 More WIP decrypt
4 years ago
bobloy d9494364d8 Initial commit, still WIP
4 years ago

@ -59,4 +59,4 @@
'cog: unicode':
- unicode/*
'cog: werewolf':
- werewolf/*
- werewolf

@ -6,7 +6,7 @@
# https://github.com/actions/labeler
name: Labeler
on: [pull_request_target]
on: [pull_request]
jobs:
label:

1
.gitignore vendored

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

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

@ -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()
try:
argument = view.get_quoted_word()
value = await rc.convert(ctx, argument)
except (CommandError, ArgumentParsingError):
view.index = previous
break
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
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
class CCRole(commands.Cog):
@ -72,7 +48,8 @@ class CCRole(commands.Cog):
"""Custom commands management with roles
Highly customizable custom commands with role management."""
pass
if not ctx.invoked_subcommand:
pass
@ccrole.command(name="add")
@checks.mod_or_permissions(administrator=True)
@ -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 (
@ -315,13 +292,13 @@ class CCRole(commands.Cog):
# Thank you Cog-Creators
cmd = ctx.invoked_with
cmd = cmd.lower() # Continues the proud case-insensitivity tradition of ccrole
cmd = cmd.lower() # Continues the proud case_insentivity tradition of ccrole
guild = ctx.guild
# message = ctx.message # Unneeded since switch to `on_message_without_command` from `on_command_error`
cmd_list = self.config.guild(guild).cmdlist
cmdlist = self.config.guild(guild).cmdlist
# cmd = message.content[len(prefix) :].split()[0].lower()
cmd = await cmd_list.get_raw(cmd, default=None)
cmd = await cmdlist.get_raw(cmd, default=None)
if cmd is not None:
await self.eval_cc(cmd, message, ctx)
@ -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,62 @@ 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
```
python -m spacy download en_core_web_lg # ~750 MB (CPU Optimized)
### Linux - Manually
python -m spacy download en_core_web_trf # ~500 MB (GPU Optimized)
#### Step 1: Built-in Downloader
```
[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,24 +2,19 @@ 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
from chatterbot.comparisons import JaccardSimilarity, LevenshteinDistance, SpacySimilarity
from chatterbot.response_selection import get_random_response
from chatterbot.trainers import ChatterBotCorpusTrainer, ListTrainer, UbuntuCorpusTrainer
from redbot.core import Config, checks, commands
from redbot.core import Config, commands
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,47 +153,16 @@ 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"
)
trainer = UbuntuCorpusTrainer(self.chatbot)
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 +174,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 +188,9 @@ 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()
@chatter.command(name="channel")
async def chatter_channel(
self, ctx: commands.Context, channel: Optional[discord.TextChannel] = None
@ -269,55 +210,12 @@ 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()
@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 applies to all guilds.
This command will erase all training data and reset your configuration settings
Use `[p]chatter cleardata True` to confirm.
Use `[p]chatter cleardata True`
"""
if not confirm:
@ -344,18 +242,20 @@ class Chatter(Cog):
await ctx.tick()
@commands.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
@ -367,33 +267,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 = threshold
self.similarity_algo = algos[algo_number]
async with ctx.typing():
self.chatbot = self._create_chatbot()
await ctx.tick()
@commands.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 +304,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 +312,7 @@ 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")
@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 +323,11 @@ 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")
@chatter.command(name="age")
async def age(self, ctx: commands.Context, days: int):
"""
Sets the number of days to look back
@ -452,16 +341,6 @@ 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()
@chatter.command(name="backup")
async def backup(self, ctx, backupname):
"""
@ -483,71 +362,7 @@ 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")
@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,8 +370,8 @@ class Chatter(Cog):
if not confirmation:
await ctx.maybe_send_embed(
"Warning: This command downloads ~500MB and is CPU intensive during training\n"
"If you're sure you want to continue, run `[p]chatter train ubuntu True`"
"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 +379,11 @@ 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")
@chatter.command(name="trainenglish")
async def chatter_train_english(self, ctx: commands.Context):
"""
Trains the bot in english
@ -581,32 +396,11 @@ 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']
@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 +409,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 +451,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 +463,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 +478,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",
"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",
"min_bot_version": "3.4.0",
"description": "Create an offline chatbot that talks like your average member using Machine Learning",
"hidden": false,
"install_msg": "Thank you for installing Chatter! 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,28 +53,23 @@ 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:
await self._conquest_current(ctx)
if ctx.invoked_subcommand is None:
if self.current_map is not None:
await self._conquest_current(ctx)
@conquest.command(name="list")
async def _conquest_list(self, ctx: commands.Context):
@ -88,13 +80,14 @@ 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"""
pass
if ctx.invoked_subcommand is None:
pass
@conquest_set.command(name="resetzoom")
async def _conquest_set_resetzoom(self, ctx: commands.Context):

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

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

@ -0,0 +1,5 @@
from .decrypt import Decrypt
async def setup(bot):
bot.add_cog(Decrypt(bot))

@ -0,0 +1,60 @@
import asyncio
import logging
import os
from concurrent.futures.process import ProcessPoolExecutor
from concurrent.futures.thread import ThreadPoolExecutor
from typing import Optional
import discord
from ciphey import decrypt, iface
from redbot.core import Config, commands
from redbot.core.bot import Red
from ciphey.__main__ import main
log = logging.getLogger("red.fox_v3.decrypt")
class Decrypt(commands.Cog):
"""
Fast generic decryption on any string
"""
def __init__(self, bot: Red):
super().__init__()
self.bot = bot
self.config = Config.get_conf(self, identifier=0, force_registration=True)
default_guild = {}
self.config.register_guild(**default_guild)
async def red_delete_data_for_user(self, **kwargs):
"""Nothing to delete"""
return
@commands.command(name="decrypt")
async def _decrypt(self, ctx: commands.Context, *, encrypted_string):
"""Attempt to decrypt any encrypted text"""
async with ctx.typing():
iconfig = iface.Config().library_default()
iconfig.timeout = 10
iconfig.complete_config()
future = self.bot.loop.run_in_executor(
None,
decrypt,
iconfig,
encrypted_string,
)
# TODO: This kills the bot somehow, waiting
try:
result = await asyncio.wait_for(future, timeout=15, loop=self.bot.loop)
except asyncio.TimeoutError:
result = None
if result:
await ctx.maybe_send_embed(result)
else:
await ctx.maybe_send_embed("Failed to decrypt")

@ -27,7 +27,8 @@ class ExclusiveRole(Cog):
async def exclusive(self, ctx):
"""Base command for managing exclusive roles"""
pass
if not ctx.invoked_subcommand:
pass
@exclusive.command(name="add")
@checks.mod_or_permissions(administrator=True)
@ -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:

@ -1,15 +1,5 @@
import sys
from .fifo import FIFO
# Applying fix from: https://github.com/Azure/azure-functions-python-worker/issues/640
# [Fix] Create a wrapper for importing imgres
from .date_trigger import *
from . import CustomDateTrigger
# [Fix] Register imgres into system modules
sys.modules["CustomDateTrigger"] = CustomDateTrigger
async def setup(bot):
cog = FIFO(bot)

@ -1,10 +0,0 @@
from apscheduler.triggers.date import DateTrigger
class CustomDateTrigger(DateTrigger):
def get_next_fire_time(self, previous_fire_time, now):
next_run = super().get_next_fire_time(previous_fire_time, now)
return next_run if next_run is not None and next_run >= now else None
def __getstate__(self):
return {"version": 1, "run_date": self.run_date}

@ -1,10 +1,8 @@
import itertools
import logging
from datetime import MAXYEAR, datetime, timedelta, tzinfo
from datetime import datetime, timedelta, tzinfo
from typing import Optional, Union
import discord
import pytz
from apscheduler.job import Job
from apscheduler.jobstores.base import JobLookupError
from apscheduler.schedulers.asyncio import AsyncIOScheduler
@ -12,7 +10,7 @@ from apscheduler.schedulers.base import STATE_PAUSED, STATE_RUNNING
from redbot.core import Config, checks, commands
from redbot.core.bot import Red
from redbot.core.commands import TimedeltaConverter
from redbot.core.utils.chat_formatting import humanize_timedelta, pagify
from redbot.core.utils.chat_formatting import pagify
from .datetime_cron_converters import CronConverter, DatetimeConverter, TimezoneConverter
from .task import Task
@ -23,12 +21,11 @@ schedule_log.setLevel(logging.DEBUG)
log = logging.getLogger("red.fox_v3.fifo")
async def _execute_task(**task_state):
log.info(f"Executing {task_state.get('name')}")
async def _execute_task(task_state):
log.info(f"Executing {task_state=}")
task = Task(**task_state)
if await task.load_from_config():
return await task.execute()
log.warning(f"Failed to load data on {task_state=}")
return False
@ -40,37 +37,6 @@ def _disassemble_job_id(job_id: str):
return job_id.split("_")
def _get_run_times(job: Job, now: datetime = None):
"""
Computes the scheduled run times between ``next_run_time`` and ``now`` (inclusive).
Modified to be asynchronous and yielding instead of all-or-nothing
"""
if not job.next_run_time:
raise StopIteration()
if now is None:
now = datetime(MAXYEAR, 12, 31, 23, 59, 59, 999999, tzinfo=job.next_run_time.tzinfo)
yield from _get_run_times(job, now) # Recursion
raise StopIteration()
next_run_time = job.next_run_time
while next_run_time and next_run_time <= now:
yield next_run_time
next_run_time = job.trigger.get_next_fire_time(next_run_time, now)
class CapturePrint:
"""Silly little class to get `print` output"""
def __init__(self):
self.string = None
def write(self, string):
self.string = string if self.string is None else self.string + "\n" + string
class FIFO(commands.Cog):
"""
Simple Scheduling Cog
@ -89,7 +55,7 @@ class FIFO(commands.Cog):
self.config.register_global(**default_global)
self.config.register_guild(**default_guild)
self.scheduler: Optional[AsyncIOScheduler] = None
self.scheduler = None
self.jobstore = None
self.tz_cog = None
@ -105,22 +71,17 @@ class FIFO(commands.Cog):
async def initialize(self):
job_defaults = {
"coalesce": True, # Multiple missed triggers within the grace time will only fire once
"max_instances": 5, # This is probably way too high, should likely only be one
"misfire_grace_time": 15, # 15 seconds ain't much, but it's honest work
"replace_existing": True, # Very important for persistent data
}
job_defaults = {"coalesce": False, "max_instances": 1}
# executors = {"default": AsyncIOExecutor()}
# Default executor is already AsyncIOExecutor
self.scheduler = AsyncIOScheduler(job_defaults=job_defaults, logger=schedule_log)
from .redconfigjobstore import RedConfigJobStore # Wait to import to prevent cyclic import
from .redconfigjobstore import RedConfigJobStore
self.jobstore = RedConfigJobStore(self.config, self.bot)
await self.jobstore.load_from_config()
await self.jobstore.load_from_config(self.scheduler, "default")
self.scheduler.add_jobstore(self.jobstore, "default")
self.scheduler.start()
@ -143,59 +104,41 @@ class FIFO(commands.Cog):
await task.delete_self()
async def _process_task(self, task: Task):
# None of this is necessar, we have `replace_existing` already
# job: Union[Job, None] = await self._get_job(task)
# if job is not None:
# combined_trigger_ = await task.get_combined_trigger()
# if combined_trigger_ is None:
# job.remove()
# else:
# job.reschedule(combined_trigger_)
# return job
job: Union[Job, None] = await self._get_job(task)
if job is not None:
job.reschedule(await task.get_combined_trigger())
return job
return await self._add_job(task)
async def _get_job(self, task: Task) -> Job:
return self.scheduler.get_job(_assemble_job_id(task.name, task.guild_id))
async def _add_job(self, task: Task):
combined_trigger_ = await task.get_combined_trigger()
if combined_trigger_ is None:
return None
return self.scheduler.add_job(
_execute_task,
kwargs=task.__getstate__(),
args=[task.__getstate__()],
id=_assemble_job_id(task.name, task.guild_id),
trigger=combined_trigger_,
name=task.name,
replace_existing=True,
trigger=await task.get_combined_trigger(),
)
async def _resume_job(self, task: Task):
job: Union[Job, None] = await self._get_job(task)
if job is not None:
job.resume()
else:
try:
job = self.scheduler.resume_job(job_id=_assemble_job_id(task.name, task.guild_id))
except JobLookupError:
job = await self._process_task(task)
return job
async def _pause_job(self, task: Task):
try:
return self.scheduler.pause_job(job_id=_assemble_job_id(task.name, task.guild_id))
except JobLookupError:
return False
return self.scheduler.pause_job(job_id=_assemble_job_id(task.name, task.guild_id))
async def _remove_job(self, task: Task):
try:
self.scheduler.remove_job(job_id=_assemble_job_id(task.name, task.guild_id))
except JobLookupError:
pass
return self.scheduler.remove_job(job_id=_assemble_job_id(task.name, task.guild_id))
async def _get_tz(self, user: Union[discord.User, discord.Member]) -> Union[None, tzinfo]:
if self.tz_cog is None:
self.tz_cog = self.bot.get_cog("Timezone")
if self.tz_cog is None:
self.tz_cog = False # only try once to get the timezone cog
if self.tz_cog is None:
self.tz_cog = False # only try once to get the timezone cog
if not self.tz_cog:
return None
@ -227,42 +170,8 @@ class FIFO(commands.Cog):
"""
Base command for handling scheduling of tasks
"""
pass
@fifo.command(name="wakeup")
async def fifo_wakeup(self, ctx: commands.Context):
"""Debug command to fix missed executions.
If you see a negative "Next run time" when adding a trigger, this may help resolve it.
Check the logs when using this command.
"""
self.scheduler.wakeup()
await ctx.tick()
@fifo.command(name="checktask", aliases=["checkjob", "check"])
async def fifo_checktask(self, ctx: commands.Context, task_name: str):
"""Returns the next 10 scheduled executions of the task"""
task = Task(task_name, ctx.guild.id, self.config, bot=self.bot)
await task.load_from_config()
if task.data is None:
await ctx.maybe_send_embed(
f"Task by the name of {task_name} is not found in this guild"
)
return
job = await self._get_job(task)
if job is None:
await ctx.maybe_send_embed("No job scheduled for this task")
return
now = datetime.now(job.next_run_time.tzinfo)
times = [
humanize_timedelta(timedelta=x - now)
for x in itertools.islice(_get_run_times(job), 10)
]
await ctx.maybe_send_embed("\n\n".join(times))
if ctx.invoked_subcommand is None:
pass
@fifo.command(name="set")
async def fifo_set(
@ -391,14 +300,10 @@ class FIFO(commands.Cog):
else:
embed.add_field(name="Server", value="Server not found", inline=False)
triggers, expired_triggers = await task.get_triggers()
trigger_str = "\n".join(str(t) for t in triggers)
expired_str = "\n".join(str(t) for t in expired_triggers)
trigger_str = "\n".join(str(t) for t in await task.get_triggers())
if trigger_str:
embed.add_field(name="Triggers", value=trigger_str, inline=False)
if expired_str:
embed.add_field(name="Expired Triggers", value=expired_str, inline=False)
job = await self._get_job(task)
if job and job.next_run_time:
@ -414,12 +319,12 @@ class FIFO(commands.Cog):
Do `[p]fifo list True` to see tasks from all guilds
"""
if all_guilds:
pass # TODO: All guilds
pass
else:
out = ""
all_tasks = await self.config.guild(ctx.guild).tasks()
for task_name, task_data in all_tasks.items():
out += f"{task_name}: {task_data}\n\n"
out += f"{task_name}: {task_data}\n"
if out:
if len(out) > 2000:
@ -430,28 +335,6 @@ class FIFO(commands.Cog):
else:
await ctx.maybe_send_embed("No tasks to list")
@fifo.command(name="printschedule")
async def fifo_printschedule(self, ctx: commands.Context):
"""
Print the current schedule of execution.
Useful for debugging.
"""
cp = CapturePrint()
self.scheduler.print_jobs(out=cp)
out = cp.string
out=out.replace("*","\*")
if out:
if len(out) > 2000:
for page in pagify(out):
await ctx.maybe_send_embed(page)
else:
await ctx.maybe_send_embed(out)
else:
await ctx.maybe_send_embed("Failed to get schedule from scheduler")
@fifo.command(name="add")
async def fifo_add(self, ctx: commands.Context, task_name: str, *, command_to_execute: str):
"""
@ -511,7 +394,6 @@ class FIFO(commands.Cog):
return
await task.clear_triggers()
await self._remove_job(task)
await ctx.tick()
@fifo.group(name="addtrigger", aliases=["trigger"])
@ -519,7 +401,8 @@ class FIFO(commands.Cog):
"""
Add a new trigger for a task from the current guild.
"""
pass
if ctx.invoked_subcommand is None:
pass
@fifo_trigger.command(name="interval")
async def fifo_trigger_interval(
@ -530,7 +413,7 @@ class FIFO(commands.Cog):
"""
task = Task(task_name, ctx.guild.id, self.config, bot=self.bot)
await task.load_from_config() # Will set the channel and author
await task.load_from_config()
if task.data is None:
await ctx.maybe_send_embed(
@ -552,40 +435,6 @@ class FIFO(commands.Cog):
f"Next run time: {job.next_run_time} ({delta_from_now.total_seconds()} seconds)"
)
@fifo_trigger.command(name="relative")
async def fifo_trigger_relative(
self, ctx: commands.Context, task_name: str, *, time_from_now: TimedeltaConverter
):
"""
Add a "run once" trigger at a time relative from now to the specified task
"""
task = Task(task_name, ctx.guild.id, self.config, bot=self.bot)
await task.load_from_config()
if task.data is None:
await ctx.maybe_send_embed(
f"Task by the name of {task_name} is not found in this guild"
)
return
time_to_run = datetime.now(pytz.utc) + time_from_now
result = await task.add_trigger("date", time_to_run, time_to_run.tzinfo)
if not result:
await ctx.maybe_send_embed(
"Failed to add a date trigger to this task, see console for logs"
)
return
await task.save_data()
job: Job = await self._process_task(task)
delta_from_now: timedelta = job.next_run_time - datetime.now(job.next_run_time.tzinfo)
await ctx.maybe_send_embed(
f"Task `{task_name}` added {time_to_run} to its scheduled runtimes\n"
f"Next run time: {job.next_run_time} ({delta_from_now.total_seconds()} seconds)"
)
@fifo_trigger.command(name="date")
async def fifo_trigger_date(
self, ctx: commands.Context, task_name: str, *, datetime_str: DatetimeConverter
@ -594,7 +443,7 @@ class FIFO(commands.Cog):
Add a "run once" datetime trigger to the specified task
"""
task = Task(task_name, ctx.guild.id, self.config, bot=self.bot)
task = Task(task_name, ctx.guild.id, self.config)
await task.load_from_config()
if task.data is None:
@ -634,7 +483,7 @@ class FIFO(commands.Cog):
See https://crontab.guru/ for help generating the cron_str
"""
task = Task(task_name, ctx.guild.id, self.config, bot=self.bot)
task = Task(task_name, ctx.guild.id, self.config)
await task.load_from_config()
if task.data is None:

@ -10,8 +10,7 @@
"end_user_data_statement": "This cog does not store any End User Data",
"requirements": [
"apscheduler",
"pytz",
"python-dateutil"
"pytz"
],
"tags": [
"bobloy",

@ -2,14 +2,17 @@ import asyncio
import base64
import logging
import pickle
from datetime import datetime
from typing import Tuple, Union
from apscheduler.job import Job
from apscheduler.jobstores.base import ConflictingIdError, JobLookupError
from apscheduler.jobstores.memory import MemoryJobStore
from apscheduler.schedulers.asyncio import run_in_event_loop
from apscheduler.util import datetime_to_utc_timestamp
from redbot.core import Config
# TODO: use get_lock on config maybe
# TODO: use get_lock on config
from redbot.core.bot import Red
from redbot.core.utils import AsyncIter
@ -25,55 +28,44 @@ class RedConfigJobStore(MemoryJobStore):
self.config = config
self.bot = bot
self.pickle_protocol = pickle.HIGHEST_PROTOCOL
self._eventloop = self.bot.loop # Used for @run_in_event_loop
self._eventloop = self.bot.loop
# TODO: self.config.jobs_index is never used,
# fine but maybe a sign of inefficient use of config
# task = asyncio.create_task(self.load_from_config())
# while not task.done():
# sleep(0.1)
# future = asyncio.ensure_future(self.load_from_config(), loop=self.bot.loop)
@run_in_event_loop
def start(self, scheduler, alias):
super().start(scheduler, alias)
for job, timestamp in self._jobs:
job._scheduler = self._scheduler
job._jobstore_alias = self._alias
async def load_from_config(self):
async def load_from_config(self, scheduler, alias):
super().start(scheduler, alias)
_jobs = await self.config.jobs()
# self._jobs = [
# (await self._decode_job(job), timestamp) async for (job, timestamp) in AsyncIter(_jobs)
# ]
async for job, timestamp in AsyncIter(_jobs, steps=5):
job = await self._decode_job(job)
index = self._get_job_index(timestamp, job.id)
self._jobs.insert(index, (job, timestamp))
self._jobs_index[job.id] = (job, timestamp)
async def save_to_config(self):
"""Yea that's basically it"""
await self.config.jobs.set(
[(self._encode_job(job), timestamp) for job, timestamp in self._jobs]
)
self._jobs = [
(await self._decode_job(job), timestamp) async for (job, timestamp) in AsyncIter(_jobs)
]
# self._jobs_index = await self.config.jobs_index.all() # Overwritten by next
# self._jobs_index = {job.id: (job, timestamp) for job, timestamp in self._jobs}
self._jobs_index = {job.id: (job, timestamp) for job, timestamp in self._jobs}
def _encode_job(self, job: Job):
job_state = job.__getstate__()
job_state["kwargs"]["config"] = None
job_state["kwargs"]["bot"] = None
# new_kwargs = job_state["kwargs"]
# new_kwargs["config"] = None
# new_kwargs["bot"] = None
# job_state["kwargs"] = new_kwargs
new_args = list(job_state["args"])
new_args[0]["config"] = None
new_args[0]["bot"] = None
job_state["args"] = tuple(new_args)
encoded = base64.b64encode(pickle.dumps(job_state, self.pickle_protocol))
out = {
"_id": job.id,
"next_run_time": datetime_to_utc_timestamp(job.next_run_time),
"job_state": encoded.decode("ascii"),
}
job_state["kwargs"]["config"] = self.config
job_state["kwargs"]["bot"] = self.bot
# new_kwargs = job_state["kwargs"]
# new_kwargs["config"] = self.config
# new_kwargs["bot"] = self.bot
# job_state["kwargs"] = new_kwargs
new_args = list(job_state["args"])
new_args[0]["config"] = self.config
new_args[0]["bot"] = self.bot
job_state["args"] = tuple(new_args)
# log.debug(f"Encoding job id: {job.id}\n"
# f"Encoded as: {out}")
@ -84,15 +76,10 @@ class RedConfigJobStore(MemoryJobStore):
return None
job_state = in_job["job_state"]
job_state = pickle.loads(base64.b64decode(job_state))
if job_state["args"]: # Backwards compatibility on args to kwargs
job_state["kwargs"] = {**job_state["args"][0]}
job_state["args"] = []
job_state["kwargs"]["config"] = self.config
job_state["kwargs"]["bot"] = self.bot
# new_kwargs = job_state["kwargs"]
# new_kwargs["config"] = self.config
# new_kwargs["bot"] = self.bot
# job_state["kwargs"] = new_kwargs
new_args = list(job_state["args"])
new_args[0]["config"] = self.config
new_args[0]["bot"] = self.bot
job_state["args"] = tuple(new_args)
job = Job.__new__(Job)
job.__setstate__(job_state)
job._scheduler = self._scheduler
@ -109,6 +96,79 @@ class RedConfigJobStore(MemoryJobStore):
return job
@run_in_event_loop
def add_job(self, job: Job):
if job.id in self._jobs_index:
raise ConflictingIdError(job.id)
# log.debug(f"Check job args: {job.args=}")
timestamp = datetime_to_utc_timestamp(job.next_run_time)
index = self._get_job_index(timestamp, job.id) # This is fine
self._jobs.insert(index, (job, timestamp))
self._jobs_index[job.id] = (job, timestamp)
asyncio.create_task(self._async_add_job(job, index, timestamp))
# log.debug(f"Added job: {self._jobs[index][0].args}")
async def _async_add_job(self, job, index, timestamp):
encoded_job = self._encode_job(job)
job_tuple = tuple([encoded_job, timestamp])
async with self.config.jobs() as jobs:
jobs.insert(index, job_tuple)
# await self.config.jobs_index.set_raw(job.id, value=job_tuple)
return True
@run_in_event_loop
def update_job(self, job):
old_tuple: Tuple[Union[Job, None], Union[datetime, None]] = self._jobs_index.get(
job.id, (None, None)
)
old_job = old_tuple[0]
old_timestamp = old_tuple[1]
if old_job is None:
raise JobLookupError(job.id)
# If the next run time has not changed, simply replace the job in its present index.
# Otherwise, reinsert the job to the list to preserve the ordering.
old_index = self._get_job_index(old_timestamp, old_job.id)
new_timestamp = datetime_to_utc_timestamp(job.next_run_time)
asyncio.create_task(
self._async_update_job(job, new_timestamp, old_index, old_job, old_timestamp)
)
async def _async_update_job(self, job, new_timestamp, old_index, old_job, old_timestamp):
encoded_job = self._encode_job(job)
if old_timestamp == new_timestamp:
self._jobs[old_index] = (job, new_timestamp)
async with self.config.jobs() as jobs:
jobs[old_index] = (encoded_job, new_timestamp)
else:
del self._jobs[old_index]
new_index = self._get_job_index(new_timestamp, job.id) # This is fine
self._jobs.insert(new_index, (job, new_timestamp))
async with self.config.jobs() as jobs:
del jobs[old_index]
jobs.insert(new_index, (encoded_job, new_timestamp))
self._jobs_index[old_job.id] = (job, new_timestamp)
# await self.config.jobs_index.set_raw(old_job.id, value=(encoded_job, new_timestamp))
log.debug(f"Async Updated {job.id=}")
log.debug(f"Check job args: {job.args=}")
@run_in_event_loop
def remove_job(self, job_id):
job, timestamp = self._jobs_index.get(job_id, (None, None))
if job is None:
raise JobLookupError(job_id)
index = self._get_job_index(timestamp, job_id)
del self._jobs[index]
del self._jobs_index[job.id]
asyncio.create_task(self._async_remove_job(index, job))
async def _async_remove_job(self, index, job):
async with self.config.jobs() as jobs:
del jobs[index]
# await self.config.jobs_index.clear_raw(job.id)
@run_in_event_loop
def remove_all_jobs(self):
super().remove_all_jobs()
@ -120,9 +180,4 @@ class RedConfigJobStore(MemoryJobStore):
def shutdown(self):
"""Removes all jobs without clearing config"""
asyncio.create_task(self.async_shutdown())
async def async_shutdown(self):
await self.save_to_config()
self._jobs = []
self._jobs_index = {}
super().remove_all_jobs()

@ -1,19 +1,18 @@
import logging
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Tuple, Union
from typing import Dict, List, Union
import discord
import pytz
from apscheduler.triggers.base import BaseTrigger
from apscheduler.triggers.combining import OrTrigger
from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.date import DateTrigger
from apscheduler.triggers.interval import IntervalTrigger
from discord.utils import time_snowflake
from pytz import timezone
from redbot.core import Config, commands
from redbot.core.bot import Red
from fifo.date_trigger import CustomDateTrigger
log = logging.getLogger("red.fox_v3.fifo.task")
@ -27,7 +26,7 @@ def get_trigger(data):
return IntervalTrigger(days=parsed_time.days, seconds=parsed_time.seconds)
if data["type"] == "date":
return CustomDateTrigger(data["time_data"], timezone=data["tzinfo"])
return DateTrigger(data["time_data"], timezone=data["tzinfo"])
if data["type"] == "cron":
return CronTrigger.from_crontab(data["time_data"], timezone=data["tzinfo"])
@ -35,127 +34,20 @@ def get_trigger(data):
return False
def check_expired_trigger(trigger: BaseTrigger):
return trigger.get_next_fire_time(None, datetime.now(pytz.utc)) is None
def parse_triggers(data: Union[Dict, None]):
if data is None or not data.get("triggers", False): # No triggers
return None
if len(data["triggers"]) > 1: # Multiple triggers
triggers_list = [get_trigger(t_data) for t_data in data["triggers"]]
triggers_list = [t for t in triggers_list if not check_expired_trigger(t)]
if not triggers_list:
return None
return OrTrigger(triggers_list)
else:
trigger = get_trigger(data["triggers"][0])
if check_expired_trigger(trigger):
return None
return trigger
# class FakeMessage:
# def __init__(self, message: discord.Message):
# d = {k: getattr(message, k, None) for k in dir(message)}
# self.__dict__.update(**d)
# Potential FakeMessage subclass of Message
# class DeleteSlots(type):
# @classmethod
# def __prepare__(metacls, name, bases):
# """Borrowed a bit from https://stackoverflow.com/q/56579348"""
# super_prepared = super().__prepare__(name, bases)
# print(super_prepared)
# return super_prepared
things_for_fakemessage_to_steal = [
"_state",
"id",
"webhook_id",
# "reactions",
# "attachments",
"embeds",
"application",
"activity",
"channel",
"_edited_time",
"type",
"pinned",
"flags",
"mention_everyone",
"tts",
"content",
"nonce",
"reference",
"_edited_timestamp" # New 7/23/21
]
things_fakemessage_sets_by_default = {
"attachments": [],
"reactions": [],
}
class FakeMessage(discord.Message):
def __init__(self, *args, message: discord.Message, **kwargs):
d = {k: getattr(message, k, None) for k in things_for_fakemessage_to_steal}
d.update(things_fakemessage_sets_by_default)
for k, v in d.items():
try:
# log.debug(f"{k=} {v=}")
setattr(self, k, v)
except TypeError:
# log.exception("This is fine")
pass
except AttributeError:
# log.exception("This is fine")
pass
self.id = time_snowflake(datetime.utcnow(), high=False) # Pretend to be now
self.type = discord.MessageType.default
def process_the_rest(
self,
author: discord.Member,
channel: discord.TextChannel,
content,
):
# self.content = content
# log.debug(self.content)
# for handler in ('author', 'member', 'mentions', 'mention_roles', 'call', 'flags'):
# try:
# getattr(self, '_handle_%s' % handler)(data[handler])
# except KeyError:
# continue
self.author = author
# self._handle_author(author._user._to_minimal_user_json())
# self._handle_member(author)
self._rebind_channel_reference(channel)
self._update(
{
"content": content,
}
)
self._update(
{
"mention_roles": self.raw_role_mentions,
"mentions": [{"id": _id} for _id in self.raw_mentions],
}
)
return OrTrigger(get_trigger(t_data) for t_data in data["triggers"])
# self._handle_content(content)
# log.debug(self.content)
return get_trigger(data["triggers"][0])
self.mention_everyone = "@everyone" in self.content or "@here" in self.content
# self._handle_mention_roles(self.raw_role_mentions)
# self._handle_mentions(self.raw_mentions)
# self.__dict__.update(**d)
class FakeMessage:
def __init__(self, message: discord.Message):
d = {k: getattr(message, k, None) for k in dir(message)}
self.__dict__.update(**d)
def neuter_message(message: FakeMessage):
@ -174,11 +66,11 @@ def neuter_message(message: FakeMessage):
class Task:
default_task_data = {"triggers": [], "command_str": "", "expired_triggers": []}
default_task_data = {"triggers": [], "command_str": ""}
default_trigger = {
"type": "",
"time_data": None,
"time_data": None, # Used for Interval and Date Triggers
"tzinfo": None,
}
@ -195,10 +87,9 @@ class Task:
async def _encode_time_triggers(self):
if not self.data or not self.data.get("triggers", None):
return [], []
return []
triggers = []
expired_triggers = []
for t in self.data["triggers"]:
if t["type"] == "interval": # Convert into timedelta
td: timedelta = t["time_data"]
@ -210,15 +101,27 @@ class Task:
if t["type"] == "date": # Convert into datetime
dt: datetime = t["time_data"]
data_to_append = {
"type": t["type"],
"time_data": dt.isoformat(),
"tzinfo": getattr(t["tzinfo"], "zone", None),
}
if dt < datetime.now(pytz.utc):
expired_triggers.append(data_to_append)
else:
triggers.append(data_to_append)
triggers.append(
{
"type": t["type"],
"time_data": dt.isoformat(),
"tzinfo": getattr(t["tzinfo"], "zone", None),
}
)
# triggers.append(
# {
# "type": t["type"],
# "time_data": {
# "year": dt.year,
# "month": dt.month,
# "day": dt.day,
# "hour": dt.hour,
# "minute": dt.minute,
# "second": dt.second,
# "tzinfo": dt.tzinfo,
# },
# }
# )
continue
if t["type"] == "cron":
@ -236,7 +139,7 @@ class Task:
raise NotImplemented
return triggers, expired_triggers
return triggers
async def _decode_time_triggers(self):
if not self.data or not self.data.get("triggers", None):
@ -249,7 +152,7 @@ class Task:
# First decode timezone if there is one
if t["tzinfo"] is not None:
t["tzinfo"] = pytz.timezone(t["tzinfo"])
t["tzinfo"] = timezone(t["tzinfo"])
if t["type"] == "interval": # Convert into timedelta
t["time_data"] = timedelta(**t["time_data"])
@ -277,7 +180,7 @@ class Task:
return
self.author_id = data["author_id"]
self.guild_id = data["guild_id"] # Weird I'm doing this, since self.guild_id was just used
self.guild_id = data["guild_id"]
self.channel_id = data["channel_id"]
self.data = data["data"]
@ -285,23 +188,14 @@ class Task:
await self._decode_time_triggers()
return self.data
async def get_triggers(self) -> Tuple[List[BaseTrigger], List[BaseTrigger]]:
async def get_triggers(self) -> List[Union[IntervalTrigger, DateTrigger]]:
if not self.data:
await self.load_from_config()
if self.data is None or "triggers" not in self.data: # No triggers
return [], []
trigs = []
expired_trigs = []
for t in self.data["triggers"]:
trig = get_trigger(t)
if check_expired_trigger(trig):
expired_trigs.append(t)
else:
trigs.append(t)
return []
return trigs, expired_trigs
return [get_trigger(t) for t in self.data["triggers"]]
async def get_combined_trigger(self) -> Union[BaseTrigger, None]:
if not self.data:
@ -321,10 +215,7 @@ class Task:
data_to_save = self.default_task_data.copy()
if self.data:
data_to_save["command_str"] = self.get_command_str()
(
data_to_save["triggers"],
data_to_save["expired_triggers"],
) = await self._encode_time_triggers()
data_to_save["triggers"] = await self._encode_time_triggers()
to_save = {
"guild_id": self.guild_id,
@ -340,10 +231,7 @@ class Task:
return
data_to_save = self.data.copy()
(
data_to_save["triggers"],
data_to_save["expired_triggers"],
) = await self._encode_time_triggers()
data_to_save["triggers"] = await self._encode_time_triggers()
await self.config.guild_from_id(self.guild_id).tasks.set_raw(
self.name, "data", value=data_to_save
@ -351,87 +239,63 @@ class Task:
async def execute(self):
if not self.data or not self.get_command_str():
log.warning(f"Could not execute Task[{self.name}] due to data problem: {self.data=}")
log.warning(f"Could not execute task due to data problem: {self.data=}")
return False
guild: discord.Guild = self.bot.get_guild(self.guild_id) # used for get_prefix
if guild is None:
log.warning(
f"Could not execute Task[{self.name}] due to missing guild: {self.guild_id}"
)
log.warning(f"Could not execute task due to missing guild: {self.guild_id}")
return False
channel: discord.TextChannel = guild.get_channel(self.channel_id)
if channel is None:
log.warning(
f"Could not execute Task[{self.name}] due to missing channel: {self.channel_id}"
)
log.warning(f"Could not execute task due to missing channel: {self.channel_id}")
return False
author: discord.Member = guild.get_member(self.author_id)
author: discord.User = guild.get_member(self.author_id)
if author is None:
log.warning(
f"Could not execute Task[{self.name}] due to missing author: {self.author_id}"
)
log.warning(f"Could not execute task due to missing author: {self.author_id}")
return False
actual_message: Optional[discord.Message] = channel.last_message
actual_message: discord.Message = channel.last_message
# I'd like to present you my chain of increasingly desperate message fetching attempts
if actual_message is None:
# log.warning("No message found in channel cache yet, skipping execution")
# return
if channel.last_message_id is not None:
try:
actual_message = await channel.fetch_message(channel.last_message_id)
except discord.NotFound:
actual_message = None
actual_message = await channel.fetch_message(channel.last_message_id)
if actual_message is None: # last_message_id was an invalid message I guess
actual_message = await channel.history(limit=1).flatten()
if not actual_message: # Basically only happens if the channel has no messages
actual_message = await author.history(limit=1).flatten()
if not actual_message: # Okay, the *author* has never sent a message?
log.warning("No message found in channel cache yet, skipping execution")
return False
return
actual_message = actual_message[0]
# message._handle_author(author) # Option when message is subclass
# message._state = self.bot._get_state()
# Time to set the relevant attributes
# message.author = author
# Don't need guild with subclass, guild is just channel.guild
# message.guild = guild # Just in case we got desperate, see above
# message.channel = channel
message = FakeMessage(actual_message)
# message = FakeMessage2
message.author = author
message.guild = guild # Just in case we got desperate
message.channel = channel
message.id = time_snowflake(datetime.now()) # Pretend to be now
message = neuter_message(message)
# absolutely weird that this takes a message object instead of guild
prefixes = await self.bot.get_prefix(actual_message)
prefixes = await self.bot.get_prefix(message)
if isinstance(prefixes, str):
prefix = prefixes
else:
prefix = prefixes[0]
new_content = f"{prefix}{self.get_command_str()}"
# log.debug(f"{new_content=}")
message.content = f"{prefix}{self.get_command_str()}"
message = FakeMessage(message=actual_message)
message = neuter_message(message)
message.process_the_rest(author=author, channel=channel, content=new_content)
if (
not message.guild
or not message.author
or not message.content
or message.content == prefix
):
log.warning(
f"Could not execute Task[{self.name}] due to message problem: "
f"{message.guild=}, {message.author=}, {message.content=}"
)
if not message.guild or not message.author or not message.content:
log.warning(f"Could not execute task due to message problem: {message}")
return False
new_ctx: commands.Context = await self.bot.get_context(message)
new_ctx.assume_yes = True
if not new_ctx.valid:
log.warning(
f"Could not execute Task[{self.name}] due invalid context: "
f"{new_ctx.invoked_with=} {new_ctx.prefix=} {new_ctx.command=}"
f"Could not execute Task[{self.name}] due invalid context: {new_ctx.invoked_with}"
)
return False

@ -5,8 +5,6 @@ All credit to https://github.com/prefrontal/dateutil-parser-timezones
"""
# from dateutil.tz import gettz
from datetime import datetime
from pytz import timezone
@ -229,6 +227,4 @@ def assemble_timezones():
timezones["YAKT"] = timezone("Asia/Yakutsk") # Yakutsk Time (UTC+09)
timezones["YEKT"] = timezone("Asia/Yekaterinburg") # Yekaterinburg Time (UTC+05)
dt = datetime(2020, 1, 1)
timezones.update((x, y.localize(dt).tzinfo) for x, y in timezones.items())
return timezones

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

@ -147,7 +147,8 @@ class Hangman(Cog):
@checks.mod_or_permissions(administrator=True)
async def hangset(self, ctx):
"""Adjust hangman settings"""
pass
if ctx.invoked_subcommand is None:
pass
@hangset.command()
async def face(self, ctx: commands.Context, theface):
@ -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

@ -1,7 +1,5 @@
from .infochannel import InfoChannel
async def setup(bot):
ic_cog = InfoChannel(bot)
bot.add_cog(ic_cog)
await ic_cog.initialize()
def setup(bot):
bot.add_cog(InfoChannel(bot))

@ -1,53 +1,25 @@
import asyncio
import logging
from collections import defaultdict
from typing import Dict, Optional, Union
from typing import Union
import discord
from redbot.core import Config, checks, commands
from redbot.core.bot import Red
from redbot.core.commands import Cog
# 10 minutes. Rate limit is 2 per 10, so 1 per 6 is safe.
RATE_LIMIT_DELAY = 60 * 6 # If you're willing to risk rate limiting, you can decrease the delay
log = logging.getLogger("red.fox_v3.infochannel")
async def get_channel_counts(category, guild):
# Gets count of bots
bot_num = len([m for m in guild.members if m.bot])
# Gets count of roles in the server
roles_num = len(guild.roles) - 1
# Gets count of channels in the server
# <number of total channels> - <number of channels in the stats category> - <categories>
channels_num = len(guild.channels) - len(category.voice_channels) - len(guild.categories)
# Gets all counts of members
members = guild.member_count
offline_num = len(list(filter(lambda m: m.status is discord.Status.offline, guild.members)))
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,
"online": online_num,
"offline": offline_num,
}
# Cog: Any = getattr(commands, "Cog", object)
# listener = getattr(commands.Cog, "listener", None) # Trusty + Sinbad
# if listener is None:
# def listener(name=None):
# return lambda x: x
RATE_LIMIT_DELAY = 60 * 10 # If you're willing to risk rate limiting, you can decrease the delay
class InfoChannel(Cog):
"""
Create a channel with updating server info
This relies on editing channels, which is a strictly rate-limited activity.
As such, updates will not be frequent. Currently capped at 1 per 5 minutes per server.
Less important information about the cog
"""
def __init__(self, bot: Red):
@ -57,56 +29,23 @@ class InfoChannel(Cog):
self, identifier=731101021116710497110110101108, force_registration=True
)
# self. so I can get the keys from this later
self.default_channel_names = {
"members": "Members: {count}",
"humans": "Humans: {count}",
"boosters": "Boosters: {count}",
"bots": "Bots: {count}",
"roles": "Roles: {count}",
"channels": "Channels: {count}",
"online": "Online: {count}",
"offline": "Offline: {count}",
}
default_channel_ids = {k: None for k in self.default_channel_names}
# Only members is enabled by default
default_enabled_counts = {k: k == "members" for k in self.default_channel_names}
default_guild = {
"category_id": None,
"channel_ids": default_channel_ids,
"enabled_channels": default_enabled_counts,
"channel_names": self.default_channel_names,
"channel_id": None,
"botchannel_id": None,
"onlinechannel_id": None,
"member_count": True,
"bot_count": False,
"online_count": False,
}
self.config.register_guild(**default_guild)
self.default_role = {"enabled": False, "channel_id": None, "name": "{role}: {count}"}
self.config.register_role(**self.default_role)
self._critical_section_wooah_ = 0
self.channel_data = defaultdict(dict)
self.edit_queue = defaultdict(lambda: defaultdict(lambda: asyncio.Queue(maxsize=2)))
self._rate_limited_edits: Dict[int, Dict[str, Optional[asyncio.Task]]] = defaultdict(
lambda: defaultdict(lambda: None)
)
async def red_delete_data_for_user(self, **kwargs):
"""Nothing to delete"""
return
async def initialize(self):
for guild in self.bot.guilds:
await self.update_infochannel(guild)
def cog_unload(self):
self.stop_all_queues()
@commands.command()
@checks.admin()
async def infochannel(self, ctx: commands.Context):
@ -122,466 +61,233 @@ class InfoChannel(Cog):
)
guild: discord.Guild = ctx.guild
category_id = await self.config.guild(guild).category_id()
category = None
if category_id is not None:
category: Union[discord.CategoryChannel, None] = guild.get_channel(category_id)
channel_id = await self.config.guild(guild).channel_id()
channel = None
if channel_id is not None:
channel: Union[discord.VoiceChannel, None] = guild.get_channel(channel_id)
if category_id is not None and category is None:
await ctx.maybe_send_embed("Info category has been deleted, recreate it?")
elif category_id is None:
await ctx.maybe_send_embed("Enable info channels on this server?")
if channel_id is not None and channel is None:
await ctx.send("Info channel has been deleted, recreate it?")
elif channel_id is None:
await ctx.send("Enable info channel on this server?")
else:
await ctx.maybe_send_embed("Do you wish to delete current info channels?")
await ctx.send("Do you wish to delete current info channels?")
msg = await self.bot.wait_for("message", check=check)
if msg.content.upper() in ["N", "NO"]:
await ctx.maybe_send_embed("Cancelled")
await ctx.send("Cancelled")
return
if category is None:
if channel is None:
try:
await self.make_infochannel(guild)
except discord.Forbidden:
await ctx.maybe_send_embed(
"Failure: Missing permission to create necessary channels"
)
await ctx.send("Failure: Missing permission to create voice channel")
return
else:
await self.delete_all_infochannels(guild)
ctx.message = msg
if not await ctx.tick():
await ctx.maybe_send_embed("Done!")
await ctx.send("Done!")
@commands.group(aliases=["icset"])
@commands.group()
@checks.admin()
async def infochannelset(self, ctx: commands.Context):
"""
Toggle different types of infochannels
"""
pass
@infochannelset.command(name="togglechannel")
async def _infochannelset_togglechannel(
self, ctx: commands.Context, channel_type: str, enabled: Optional[bool] = None
):
"""Toggles the infochannel for the specified channel type.
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,
- `online`: Total online members,
- `offline`: Total offline members,
if not ctx.invoked_subcommand:
pass
@infochannelset.command(name="botcount")
async def _infochannelset_botcount(self, ctx: commands.Context, enabled: bool = None):
"""
Toggle an infochannel that shows the amount of bots in the server
"""
guild = ctx.guild
if channel_type not in self.default_channel_names.keys():
await ctx.maybe_send_embed("Invalid channel type provided.")
return
if enabled is None:
enabled = not await self.config.guild(guild).enabled_channels.get_raw(channel_type)
await self.config.guild(guild).enabled_channels.set_raw(channel_type, value=enabled)
await self.make_infochannel(ctx.guild, channel_type=channel_type)
if enabled:
await ctx.maybe_send_embed(f"InfoChannel `{channel_type}` has been enabled.")
else:
await ctx.maybe_send_embed(f"InfoChannel `{channel_type}` has been disabled.")
@infochannelset.command(name="togglerole")
async def _infochannelset_rolecount(
self, ctx: commands.Context, role: discord.Role, enabled: bool = None
):
"""Toggle an infochannel that shows the count of users with the specified role"""
if enabled is None:
enabled = not await self.config.role(role).enabled()
enabled = not await self.config.guild(guild).bot_count()
await self.config.role(role).enabled.set(enabled)
await self.make_infochannel(ctx.guild, channel_role=role)
await self.config.guild(guild).bot_count.set(enabled)
await self.make_infochannel(ctx.guild)
if enabled:
await ctx.maybe_send_embed(f"InfoChannel for {role.name} count has been enabled.")
await ctx.send("InfoChannel for bot count has been enabled.")
else:
await ctx.maybe_send_embed(f"InfoChannel for {role.name} count has been disabled.")
await ctx.send("InfoChannel for bot count has been disabled.")
@infochannelset.command(name="name")
async def _infochannelset_name(self, ctx: commands.Context, channel_type: str, *, text=None):
@infochannelset.command(name="onlinecount")
async def _infochannelset_onlinecount(self, ctx: commands.Context, enabled: bool = None):
"""
Change the name of the infochannel for the specified channel type.
{count} must be used to display number of total members in the server.
Leave blank to set back to default.
Examples:
- `[p]infochannelset name members Cool Cats: {count}`
- `[p]infochannelset name bots {count} Robot Overlords`
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
- `online`: Total online members
- `offline`: Total offline members
Warning: This command counts against the channel update rate limit and may be queued.
Toggle an infochannel that shows the amount of online users in the server
"""
guild = ctx.guild
if channel_type not in self.default_channel_names.keys():
await ctx.maybe_send_embed("Invalid channel type provided.")
return
if text is None:
text = self.default_channel_names.get(channel_type)
elif "{count}" not in text:
await ctx.maybe_send_embed(
"Improperly formatted. Make sure to use `{count}` in your channel name"
)
return
elif len(text) > 93:
await ctx.maybe_send_embed("Name is too long, max length is 93.")
return
await self.config.guild(guild).channel_names.set_raw(channel_type, value=text)
await self.update_infochannel(guild, channel_type=channel_type)
if not await ctx.tick():
await ctx.maybe_send_embed("Done!")
@infochannelset.command(name="rolename")
async def _infochannelset_rolename(
self, ctx: commands.Context, role: discord.Role, *, text=None
):
"""
Change the name of the infochannel for specific roles.
{count} must be used to display number members with the given role.
{role} can be used for the roles name.
Leave blank to set back to default.
Default is set to: `{role}: {count}`
Examples:
- `[p]infochannelset rolename @Patrons {role}: {count}`
- `[p]infochannelset rolename Elite {count} members with {role} role`
- `[p]infochannelset rolename "Space Role" Total boosters: {count}`
Warning: This command counts against the channel update rate limit and may be queued.
"""
guild = ctx.message.guild
if text is None:
text = self.default_role["name"]
elif "{count}" not in text:
await ctx.maybe_send_embed(
"Improperly formatted. Make sure to use `{count}` in your channel name"
)
return
await self.config.role(role).name.set(text)
await self.update_infochannel(guild, channel_role=role)
if not await ctx.tick():
await ctx.maybe_send_embed("Done!")
async def create_individual_channel(
self, guild, category: discord.CategoryChannel, overwrites, channel_type, count
):
# Delete the channel if it exists
channel_id = await self.config.guild(guild).channel_ids.get_raw(channel_type)
if channel_id is not None:
channel: discord.VoiceChannel = guild.get_channel(channel_id)
if channel:
self.stop_queue(guild.id, channel_type)
await channel.delete(reason="InfoChannel delete")
if enabled is None:
enabled = not await self.config.guild(guild).online_count()
# Only make the channel if it's enabled
if await self.config.guild(guild).enabled_channels.get_raw(channel_type):
name = await self.config.guild(guild).channel_names.get_raw(channel_type)
name = name.format(count=count)
channel = await category.create_voice_channel(
name, reason="InfoChannel make", overwrites=overwrites
)
await self.config.guild(guild).channel_ids.set_raw(channel_type, value=channel.id)
return channel
return None
async def create_role_channel(
self, guild, category: discord.CategoryChannel, overwrites, role: discord.Role
):
# Delete the channel if it exists
channel_id = await self.config.role(role).channel_id()
if channel_id is not None:
channel: discord.VoiceChannel = guild.get_channel(channel_id)
if channel:
self.stop_queue(guild.id, role.id)
await channel.delete(reason="InfoChannel delete")
await self.config.guild(guild).online_count.set(enabled)
await self.make_infochannel(ctx.guild)
# Only make the channel if it's enabled
if await self.config.role(role).enabled():
count = len(role.members)
name = await self.config.role(role).name()
name = name.format(role=role.name, count=count)
channel = await category.create_voice_channel(
name, reason="InfoChannel make", overwrites=overwrites
)
await self.config.role(role).channel_id.set(channel.id)
return channel
return None
if enabled:
await ctx.send("InfoChannel for online user count has been enabled.")
else:
await ctx.send("InfoChannel for online user count has been disabled.")
async def make_infochannel(self, guild: discord.Guild, channel_type=None, channel_role=None):
async def make_infochannel(self, guild: discord.Guild):
botcount = await self.config.guild(guild).bot_count()
onlinecount = await self.config.guild(guild).online_count()
overwrites = {
guild.default_role: discord.PermissionOverwrite(connect=False),
guild.me: discord.PermissionOverwrite(manage_channels=True, connect=True),
}
# Check for and create the Infochannel category
category_id = await self.config.guild(guild).category_id()
if category_id is not None:
category: discord.CategoryChannel = guild.get_channel(category_id)
if category is None: # Category id is invalid, probably deleted.
category_id = None
if category_id is None:
category: discord.CategoryChannel = await guild.create_category(
"Server Stats", reason="InfoChannel Category make"
)
await self.config.guild(guild).category_id.set(category.id)
await category.edit(position=0)
category_id = category.id
category: discord.CategoryChannel = guild.get_channel(category_id)
# Remove the old info channel first
channel_id = await self.config.guild(guild).channel_id()
if channel_id is not None:
channel: discord.VoiceChannel = guild.get_channel(channel_id)
if channel:
await channel.delete(reason="InfoChannel delete")
channel_data = await get_channel_counts(category, guild)
# Then create the new one
channel = await guild.create_voice_channel(
"Total Humans:", reason="InfoChannel make", overwrites=overwrites
)
await self.config.guild(guild).channel_id.set(channel.id)
# Only update a single channel
if channel_type is not None:
await self.create_individual_channel(
guild, category, overwrites, channel_type, channel_data[channel_type]
)
return
if channel_role is not None:
await self.create_role_channel(guild, category, overwrites, channel_role)
return
if botcount:
# Remove the old bot channel first
botchannel_id = await self.config.guild(guild).botchannel_id()
if channel_id is not None:
botchannel: discord.VoiceChannel = guild.get_channel(botchannel_id)
if botchannel:
await botchannel.delete(reason="InfoChannel delete")
# Update all channels
for channel_type in self.default_channel_names.keys():
await self.create_individual_channel(
guild, category, overwrites, channel_type, channel_data[channel_type]
# Then create the new one
botchannel = await guild.create_voice_channel(
"Bots:", reason="InfoChannel botcount", overwrites=overwrites
)
for role in guild.roles:
await self.create_role_channel(guild, category, overwrites, role)
# await self.update_infochannel(guild)
async def delete_all_infochannels(self, guild: discord.Guild):
self.stop_guild_queues(guild.id) # Stop processing edits
# Delete regular channels
for channel_type in self.default_channel_names.keys():
channel_id = await self.config.guild(guild).channel_ids.get_raw(channel_type)
if channel_id is not None:
channel = guild.get_channel(channel_id)
if channel is not None:
await channel.delete(reason="InfoChannel delete")
await self.config.guild(guild).channel_ids.clear_raw(channel_type)
# Delete role channels
for role in guild.roles:
channel_id = await self.config.role(role).channel_id()
await self.config.guild(guild).botchannel_id.set(botchannel.id)
if onlinecount:
# Remove the old online channel first
onlinechannel_id = await self.config.guild(guild).onlinechannel_id()
if channel_id is not None:
channel = guild.get_channel(channel_id)
if channel is not None:
await channel.delete(reason="InfoChannel delete")
await self.config.role(role).channel_id.clear()
# Delete the category last
category_id = await self.config.guild(guild).category_id()
if category_id is not None:
category = guild.get_channel(category_id)
if category is not None:
await category.delete(reason="InfoChannel delete")
async def add_to_queue(self, guild, channel, identifier, count, formatted_name):
self.channel_data[guild.id][identifier] = (count, formatted_name, channel.id)
if not self.edit_queue[guild.id][identifier].full():
try:
self.edit_queue[guild.id][identifier].put_nowait(identifier)
except asyncio.QueueFull:
pass # If queue is full, disregard
if self._rate_limited_edits[guild.id][identifier] is None:
await self.start_queue(guild.id, identifier)
onlinechannel: discord.VoiceChannel = guild.get_channel(onlinechannel_id)
if onlinechannel:
await onlinechannel.delete(reason="InfoChannel delete")
async def update_individual_channel(self, guild, channel_type, count, guild_data):
name = guild_data["channel_names"][channel_type]
name = name.format(count=count)
channel = guild.get_channel(guild_data["channel_ids"][channel_type])
if channel is None:
return # abort
await self.add_to_queue(guild, channel, channel_type, count, name)
async def update_role_channel(self, guild, role: discord.Role, role_data):
if not role_data["enabled"]:
return # Not enabled
count = len(role.members)
name = role_data["name"]
name = name.format(role=role.name, count=count)
channel = guild.get_channel(role_data["channel_id"])
if channel is None:
return # abort
await self.add_to_queue(guild, channel, role.id, count, name)
async def update_infochannel(self, guild: discord.Guild, channel_type=None, channel_role=None):
if channel_type is None and channel_role is None:
return await self.trigger_updates_for(
guild,
members=True,
humans=True,
boosters=True,
bots=True,
roles=True,
channels=True,
online=True,
offline=True,
extra_roles=set(guild.roles),
# Then create the new one
onlinechannel = await guild.create_voice_channel(
"Online:", reason="InfoChannel onlinecount", overwrites=overwrites
)
await self.config.guild(guild).onlinechannel_id.set(onlinechannel.id)
if channel_type is not None:
return await self.trigger_updates_for(guild, **{channel_type: True})
return await self.trigger_updates_for(guild, extra_roles={channel_role})
async def start_queue(self, guild_id, identifier):
self._rate_limited_edits[guild_id][identifier] = asyncio.create_task(
self._process_queue(guild_id, identifier)
)
def stop_queue(self, guild_id, identifier):
if self._rate_limited_edits[guild_id][identifier] is not None:
self._rate_limited_edits[guild_id][identifier].cancel()
def stop_guild_queues(self, guild_id):
for identifier in self._rate_limited_edits[guild_id].keys():
self.stop_queue(guild_id, identifier)
def stop_all_queues(self):
for guild_id in self._rate_limited_edits.keys():
self.stop_guild_queues(guild_id)
await self.update_infochannel(guild)
async def _process_queue(self, guild_id, identifier):
while True:
identifier = await self.edit_queue[guild_id][identifier].get() # Waits forever
count, formatted_name, channel_id = self.channel_data[guild_id][identifier]
channel: discord.VoiceChannel = self.bot.get_channel(channel_id)
if channel.name == formatted_name:
continue # Nothing to process
log.debug(f"Processing guild_id: {guild_id} - identifier: {identifier}")
try:
await channel.edit(reason="InfoChannel update", name=formatted_name)
except (discord.Forbidden, discord.HTTPException):
pass # Don't bother figuring it out
except discord.InvalidArgument:
log.exception(f"Invalid formatted infochannel: {formatted_name}")
else:
await asyncio.sleep(RATE_LIMIT_DELAY) # Wait a reasonable amount of time
async def trigger_updates_for(self, guild, **kwargs):
extra_roles: Optional[set] = kwargs.pop("extra_roles", False)
async def delete_all_infochannels(self, guild: discord.Guild):
guild_data = await self.config.guild(guild).all()
to_update = (
kwargs.keys() & [key for key, value in guild_data["enabled_channels"].items() if value]
) # Value in kwargs doesn't matter
if to_update or extra_roles:
log.debug(f"{to_update=}\n"
f"{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)
if to_update:
for channel_type in to_update:
await self.update_individual_channel(
guild, channel_type, channel_data[channel_type], guild_data
)
if extra_roles:
role_data = await self.config.all_roles()
for channel_role in extra_roles:
if channel_role.id in role_data:
await self.update_role_channel(
guild, channel_role, role_data[channel_role.id]
)
@Cog.listener(name="on_member_join")
@Cog.listener(name="on_member_remove")
async def on_member_join_remove(self, member: discord.Member):
if await self.bot.cog_disabled_in_guild(self, member.guild):
return
if member.bot:
await self.trigger_updates_for(
member.guild, members=True, bots=True, online=True, offline=True
)
else:
await self.trigger_updates_for(
member.guild, members=True, humans=True, online=True, offline=True
)
botchannel_id = guild_data["botchannel_id"]
onlinechannel_id = guild_data["onlinechannel_id"]
botchannel: discord.VoiceChannel = guild.get_channel(botchannel_id)
onlinechannel: discord.VoiceChannel = guild.get_channel(onlinechannel_id)
channel_id = guild_data["channel_id"]
channel: discord.VoiceChannel = guild.get_channel(channel_id)
await channel.delete(reason="InfoChannel delete")
if botchannel_id is not None:
await botchannel.delete(reason="InfoChannel delete")
if onlinechannel_id is not None:
await onlinechannel.delete(reason="InfoChannel delete")
await self.config.guild(guild).clear()
async def update_infochannel(self, guild: discord.Guild):
guild_data = await self.config.guild(guild).all()
botcount = guild_data["bot_count"]
onlinecount = guild_data["online_count"]
# Gets count of bots
# bots = lambda x: x.bot
# def bots(x): return x.bot
bot_num = len([m for m in guild.members if m.bot])
# bot_msg = f"Bots: {num}"
# Gets count of online users
members = guild.member_count
offline = len(list(filter(lambda m: m.status is discord.Status.offline, guild.members)))
online_num = members - offline
# online_msg = f"Online: {num}"
# Gets count of actual users
total = lambda x: not x.bot
human_num = len([m for m in guild.members if total(m)])
# human_msg = f"Total Humans: {num}"
channel_id = guild_data["channel_id"]
if channel_id is None:
return False
botchannel_id = guild_data["botchannel_id"]
onlinechannel_id = guild_data["onlinechannel_id"]
channel_id = guild_data["channel_id"]
channel: discord.VoiceChannel = guild.get_channel(channel_id)
botchannel: discord.VoiceChannel = guild.get_channel(botchannel_id)
onlinechannel: discord.VoiceChannel = guild.get_channel(onlinechannel_id)
if guild_data["member_count"]:
name = f"{channel.name.split(':')[0]}: {human_num}"
await channel.edit(reason="InfoChannel update", name=name)
if botcount:
name = f"{botchannel.name.split(':')[0]}: {bot_num}"
await botchannel.edit(reason="InfoChannel update", name=name)
if onlinecount:
name = f"{onlinechannel.name.split(':')[0]}: {online_num}"
await onlinechannel.edit(reason="InfoChannel update", name=name)
async def update_infochannel_with_cooldown(self, guild):
"""My attempt at preventing rate limits, lets see how it goes"""
if self._critical_section_wooah_:
if self._critical_section_wooah_ == 2:
# print("Already pending, skipping")
return # Another one is already pending, don't queue more than one
# print("Queuing another update")
self._critical_section_wooah_ = 2
while self._critical_section_wooah_:
await asyncio.sleep(
RATE_LIMIT_DELAY // 4
) # Max delay ends up as 1.25 * RATE_LIMIT_DELAY
# print("Issuing queued update")
return await self.update_infochannel_with_cooldown(guild)
# print("Entering critical")
self._critical_section_wooah_ = 1
await self.update_infochannel(guild)
await asyncio.sleep(RATE_LIMIT_DELAY)
self._critical_section_wooah_ = 0
# print("Exiting critical")
@Cog.listener()
async def on_member_update(self, before: discord.Member, after: discord.Member):
if await self.bot.cog_disabled_in_guild(self, after.guild):
return
if before.status != after.status:
return await self.trigger_updates_for(after.guild, online=True, offline=True)
# XOR
c = set(after.roles) ^ set(before.roles)
if c:
await self.trigger_updates_for(after.guild, extra_roles=c)
@Cog.listener("on_guild_channel_create")
@Cog.listener("on_guild_channel_delete")
async def on_guild_channel_create_delete(self, channel: discord.TextChannel):
if await self.bot.cog_disabled_in_guild(self, channel.guild):
async def on_member_join(self, member: discord.Member):
if await self.bot.cog_disabled_in_guild(self, member.guild):
return
await self.trigger_updates_for(channel.guild, channels=True)
await self.update_infochannel_with_cooldown(member.guild)
@Cog.listener()
async def on_guild_role_create(self, role):
if await self.bot.cog_disabled_in_guild(self, role.guild):
async def on_member_remove(self, member: discord.Member):
if await self.bot.cog_disabled_in_guild(self, member.guild):
return
await self.trigger_updates_for(role.guild, roles=True)
await self.update_infochannel_with_cooldown(member.guild)
@Cog.listener()
async def on_guild_role_delete(self, role):
if await self.bot.cog_disabled_in_guild(self, role.guild):
async def on_member_update(self, before: discord.Member, after: discord.Member):
if await self.bot.cog_disabled_in_guild(self, after.guild):
return
await self.trigger_updates_for(role.guild, roles=True)
role_channel_id = await self.config.role(role).channel_id()
if role_channel_id is not None:
rolechannel: discord.VoiceChannel = role.guild.get_channel(role_channel_id)
if rolechannel:
await rolechannel.delete(reason="InfoChannel delete")
await self.config.role(role).clear()
onlinecount = await self.config.guild(after.guild).online_count()
if onlinecount:
if before.status != after.status:
await self.update_infochannel_with_cooldown(after.guild)

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

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

@ -1,7 +1,7 @@
import asyncio
import functools
import logging
import re
import discord
import launchlibrary as ll
from redbot.core import Config, commands
@ -14,7 +14,9 @@ log = logging.getLogger("red.fox_v3.launchlib")
class LaunchLib(commands.Cog):
"""
Cog using `thespacedevs` API to get details about rocket launches
Cog Description
Less important information about the cog
"""
def __init__(self, bot: Red):
@ -35,30 +37,27 @@ class LaunchLib(commands.Cog):
return
async def _embed_launch_data(self, launch: ll.AsyncLaunch):
# status: ll.AsyncLaunchStatus = await launch.get_status()
status = launch.status
status: ll.AsyncLaunchStatus = await launch.get_status()
rocket: ll.AsyncRocket = launch.rocket
title = launch.name
description = status["name"]
description = status.description
urls = launch.vid_urls + launch.info_urls
if rocket:
urls += [rocket.info_url, rocket.wiki_url]
if launch.pad:
urls += [launch.pad.info_url, launch.pad.wiki_url]
if not urls and rocket:
urls = rocket.info_urls + [rocket.wiki_url]
if urls:
url = urls[0]
else:
url = None
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()
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)
if rocket and rocket.image_url and rocket.image_url != "Array":
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)
if agency is not None:
@ -90,18 +89,6 @@ class LaunchLib(commands.Cog):
data = mission.get(f[0], None)
if data is not None and 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:
em.add_field(name="Rocket Family", value=rocket.family)
@ -114,16 +101,11 @@ class LaunchLib(commands.Cog):
@commands.group()
async def launchlib(self, ctx: commands.Context):
"""Base command for getting launches"""
pass
if ctx.invoked_subcommand is None:
pass
@launchlib.command()
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)
# loop = asyncio.get_running_loop()
#
@ -133,8 +115,6 @@ class LaunchLib(commands.Cog):
#
launches = await self.api.async_fetch_launch(num=num_launches)
# log.debug(str(launches))
async with ctx.typing():
for x, launch in enumerate(launches):
if x >= num_launches:

@ -25,7 +25,8 @@ class Leaver(Cog):
@checks.mod_or_permissions(administrator=True)
async def leaverset(self, ctx):
"""Adjust leaver settings"""
pass
if ctx.invoked_subcommand is None:
pass
@leaverset.command()
async def channel(self, ctx: Context):
@ -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,12 +45,14 @@ 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"""
pass
if ctx.invoked_subcommand is None:
pass
@lset.command(name="toggle")
async def lset_toggle(self, ctx: commands.Context):
@ -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)
await ctx.send(embed=embed)
@commands.Cog.listener()

@ -8,7 +8,9 @@ from redbot.core.data_manager import cog_data_path
class Nudity(commands.Cog):
"""Monitor images for NSFW content and moves them to a nsfw channel if possible"""
"""
V3 Cog Template
"""
def __init__(self, bot: Red):
super().__init__()

@ -111,8 +111,9 @@ async def _withdraw_points(gardener: Gardener, amount):
if (gardener.points - amount) < 0:
return False
gardener.points -= amount
return True
else:
gardener.points -= amount
return True
class PlantTycoon(commands.Cog):
@ -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
[
self.products[product]["modifier"]
for product in gardener.products
if gardener.products[product] > 0
]
)
degradation = (
@ -287,31 +290,38 @@ 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:
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":
emoji = ":sweat_drops:"
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)
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 == "water":
emoji = ":sweat_drops:"
elif product_category == "fertilizer":
emoji = ":poop:"
# elif product_category == "tool":
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)
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:
message = "You have no {}. Go buy some!".format(product)
else:
message = "You don't have a {}. Go buy one!".format(product)
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())
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,8 +629,7 @@ 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)
await ctx.send(embed=em)
@_gardening.command(name="convert")
async def _convert(self, ctx: commands.Context, amount: int):
@ -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):
@ -775,7 +793,7 @@ class PlantTycoon(commands.Cog):
pass
await asyncio.sleep(self.defaults["timers"]["notification"] * 60)
def cog_unload(self):
def __unload(self):
self.completion_task.cancel()
# self.degradation_task.cancel()
self.notification_task.cancel()

@ -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,7 +210,8 @@ class ReactRestrict(Cog):
"""
Base command for this cog. Check help for the commands list.
"""
pass
if ctx.invoked_subcommand is None:
pass
@reactrestrict.command()
async def add(self, ctx: commands.Context, message_id: int, *, role: discord.Role):

@ -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
)
)
timeoutcount += 1
await ctx.send(
"``{}`` fell down the conveyor belt to be sorted again!".format(used["object"])
)
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()
@ -17,16 +16,16 @@ log = logging.getLogger("red.fox_v3.stealemoji")
async def check_guild(guild, emoji):
if len(guild.emojis) >= 2 * guild.emoji_limit:
if len(guild.emojis) >= 100:
return False
if len(guild.emojis) < guild.emoji_limit:
if len(guild.emojis) < 50:
return True
if emoji.animated:
return sum(e.animated for e in guild.emojis) < guild.emoji_limit
return sum(e.animated for e in guild.emojis) < 50
else:
return sum(not e.animated for e in guild.emojis) < guild.emoji_limit
return sum(not e.animated for e in guild.emojis) < 50
class StealEmoji(Cog):
@ -51,7 +50,6 @@ class StealEmoji(Cog):
default_global = {
"stolemoji": {},
"guildbanks": [],
"autobanked_guilds": [],
"on": False,
"notify": 0,
"autobank": False,
@ -70,7 +68,8 @@ class StealEmoji(Cog):
"""
Base command for this cog. Check help for the commands list.
"""
pass
if ctx.invoked_subcommand is None:
pass
@checks.is_owner()
@stealemoji.command(name="clearemojis")
@ -100,8 +99,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")
@ -147,54 +145,11 @@ class StealEmoji(Cog):
await ctx.maybe_send_embed("AutoBanking is now " + str(not curr_setting))
@checks.is_owner()
@commands.guild_only()
@stealemoji.command(name="deleteserver", aliases=["deleteguild"])
async def se_deleteserver(self, ctx: commands.Context, guild_id=None):
"""Delete servers the bot is the owner of.
Useful for auto-generated guildbanks."""
if guild_id is None:
guild = ctx.guild
else:
guild = await self.bot.get_guild(guild_id)
if guild is None:
await ctx.maybe_send_embed("Failed to get guild, cancelling")
return
guild: discord.Guild
await ctx.maybe_send_embed(
f"Will attempt to delete {guild.name} ({guild.id})\n" f"Okay to continue? (yes/no)"
)
def check(m):
return m.author == ctx.author and m.channel == ctx.channel
try:
answer = await self.bot.wait_for("message", timeout=120, check=check)
except asyncio.TimeoutError:
await ctx.send("Timed out, canceling")
return
if answer.content.upper() not in ["Y", "YES"]:
await ctx.maybe_send_embed("Cancelling")
return
try:
await guild.delete()
except discord.Forbidden:
log.exception("No permission to delete. I'm probably not the guild owner")
await ctx.maybe_send_embed("No permission to delete. I'm probably not the guild owner")
except discord.HTTPException:
log.exception("Unexpected error when deleting guild")
await ctx.maybe_send_embed("Unexpected error when deleting guild")
else:
await self.bot.send_to_owners(f"Guild {guild.name} deleted")
@checks.is_owner()
@commands.guild_only()
@stealemoji.command(name="bank")
async def se_bank(self, ctx):
"""Add or remove current server as emoji bank"""
"""Add current server as emoji bank"""
def check(m):
return (
@ -269,36 +224,34 @@ class StealEmoji(Cog):
break
if guildbank is None:
if not await self.config.autobank():
return
try:
guildbank: discord.Guild = await self.bot.create_guild(
"StealEmoji Guildbank", code="S93bqTqKQ9rM"
)
except discord.HTTPException:
await self.config.autobank.set(False)
log.exception("Unable to create guilds, disabling autobank")
if await self.config.autobank():
try:
guildbank: discord.Guild = await self.bot.create_guild(
"StealEmoji Guildbank", code="S93bqTqKQ9rM"
)
except discord.HTTPException:
await self.config.autobank.set(False)
log.exception("Unable to create guilds, disabling autobank")
return
async with self.config.guildbanks() as guildbanks:
guildbanks.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
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)
if str(emoji.id) in await self.config.stolemoji():

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

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

@ -71,7 +71,6 @@ W1, W2, W5, W6 = Random Werewolf
N1 = Benign Neutral
0001-1112T11W112N2
which translates to
0,0,0,1,11,12,E1,R1,R1,R1,R2,P2
pre-letter = exact role position
@ -90,7 +89,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 +115,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 +129,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:

@ -526,10 +526,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)
@ -556,7 +555,10 @@ class Game:
async def generate_targets(self, channel, with_roles=False):
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}",
@ -577,7 +579,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 +706,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 +757,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

@ -72,9 +72,6 @@ class Role(WolfListener):
self.blocked = False
self.properties = {} # Extra data for other roles (i.e. arsonist)
def __str__(self):
return self.__repr__()
def __repr__(self):
return f"{self.__class__.__name__}({self.player.__repr__()})"
@ -89,7 +86,7 @@ class Role(WolfListener):
log.debug(f"Assigned {self} to {player}")
async def get_alignment(self, source=None): # TODO: Rework to be "strength" tiers
async def get_alignment(self, source=None):
"""
Interaction for powerful access of alignment
(Village, Werewolf, Other)

@ -1,10 +1,11 @@
import logging
from typing import Optional
from typing import List, Union
import discord
from redbot.core import Config, checks, commands
from redbot.core.bot import Red
from redbot.core.commands import Cog
from redbot.core.utils import AsyncIter
from redbot.core.utils.menus import DEFAULT_CONTROLS, menu
from werewolf.builder import (
@ -14,11 +15,19 @@ from werewolf.builder import (
role_from_id,
role_from_name,
)
from werewolf.game import Game, anyone_has_role
from werewolf.game import Game
log = logging.getLogger("red.fox_v3.werewolf")
async def anyone_has_role(
member_list: List[discord.Member], role: discord.Role
) -> Union[None, discord.Member]:
return await AsyncIter(member_list).find(
lambda m: AsyncIter(m.roles).find(lambda r: r.id == role.id)
)
class Werewolf(Cog):
"""
Base to host werewolf on a guild
@ -47,19 +56,17 @@ class Werewolf(Cog):
"""Nothing to delete"""
return
def cog_unload(self):
def __unload(self):
log.debug("Unload called")
for key in self.games.keys():
del self.games[key]
for game in self.games.values():
del game
@commands.command()
async def buildgame(self, ctx: commands.Context):
"""
Create game codes to run custom games.
Pick the roles or randomized roles you want to include in a game.
Note: The same role can be picked more than once.
Pick the roles or randomized roles you want to include in a game
"""
gb = GameBuilder()
code = await gb.build_game(ctx)
@ -75,7 +82,8 @@ class Werewolf(Cog):
"""
Base command to adjust settings. Check help for command list.
"""
pass
if ctx.invoked_subcommand is None:
pass
@commands.guild_only()
@wwset.command(name="list")
@ -84,6 +92,9 @@ class Werewolf(Cog):
Lists current guild settings
"""
valid, role, category, channel, log_channel = await self._get_settings(ctx)
# if not valid:
# await ctx.send("Failed to get settings")
# return None
embed = discord.Embed(
title="Current Guild Settings",
@ -165,7 +176,8 @@ class Werewolf(Cog):
"""
Base command for this cog. Check help for the commands list.
"""
pass
if ctx.invoked_subcommand is None:
pass
@commands.guild_only()
@ww.command(name="new")
@ -251,7 +263,6 @@ class Werewolf(Cog):
game = await self._get_game(ctx)
if not game:
await ctx.maybe_send_embed("No game running, cannot start")
return
if not await game.setup(ctx):
pass # ToDo something?
@ -274,8 +285,7 @@ class Werewolf(Cog):
game = await self._get_game(ctx)
game.game_over = True
if game.current_action:
game.current_action.cancel()
game.current_action.cancel()
await ctx.maybe_send_embed("Game has been stopped")
@commands.guild_only()
@ -346,7 +356,8 @@ class Werewolf(Cog):
"""
Find custom roles by name, alignment, category, or ID
"""
pass
if ctx.invoked_subcommand is None or ctx.invoked_subcommand == self.ww_search:
pass
@ww_search.command(name="name")
async def ww_search_name(self, ctx: commands.Context, *, name):
@ -388,7 +399,7 @@ class Werewolf(Cog):
else:
await ctx.maybe_send_embed("Role ID not found")
async def _get_game(self, ctx: commands.Context, game_code=None) -> Optional[Game]:
async def _get_game(self, ctx: commands.Context, game_code=None) -> Union[Game, None]:
guild: discord.Guild = getattr(ctx, "guild", None)
if guild is None:
@ -415,7 +426,7 @@ class Werewolf(Cog):
return self.games[guild.id]
async def _game_start(self, game: Game):
async def _game_start(self, game):
await game.start()
async def _get_settings(self, ctx):

Loading…
Cancel
Save