Merge branch 'master' into conquest_develop
# Conflicts: # conquest/conquest.py # conquest/mapmaker.py
This commit is contained in:
commit
80db00890d
.github
.gitignoreREADME.mdannouncedaily
audiotrivia
ccrole
chatter
coglint
conquest
exclusiverole
fifo
flag
hangman
infochannel
isitdown
launchlib
leaver
lovecalculator
lseen
nudity
planttycoon
qrinvite
reactrestrict
recyclingplant
rpsls
scp
stealemoji
timerole
tts
unicode
werewolf
2
.github/labeler.yml
vendored
2
.github/labeler.yml
vendored
@ -59,4 +59,4 @@
|
|||||||
'cog: unicode':
|
'cog: unicode':
|
||||||
- unicode/*
|
- unicode/*
|
||||||
'cog: werewolf':
|
'cog: werewolf':
|
||||||
- werewolf
|
- werewolf/*
|
2
.github/workflows/labeler.yml
vendored
2
.github/workflows/labeler.yml
vendored
@ -6,7 +6,7 @@
|
|||||||
# https://github.com/actions/labeler
|
# https://github.com/actions/labeler
|
||||||
|
|
||||||
name: Labeler
|
name: Labeler
|
||||||
on: [pull_request]
|
on: [pull_request_target]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
label:
|
label:
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -4,3 +4,4 @@ venv/
|
|||||||
v-data/
|
v-data/
|
||||||
database.sqlite3
|
database.sqlite3
|
||||||
/venv3.4/
|
/venv3.4/
|
||||||
|
/.venv/
|
||||||
|
@ -53,7 +53,7 @@ Check out *Deprecated* my V2 cogs at [Fox-Cogs v2](https://github.com/bobloy/Fox
|
|||||||
# Contact
|
# Contact
|
||||||
Get support on the [Third Party Cog Server](https://discord.gg/GET4DVk)
|
Get support on the [Third Party Cog Server](https://discord.gg/GET4DVk)
|
||||||
|
|
||||||
Feel free to @ me in the #support_othercogs channel
|
Feel free to @ me in the #support_fox-v3 channel
|
||||||
|
|
||||||
Discord: Bobloy#6513
|
Discord: Bobloy#6513
|
||||||
|
|
||||||
|
@ -54,8 +54,7 @@ class AnnounceDaily(Cog):
|
|||||||
|
|
||||||
Do `[p]help annd <subcommand>` for more details
|
Do `[p]help annd <subcommand>` for more details
|
||||||
"""
|
"""
|
||||||
if ctx.invoked_subcommand is None:
|
pass
|
||||||
pass
|
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@checks.guildowner()
|
@checks.guildowner()
|
||||||
|
@ -168,7 +168,7 @@ class AudioTrivia(Trivia):
|
|||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def audiotrivia_list(self, ctx: commands.Context):
|
async def audiotrivia_list(self, ctx: commands.Context):
|
||||||
"""List available trivia including audio categories."""
|
"""List available trivia including audio categories."""
|
||||||
lists = set(p.stem for p in self._all_audio_lists())
|
lists = {p.stem for p in self._all_audio_lists()}
|
||||||
if await ctx.embed_requested():
|
if await ctx.embed_requested():
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
embed=discord.Embed(
|
embed=discord.Embed(
|
||||||
|
@ -48,8 +48,7 @@ class CCRole(commands.Cog):
|
|||||||
"""Custom commands management with roles
|
"""Custom commands management with roles
|
||||||
|
|
||||||
Highly customizable custom commands with role management."""
|
Highly customizable custom commands with role management."""
|
||||||
if not ctx.invoked_subcommand:
|
pass
|
||||||
pass
|
|
||||||
|
|
||||||
@ccrole.command(name="add")
|
@ccrole.command(name="add")
|
||||||
@checks.mod_or_permissions(administrator=True)
|
@checks.mod_or_permissions(administrator=True)
|
||||||
@ -228,7 +227,7 @@ class CCRole(commands.Cog):
|
|||||||
if not role_list:
|
if not role_list:
|
||||||
return "None"
|
return "None"
|
||||||
return ", ".join(
|
return ", ".join(
|
||||||
[discord.utils.get(ctx.guild.roles, id=roleid).name for roleid in role_list]
|
discord.utils.get(ctx.guild.roles, id=roleid).name for roleid in role_list
|
||||||
)
|
)
|
||||||
|
|
||||||
embed.add_field(name="Text", value="```{}```".format(cmd["text"]), inline=False)
|
embed.add_field(name="Text", value="```{}```".format(cmd["text"]), inline=False)
|
||||||
@ -252,7 +251,7 @@ class CCRole(commands.Cog):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
cmd_list = ", ".join([ctx.prefix + c for c in sorted(cmd_list.keys())])
|
cmd_list = ", ".join(ctx.prefix + c for c in sorted(cmd_list.keys()))
|
||||||
cmd_list = "Custom commands:\n\n" + cmd_list
|
cmd_list = "Custom commands:\n\n" + cmd_list
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -292,13 +291,13 @@ class CCRole(commands.Cog):
|
|||||||
# Thank you Cog-Creators
|
# Thank you Cog-Creators
|
||||||
|
|
||||||
cmd = ctx.invoked_with
|
cmd = ctx.invoked_with
|
||||||
cmd = cmd.lower() # Continues the proud case_insentivity tradition of ccrole
|
cmd = cmd.lower() # Continues the proud case-insensitivity tradition of ccrole
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
# message = ctx.message # Unneeded since switch to `on_message_without_command` from `on_command_error`
|
# message = ctx.message # Unneeded since switch to `on_message_without_command` from `on_command_error`
|
||||||
|
|
||||||
cmdlist = self.config.guild(guild).cmdlist
|
cmd_list = self.config.guild(guild).cmdlist
|
||||||
# cmd = message.content[len(prefix) :].split()[0].lower()
|
# cmd = message.content[len(prefix) :].split()[0].lower()
|
||||||
cmd = await cmdlist.get_raw(cmd, default=None)
|
cmd = await cmd_list.get_raw(cmd, default=None)
|
||||||
|
|
||||||
if cmd is not None:
|
if cmd is not None:
|
||||||
await self.eval_cc(cmd, message, ctx)
|
await self.eval_cc(cmd, message, ctx)
|
||||||
@ -325,9 +324,7 @@ class CCRole(commands.Cog):
|
|||||||
|
|
||||||
async def eval_cc(self, cmd, message: discord.Message, ctx: commands.Context):
|
async def eval_cc(self, cmd, message: discord.Message, ctx: commands.Context):
|
||||||
"""Does all the work"""
|
"""Does all the work"""
|
||||||
if cmd["proles"] and not (
|
if cmd["proles"] and not {role.id for role in message.author.roles} & set(cmd["proles"]):
|
||||||
set(role.id for role in message.author.roles) & set(cmd["proles"])
|
|
||||||
):
|
|
||||||
log.debug(f"{message.author} missing required role to execute {ctx.invoked_with}")
|
log.debug(f"{message.author} missing required role to execute {ctx.invoked_with}")
|
||||||
return # Not authorized, do nothing
|
return # Not authorized, do nothing
|
||||||
|
|
||||||
|
@ -59,62 +59,50 @@ Install these on your windows machine before attempting the installation:
|
|||||||
[Pandoc - Universal Document Converter](https://pandoc.org/installing.html)
|
[Pandoc - Universal Document Converter](https://pandoc.org/installing.html)
|
||||||
|
|
||||||
## Methods
|
## Methods
|
||||||
### Windows - Manually
|
### Automatic
|
||||||
#### Step 1: Built-in Downloader
|
|
||||||
|
|
||||||
You need to get a copy of the requirements.txt provided with chatter, I recommend this method.
|
This method requires some luck to pull off.
|
||||||
|
|
||||||
|
#### Step 1: Add repo and install cog
|
||||||
|
|
||||||
```
|
```
|
||||||
[p]repo add Fox https://github.com/bobloy/Fox-V3
|
[p]repo add Fox https://github.com/bobloy/Fox-V3
|
||||||
```
|
|
||||||
|
|
||||||
#### Step 2: Install Requirements
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
In a terminal running as an admin, navigate to the directory containing this repo.
|
|
||||||
|
|
||||||
I've used my install directory as an example.
|
|
||||||
|
|
||||||
```
|
|
||||||
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
|
|
||||||
|
|
||||||
```
|
|
||||||
[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]cog install Fox chatter
|
||||||
|
```
|
||||||
|
|
||||||
|
If you get an error at this step, stop and skip to one of the manual methods below.
|
||||||
|
|
||||||
|
#### Step 2: Install additional dependencies
|
||||||
|
|
||||||
|
Here you need to decide which training models you want to have available to you.
|
||||||
|
|
||||||
|
Shutdown the bot and run any number of these in the console:
|
||||||
|
|
||||||
|
```
|
||||||
|
python -m spacy download en_core_web_sm # ~15 MB
|
||||||
|
|
||||||
|
python -m spacy download en_core_web_md # ~50 MB
|
||||||
|
|
||||||
|
python -m spacy download en_core_web_lg # ~750 MB (CPU Optimized)
|
||||||
|
|
||||||
|
python -m spacy download en_core_web_trf # ~500 MB (GPU Optimized)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 3: Load the cog and get started
|
||||||
|
|
||||||
|
```
|
||||||
[p]load chatter
|
[p]load chatter
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Windows - Manually
|
||||||
|
Deprecated
|
||||||
|
|
||||||
### Linux - Manually
|
### Linux - Manually
|
||||||
|
Deprecated
|
||||||
#### Step 1: Built-in Downloader
|
|
||||||
|
|
||||||
```
|
|
||||||
[p]cog install <Fox> Chatter
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Step 2: Install Requirements
|
|
||||||
|
|
||||||
In your console with your virtual environment activated:
|
|
||||||
|
|
||||||
```
|
|
||||||
pip install --no-deps "chatterbot>=1.1"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 3: Load Chatter
|
|
||||||
|
|
||||||
```
|
|
||||||
[p]load chatter
|
|
||||||
```
|
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
|
|
||||||
Chatter works out the the box without any training by learning as it goes,
|
Chatter works out the box without any training by learning as it goes,
|
||||||
but will have very poor and repetitive responses at first.
|
but will have very poor and repetitive responses at first.
|
||||||
|
|
||||||
Initial training is recommended to speed up its learning.
|
Initial training is recommended to speed up its learning.
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
from .chat import Chatter
|
from .chat import Chatter
|
||||||
|
|
||||||
|
|
||||||
def setup(bot):
|
async def setup(bot):
|
||||||
bot.add_cog(Chatter(bot))
|
cog = Chatter(bot)
|
||||||
|
await cog.initialize()
|
||||||
|
bot.add_cog(cog)
|
||||||
|
|
||||||
|
|
||||||
# __all__ = (
|
# __all__ = (
|
||||||
|
396
chatter/chat.py
396
chatter/chat.py
@ -2,19 +2,24 @@ import asyncio
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
|
from collections import defaultdict
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Optional
|
from functools import partial
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from chatterbot import ChatBot
|
from chatterbot import ChatBot
|
||||||
from chatterbot.comparisons import JaccardSimilarity, LevenshteinDistance, SpacySimilarity
|
from chatterbot.comparisons import JaccardSimilarity, LevenshteinDistance, SpacySimilarity
|
||||||
from chatterbot.response_selection import get_random_response
|
from chatterbot.response_selection import get_random_response
|
||||||
from chatterbot.trainers import ChatterBotCorpusTrainer, ListTrainer, UbuntuCorpusTrainer
|
from chatterbot.trainers import ChatterBotCorpusTrainer, ListTrainer, UbuntuCorpusTrainer
|
||||||
from redbot.core import Config, commands
|
from redbot.core import Config, checks, commands
|
||||||
from redbot.core.commands import Cog
|
from redbot.core.commands import Cog
|
||||||
from redbot.core.data_manager import cog_data_path
|
from redbot.core.data_manager import cog_data_path
|
||||||
from redbot.core.utils.predicates import MessagePredicate
|
from redbot.core.utils.predicates import MessagePredicate
|
||||||
|
|
||||||
|
from chatter.trainers import MovieTrainer, TwitterCorpusTrainer, UbuntuCorpusTrainer2
|
||||||
|
|
||||||
|
chatterbot_log = logging.getLogger("red.fox_v3.chatterbot")
|
||||||
log = logging.getLogger("red.fox_v3.chatter")
|
log = logging.getLogger("red.fox_v3.chatter")
|
||||||
|
|
||||||
|
|
||||||
@ -25,6 +30,12 @@ def my_local_get_prefix(prefixes, content):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class ENG_TRF:
|
||||||
|
ISO_639_1 = "en_core_web_trf"
|
||||||
|
ISO_639 = "eng"
|
||||||
|
ENGLISH_NAME = "English"
|
||||||
|
|
||||||
|
|
||||||
class ENG_LG:
|
class ENG_LG:
|
||||||
ISO_639_1 = "en_core_web_lg"
|
ISO_639_1 = "en_core_web_lg"
|
||||||
ISO_639 = "eng"
|
ISO_639 = "eng"
|
||||||
@ -48,50 +59,77 @@ class Chatter(Cog):
|
|||||||
This cog trains a chatbot that will talk like members of your Guild
|
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):
|
def __init__(self, bot):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.config = Config.get_conf(self, identifier=6710497116116101114)
|
self.config = Config.get_conf(self, identifier=6710497116116101114)
|
||||||
default_global = {}
|
default_global = {"learning": True, "model_number": 0, "algo_number": 0, "threshold": 0.90}
|
||||||
default_guild = {"whitelist": None, "days": 1, "convo_delta": 15, "chatchannel": None}
|
self.default_guild = {
|
||||||
|
"whitelist": None,
|
||||||
|
"days": 1,
|
||||||
|
"convo_delta": 15,
|
||||||
|
"chatchannel": None,
|
||||||
|
"reply": True,
|
||||||
|
}
|
||||||
path: pathlib.Path = cog_data_path(self)
|
path: pathlib.Path = cog_data_path(self)
|
||||||
self.data_path = path / "database.sqlite3"
|
self.data_path = path / "database.sqlite3"
|
||||||
|
|
||||||
# TODO: Move training_model and similarity_algo to config
|
# TODO: Move training_model and similarity_algo to config
|
||||||
# TODO: Add an option to see current settings
|
# TODO: Add an option to see current settings
|
||||||
|
|
||||||
self.tagger_language = ENG_MD
|
self.tagger_language = ENG_SM
|
||||||
self.similarity_algo = SpacySimilarity
|
self.similarity_algo = SpacySimilarity
|
||||||
self.similarity_threshold = 0.90
|
self.similarity_threshold = 0.90
|
||||||
self.chatbot = self._create_chatbot()
|
self.chatbot = None
|
||||||
# self.chatbot.set_trainer(ListTrainer)
|
# self.chatbot.set_trainer(ListTrainer)
|
||||||
|
|
||||||
# self.trainer = ListTrainer(self.chatbot)
|
# self.trainer = ListTrainer(self.chatbot)
|
||||||
|
|
||||||
self.config.register_global(**default_global)
|
self.config.register_global(**default_global)
|
||||||
self.config.register_guild(**default_guild)
|
self.config.register_guild(**self.default_guild)
|
||||||
|
|
||||||
self.loop = asyncio.get_event_loop()
|
self.loop = asyncio.get_event_loop()
|
||||||
|
|
||||||
|
self._guild_cache = defaultdict(dict)
|
||||||
|
self._global_cache = {}
|
||||||
|
|
||||||
|
self._last_message_per_channel: Dict[Optional[discord.Message]] = defaultdict(lambda: None)
|
||||||
|
|
||||||
async def red_delete_data_for_user(self, **kwargs):
|
async def red_delete_data_for_user(self, **kwargs):
|
||||||
"""Nothing to delete"""
|
"""Nothing to delete"""
|
||||||
return
|
return
|
||||||
|
|
||||||
|
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):
|
def _create_chatbot(self):
|
||||||
|
|
||||||
return ChatBot(
|
return ChatBot(
|
||||||
"ChatterBot",
|
"ChatterBot",
|
||||||
storage_adapter="chatterbot.storage.SQLStorageAdapter",
|
# storage_adapter="chatterbot.storage.SQLStorageAdapter",
|
||||||
|
storage_adapter="chatter.storage_adapters.MyDumbSQLStorageAdapter",
|
||||||
database_uri="sqlite:///" + str(self.data_path),
|
database_uri="sqlite:///" + str(self.data_path),
|
||||||
statement_comparison_function=self.similarity_algo,
|
statement_comparison_function=self.similarity_algo,
|
||||||
response_selection_method=get_random_response,
|
response_selection_method=get_random_response,
|
||||||
logic_adapters=["chatterbot.logic.BestMatch"],
|
logic_adapters=["chatterbot.logic.BestMatch"],
|
||||||
maximum_similarity_threshold=self.similarity_threshold,
|
maximum_similarity_threshold=self.similarity_threshold,
|
||||||
tagger_language=self.tagger_language,
|
tagger_language=self.tagger_language,
|
||||||
logger=log,
|
logger=chatterbot_log,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _get_conversation(self, ctx, in_channel: discord.TextChannel = None):
|
async def _get_conversation(self, ctx, in_channels: List[discord.TextChannel]):
|
||||||
"""
|
"""
|
||||||
Compiles all conversation in the Guild this bot can get it's hands on
|
Compiles all conversation in the Guild this bot can get it's hands on
|
||||||
Currently takes a stupid long time
|
Currently takes a stupid long time
|
||||||
@ -105,20 +143,12 @@ class Chatter(Cog):
|
|||||||
return msg.clean_content
|
return msg.clean_content
|
||||||
|
|
||||||
def new_conversation(msg, sent, out_in, delta):
|
def new_conversation(msg, sent, out_in, delta):
|
||||||
# if sent is None:
|
# Should always be positive numbers
|
||||||
# return False
|
|
||||||
|
|
||||||
# Don't do "too short" processing here. Sometimes people don't respond.
|
|
||||||
# if len(out_in) < 2:
|
|
||||||
# return False
|
|
||||||
|
|
||||||
# print(msg.created_at - sent)
|
|
||||||
|
|
||||||
return msg.created_at - sent >= delta
|
return msg.created_at - sent >= delta
|
||||||
|
|
||||||
for channel in ctx.guild.text_channels:
|
for channel in in_channels:
|
||||||
if in_channel:
|
# if in_channel:
|
||||||
channel = in_channel
|
# channel = in_channel
|
||||||
await ctx.maybe_send_embed("Gathering {}".format(channel.mention))
|
await ctx.maybe_send_embed("Gathering {}".format(channel.mention))
|
||||||
user = None
|
user = None
|
||||||
i = 0
|
i = 0
|
||||||
@ -153,16 +183,47 @@ class Chatter(Cog):
|
|||||||
except discord.HTTPException:
|
except discord.HTTPException:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if in_channel:
|
# if in_channel:
|
||||||
break
|
# break
|
||||||
|
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
def _train_twitter(self, *args, **kwargs):
|
||||||
|
trainer = TwitterCorpusTrainer(self.chatbot)
|
||||||
|
trainer.train(*args, **kwargs)
|
||||||
|
return True
|
||||||
|
|
||||||
def _train_ubuntu(self):
|
def _train_ubuntu(self):
|
||||||
trainer = UbuntuCorpusTrainer(self.chatbot)
|
trainer = UbuntuCorpusTrainer(
|
||||||
|
self.chatbot, ubuntu_corpus_data_directory=cog_data_path(self) / "ubuntu_data"
|
||||||
|
)
|
||||||
trainer.train()
|
trainer.train()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
async def _train_movies(self):
|
||||||
|
trainer = MovieTrainer(self.chatbot, cog_data_path(self))
|
||||||
|
return await trainer.asynctrain()
|
||||||
|
|
||||||
|
async def _train_ubuntu2(self, intensity):
|
||||||
|
train_kwarg = {}
|
||||||
|
if intensity == 196:
|
||||||
|
train_kwarg["train_dialogue"] = False
|
||||||
|
train_kwarg["train_196"] = True
|
||||||
|
elif intensity == 301:
|
||||||
|
train_kwarg["train_dialogue"] = False
|
||||||
|
train_kwarg["train_301"] = True
|
||||||
|
elif intensity == 497:
|
||||||
|
train_kwarg["train_dialogue"] = False
|
||||||
|
train_kwarg["train_196"] = True
|
||||||
|
train_kwarg["train_301"] = True
|
||||||
|
elif intensity >= 9000: # NOT 9000!
|
||||||
|
train_kwarg["train_dialogue"] = True
|
||||||
|
train_kwarg["train_196"] = True
|
||||||
|
train_kwarg["train_301"] = True
|
||||||
|
|
||||||
|
trainer = UbuntuCorpusTrainer2(self.chatbot, cog_data_path(self))
|
||||||
|
return await trainer.asynctrain(**train_kwarg)
|
||||||
|
|
||||||
def _train_english(self):
|
def _train_english(self):
|
||||||
trainer = ChatterBotCorpusTrainer(self.chatbot)
|
trainer = ChatterBotCorpusTrainer(self.chatbot)
|
||||||
# try:
|
# try:
|
||||||
@ -174,13 +235,10 @@ class Chatter(Cog):
|
|||||||
def _train(self, data):
|
def _train(self, data):
|
||||||
trainer = ListTrainer(self.chatbot)
|
trainer = ListTrainer(self.chatbot)
|
||||||
total = len(data)
|
total = len(data)
|
||||||
# try:
|
|
||||||
for c, convo in enumerate(data, 1):
|
for c, convo in enumerate(data, 1):
|
||||||
|
log.info(f"{c} / {total}")
|
||||||
if len(convo) > 1: # TODO: Toggleable skipping short conversations
|
if len(convo) > 1: # TODO: Toggleable skipping short conversations
|
||||||
print(f"{c} / {total}")
|
|
||||||
trainer.train(convo)
|
trainer.train(convo)
|
||||||
# except:
|
|
||||||
# return False
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@commands.group(invoke_without_command=False)
|
@commands.group(invoke_without_command=False)
|
||||||
@ -188,9 +246,10 @@ class Chatter(Cog):
|
|||||||
"""
|
"""
|
||||||
Base command for this cog. Check help for the commands list.
|
Base command for this cog. Check help for the commands list.
|
||||||
"""
|
"""
|
||||||
if ctx.invoked_subcommand is None:
|
self._guild_cache[ctx.guild.id] = {} # Clear cache when modifying values
|
||||||
pass
|
self._global_cache = {}
|
||||||
|
|
||||||
|
@commands.admin()
|
||||||
@chatter.command(name="channel")
|
@chatter.command(name="channel")
|
||||||
async def chatter_channel(
|
async def chatter_channel(
|
||||||
self, ctx: commands.Context, channel: Optional[discord.TextChannel] = None
|
self, ctx: commands.Context, channel: Optional[discord.TextChannel] = None
|
||||||
@ -210,12 +269,55 @@ class Chatter(Cog):
|
|||||||
await self.config.guild(ctx.guild).chatchannel.set(channel.id)
|
await self.config.guild(ctx.guild).chatchannel.set(channel.id)
|
||||||
await ctx.maybe_send_embed(f"Chat channel is now {channel.mention}")
|
await ctx.maybe_send_embed(f"Chat channel is now {channel.mention}")
|
||||||
|
|
||||||
|
@commands.admin()
|
||||||
|
@chatter.command(name="reply")
|
||||||
|
async def chatter_reply(self, ctx: commands.Context, toggle: Optional[bool] = None):
|
||||||
|
"""
|
||||||
|
Toggle bot reply to messages if conversation continuity is not present
|
||||||
|
|
||||||
|
"""
|
||||||
|
reply = await self.config.guild(ctx.guild).reply()
|
||||||
|
if toggle is None:
|
||||||
|
toggle = not reply
|
||||||
|
await self.config.guild(ctx.guild).reply.set(toggle)
|
||||||
|
|
||||||
|
if toggle:
|
||||||
|
await ctx.maybe_send_embed(
|
||||||
|
"I will now respond to you if conversation continuity is not present"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await ctx.maybe_send_embed(
|
||||||
|
"I will not reply to your message if conversation continuity is not present, anymore"
|
||||||
|
)
|
||||||
|
|
||||||
|
@commands.is_owner()
|
||||||
|
@chatter.command(name="learning")
|
||||||
|
async def chatter_learning(self, ctx: commands.Context, toggle: Optional[bool] = None):
|
||||||
|
"""
|
||||||
|
Toggle the bot learning from its conversations.
|
||||||
|
|
||||||
|
This is a global setting.
|
||||||
|
This is on by default.
|
||||||
|
"""
|
||||||
|
learning = await self.config.learning()
|
||||||
|
if toggle is None:
|
||||||
|
toggle = not learning
|
||||||
|
await self.config.learning.set(toggle)
|
||||||
|
|
||||||
|
if toggle:
|
||||||
|
await ctx.maybe_send_embed("I will now learn from conversations.")
|
||||||
|
else:
|
||||||
|
await ctx.maybe_send_embed("I will no longer learn from conversations.")
|
||||||
|
|
||||||
|
@commands.is_owner()
|
||||||
@chatter.command(name="cleardata")
|
@chatter.command(name="cleardata")
|
||||||
async def chatter_cleardata(self, ctx: commands.Context, confirm: bool = False):
|
async def chatter_cleardata(self, ctx: commands.Context, confirm: bool = False):
|
||||||
"""
|
"""
|
||||||
This command will erase all training data and reset your configuration settings
|
This command will erase all training data and reset your configuration settings.
|
||||||
|
|
||||||
Use `[p]chatter cleardata True`
|
This applies to all guilds.
|
||||||
|
|
||||||
|
Use `[p]chatter cleardata True` to confirm.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not confirm:
|
if not confirm:
|
||||||
@ -242,20 +344,18 @@ class Chatter(Cog):
|
|||||||
|
|
||||||
await ctx.tick()
|
await ctx.tick()
|
||||||
|
|
||||||
|
@commands.is_owner()
|
||||||
@chatter.command(name="algorithm", aliases=["algo"])
|
@chatter.command(name="algorithm", aliases=["algo"])
|
||||||
async def chatter_algorithm(
|
async def chatter_algorithm(
|
||||||
self, ctx: commands.Context, algo_number: int, threshold: float = None
|
self, ctx: commands.Context, algo_number: int, threshold: float = None
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Switch the active logic algorithm to one of the three. Default after reload is Spacy
|
Switch the active logic algorithm to one of the three. Default is Spacy
|
||||||
|
|
||||||
0: Spacy
|
0: Spacy
|
||||||
1: Jaccard
|
1: Jaccard
|
||||||
2: Levenshtein
|
2: Levenshtein
|
||||||
"""
|
"""
|
||||||
|
|
||||||
algos = [SpacySimilarity, JaccardSimilarity, LevenshteinDistance]
|
|
||||||
|
|
||||||
if algo_number < 0 or algo_number > 2:
|
if algo_number < 0 or algo_number > 2:
|
||||||
await ctx.send_help()
|
await ctx.send_help()
|
||||||
return
|
return
|
||||||
@ -267,31 +367,33 @@ class Chatter(Cog):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
self.similarity_algo = threshold
|
self.similarity_threshold = threshold
|
||||||
|
await self.config.threshold.set(self.similarity_threshold)
|
||||||
|
|
||||||
|
self.similarity_algo = self.algos[algo_number]
|
||||||
|
await self.config.algo_number.set(algo_number)
|
||||||
|
|
||||||
self.similarity_algo = algos[algo_number]
|
|
||||||
async with ctx.typing():
|
async with ctx.typing():
|
||||||
self.chatbot = self._create_chatbot()
|
self.chatbot = self._create_chatbot()
|
||||||
|
|
||||||
await ctx.tick()
|
await ctx.tick()
|
||||||
|
|
||||||
|
@commands.is_owner()
|
||||||
@chatter.command(name="model")
|
@chatter.command(name="model")
|
||||||
async def chatter_model(self, ctx: commands.Context, model_number: int):
|
async def chatter_model(self, ctx: commands.Context, model_number: int):
|
||||||
"""
|
"""
|
||||||
Switch the active model to one of the three. Default after reload is Medium
|
Switch the active model to one of the three. Default is Small
|
||||||
|
|
||||||
0: Small
|
0: Small
|
||||||
1: Medium
|
1: Medium (Requires additional setup)
|
||||||
2: Large (Requires additional setup)
|
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()
|
await ctx.send_help()
|
||||||
return
|
return
|
||||||
|
|
||||||
if model_number == 2:
|
if model_number >= 0:
|
||||||
await ctx.maybe_send_embed(
|
await ctx.maybe_send_embed(
|
||||||
"Additional requirements needed. See guide before continuing.\n" "Continue?"
|
"Additional requirements needed. See guide before continuing.\n" "Continue?"
|
||||||
)
|
)
|
||||||
@ -304,7 +406,8 @@ class Chatter(Cog):
|
|||||||
if not pred.result:
|
if not pred.result:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.tagger_language = models[model_number]
|
self.tagger_language = self.models[model_number]
|
||||||
|
await self.config.model_number.set(model_number)
|
||||||
async with ctx.typing():
|
async with ctx.typing():
|
||||||
self.chatbot = self._create_chatbot()
|
self.chatbot = self._create_chatbot()
|
||||||
|
|
||||||
@ -312,7 +415,14 @@ class Chatter(Cog):
|
|||||||
f"Model has been switched to {self.tagger_language.ISO_639_1}"
|
f"Model has been switched to {self.tagger_language.ISO_639_1}"
|
||||||
)
|
)
|
||||||
|
|
||||||
@chatter.command(name="minutes")
|
@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")
|
||||||
async def minutes(self, ctx: commands.Context, minutes: int):
|
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
|
Sets the number of minutes the bot will consider a break in a conversation during training
|
||||||
@ -323,11 +433,12 @@ class Chatter(Cog):
|
|||||||
await ctx.send_help()
|
await ctx.send_help()
|
||||||
return
|
return
|
||||||
|
|
||||||
await self.config.guild(ctx.guild).convo_length.set(minutes)
|
await self.config.guild(ctx.guild).convo_delta.set(minutes)
|
||||||
|
|
||||||
await ctx.tick()
|
await ctx.tick()
|
||||||
|
|
||||||
@chatter.command(name="age")
|
@commands.is_owner()
|
||||||
|
@chatter_trainset.command(name="age")
|
||||||
async def age(self, ctx: commands.Context, days: int):
|
async def age(self, ctx: commands.Context, days: int):
|
||||||
"""
|
"""
|
||||||
Sets the number of days to look back
|
Sets the number of days to look back
|
||||||
@ -341,6 +452,16 @@ class Chatter(Cog):
|
|||||||
await self.config.guild(ctx.guild).days.set(days)
|
await self.config.guild(ctx.guild).days.set(days)
|
||||||
await ctx.tick()
|
await ctx.tick()
|
||||||
|
|
||||||
|
@commands.is_owner()
|
||||||
|
@chatter.command(name="kaggle")
|
||||||
|
async def chatter_kaggle(self, ctx: commands.Context):
|
||||||
|
"""Register with the kaggle API to download additional datasets for training"""
|
||||||
|
if not await self.check_for_kaggle():
|
||||||
|
await ctx.maybe_send_embed(
|
||||||
|
"[Click here for instructions to setup the kaggle api](https://github.com/Kaggle/kaggle-api#api-credentials)"
|
||||||
|
)
|
||||||
|
|
||||||
|
@commands.is_owner()
|
||||||
@chatter.command(name="backup")
|
@chatter.command(name="backup")
|
||||||
async def backup(self, ctx, backupname):
|
async def backup(self, ctx, backupname):
|
||||||
"""
|
"""
|
||||||
@ -362,7 +483,71 @@ class Chatter(Cog):
|
|||||||
else:
|
else:
|
||||||
await ctx.maybe_send_embed("Error occurred :(")
|
await ctx.maybe_send_embed("Error occurred :(")
|
||||||
|
|
||||||
@chatter.command(name="trainubuntu")
|
@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")
|
||||||
async def chatter_train_ubuntu(self, ctx: commands.Context, confirmation: bool = False):
|
async def chatter_train_ubuntu(self, ctx: commands.Context, confirmation: bool = False):
|
||||||
"""
|
"""
|
||||||
WARNING: Large Download! Trains the bot using Ubuntu Dialog Corpus data.
|
WARNING: Large Download! Trains the bot using Ubuntu Dialog Corpus data.
|
||||||
@ -370,8 +555,8 @@ class Chatter(Cog):
|
|||||||
|
|
||||||
if not confirmation:
|
if not confirmation:
|
||||||
await ctx.maybe_send_embed(
|
await ctx.maybe_send_embed(
|
||||||
"Warning: This command downloads ~500MB then eats your CPU for training\n"
|
"Warning: This command downloads ~500MB and is CPU intensive during training\n"
|
||||||
"If you're sure you want to continue, run `[p]chatter trainubuntu True`"
|
"If you're sure you want to continue, run `[p]chatter train ubuntu True`"
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -379,11 +564,11 @@ class Chatter(Cog):
|
|||||||
future = await self.loop.run_in_executor(None, self._train_ubuntu)
|
future = await self.loop.run_in_executor(None, self._train_ubuntu)
|
||||||
|
|
||||||
if future:
|
if future:
|
||||||
await ctx.send("Training successful!")
|
await ctx.maybe_send_embed("Training successful!")
|
||||||
else:
|
else:
|
||||||
await ctx.send("Error occurred :(")
|
await ctx.maybe_send_embed("Error occurred :(")
|
||||||
|
|
||||||
@chatter.command(name="trainenglish")
|
@chatter_train.command(name="english")
|
||||||
async def chatter_train_english(self, ctx: commands.Context):
|
async def chatter_train_english(self, ctx: commands.Context):
|
||||||
"""
|
"""
|
||||||
Trains the bot in english
|
Trains the bot in english
|
||||||
@ -396,11 +581,32 @@ class Chatter(Cog):
|
|||||||
else:
|
else:
|
||||||
await ctx.maybe_send_embed("Error occurred :(")
|
await ctx.maybe_send_embed("Error occurred :(")
|
||||||
|
|
||||||
@chatter.command()
|
@chatter_train.command(name="list")
|
||||||
async def train(self, ctx: commands.Context, channel: discord.TextChannel):
|
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']
|
||||||
"""
|
"""
|
||||||
Trains the bot based on language in this guild
|
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.
|
||||||
|
"""
|
||||||
|
if not channels:
|
||||||
|
await ctx.send_help()
|
||||||
|
return
|
||||||
|
|
||||||
await ctx.maybe_send_embed(
|
await ctx.maybe_send_embed(
|
||||||
"Warning: The cog may use significant RAM or CPU if trained on large data sets.\n"
|
"Warning: The cog may use significant RAM or CPU if trained on large data sets.\n"
|
||||||
@ -409,7 +615,7 @@ class Chatter(Cog):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async with ctx.typing():
|
async with ctx.typing():
|
||||||
conversation = await self._get_conversation(ctx, channel)
|
conversation = await self._get_conversation(ctx, channels)
|
||||||
|
|
||||||
if not conversation:
|
if not conversation:
|
||||||
await ctx.maybe_send_embed("Failed to gather training data")
|
await ctx.maybe_send_embed("Failed to gather training data")
|
||||||
@ -451,7 +657,7 @@ class Chatter(Cog):
|
|||||||
|
|
||||||
guild: discord.Guild = getattr(message, "guild", None)
|
guild: discord.Guild = getattr(message, "guild", None)
|
||||||
|
|
||||||
if await self.bot.cog_disabled_in_guild(self, guild):
|
if guild is None or await self.bot.cog_disabled_in_guild(self, guild):
|
||||||
return
|
return
|
||||||
|
|
||||||
ctx: commands.Context = await self.bot.get_context(message)
|
ctx: commands.Context = await self.bot.get_context(message)
|
||||||
@ -463,7 +669,18 @@ class Chatter(Cog):
|
|||||||
# Thank you Cog-Creators
|
# Thank you Cog-Creators
|
||||||
channel: discord.TextChannel = message.channel
|
channel: discord.TextChannel = message.channel
|
||||||
|
|
||||||
if guild is not None and channel.id == await self.config.guild(guild).chatchannel():
|
if not self._guild_cache[guild.id]:
|
||||||
|
self._guild_cache[guild.id] = await self.config.guild(guild).all()
|
||||||
|
|
||||||
|
is_reply = False # this is only useful with in_response_to
|
||||||
|
if (
|
||||||
|
message.reference is not None
|
||||||
|
and isinstance(message.reference.resolved, discord.Message)
|
||||||
|
and message.reference.resolved.author.id == self.bot.user.id
|
||||||
|
):
|
||||||
|
is_reply = True # this is only useful with in_response_to
|
||||||
|
pass # this is a reply to the bot, good to go
|
||||||
|
elif guild is not None and channel.id == self._guild_cache[guild.id]["chatchannel"]:
|
||||||
pass # good to go
|
pass # good to go
|
||||||
else:
|
else:
|
||||||
when_mentionables = commands.when_mentioned(self.bot, message)
|
when_mentionables = commands.when_mentioned(self.bot, message)
|
||||||
@ -478,10 +695,57 @@ class Chatter(Cog):
|
|||||||
|
|
||||||
text = message.clean_content
|
text = message.clean_content
|
||||||
|
|
||||||
async with channel.typing():
|
async with ctx.typing():
|
||||||
future = await self.loop.run_in_executor(None, self.chatbot.get_response, text)
|
|
||||||
|
if is_reply:
|
||||||
|
in_response_to = message.reference.resolved.content
|
||||||
|
elif self._last_message_per_channel[ctx.channel.id] is not None:
|
||||||
|
last_m: discord.Message = self._last_message_per_channel[ctx.channel.id]
|
||||||
|
minutes = self._guild_cache[ctx.guild.id]["convo_delta"]
|
||||||
|
if (datetime.utcnow() - last_m.created_at).seconds > minutes * 60:
|
||||||
|
in_response_to = None
|
||||||
|
else:
|
||||||
|
in_response_to = last_m.content
|
||||||
|
else:
|
||||||
|
in_response_to = None
|
||||||
|
|
||||||
|
# Always use generate reponse
|
||||||
|
# Chatterbot tries to learn based on the result it comes up with, which is dumb
|
||||||
|
log.debug("Generating response")
|
||||||
|
Statement = self.chatbot.storage.get_object("statement")
|
||||||
|
future = await self.loop.run_in_executor(
|
||||||
|
None, self.chatbot.generate_response, Statement(text)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not self._global_cache:
|
||||||
|
self._global_cache = await self.config.all()
|
||||||
|
|
||||||
|
if in_response_to is not None and self._global_cache["learning"]:
|
||||||
|
log.debug("learning response")
|
||||||
|
await self.loop.run_in_executor(
|
||||||
|
None,
|
||||||
|
partial(
|
||||||
|
self.chatbot.learn_response,
|
||||||
|
Statement(text),
|
||||||
|
previous_statement=in_response_to,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
replying = None
|
||||||
|
if (
|
||||||
|
"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
|
||||||
|
|
||||||
if future and str(future):
|
if future and str(future):
|
||||||
await channel.send(str(future))
|
self._last_message_per_channel[ctx.channel.id] = await channel.send(
|
||||||
|
str(future), reference=replying
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await channel.send(":thinking:")
|
await ctx.send(":thinking:")
|
||||||
|
|
||||||
|
async def check_for_kaggle(self):
|
||||||
|
"""Check whether Kaggle is installed and configured properly"""
|
||||||
|
# TODO: This
|
||||||
|
return False
|
||||||
|
@ -2,22 +2,15 @@
|
|||||||
"author": [
|
"author": [
|
||||||
"Bobloy"
|
"Bobloy"
|
||||||
],
|
],
|
||||||
"min_bot_version": "3.4.0",
|
"min_bot_version": "3.4.6",
|
||||||
"description": "Create an offline chatbot that talks like your average member using Machine Learning",
|
"description": "Create an offline chatbot that talks like your average member using Machine Learning. See setup instructions at https://github.com/bobloy/Fox-V3/tree/master/chatter",
|
||||||
"hidden": false,
|
"hidden": false,
|
||||||
"install_msg": "Thank you for installing Chatter! Please make sure you check the install instructions at https://github.com/bobloy/Fox-V3/blob/master/chatter/README.md\nAfter that, get started ith `[p]load chatter` and `[p]help Chatter`",
|
"install_msg": "Thank you for installing Chatter! Please make sure you check the install instructions at https://github.com/bobloy/Fox-V3/blob/master/chatter/README.md\nAfter that, get started ith `[p]load chatter` and `[p]help Chatter`",
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"git+git://github.com/gunthercox/chatterbot-corpus@master#egg=chatterbot_corpus",
|
"git+git://github.com/bobloy/ChatterBot@fox#egg=ChatterBot",
|
||||||
"mathparse>=0.1,<0.2",
|
"kaggle",
|
||||||
"nltk>=3.2,<4.0",
|
"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",
|
||||||
"pint>=0.8.1",
|
"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"
|
||||||
"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",
|
"short": "Local Chatbot run on machine learning",
|
||||||
"end_user_data_statement": "This cog only stores anonymous conversations data; no End User Data is stored.",
|
"end_user_data_statement": "This cog only stores anonymous conversations data; no End User Data is stored.",
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
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
|
|
71
chatter/storage_adapters.py
Normal file
71
chatter/storage_adapters.py
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
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)
|
351
chatter/trainers.py
Normal file
351
chatter/trainers.py
Normal file
@ -0,0 +1,351 @@
|
|||||||
|
import asyncio
|
||||||
|
import csv
|
||||||
|
import html
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
import time
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
|
from chatterbot import utils
|
||||||
|
from chatterbot.conversation import Statement
|
||||||
|
from chatterbot.tagging import PosLemmaTagger
|
||||||
|
from chatterbot.trainers import Trainer
|
||||||
|
from redbot.core.bot import Red
|
||||||
|
from dateutil import parser as date_parser
|
||||||
|
from redbot.core.utils import AsyncIter
|
||||||
|
|
||||||
|
log = logging.getLogger("red.fox_v3.chatter.trainers")
|
||||||
|
|
||||||
|
|
||||||
|
class KaggleTrainer(Trainer):
|
||||||
|
def __init__(self, chatbot, datapath: pathlib.Path, **kwargs):
|
||||||
|
super().__init__(chatbot, **kwargs)
|
||||||
|
|
||||||
|
self.data_directory = datapath / kwargs.get("downloadpath", "kaggle_download")
|
||||||
|
|
||||||
|
self.kaggle_dataset = kwargs.get(
|
||||||
|
"kaggle_dataset",
|
||||||
|
"Cornell-University/movie-dialog-corpus",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create the data directory if it does not already exist
|
||||||
|
if not os.path.exists(self.data_directory):
|
||||||
|
os.makedirs(self.data_directory)
|
||||||
|
|
||||||
|
def is_downloaded(self, file_path):
|
||||||
|
"""
|
||||||
|
Check if the data file is already downloaded.
|
||||||
|
"""
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
self.chatbot.logger.info("File is already downloaded")
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def download(self, dataset):
|
||||||
|
import kaggle # This triggers the API token check
|
||||||
|
|
||||||
|
future = await asyncio.get_event_loop().run_in_executor(
|
||||||
|
None,
|
||||||
|
partial(
|
||||||
|
kaggle.api.dataset_download_files,
|
||||||
|
dataset=dataset,
|
||||||
|
path=self.data_directory,
|
||||||
|
quiet=False,
|
||||||
|
unzip=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def train(self, *args, **kwargs):
|
||||||
|
log.error("See asynctrain instead")
|
||||||
|
|
||||||
|
def asynctrain(self, *args, **kwargs):
|
||||||
|
raise self.TrainerInitializationException()
|
||||||
|
|
||||||
|
|
||||||
|
class SouthParkTrainer(KaggleTrainer):
|
||||||
|
def __init__(self, chatbot, datapath: pathlib.Path, **kwargs):
|
||||||
|
super().__init__(
|
||||||
|
chatbot,
|
||||||
|
datapath,
|
||||||
|
downloadpath="ubuntu_data_v2",
|
||||||
|
kaggle_dataset="tovarischsukhov/southparklines",
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MovieTrainer(KaggleTrainer):
|
||||||
|
def __init__(self, chatbot, datapath: pathlib.Path, **kwargs):
|
||||||
|
super().__init__(
|
||||||
|
chatbot,
|
||||||
|
datapath,
|
||||||
|
downloadpath="kaggle_movies",
|
||||||
|
kaggle_dataset="Cornell-University/movie-dialog-corpus",
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def run_movie_training(self):
|
||||||
|
dialogue_file = "movie_lines.tsv"
|
||||||
|
conversation_file = "movie_conversations.tsv"
|
||||||
|
log.info(f"Beginning dialogue training on {dialogue_file}")
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
tagger = PosLemmaTagger(language=self.chatbot.storage.tagger.language)
|
||||||
|
|
||||||
|
# [lineID, characterID, movieID, character name, text of utterance]
|
||||||
|
# File parsing from https://www.kaggle.com/mushaya/conversation-chatbot
|
||||||
|
|
||||||
|
with open(self.data_directory / conversation_file, "r", encoding="utf-8-sig") as conv_tsv:
|
||||||
|
conv_lines = conv_tsv.readlines()
|
||||||
|
with open(self.data_directory / dialogue_file, "r", encoding="utf-8-sig") as lines_tsv:
|
||||||
|
dialog_lines = lines_tsv.readlines()
|
||||||
|
|
||||||
|
# trans_dict = str.maketrans({"<u>": "__", "</u>": "__", '""': '"'})
|
||||||
|
|
||||||
|
lines_dict = {}
|
||||||
|
for line in dialog_lines:
|
||||||
|
_line = line[:-1].strip('"').split("\t")
|
||||||
|
if len(_line) >= 5: # Only good lines
|
||||||
|
lines_dict[_line[0]] = (
|
||||||
|
html.unescape(("".join(_line[4:])).strip())
|
||||||
|
.replace("<u>", "__")
|
||||||
|
.replace("</u>", "__")
|
||||||
|
.replace('""', '"')
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
log.debug(f"Bad line {_line}")
|
||||||
|
|
||||||
|
# collecting line ids for each conversation
|
||||||
|
conv = []
|
||||||
|
for line in conv_lines[:-1]:
|
||||||
|
_line = line[:-1].split("\t")[-1][1:-1].replace("'", "").replace(" ", ",")
|
||||||
|
conv.append(_line.split(","))
|
||||||
|
|
||||||
|
# conversations = csv.reader(conv_tsv, delimiter="\t")
|
||||||
|
#
|
||||||
|
# reader = csv.reader(lines_tsv, delimiter="\t")
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# lines_dict = {}
|
||||||
|
# for row in reader:
|
||||||
|
# try:
|
||||||
|
# lines_dict[row[0].strip('"')] = row[4]
|
||||||
|
# except:
|
||||||
|
# log.exception(f"Bad line: {row}")
|
||||||
|
# pass
|
||||||
|
# else:
|
||||||
|
# # log.info(f"Good line: {row}")
|
||||||
|
# pass
|
||||||
|
#
|
||||||
|
# # lines_dict = {row[0].strip('"'): row[4] for row in reader_list}
|
||||||
|
|
||||||
|
statements_from_file = []
|
||||||
|
save_every = 300
|
||||||
|
count = 0
|
||||||
|
|
||||||
|
# [characterID of first, characterID of second, movieID, list of utterances]
|
||||||
|
async for lines in AsyncIter(conv):
|
||||||
|
previous_statement_text = None
|
||||||
|
previous_statement_search_text = ""
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
text = lines_dict[line]
|
||||||
|
statement = Statement(
|
||||||
|
text=text,
|
||||||
|
in_response_to=previous_statement_text,
|
||||||
|
conversation="training",
|
||||||
|
)
|
||||||
|
|
||||||
|
for preprocessor in self.chatbot.preprocessors:
|
||||||
|
statement = preprocessor(statement)
|
||||||
|
|
||||||
|
statement.search_text = tagger.get_text_index_string(statement.text)
|
||||||
|
statement.search_in_response_to = previous_statement_search_text
|
||||||
|
|
||||||
|
previous_statement_text = statement.text
|
||||||
|
previous_statement_search_text = statement.search_text
|
||||||
|
|
||||||
|
statements_from_file.append(statement)
|
||||||
|
|
||||||
|
count += 1
|
||||||
|
if count >= save_every:
|
||||||
|
if statements_from_file:
|
||||||
|
self.chatbot.storage.create_many(statements_from_file)
|
||||||
|
statements_from_file = []
|
||||||
|
count = 0
|
||||||
|
|
||||||
|
if statements_from_file:
|
||||||
|
self.chatbot.storage.create_many(statements_from_file)
|
||||||
|
|
||||||
|
log.info(f"Training took {time.time() - start_time} seconds.")
|
||||||
|
|
||||||
|
async def asynctrain(self, *args, **kwargs):
|
||||||
|
extracted_lines = self.data_directory / "movie_lines.tsv"
|
||||||
|
extracted_lines: pathlib.Path
|
||||||
|
|
||||||
|
# Download and extract the Ubuntu dialog corpus if needed
|
||||||
|
if not extracted_lines.exists():
|
||||||
|
await self.download(self.kaggle_dataset)
|
||||||
|
else:
|
||||||
|
log.info("Movie dialog already downloaded")
|
||||||
|
if not extracted_lines.exists():
|
||||||
|
raise FileNotFoundError(f"{extracted_lines}")
|
||||||
|
|
||||||
|
await self.run_movie_training()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
# train_dialogue = kwargs.get("train_dialogue", True)
|
||||||
|
# train_196_dialogue = kwargs.get("train_196", False)
|
||||||
|
# train_301_dialogue = kwargs.get("train_301", False)
|
||||||
|
#
|
||||||
|
# if train_dialogue:
|
||||||
|
# await self.run_dialogue_training(extracted_dir, "dialogueText.csv")
|
||||||
|
#
|
||||||
|
# if train_196_dialogue:
|
||||||
|
# await self.run_dialogue_training(extracted_dir, "dialogueText_196.csv")
|
||||||
|
#
|
||||||
|
# if train_301_dialogue:
|
||||||
|
# await self.run_dialogue_training(extracted_dir, "dialogueText_301.csv")
|
||||||
|
|
||||||
|
|
||||||
|
class UbuntuCorpusTrainer2(KaggleTrainer):
|
||||||
|
def __init__(self, chatbot, datapath: pathlib.Path, **kwargs):
|
||||||
|
super().__init__(
|
||||||
|
chatbot,
|
||||||
|
datapath,
|
||||||
|
downloadpath="kaggle_ubuntu",
|
||||||
|
kaggle_dataset="rtatman/ubuntu-dialogue-corpus",
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def asynctrain(self, *args, **kwargs):
|
||||||
|
extracted_dir = self.data_directory / "Ubuntu-dialogue-corpus"
|
||||||
|
|
||||||
|
# Download and extract the Ubuntu dialog corpus if needed
|
||||||
|
if not extracted_dir.exists():
|
||||||
|
await self.download(self.kaggle_dataset)
|
||||||
|
else:
|
||||||
|
log.info("Ubuntu dialogue already downloaded")
|
||||||
|
if not extracted_dir.exists():
|
||||||
|
raise FileNotFoundError("Did not extract in the expected way")
|
||||||
|
|
||||||
|
train_dialogue = kwargs.get("train_dialogue", True)
|
||||||
|
train_196_dialogue = kwargs.get("train_196", False)
|
||||||
|
train_301_dialogue = kwargs.get("train_301", False)
|
||||||
|
|
||||||
|
if train_dialogue:
|
||||||
|
await self.run_dialogue_training(extracted_dir, "dialogueText.csv")
|
||||||
|
|
||||||
|
if train_196_dialogue:
|
||||||
|
await self.run_dialogue_training(extracted_dir, "dialogueText_196.csv")
|
||||||
|
|
||||||
|
if train_301_dialogue:
|
||||||
|
await self.run_dialogue_training(extracted_dir, "dialogueText_301.csv")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def run_dialogue_training(self, extracted_dir, dialogue_file):
|
||||||
|
log.info(f"Beginning dialogue training on {dialogue_file}")
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
tagger = PosLemmaTagger(language=self.chatbot.storage.tagger.language)
|
||||||
|
|
||||||
|
with open(extracted_dir / dialogue_file, "r", encoding="utf-8") as dg:
|
||||||
|
reader = csv.DictReader(dg)
|
||||||
|
|
||||||
|
next(reader) # Skip the header
|
||||||
|
|
||||||
|
last_dialogue_id = None
|
||||||
|
previous_statement_text = None
|
||||||
|
previous_statement_search_text = ""
|
||||||
|
statements_from_file = []
|
||||||
|
|
||||||
|
save_every = 50
|
||||||
|
count = 0
|
||||||
|
|
||||||
|
async for row in AsyncIter(reader):
|
||||||
|
dialogue_id = row["dialogueID"]
|
||||||
|
if dialogue_id != last_dialogue_id:
|
||||||
|
previous_statement_text = None
|
||||||
|
previous_statement_search_text = ""
|
||||||
|
last_dialogue_id = dialogue_id
|
||||||
|
count += 1
|
||||||
|
if count >= save_every:
|
||||||
|
if statements_from_file:
|
||||||
|
self.chatbot.storage.create_many(statements_from_file)
|
||||||
|
statements_from_file = []
|
||||||
|
count = 0
|
||||||
|
|
||||||
|
if len(row) > 0:
|
||||||
|
statement = Statement(
|
||||||
|
text=row["text"],
|
||||||
|
in_response_to=previous_statement_text,
|
||||||
|
conversation="training",
|
||||||
|
# created_at=date_parser.parse(row["date"]),
|
||||||
|
persona=row["from"],
|
||||||
|
)
|
||||||
|
|
||||||
|
for preprocessor in self.chatbot.preprocessors:
|
||||||
|
statement = preprocessor(statement)
|
||||||
|
|
||||||
|
statement.search_text = tagger.get_text_index_string(statement.text)
|
||||||
|
statement.search_in_response_to = previous_statement_search_text
|
||||||
|
|
||||||
|
previous_statement_text = statement.text
|
||||||
|
previous_statement_search_text = statement.search_text
|
||||||
|
|
||||||
|
statements_from_file.append(statement)
|
||||||
|
|
||||||
|
if statements_from_file:
|
||||||
|
self.chatbot.storage.create_many(statements_from_file)
|
||||||
|
|
||||||
|
log.info(f"Training took {time.time() - start_time} seconds.")
|
||||||
|
|
||||||
|
|
||||||
|
class TwitterCorpusTrainer(Trainer):
|
||||||
|
pass
|
||||||
|
# def train(self, *args, **kwargs):
|
||||||
|
# """
|
||||||
|
# Train the chat bot based on the provided list of
|
||||||
|
# statements that represents a single conversation.
|
||||||
|
# """
|
||||||
|
# import twint
|
||||||
|
#
|
||||||
|
# c = twint.Config()
|
||||||
|
# c.__dict__.update(kwargs)
|
||||||
|
# twint.run.Search(c)
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# previous_statement_text = None
|
||||||
|
# previous_statement_search_text = ''
|
||||||
|
#
|
||||||
|
# statements_to_create = []
|
||||||
|
#
|
||||||
|
# for conversation_count, text in enumerate(conversation):
|
||||||
|
# if self.show_training_progress:
|
||||||
|
# utils.print_progress_bar(
|
||||||
|
# 'List Trainer',
|
||||||
|
# conversation_count + 1, len(conversation)
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# statement_search_text = self.chatbot.storage.tagger.get_text_index_string(text)
|
||||||
|
#
|
||||||
|
# statement = self.get_preprocessed_statement(
|
||||||
|
# Statement(
|
||||||
|
# text=text,
|
||||||
|
# search_text=statement_search_text,
|
||||||
|
# in_response_to=previous_statement_text,
|
||||||
|
# search_in_response_to=previous_statement_search_text,
|
||||||
|
# conversation='training'
|
||||||
|
# )
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# previous_statement_text = statement.text
|
||||||
|
# previous_statement_search_text = statement_search_text
|
||||||
|
#
|
||||||
|
# statements_to_create.append(statement)
|
||||||
|
#
|
||||||
|
# self.chatbot.storage.create_many(statements_to_create)
|
@ -58,11 +58,7 @@ class CogLint(Cog):
|
|||||||
|
|
||||||
future = await self.bot.loop.run_in_executor(None, lint.py_run, path, "return_std=True")
|
future = await self.bot.loop.run_in_executor(None, lint.py_run, path, "return_std=True")
|
||||||
|
|
||||||
if future:
|
(pylint_stdout, pylint_stderr) = future or (None, None)
|
||||||
(pylint_stdout, pylint_stderr) = future
|
|
||||||
else:
|
|
||||||
(pylint_stdout, pylint_stderr) = None, None
|
|
||||||
|
|
||||||
# print(pylint_stderr)
|
# print(pylint_stderr)
|
||||||
# print(pylint_stdout)
|
# print(pylint_stdout)
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"maps": [
|
"maps": [
|
||||||
"simple_blank_map",
|
"simple",
|
||||||
"test",
|
"ck2",
|
||||||
"test2"
|
"HoI"
|
||||||
]
|
]
|
||||||
}
|
}
|
@ -98,7 +98,7 @@ def floodfill(image, xy, value, border=None, thresh=0) -> set:
|
|||||||
if border is None:
|
if border is None:
|
||||||
fill = _color_diff(p, background) <= thresh
|
fill = _color_diff(p, background) <= thresh
|
||||||
else:
|
else:
|
||||||
fill = p != value and p != border
|
fill = p not in [value, border]
|
||||||
if fill:
|
if fill:
|
||||||
pixel[s, t] = value
|
pixel[s, t] = value
|
||||||
new_edge.add((s, t))
|
new_edge.add((s, t))
|
||||||
|
@ -27,8 +27,7 @@ class ExclusiveRole(Cog):
|
|||||||
async def exclusive(self, ctx):
|
async def exclusive(self, ctx):
|
||||||
"""Base command for managing exclusive roles"""
|
"""Base command for managing exclusive roles"""
|
||||||
|
|
||||||
if not ctx.invoked_subcommand:
|
pass
|
||||||
pass
|
|
||||||
|
|
||||||
@exclusive.command(name="add")
|
@exclusive.command(name="add")
|
||||||
@checks.mod_or_permissions(administrator=True)
|
@checks.mod_or_permissions(administrator=True)
|
||||||
@ -85,7 +84,7 @@ class ExclusiveRole(Cog):
|
|||||||
if role_set is None:
|
if role_set is None:
|
||||||
role_set = set(await self.config.guild(member.guild).role_list())
|
role_set = set(await self.config.guild(member.guild).role_list())
|
||||||
|
|
||||||
member_set = set([role.id for role in member.roles])
|
member_set = {role.id for role in member.roles}
|
||||||
to_remove = (member_set - role_set) - {member.guild.default_role.id}
|
to_remove = (member_set - role_set) - {member.guild.default_role.id}
|
||||||
|
|
||||||
if to_remove and member_set & role_set:
|
if to_remove and member_set & role_set:
|
||||||
@ -103,7 +102,7 @@ class ExclusiveRole(Cog):
|
|||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
role_set = set(await self.config.guild(after.guild).role_list())
|
role_set = set(await self.config.guild(after.guild).role_list())
|
||||||
member_set = set([role.id for role in after.roles])
|
member_set = {role.id for role in after.roles}
|
||||||
|
|
||||||
if role_set & member_set:
|
if role_set & member_set:
|
||||||
try:
|
try:
|
||||||
|
@ -1,5 +1,15 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
from .fifo import FIFO
|
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):
|
async def setup(bot):
|
||||||
cog = FIFO(bot)
|
cog = FIFO(bot)
|
||||||
|
10
fifo/date_trigger.py
Normal file
10
fifo/date_trigger.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
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}
|
212
fifo/fifo.py
212
fifo/fifo.py
@ -1,8 +1,10 @@
|
|||||||
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timedelta, tzinfo
|
from datetime import MAXYEAR, datetime, timedelta, tzinfo
|
||||||
from typing import Optional, Union
|
from typing import Optional, Union
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
|
import pytz
|
||||||
from apscheduler.job import Job
|
from apscheduler.job import Job
|
||||||
from apscheduler.jobstores.base import JobLookupError
|
from apscheduler.jobstores.base import JobLookupError
|
||||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||||
@ -10,7 +12,7 @@ from apscheduler.schedulers.base import STATE_PAUSED, STATE_RUNNING
|
|||||||
from redbot.core import Config, checks, commands
|
from redbot.core import Config, checks, commands
|
||||||
from redbot.core.bot import Red
|
from redbot.core.bot import Red
|
||||||
from redbot.core.commands import TimedeltaConverter
|
from redbot.core.commands import TimedeltaConverter
|
||||||
from redbot.core.utils.chat_formatting import pagify
|
from redbot.core.utils.chat_formatting import humanize_timedelta, pagify
|
||||||
|
|
||||||
from .datetime_cron_converters import CronConverter, DatetimeConverter, TimezoneConverter
|
from .datetime_cron_converters import CronConverter, DatetimeConverter, TimezoneConverter
|
||||||
from .task import Task
|
from .task import Task
|
||||||
@ -21,11 +23,12 @@ schedule_log.setLevel(logging.DEBUG)
|
|||||||
log = logging.getLogger("red.fox_v3.fifo")
|
log = logging.getLogger("red.fox_v3.fifo")
|
||||||
|
|
||||||
|
|
||||||
async def _execute_task(task_state):
|
async def _execute_task(**task_state):
|
||||||
log.info(f"Executing {task_state=}")
|
log.info(f"Executing {task_state.get('name')}")
|
||||||
task = Task(**task_state)
|
task = Task(**task_state)
|
||||||
if await task.load_from_config():
|
if await task.load_from_config():
|
||||||
return await task.execute()
|
return await task.execute()
|
||||||
|
log.warning(f"Failed to load data on {task_state=}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@ -37,6 +40,37 @@ def _disassemble_job_id(job_id: str):
|
|||||||
return job_id.split("_")
|
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):
|
class FIFO(commands.Cog):
|
||||||
"""
|
"""
|
||||||
Simple Scheduling Cog
|
Simple Scheduling Cog
|
||||||
@ -55,7 +89,7 @@ class FIFO(commands.Cog):
|
|||||||
self.config.register_global(**default_global)
|
self.config.register_global(**default_global)
|
||||||
self.config.register_guild(**default_guild)
|
self.config.register_guild(**default_guild)
|
||||||
|
|
||||||
self.scheduler = None
|
self.scheduler: Optional[AsyncIOScheduler] = None
|
||||||
self.jobstore = None
|
self.jobstore = None
|
||||||
|
|
||||||
self.tz_cog = None
|
self.tz_cog = None
|
||||||
@ -71,17 +105,22 @@ class FIFO(commands.Cog):
|
|||||||
|
|
||||||
async def initialize(self):
|
async def initialize(self):
|
||||||
|
|
||||||
job_defaults = {"coalesce": False, "max_instances": 1}
|
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
|
||||||
|
}
|
||||||
|
|
||||||
# executors = {"default": AsyncIOExecutor()}
|
# executors = {"default": AsyncIOExecutor()}
|
||||||
|
|
||||||
# Default executor is already AsyncIOExecutor
|
# Default executor is already AsyncIOExecutor
|
||||||
self.scheduler = AsyncIOScheduler(job_defaults=job_defaults, logger=schedule_log)
|
self.scheduler = AsyncIOScheduler(job_defaults=job_defaults, logger=schedule_log)
|
||||||
|
|
||||||
from .redconfigjobstore import RedConfigJobStore
|
from .redconfigjobstore import RedConfigJobStore # Wait to import to prevent cyclic import
|
||||||
|
|
||||||
self.jobstore = RedConfigJobStore(self.config, self.bot)
|
self.jobstore = RedConfigJobStore(self.config, self.bot)
|
||||||
await self.jobstore.load_from_config(self.scheduler, "default")
|
await self.jobstore.load_from_config()
|
||||||
self.scheduler.add_jobstore(self.jobstore, "default")
|
self.scheduler.add_jobstore(self.jobstore, "default")
|
||||||
|
|
||||||
self.scheduler.start()
|
self.scheduler.start()
|
||||||
@ -104,41 +143,59 @@ class FIFO(commands.Cog):
|
|||||||
await task.delete_self()
|
await task.delete_self()
|
||||||
|
|
||||||
async def _process_task(self, task: Task):
|
async def _process_task(self, task: Task):
|
||||||
job: Union[Job, None] = await self._get_job(task)
|
# None of this is necessar, we have `replace_existing` already
|
||||||
if job is not None:
|
# job: Union[Job, None] = await self._get_job(task)
|
||||||
job.reschedule(await task.get_combined_trigger())
|
# if job is not None:
|
||||||
return job
|
# combined_trigger_ = await task.get_combined_trigger()
|
||||||
|
# if combined_trigger_ is None:
|
||||||
|
# job.remove()
|
||||||
|
# else:
|
||||||
|
# job.reschedule(combined_trigger_)
|
||||||
|
# return job
|
||||||
return await self._add_job(task)
|
return await self._add_job(task)
|
||||||
|
|
||||||
async def _get_job(self, task: Task) -> Job:
|
async def _get_job(self, task: Task) -> Job:
|
||||||
return self.scheduler.get_job(_assemble_job_id(task.name, task.guild_id))
|
return self.scheduler.get_job(_assemble_job_id(task.name, task.guild_id))
|
||||||
|
|
||||||
async def _add_job(self, task: Task):
|
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(
|
return self.scheduler.add_job(
|
||||||
_execute_task,
|
_execute_task,
|
||||||
args=[task.__getstate__()],
|
kwargs=task.__getstate__(),
|
||||||
id=_assemble_job_id(task.name, task.guild_id),
|
id=_assemble_job_id(task.name, task.guild_id),
|
||||||
trigger=await task.get_combined_trigger(),
|
trigger=combined_trigger_,
|
||||||
|
name=task.name,
|
||||||
|
replace_existing=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _resume_job(self, task: Task):
|
async def _resume_job(self, task: Task):
|
||||||
try:
|
job: Union[Job, None] = await self._get_job(task)
|
||||||
job = self.scheduler.resume_job(job_id=_assemble_job_id(task.name, task.guild_id))
|
if job is not None:
|
||||||
except JobLookupError:
|
job.resume()
|
||||||
|
else:
|
||||||
job = await self._process_task(task)
|
job = await self._process_task(task)
|
||||||
return job
|
return job
|
||||||
|
|
||||||
async def _pause_job(self, task: Task):
|
async def _pause_job(self, task: Task):
|
||||||
return self.scheduler.pause_job(job_id=_assemble_job_id(task.name, task.guild_id))
|
try:
|
||||||
|
return self.scheduler.pause_job(job_id=_assemble_job_id(task.name, task.guild_id))
|
||||||
|
except JobLookupError:
|
||||||
|
return False
|
||||||
|
|
||||||
async def _remove_job(self, task: Task):
|
async def _remove_job(self, task: Task):
|
||||||
return self.scheduler.remove_job(job_id=_assemble_job_id(task.name, task.guild_id))
|
try:
|
||||||
|
self.scheduler.remove_job(job_id=_assemble_job_id(task.name, task.guild_id))
|
||||||
|
except JobLookupError:
|
||||||
|
pass
|
||||||
|
|
||||||
async def _get_tz(self, user: Union[discord.User, discord.Member]) -> Union[None, tzinfo]:
|
async def _get_tz(self, user: Union[discord.User, discord.Member]) -> Union[None, tzinfo]:
|
||||||
if self.tz_cog is None:
|
if self.tz_cog is None:
|
||||||
self.tz_cog = self.bot.get_cog("Timezone")
|
self.tz_cog = self.bot.get_cog("Timezone")
|
||||||
if self.tz_cog is None:
|
if self.tz_cog is None:
|
||||||
self.tz_cog = False # only try once to get the timezone cog
|
self.tz_cog = False # only try once to get the timezone cog
|
||||||
|
|
||||||
if not self.tz_cog:
|
if not self.tz_cog:
|
||||||
return None
|
return None
|
||||||
@ -170,8 +227,42 @@ class FIFO(commands.Cog):
|
|||||||
"""
|
"""
|
||||||
Base command for handling scheduling of tasks
|
Base command for handling scheduling of tasks
|
||||||
"""
|
"""
|
||||||
if ctx.invoked_subcommand is None:
|
pass
|
||||||
pass
|
|
||||||
|
@fifo.command(name="wakeup")
|
||||||
|
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))
|
||||||
|
|
||||||
@fifo.command(name="set")
|
@fifo.command(name="set")
|
||||||
async def fifo_set(
|
async def fifo_set(
|
||||||
@ -300,10 +391,14 @@ class FIFO(commands.Cog):
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
embed.add_field(name="Server", value="Server not found", inline=False)
|
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 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)
|
||||||
if trigger_str:
|
if trigger_str:
|
||||||
embed.add_field(name="Triggers", value=trigger_str, inline=False)
|
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)
|
job = await self._get_job(task)
|
||||||
if job and job.next_run_time:
|
if job and job.next_run_time:
|
||||||
@ -319,12 +414,12 @@ class FIFO(commands.Cog):
|
|||||||
Do `[p]fifo list True` to see tasks from all guilds
|
Do `[p]fifo list True` to see tasks from all guilds
|
||||||
"""
|
"""
|
||||||
if all_guilds:
|
if all_guilds:
|
||||||
pass
|
pass # TODO: All guilds
|
||||||
else:
|
else:
|
||||||
out = ""
|
out = ""
|
||||||
all_tasks = await self.config.guild(ctx.guild).tasks()
|
all_tasks = await self.config.guild(ctx.guild).tasks()
|
||||||
for task_name, task_data in all_tasks.items():
|
for task_name, task_data in all_tasks.items():
|
||||||
out += f"{task_name}: {task_data}\n"
|
out += f"{task_name}: {task_data}\n\n"
|
||||||
|
|
||||||
if out:
|
if out:
|
||||||
if len(out) > 2000:
|
if len(out) > 2000:
|
||||||
@ -335,6 +430,27 @@ class FIFO(commands.Cog):
|
|||||||
else:
|
else:
|
||||||
await ctx.maybe_send_embed("No tasks to list")
|
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
|
||||||
|
|
||||||
|
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")
|
@fifo.command(name="add")
|
||||||
async def fifo_add(self, ctx: commands.Context, task_name: str, *, command_to_execute: str):
|
async def fifo_add(self, ctx: commands.Context, task_name: str, *, command_to_execute: str):
|
||||||
"""
|
"""
|
||||||
@ -394,6 +510,7 @@ class FIFO(commands.Cog):
|
|||||||
return
|
return
|
||||||
|
|
||||||
await task.clear_triggers()
|
await task.clear_triggers()
|
||||||
|
await self._remove_job(task)
|
||||||
await ctx.tick()
|
await ctx.tick()
|
||||||
|
|
||||||
@fifo.group(name="addtrigger", aliases=["trigger"])
|
@fifo.group(name="addtrigger", aliases=["trigger"])
|
||||||
@ -401,8 +518,7 @@ class FIFO(commands.Cog):
|
|||||||
"""
|
"""
|
||||||
Add a new trigger for a task from the current guild.
|
Add a new trigger for a task from the current guild.
|
||||||
"""
|
"""
|
||||||
if ctx.invoked_subcommand is None:
|
pass
|
||||||
pass
|
|
||||||
|
|
||||||
@fifo_trigger.command(name="interval")
|
@fifo_trigger.command(name="interval")
|
||||||
async def fifo_trigger_interval(
|
async def fifo_trigger_interval(
|
||||||
@ -413,7 +529,7 @@ class FIFO(commands.Cog):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
task = Task(task_name, ctx.guild.id, self.config, bot=self.bot)
|
task = Task(task_name, ctx.guild.id, self.config, bot=self.bot)
|
||||||
await task.load_from_config()
|
await task.load_from_config() # Will set the channel and author
|
||||||
|
|
||||||
if task.data is None:
|
if task.data is None:
|
||||||
await ctx.maybe_send_embed(
|
await ctx.maybe_send_embed(
|
||||||
@ -435,6 +551,40 @@ class FIFO(commands.Cog):
|
|||||||
f"Next run time: {job.next_run_time} ({delta_from_now.total_seconds()} seconds)"
|
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")
|
@fifo_trigger.command(name="date")
|
||||||
async def fifo_trigger_date(
|
async def fifo_trigger_date(
|
||||||
self, ctx: commands.Context, task_name: str, *, datetime_str: DatetimeConverter
|
self, ctx: commands.Context, task_name: str, *, datetime_str: DatetimeConverter
|
||||||
@ -443,7 +593,7 @@ class FIFO(commands.Cog):
|
|||||||
Add a "run once" datetime trigger to the specified task
|
Add a "run once" datetime trigger to the specified task
|
||||||
"""
|
"""
|
||||||
|
|
||||||
task = Task(task_name, ctx.guild.id, self.config)
|
task = Task(task_name, ctx.guild.id, self.config, bot=self.bot)
|
||||||
await task.load_from_config()
|
await task.load_from_config()
|
||||||
|
|
||||||
if task.data is None:
|
if task.data is None:
|
||||||
@ -483,7 +633,7 @@ class FIFO(commands.Cog):
|
|||||||
|
|
||||||
See https://crontab.guru/ for help generating the cron_str
|
See https://crontab.guru/ for help generating the cron_str
|
||||||
"""
|
"""
|
||||||
task = Task(task_name, ctx.guild.id, self.config)
|
task = Task(task_name, ctx.guild.id, self.config, bot=self.bot)
|
||||||
await task.load_from_config()
|
await task.load_from_config()
|
||||||
|
|
||||||
if task.data is None:
|
if task.data is None:
|
||||||
|
@ -10,7 +10,8 @@
|
|||||||
"end_user_data_statement": "This cog does not store any End User Data",
|
"end_user_data_statement": "This cog does not store any End User Data",
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"apscheduler",
|
"apscheduler",
|
||||||
"pytz"
|
"pytz",
|
||||||
|
"python-dateutil"
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"bobloy",
|
"bobloy",
|
||||||
|
@ -2,17 +2,14 @@ import asyncio
|
|||||||
import base64
|
import base64
|
||||||
import logging
|
import logging
|
||||||
import pickle
|
import pickle
|
||||||
from datetime import datetime
|
|
||||||
from typing import Tuple, Union
|
|
||||||
|
|
||||||
from apscheduler.job import Job
|
from apscheduler.job import Job
|
||||||
from apscheduler.jobstores.base import ConflictingIdError, JobLookupError
|
|
||||||
from apscheduler.jobstores.memory import MemoryJobStore
|
from apscheduler.jobstores.memory import MemoryJobStore
|
||||||
from apscheduler.schedulers.asyncio import run_in_event_loop
|
from apscheduler.schedulers.asyncio import run_in_event_loop
|
||||||
from apscheduler.util import datetime_to_utc_timestamp
|
from apscheduler.util import datetime_to_utc_timestamp
|
||||||
from redbot.core import Config
|
from redbot.core import Config
|
||||||
|
|
||||||
# TODO: use get_lock on config
|
# TODO: use get_lock on config maybe
|
||||||
from redbot.core.bot import Red
|
from redbot.core.bot import Red
|
||||||
from redbot.core.utils import AsyncIter
|
from redbot.core.utils import AsyncIter
|
||||||
|
|
||||||
@ -28,44 +25,55 @@ class RedConfigJobStore(MemoryJobStore):
|
|||||||
self.config = config
|
self.config = config
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.pickle_protocol = pickle.HIGHEST_PROTOCOL
|
self.pickle_protocol = pickle.HIGHEST_PROTOCOL
|
||||||
self._eventloop = self.bot.loop
|
self._eventloop = self.bot.loop # Used for @run_in_event_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
|
@run_in_event_loop
|
||||||
def start(self, scheduler, alias):
|
def start(self, scheduler, alias):
|
||||||
super().start(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, scheduler, alias):
|
async def load_from_config(self):
|
||||||
super().start(scheduler, alias)
|
|
||||||
_jobs = await self.config.jobs()
|
_jobs = await self.config.jobs()
|
||||||
self._jobs = [
|
# self._jobs = [
|
||||||
(await self._decode_job(job), timestamp) async for (job, timestamp) in AsyncIter(_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_index = await self.config.jobs_index.all() # Overwritten by next
|
# 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):
|
def _encode_job(self, job: Job):
|
||||||
job_state = job.__getstate__()
|
job_state = job.__getstate__()
|
||||||
new_args = list(job_state["args"])
|
job_state["kwargs"]["config"] = None
|
||||||
new_args[0]["config"] = None
|
job_state["kwargs"]["bot"] = None
|
||||||
new_args[0]["bot"] = None
|
# new_kwargs = job_state["kwargs"]
|
||||||
job_state["args"] = tuple(new_args)
|
# new_kwargs["config"] = None
|
||||||
|
# new_kwargs["bot"] = None
|
||||||
|
# job_state["kwargs"] = new_kwargs
|
||||||
encoded = base64.b64encode(pickle.dumps(job_state, self.pickle_protocol))
|
encoded = base64.b64encode(pickle.dumps(job_state, self.pickle_protocol))
|
||||||
out = {
|
out = {
|
||||||
"_id": job.id,
|
"_id": job.id,
|
||||||
"next_run_time": datetime_to_utc_timestamp(job.next_run_time),
|
"next_run_time": datetime_to_utc_timestamp(job.next_run_time),
|
||||||
"job_state": encoded.decode("ascii"),
|
"job_state": encoded.decode("ascii"),
|
||||||
}
|
}
|
||||||
new_args = list(job_state["args"])
|
job_state["kwargs"]["config"] = self.config
|
||||||
new_args[0]["config"] = self.config
|
job_state["kwargs"]["bot"] = self.bot
|
||||||
new_args[0]["bot"] = self.bot
|
# new_kwargs = job_state["kwargs"]
|
||||||
job_state["args"] = tuple(new_args)
|
# new_kwargs["config"] = self.config
|
||||||
|
# new_kwargs["bot"] = self.bot
|
||||||
|
# job_state["kwargs"] = new_kwargs
|
||||||
# log.debug(f"Encoding job id: {job.id}\n"
|
# log.debug(f"Encoding job id: {job.id}\n"
|
||||||
# f"Encoded as: {out}")
|
# f"Encoded as: {out}")
|
||||||
|
|
||||||
@ -76,10 +84,15 @@ class RedConfigJobStore(MemoryJobStore):
|
|||||||
return None
|
return None
|
||||||
job_state = in_job["job_state"]
|
job_state = in_job["job_state"]
|
||||||
job_state = pickle.loads(base64.b64decode(job_state))
|
job_state = pickle.loads(base64.b64decode(job_state))
|
||||||
new_args = list(job_state["args"])
|
if job_state["args"]: # Backwards compatibility on args to kwargs
|
||||||
new_args[0]["config"] = self.config
|
job_state["kwargs"] = {**job_state["args"][0]}
|
||||||
new_args[0]["bot"] = self.bot
|
job_state["args"] = []
|
||||||
job_state["args"] = tuple(new_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
|
||||||
job = Job.__new__(Job)
|
job = Job.__new__(Job)
|
||||||
job.__setstate__(job_state)
|
job.__setstate__(job_state)
|
||||||
job._scheduler = self._scheduler
|
job._scheduler = self._scheduler
|
||||||
@ -96,79 +109,6 @@ class RedConfigJobStore(MemoryJobStore):
|
|||||||
|
|
||||||
return job
|
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
|
@run_in_event_loop
|
||||||
def remove_all_jobs(self):
|
def remove_all_jobs(self):
|
||||||
super().remove_all_jobs()
|
super().remove_all_jobs()
|
||||||
@ -180,4 +120,9 @@ class RedConfigJobStore(MemoryJobStore):
|
|||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
"""Removes all jobs without clearing config"""
|
"""Removes all jobs without clearing config"""
|
||||||
super().remove_all_jobs()
|
asyncio.create_task(self.async_shutdown())
|
||||||
|
|
||||||
|
async def async_shutdown(self):
|
||||||
|
await self.save_to_config()
|
||||||
|
self._jobs = []
|
||||||
|
self._jobs_index = {}
|
||||||
|
261
fifo/task.py
261
fifo/task.py
@ -1,18 +1,19 @@
|
|||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Dict, List, Union
|
from typing import Dict, List, Optional, Tuple, Union
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
|
import pytz
|
||||||
from apscheduler.triggers.base import BaseTrigger
|
from apscheduler.triggers.base import BaseTrigger
|
||||||
from apscheduler.triggers.combining import OrTrigger
|
from apscheduler.triggers.combining import OrTrigger
|
||||||
from apscheduler.triggers.cron import CronTrigger
|
from apscheduler.triggers.cron import CronTrigger
|
||||||
from apscheduler.triggers.date import DateTrigger
|
|
||||||
from apscheduler.triggers.interval import IntervalTrigger
|
from apscheduler.triggers.interval import IntervalTrigger
|
||||||
from discord.utils import time_snowflake
|
from discord.utils import time_snowflake
|
||||||
from pytz import timezone
|
|
||||||
from redbot.core import Config, commands
|
from redbot.core import Config, commands
|
||||||
from redbot.core.bot import Red
|
from redbot.core.bot import Red
|
||||||
|
|
||||||
|
from fifo.date_trigger import CustomDateTrigger
|
||||||
|
|
||||||
log = logging.getLogger("red.fox_v3.fifo.task")
|
log = logging.getLogger("red.fox_v3.fifo.task")
|
||||||
|
|
||||||
|
|
||||||
@ -26,7 +27,7 @@ def get_trigger(data):
|
|||||||
return IntervalTrigger(days=parsed_time.days, seconds=parsed_time.seconds)
|
return IntervalTrigger(days=parsed_time.days, seconds=parsed_time.seconds)
|
||||||
|
|
||||||
if data["type"] == "date":
|
if data["type"] == "date":
|
||||||
return DateTrigger(data["time_data"], timezone=data["tzinfo"])
|
return CustomDateTrigger(data["time_data"], timezone=data["tzinfo"])
|
||||||
|
|
||||||
if data["type"] == "cron":
|
if data["type"] == "cron":
|
||||||
return CronTrigger.from_crontab(data["time_data"], timezone=data["tzinfo"])
|
return CronTrigger.from_crontab(data["time_data"], timezone=data["tzinfo"])
|
||||||
@ -34,20 +35,126 @@ def get_trigger(data):
|
|||||||
return False
|
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]):
|
def parse_triggers(data: Union[Dict, None]):
|
||||||
if data is None or not data.get("triggers", False): # No triggers
|
if data is None or not data.get("triggers", False): # No triggers
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if len(data["triggers"]) > 1: # Multiple triggers
|
if len(data["triggers"]) > 1: # Multiple triggers
|
||||||
return OrTrigger(get_trigger(t_data) for t_data in data["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)]
|
||||||
return get_trigger(data["triggers"][0])
|
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:
|
# class FakeMessage:
|
||||||
def __init__(self, message: discord.Message):
|
# def __init__(self, message: discord.Message):
|
||||||
d = {k: getattr(message, k, None) for k in dir(message)}
|
# d = {k: getattr(message, k, None) for k in dir(message)}
|
||||||
self.__dict__.update(**d)
|
# 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",
|
||||||
|
]
|
||||||
|
|
||||||
|
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],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# self._handle_content(content)
|
||||||
|
# log.debug(self.content)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
def neuter_message(message: FakeMessage):
|
def neuter_message(message: FakeMessage):
|
||||||
@ -66,11 +173,11 @@ def neuter_message(message: FakeMessage):
|
|||||||
|
|
||||||
|
|
||||||
class Task:
|
class Task:
|
||||||
default_task_data = {"triggers": [], "command_str": ""}
|
default_task_data = {"triggers": [], "command_str": "", "expired_triggers": []}
|
||||||
|
|
||||||
default_trigger = {
|
default_trigger = {
|
||||||
"type": "",
|
"type": "",
|
||||||
"time_data": None, # Used for Interval and Date Triggers
|
"time_data": None,
|
||||||
"tzinfo": None,
|
"tzinfo": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,9 +194,10 @@ class Task:
|
|||||||
|
|
||||||
async def _encode_time_triggers(self):
|
async def _encode_time_triggers(self):
|
||||||
if not self.data or not self.data.get("triggers", None):
|
if not self.data or not self.data.get("triggers", None):
|
||||||
return []
|
return [], []
|
||||||
|
|
||||||
triggers = []
|
triggers = []
|
||||||
|
expired_triggers = []
|
||||||
for t in self.data["triggers"]:
|
for t in self.data["triggers"]:
|
||||||
if t["type"] == "interval": # Convert into timedelta
|
if t["type"] == "interval": # Convert into timedelta
|
||||||
td: timedelta = t["time_data"]
|
td: timedelta = t["time_data"]
|
||||||
@ -101,27 +209,15 @@ class Task:
|
|||||||
|
|
||||||
if t["type"] == "date": # Convert into datetime
|
if t["type"] == "date": # Convert into datetime
|
||||||
dt: datetime = t["time_data"]
|
dt: datetime = t["time_data"]
|
||||||
triggers.append(
|
data_to_append = {
|
||||||
{
|
"type": t["type"],
|
||||||
"type": t["type"],
|
"time_data": dt.isoformat(),
|
||||||
"time_data": dt.isoformat(),
|
"tzinfo": getattr(t["tzinfo"], "zone", None),
|
||||||
"tzinfo": getattr(t["tzinfo"], "zone", None),
|
}
|
||||||
}
|
if dt < datetime.now(pytz.utc):
|
||||||
)
|
expired_triggers.append(data_to_append)
|
||||||
# triggers.append(
|
else:
|
||||||
# {
|
triggers.append(data_to_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
|
continue
|
||||||
|
|
||||||
if t["type"] == "cron":
|
if t["type"] == "cron":
|
||||||
@ -139,7 +235,7 @@ class Task:
|
|||||||
|
|
||||||
raise NotImplemented
|
raise NotImplemented
|
||||||
|
|
||||||
return triggers
|
return triggers, expired_triggers
|
||||||
|
|
||||||
async def _decode_time_triggers(self):
|
async def _decode_time_triggers(self):
|
||||||
if not self.data or not self.data.get("triggers", None):
|
if not self.data or not self.data.get("triggers", None):
|
||||||
@ -152,7 +248,7 @@ class Task:
|
|||||||
|
|
||||||
# First decode timezone if there is one
|
# First decode timezone if there is one
|
||||||
if t["tzinfo"] is not None:
|
if t["tzinfo"] is not None:
|
||||||
t["tzinfo"] = timezone(t["tzinfo"])
|
t["tzinfo"] = pytz.timezone(t["tzinfo"])
|
||||||
|
|
||||||
if t["type"] == "interval": # Convert into timedelta
|
if t["type"] == "interval": # Convert into timedelta
|
||||||
t["time_data"] = timedelta(**t["time_data"])
|
t["time_data"] = timedelta(**t["time_data"])
|
||||||
@ -180,7 +276,7 @@ class Task:
|
|||||||
return
|
return
|
||||||
|
|
||||||
self.author_id = data["author_id"]
|
self.author_id = data["author_id"]
|
||||||
self.guild_id = data["guild_id"]
|
self.guild_id = data["guild_id"] # Weird I'm doing this, since self.guild_id was just used
|
||||||
self.channel_id = data["channel_id"]
|
self.channel_id = data["channel_id"]
|
||||||
|
|
||||||
self.data = data["data"]
|
self.data = data["data"]
|
||||||
@ -188,14 +284,23 @@ class Task:
|
|||||||
await self._decode_time_triggers()
|
await self._decode_time_triggers()
|
||||||
return self.data
|
return self.data
|
||||||
|
|
||||||
async def get_triggers(self) -> List[Union[IntervalTrigger, DateTrigger]]:
|
async def get_triggers(self) -> Tuple[List[BaseTrigger], List[BaseTrigger]]:
|
||||||
if not self.data:
|
if not self.data:
|
||||||
await self.load_from_config()
|
await self.load_from_config()
|
||||||
|
|
||||||
if self.data is None or "triggers" not in self.data: # No triggers
|
if self.data is None or "triggers" not in self.data: # No triggers
|
||||||
return []
|
return [], []
|
||||||
|
|
||||||
return [get_trigger(t) for t in self.data["triggers"]]
|
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 trigs, expired_trigs
|
||||||
|
|
||||||
async def get_combined_trigger(self) -> Union[BaseTrigger, None]:
|
async def get_combined_trigger(self) -> Union[BaseTrigger, None]:
|
||||||
if not self.data:
|
if not self.data:
|
||||||
@ -215,7 +320,10 @@ class Task:
|
|||||||
data_to_save = self.default_task_data.copy()
|
data_to_save = self.default_task_data.copy()
|
||||||
if self.data:
|
if self.data:
|
||||||
data_to_save["command_str"] = self.get_command_str()
|
data_to_save["command_str"] = self.get_command_str()
|
||||||
data_to_save["triggers"] = await self._encode_time_triggers()
|
(
|
||||||
|
data_to_save["triggers"],
|
||||||
|
data_to_save["expired_triggers"],
|
||||||
|
) = await self._encode_time_triggers()
|
||||||
|
|
||||||
to_save = {
|
to_save = {
|
||||||
"guild_id": self.guild_id,
|
"guild_id": self.guild_id,
|
||||||
@ -231,7 +339,10 @@ class Task:
|
|||||||
return
|
return
|
||||||
|
|
||||||
data_to_save = self.data.copy()
|
data_to_save = self.data.copy()
|
||||||
data_to_save["triggers"] = await self._encode_time_triggers()
|
(
|
||||||
|
data_to_save["triggers"],
|
||||||
|
data_to_save["expired_triggers"],
|
||||||
|
) = await self._encode_time_triggers()
|
||||||
|
|
||||||
await self.config.guild_from_id(self.guild_id).tasks.set_raw(
|
await self.config.guild_from_id(self.guild_id).tasks.set_raw(
|
||||||
self.name, "data", value=data_to_save
|
self.name, "data", value=data_to_save
|
||||||
@ -239,63 +350,87 @@ class Task:
|
|||||||
|
|
||||||
async def execute(self):
|
async def execute(self):
|
||||||
if not self.data or not self.get_command_str():
|
if not self.data or not self.get_command_str():
|
||||||
log.warning(f"Could not execute task due to data problem: {self.data=}")
|
log.warning(f"Could not execute Task[{self.name}] due to data problem: {self.data=}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
guild: discord.Guild = self.bot.get_guild(self.guild_id) # used for get_prefix
|
guild: discord.Guild = self.bot.get_guild(self.guild_id) # used for get_prefix
|
||||||
if guild is None:
|
if guild is None:
|
||||||
log.warning(f"Could not execute task due to missing guild: {self.guild_id}")
|
log.warning(
|
||||||
|
f"Could not execute Task[{self.name}] due to missing guild: {self.guild_id}"
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
channel: discord.TextChannel = guild.get_channel(self.channel_id)
|
channel: discord.TextChannel = guild.get_channel(self.channel_id)
|
||||||
if channel is None:
|
if channel is None:
|
||||||
log.warning(f"Could not execute task due to missing channel: {self.channel_id}")
|
log.warning(
|
||||||
|
f"Could not execute Task[{self.name}] due to missing channel: {self.channel_id}"
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
author: discord.User = guild.get_member(self.author_id)
|
author: discord.Member = guild.get_member(self.author_id)
|
||||||
if author is None:
|
if author is None:
|
||||||
log.warning(f"Could not execute task due to missing author: {self.author_id}")
|
log.warning(
|
||||||
|
f"Could not execute Task[{self.name}] due to missing author: {self.author_id}"
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
actual_message: discord.Message = channel.last_message
|
actual_message: Optional[discord.Message] = channel.last_message
|
||||||
# I'd like to present you my chain of increasingly desperate message fetching attempts
|
# I'd like to present you my chain of increasingly desperate message fetching attempts
|
||||||
if actual_message is None:
|
if actual_message is None:
|
||||||
# log.warning("No message found in channel cache yet, skipping execution")
|
# log.warning("No message found in channel cache yet, skipping execution")
|
||||||
# return
|
# return
|
||||||
actual_message = await channel.fetch_message(channel.last_message_id)
|
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
|
||||||
if actual_message is None: # last_message_id was an invalid message I guess
|
if actual_message is None: # last_message_id was an invalid message I guess
|
||||||
actual_message = await channel.history(limit=1).flatten()
|
actual_message = await channel.history(limit=1).flatten()
|
||||||
if not actual_message: # Basically only happens if the channel has no messages
|
if not actual_message: # Basically only happens if the channel has no messages
|
||||||
actual_message = await author.history(limit=1).flatten()
|
actual_message = await author.history(limit=1).flatten()
|
||||||
if not actual_message: # Okay, the *author* has never sent a message?
|
if not actual_message: # Okay, the *author* has never sent a message?
|
||||||
log.warning("No message found in channel cache yet, skipping execution")
|
log.warning("No message found in channel cache yet, skipping execution")
|
||||||
return
|
return False
|
||||||
actual_message = actual_message[0]
|
actual_message = actual_message[0]
|
||||||
|
|
||||||
message = FakeMessage(actual_message)
|
# message._handle_author(author) # Option when message is subclass
|
||||||
# message = FakeMessage2
|
# message._state = self.bot._get_state()
|
||||||
message.author = author
|
# Time to set the relevant attributes
|
||||||
message.guild = guild # Just in case we got desperate
|
# message.author = author
|
||||||
message.channel = channel
|
# Don't need guild with subclass, guild is just channel.guild
|
||||||
message.id = time_snowflake(datetime.now()) # Pretend to be now
|
# message.guild = guild # Just in case we got desperate, see above
|
||||||
message = neuter_message(message)
|
# message.channel = channel
|
||||||
|
|
||||||
# absolutely weird that this takes a message object instead of guild
|
# absolutely weird that this takes a message object instead of guild
|
||||||
prefixes = await self.bot.get_prefix(message)
|
prefixes = await self.bot.get_prefix(actual_message)
|
||||||
if isinstance(prefixes, str):
|
if isinstance(prefixes, str):
|
||||||
prefix = prefixes
|
prefix = prefixes
|
||||||
else:
|
else:
|
||||||
prefix = prefixes[0]
|
prefix = prefixes[0]
|
||||||
|
|
||||||
message.content = f"{prefix}{self.get_command_str()}"
|
new_content = f"{prefix}{self.get_command_str()}"
|
||||||
|
# log.debug(f"{new_content=}")
|
||||||
|
|
||||||
if not message.guild or not message.author or not message.content:
|
message = FakeMessage(message=actual_message)
|
||||||
log.warning(f"Could not execute task due to message problem: {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=}"
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
new_ctx: commands.Context = await self.bot.get_context(message)
|
new_ctx: commands.Context = await self.bot.get_context(message)
|
||||||
new_ctx.assume_yes = True
|
new_ctx.assume_yes = True
|
||||||
if not new_ctx.valid:
|
if not new_ctx.valid:
|
||||||
log.warning(
|
log.warning(
|
||||||
f"Could not execute Task[{self.name}] due invalid context: {new_ctx.invoked_with}"
|
f"Could not execute Task[{self.name}] due invalid context: "
|
||||||
|
f"{new_ctx.invoked_with=} {new_ctx.prefix=} {new_ctx.command=}"
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -5,6 +5,8 @@ All credit to https://github.com/prefrontal/dateutil-parser-timezones
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# from dateutil.tz import gettz
|
# from dateutil.tz import gettz
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from pytz import timezone
|
from pytz import timezone
|
||||||
|
|
||||||
|
|
||||||
@ -227,4 +229,6 @@ def assemble_timezones():
|
|||||||
timezones["YAKT"] = timezone("Asia/Yakutsk") # Yakutsk Time (UTC+09)
|
timezones["YAKT"] = timezone("Asia/Yakutsk") # Yakutsk Time (UTC+09)
|
||||||
timezones["YEKT"] = timezone("Asia/Yekaterinburg") # Yekaterinburg Time (UTC+05)
|
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
|
return timezones
|
||||||
|
@ -53,12 +53,9 @@ class Flag(Cog):
|
|||||||
@commands.group()
|
@commands.group()
|
||||||
async def flagset(self, ctx: commands.Context):
|
async def flagset(self, ctx: commands.Context):
|
||||||
"""
|
"""
|
||||||
My custom cog
|
Commands for managing Flag settings
|
||||||
|
|
||||||
Extra information goes here
|
|
||||||
"""
|
"""
|
||||||
if ctx.invoked_subcommand is None:
|
pass
|
||||||
pass
|
|
||||||
|
|
||||||
@flagset.command(name="expire")
|
@flagset.command(name="expire")
|
||||||
async def flagset_expire(self, ctx: commands.Context, days: int):
|
async def flagset_expire(self, ctx: commands.Context, days: int):
|
||||||
|
@ -147,8 +147,7 @@ class Hangman(Cog):
|
|||||||
@checks.mod_or_permissions(administrator=True)
|
@checks.mod_or_permissions(administrator=True)
|
||||||
async def hangset(self, ctx):
|
async def hangset(self, ctx):
|
||||||
"""Adjust hangman settings"""
|
"""Adjust hangman settings"""
|
||||||
if ctx.invoked_subcommand is None:
|
pass
|
||||||
pass
|
|
||||||
|
|
||||||
@hangset.command()
|
@hangset.command()
|
||||||
async def face(self, ctx: commands.Context, theface):
|
async def face(self, ctx: commands.Context, theface):
|
||||||
@ -250,7 +249,7 @@ class Hangman(Cog):
|
|||||||
|
|
||||||
self.winbool[guild] = True
|
self.winbool[guild] = True
|
||||||
for i in self.the_data[guild]["answer"]:
|
for i in self.the_data[guild]["answer"]:
|
||||||
if i == " " or i == "-":
|
if i in [" ", "-"]:
|
||||||
out_str += i * 2
|
out_str += i * 2
|
||||||
elif i in self.the_data[guild]["guesses"] or i not in "ABCDEFGHIJKLMNOPQRSTUVWXYZ":
|
elif i in self.the_data[guild]["guesses"] or i not in "ABCDEFGHIJKLMNOPQRSTUVWXYZ":
|
||||||
out_str += "__" + i + "__ "
|
out_str += "__" + i + "__ "
|
||||||
@ -262,9 +261,7 @@ class Hangman(Cog):
|
|||||||
|
|
||||||
def _guesslist(self, guild):
|
def _guesslist(self, guild):
|
||||||
"""Returns the current letter list"""
|
"""Returns the current letter list"""
|
||||||
out_str = ""
|
out_str = "".join(str(i) + "," for i in self.the_data[guild]["guesses"])
|
||||||
for i in self.the_data[guild]["guesses"]:
|
|
||||||
out_str += str(i) + ","
|
|
||||||
out_str = out_str[:-1]
|
out_str = out_str[:-1]
|
||||||
|
|
||||||
return out_str
|
return out_str
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
from .infochannel import InfoChannel
|
from .infochannel import InfoChannel
|
||||||
|
|
||||||
|
|
||||||
def setup(bot):
|
async def setup(bot):
|
||||||
bot.add_cog(InfoChannel(bot))
|
ic_cog = InfoChannel(bot)
|
||||||
|
bot.add_cog(ic_cog)
|
||||||
|
await ic_cog.initialize()
|
||||||
|
@ -1,25 +1,50 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from typing import Union
|
import logging
|
||||||
|
from collections import defaultdict
|
||||||
|
from typing import Dict, Optional, Union
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from redbot.core import Config, checks, commands
|
from redbot.core import Config, checks, commands
|
||||||
from redbot.core.bot import Red
|
from redbot.core.bot import Red
|
||||||
from redbot.core.commands import Cog
|
from redbot.core.commands import Cog
|
||||||
|
|
||||||
# Cog: Any = getattr(commands, "Cog", object)
|
# 10 minutes. Rate limit is 2 per 10, so 1 per 6 is safe.
|
||||||
# listener = getattr(commands.Cog, "listener", None) # Trusty + Sinbad
|
RATE_LIMIT_DELAY = 60 * 6 # If you're willing to risk rate limiting, you can decrease the delay
|
||||||
# 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
|
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
|
||||||
|
return {
|
||||||
|
"members": members,
|
||||||
|
"humans": human_num,
|
||||||
|
"bots": bot_num,
|
||||||
|
"roles": roles_num,
|
||||||
|
"channels": channels_num,
|
||||||
|
"online": online_num,
|
||||||
|
"offline": offline_num,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class InfoChannel(Cog):
|
class InfoChannel(Cog):
|
||||||
"""
|
"""
|
||||||
Create a channel with updating server info
|
Create a channel with updating server info
|
||||||
|
|
||||||
Less important information about the cog
|
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.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, bot: Red):
|
def __init__(self, bot: Red):
|
||||||
@ -29,23 +54,55 @@ class InfoChannel(Cog):
|
|||||||
self, identifier=731101021116710497110110101108, force_registration=True
|
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}",
|
||||||
|
"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 = {
|
default_guild = {
|
||||||
"channel_id": None,
|
"category_id": None,
|
||||||
"botchannel_id": None,
|
"channel_ids": default_channel_ids,
|
||||||
"onlinechannel_id": None,
|
"enabled_channels": default_enabled_counts,
|
||||||
"member_count": True,
|
"channel_names": self.default_channel_names,
|
||||||
"bot_count": False,
|
|
||||||
"online_count": False,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.config.register_guild(**default_guild)
|
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._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):
|
async def red_delete_data_for_user(self, **kwargs):
|
||||||
"""Nothing to delete"""
|
"""Nothing to delete"""
|
||||||
return
|
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()
|
@commands.command()
|
||||||
@checks.admin()
|
@checks.admin()
|
||||||
async def infochannel(self, ctx: commands.Context):
|
async def infochannel(self, ctx: commands.Context):
|
||||||
@ -61,233 +118,461 @@ class InfoChannel(Cog):
|
|||||||
)
|
)
|
||||||
|
|
||||||
guild: discord.Guild = ctx.guild
|
guild: discord.Guild = ctx.guild
|
||||||
channel_id = await self.config.guild(guild).channel_id()
|
category_id = await self.config.guild(guild).category_id()
|
||||||
channel = None
|
category = None
|
||||||
if channel_id is not None:
|
|
||||||
channel: Union[discord.VoiceChannel, None] = guild.get_channel(channel_id)
|
|
||||||
|
|
||||||
if channel_id is not None and channel is None:
|
if category_id is not None:
|
||||||
await ctx.send("Info channel has been deleted, recreate it?")
|
category: Union[discord.CategoryChannel, None] = guild.get_channel(category_id)
|
||||||
elif channel_id is None:
|
|
||||||
await ctx.send("Enable info channel on this server?")
|
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?")
|
||||||
else:
|
else:
|
||||||
await ctx.send("Do you wish to delete current info channels?")
|
await ctx.maybe_send_embed("Do you wish to delete current info channels?")
|
||||||
|
|
||||||
msg = await self.bot.wait_for("message", check=check)
|
msg = await self.bot.wait_for("message", check=check)
|
||||||
|
|
||||||
if msg.content.upper() in ["N", "NO"]:
|
if msg.content.upper() in ["N", "NO"]:
|
||||||
await ctx.send("Cancelled")
|
await ctx.maybe_send_embed("Cancelled")
|
||||||
return
|
return
|
||||||
|
|
||||||
if channel is None:
|
if category is None:
|
||||||
try:
|
try:
|
||||||
await self.make_infochannel(guild)
|
await self.make_infochannel(guild)
|
||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
await ctx.send("Failure: Missing permission to create voice channel")
|
await ctx.maybe_send_embed(
|
||||||
|
"Failure: Missing permission to create necessary channels"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
await self.delete_all_infochannels(guild)
|
await self.delete_all_infochannels(guild)
|
||||||
|
|
||||||
if not await ctx.tick():
|
ctx.message = msg
|
||||||
await ctx.send("Done!")
|
|
||||||
|
|
||||||
@commands.group()
|
if not await ctx.tick():
|
||||||
|
await ctx.maybe_send_embed("Done!")
|
||||||
|
|
||||||
|
@commands.group(aliases=["icset"])
|
||||||
@checks.admin()
|
@checks.admin()
|
||||||
async def infochannelset(self, ctx: commands.Context):
|
async def infochannelset(self, ctx: commands.Context):
|
||||||
"""
|
"""
|
||||||
Toggle different types of infochannels
|
Toggle different types of infochannels
|
||||||
"""
|
"""
|
||||||
if not ctx.invoked_subcommand:
|
pass
|
||||||
pass
|
|
||||||
|
|
||||||
@infochannelset.command(name="botcount")
|
@infochannelset.command(name="togglechannel")
|
||||||
async def _infochannelset_botcount(self, ctx: commands.Context, enabled: bool = None):
|
async def _infochannelset_togglechannel(
|
||||||
"""
|
self, ctx: commands.Context, channel_type: str, enabled: Optional[bool] = None
|
||||||
Toggle an infochannel that shows the amount of bots in the server
|
):
|
||||||
|
"""Toggles the infochannel for the specified channel type.
|
||||||
|
|
||||||
|
Valid Types are:
|
||||||
|
- `members`: Total members on the server
|
||||||
|
- `humans`: Total members that aren't bots
|
||||||
|
- `bots`: Total bots
|
||||||
|
- `roles`: Total number of roles
|
||||||
|
- `channels`: Total number of channels excluding infochannels,
|
||||||
|
- `online`: Total online members,
|
||||||
|
- `offline`: Total offline members,
|
||||||
"""
|
"""
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
if enabled is None:
|
if channel_type not in self.default_channel_names.keys():
|
||||||
enabled = not await self.config.guild(guild).bot_count()
|
await ctx.maybe_send_embed("Invalid channel type provided.")
|
||||||
|
return
|
||||||
|
|
||||||
await self.config.guild(guild).bot_count.set(enabled)
|
if enabled is None:
|
||||||
await self.make_infochannel(ctx.guild)
|
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:
|
if enabled:
|
||||||
await ctx.send("InfoChannel for bot count has been enabled.")
|
await ctx.maybe_send_embed(f"InfoChannel `{channel_type}` has been enabled.")
|
||||||
else:
|
else:
|
||||||
await ctx.send("InfoChannel for bot count has been disabled.")
|
await ctx.maybe_send_embed(f"InfoChannel `{channel_type}` has been disabled.")
|
||||||
|
|
||||||
@infochannelset.command(name="onlinecount")
|
@infochannelset.command(name="togglerole")
|
||||||
async def _infochannelset_onlinecount(self, ctx: commands.Context, enabled: bool = None):
|
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()
|
||||||
|
|
||||||
|
await self.config.role(role).enabled.set(enabled)
|
||||||
|
|
||||||
|
await self.make_infochannel(ctx.guild, channel_role=role)
|
||||||
|
|
||||||
|
if enabled:
|
||||||
|
await ctx.maybe_send_embed(f"InfoChannel for {role.name} count has been enabled.")
|
||||||
|
else:
|
||||||
|
await ctx.maybe_send_embed(f"InfoChannel for {role.name} count has been disabled.")
|
||||||
|
|
||||||
|
@infochannelset.command(name="name")
|
||||||
|
async def _infochannelset_name(self, ctx: commands.Context, channel_type: str, *, text=None):
|
||||||
"""
|
"""
|
||||||
Toggle an infochannel that shows the amount of online users in the server
|
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
|
||||||
|
- `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.
|
||||||
"""
|
"""
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
if enabled is None:
|
if channel_type not in self.default_channel_names.keys():
|
||||||
enabled = not await self.config.guild(guild).online_count()
|
await ctx.maybe_send_embed("Invalid channel type provided.")
|
||||||
|
return
|
||||||
|
|
||||||
await self.config.guild(guild).online_count.set(enabled)
|
if text is None:
|
||||||
await self.make_infochannel(ctx.guild)
|
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
|
||||||
|
|
||||||
if enabled:
|
await self.config.guild(guild).channel_names.set_raw(channel_type, value=text)
|
||||||
await ctx.send("InfoChannel for online user count has been enabled.")
|
await self.update_infochannel(guild, channel_type=channel_type)
|
||||||
else:
|
if not await ctx.tick():
|
||||||
await ctx.send("InfoChannel for online user count has been disabled.")
|
await ctx.maybe_send_embed("Done!")
|
||||||
|
|
||||||
async def make_infochannel(self, guild: discord.Guild):
|
@infochannelset.command(name="rolename")
|
||||||
botcount = await self.config.guild(guild).bot_count()
|
async def _infochannelset_rolename(
|
||||||
onlinecount = await self.config.guild(guild).online_count()
|
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")
|
||||||
|
|
||||||
|
# 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")
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
async def make_infochannel(self, guild: discord.Guild, channel_type=None, channel_role=None):
|
||||||
overwrites = {
|
overwrites = {
|
||||||
guild.default_role: discord.PermissionOverwrite(connect=False),
|
guild.default_role: discord.PermissionOverwrite(connect=False),
|
||||||
guild.me: discord.PermissionOverwrite(manage_channels=True, connect=True),
|
guild.me: discord.PermissionOverwrite(manage_channels=True, connect=True),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Remove the old info channel first
|
# Check for and create the Infochannel category
|
||||||
channel_id = await self.config.guild(guild).channel_id()
|
category_id = await self.config.guild(guild).category_id()
|
||||||
if channel_id is not None:
|
if category_id is not None:
|
||||||
channel: discord.VoiceChannel = guild.get_channel(channel_id)
|
category: discord.CategoryChannel = guild.get_channel(category_id)
|
||||||
if channel:
|
if category is None: # Category id is invalid, probably deleted.
|
||||||
await channel.delete(reason="InfoChannel delete")
|
category_id = None
|
||||||
|
if category_id is None:
|
||||||
# Then create the new one
|
category: discord.CategoryChannel = await guild.create_category(
|
||||||
channel = await guild.create_voice_channel(
|
"Server Stats", reason="InfoChannel Category make"
|
||||||
"Total Humans:", reason="InfoChannel make", overwrites=overwrites
|
|
||||||
)
|
|
||||||
await self.config.guild(guild).channel_id.set(channel.id)
|
|
||||||
|
|
||||||
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")
|
|
||||||
|
|
||||||
# Then create the new one
|
|
||||||
botchannel = await guild.create_voice_channel(
|
|
||||||
"Bots:", reason="InfoChannel botcount", overwrites=overwrites
|
|
||||||
)
|
)
|
||||||
await self.config.guild(guild).botchannel_id.set(botchannel.id)
|
await self.config.guild(guild).category_id.set(category.id)
|
||||||
if onlinecount:
|
await category.edit(position=0)
|
||||||
# Remove the old online channel first
|
category_id = category.id
|
||||||
onlinechannel_id = await self.config.guild(guild).onlinechannel_id()
|
|
||||||
if channel_id is not None:
|
|
||||||
onlinechannel: discord.VoiceChannel = guild.get_channel(onlinechannel_id)
|
|
||||||
if onlinechannel:
|
|
||||||
await onlinechannel.delete(reason="InfoChannel delete")
|
|
||||||
|
|
||||||
# Then create the new one
|
category: discord.CategoryChannel = guild.get_channel(category_id)
|
||||||
onlinechannel = await guild.create_voice_channel(
|
|
||||||
"Online:", reason="InfoChannel onlinecount", overwrites=overwrites
|
channel_data = await get_channel_counts(category, guild)
|
||||||
|
|
||||||
|
# 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]
|
||||||
)
|
)
|
||||||
await self.config.guild(guild).onlinechannel_id.set(onlinechannel.id)
|
return
|
||||||
|
if channel_role is not None:
|
||||||
|
await self.create_role_channel(guild, category, overwrites, channel_role)
|
||||||
|
return
|
||||||
|
|
||||||
await self.update_infochannel(guild)
|
# 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]
|
||||||
|
)
|
||||||
|
|
||||||
|
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):
|
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()
|
||||||
|
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)
|
||||||
|
|
||||||
|
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,
|
||||||
|
bots=True,
|
||||||
|
roles=True,
|
||||||
|
channels=True,
|
||||||
|
online=True,
|
||||||
|
offline=True,
|
||||||
|
extra_roles=set(guild.roles),
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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)
|
||||||
guild_data = await self.config.guild(guild).all()
|
guild_data = await self.config.guild(guild).all()
|
||||||
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()
|
to_update = (
|
||||||
|
kwargs.keys() & guild_data["enabled_channels"].keys()
|
||||||
|
) # Value in kwargs doesn't matter
|
||||||
|
|
||||||
async def update_infochannel(self, guild: discord.Guild):
|
log.debug(f"{to_update=}")
|
||||||
guild_data = await self.config.guild(guild).all()
|
|
||||||
botcount = guild_data["bot_count"]
|
|
||||||
onlinecount = guild_data["online_count"]
|
|
||||||
|
|
||||||
# Gets count of bots
|
if to_update or extra_roles:
|
||||||
# bots = lambda x: x.bot
|
category = guild.get_channel(guild_data["category_id"])
|
||||||
# def bots(x): return x.bot
|
if category is None:
|
||||||
|
return # Nothing to update, must be off
|
||||||
|
|
||||||
bot_num = len([m for m in guild.members if m.bot])
|
channel_data = await get_channel_counts(category, guild)
|
||||||
# bot_msg = f"Bots: {num}"
|
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]
|
||||||
|
)
|
||||||
|
|
||||||
# Gets count of online users
|
@Cog.listener(name="on_member_join")
|
||||||
members = guild.member_count
|
@Cog.listener(name="on_member_remove")
|
||||||
offline = len(list(filter(lambda m: m.status is discord.Status.offline, guild.members)))
|
async def on_member_join_remove(self, member: discord.Member):
|
||||||
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_join(self, member: discord.Member):
|
|
||||||
if await self.bot.cog_disabled_in_guild(self, member.guild):
|
if await self.bot.cog_disabled_in_guild(self, member.guild):
|
||||||
return
|
return
|
||||||
await self.update_infochannel_with_cooldown(member.guild)
|
|
||||||
|
|
||||||
@Cog.listener()
|
if member.bot:
|
||||||
async def on_member_remove(self, member: discord.Member):
|
await self.trigger_updates_for(
|
||||||
if await self.bot.cog_disabled_in_guild(self, member.guild):
|
member.guild, members=True, bots=True, online=True, offline=True
|
||||||
return
|
)
|
||||||
await self.update_infochannel_with_cooldown(member.guild)
|
else:
|
||||||
|
await self.trigger_updates_for(
|
||||||
|
member.guild, members=True, humans=True, online=True, offline=True
|
||||||
|
)
|
||||||
|
|
||||||
@Cog.listener()
|
@Cog.listener()
|
||||||
async def on_member_update(self, before: discord.Member, after: discord.Member):
|
async def on_member_update(self, before: discord.Member, after: discord.Member):
|
||||||
if await self.bot.cog_disabled_in_guild(self, after.guild):
|
if await self.bot.cog_disabled_in_guild(self, after.guild):
|
||||||
return
|
return
|
||||||
onlinecount = await self.config.guild(after.guild).online_count()
|
|
||||||
if onlinecount:
|
if before.status != after.status:
|
||||||
if before.status != after.status:
|
return await self.trigger_updates_for(after.guild, online=True, offline=True)
|
||||||
await self.update_infochannel_with_cooldown(after.guild)
|
|
||||||
|
# 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):
|
||||||
|
return
|
||||||
|
await self.trigger_updates_for(channel.guild, channels=True)
|
||||||
|
|
||||||
|
@Cog.listener()
|
||||||
|
async def on_guild_role_create(self, role):
|
||||||
|
if await self.bot.cog_disabled_in_guild(self, role.guild):
|
||||||
|
return
|
||||||
|
await self.trigger_updates_for(role.guild, roles=True)
|
||||||
|
|
||||||
|
@Cog.listener()
|
||||||
|
async def on_guild_role_delete(self, role):
|
||||||
|
if await self.bot.cog_disabled_in_guild(self, role.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()
|
||||||
|
@ -10,9 +10,9 @@ log = logging.getLogger("red.fox_v3.isitdown")
|
|||||||
|
|
||||||
class IsItDown(commands.Cog):
|
class IsItDown(commands.Cog):
|
||||||
"""
|
"""
|
||||||
Cog Description
|
Cog for checking whether a website is down or not.
|
||||||
|
|
||||||
Less important information about the cog
|
Uses the `isitdown.site` API
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, bot: Red):
|
def __init__(self, bot: Red):
|
||||||
@ -36,23 +36,25 @@ class IsItDown(commands.Cog):
|
|||||||
Alias: iid
|
Alias: iid
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
resp = await self._check_if_down(url_to_check)
|
resp, url = await self._check_if_down(url_to_check)
|
||||||
except AssertionError:
|
except AssertionError:
|
||||||
await ctx.maybe_send_embed("Invalid URL provided. Make sure not to include `http://`")
|
await ctx.maybe_send_embed("Invalid URL provided. Make sure not to include `http://`")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# log.debug(resp)
|
||||||
if resp["isitdown"]:
|
if resp["isitdown"]:
|
||||||
await ctx.maybe_send_embed(f"{url_to_check} is DOWN!")
|
await ctx.maybe_send_embed(f"{url} is DOWN!")
|
||||||
else:
|
else:
|
||||||
await ctx.maybe_send_embed(f"{url_to_check} is UP!")
|
await ctx.maybe_send_embed(f"{url} is UP!")
|
||||||
|
|
||||||
async def _check_if_down(self, url_to_check):
|
async def _check_if_down(self, url_to_check):
|
||||||
url = re.compile(r"https?://(www\.)?")
|
re_compiled = re.compile(r"https?://(www\.)?")
|
||||||
url.sub("", url_to_check).strip().strip("/")
|
url = re_compiled.sub("", url_to_check).strip().strip("/")
|
||||||
|
|
||||||
url = f"https://isitdown.site/api/v3/{url}"
|
url = f"https://isitdown.site/api/v3/{url}"
|
||||||
|
# log.debug(url)
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get(url) as response:
|
async with session.get(url) as response:
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
resp = await response.json()
|
resp = await response.json()
|
||||||
return resp
|
return resp, url
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
"install_msg": "Thank you for installing LaunchLib. Get started with `[p]load launchlib`, then `[p]help LaunchLib`",
|
"install_msg": "Thank you for installing LaunchLib. Get started with `[p]load launchlib`, then `[p]help LaunchLib`",
|
||||||
"short": "Access launch data for space flights",
|
"short": "Access launch data for space flights",
|
||||||
"end_user_data_statement": "This cog does not store any End User Data",
|
"end_user_data_statement": "This cog does not store any End User Data",
|
||||||
"requirements": ["python-launch-library>=1.0.6"],
|
"requirements": ["python-launch-library>=2.0.3"],
|
||||||
"tags": [
|
"tags": [
|
||||||
"bobloy",
|
"bobloy",
|
||||||
"utils",
|
"utils",
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import functools
|
import functools
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
import discord
|
import discord
|
||||||
import launchlibrary as ll
|
import launchlibrary as ll
|
||||||
from redbot.core import Config, commands
|
from redbot.core import Config, commands
|
||||||
@ -14,9 +14,7 @@ log = logging.getLogger("red.fox_v3.launchlib")
|
|||||||
|
|
||||||
class LaunchLib(commands.Cog):
|
class LaunchLib(commands.Cog):
|
||||||
"""
|
"""
|
||||||
Cog Description
|
Cog using `thespacedevs` API to get details about rocket launches
|
||||||
|
|
||||||
Less important information about the cog
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, bot: Red):
|
def __init__(self, bot: Red):
|
||||||
@ -37,27 +35,30 @@ class LaunchLib(commands.Cog):
|
|||||||
return
|
return
|
||||||
|
|
||||||
async def _embed_launch_data(self, launch: ll.AsyncLaunch):
|
async def _embed_launch_data(self, launch: ll.AsyncLaunch):
|
||||||
status: ll.AsyncLaunchStatus = await launch.get_status()
|
|
||||||
|
# status: ll.AsyncLaunchStatus = await launch.get_status()
|
||||||
|
status = launch.status
|
||||||
|
|
||||||
rocket: ll.AsyncRocket = launch.rocket
|
rocket: ll.AsyncRocket = launch.rocket
|
||||||
|
|
||||||
title = launch.name
|
title = launch.name
|
||||||
description = status.description
|
description = status["name"]
|
||||||
|
|
||||||
urls = launch.vid_urls + launch.info_urls
|
urls = launch.vid_urls + launch.info_urls
|
||||||
if not urls and rocket:
|
if rocket:
|
||||||
urls = rocket.info_urls + [rocket.wiki_url]
|
urls += [rocket.info_url, rocket.wiki_url]
|
||||||
if urls:
|
if launch.pad:
|
||||||
url = urls[0]
|
urls += [launch.pad.info_url, launch.pad.wiki_url]
|
||||||
else:
|
|
||||||
url = None
|
|
||||||
|
|
||||||
color = discord.Color.green() if status.id in [1, 3] else discord.Color.red()
|
url = next((url for url in urls if urls is not None), None) if urls else None
|
||||||
|
color = discord.Color.green() if status["id"] in [1, 3] else discord.Color.red()
|
||||||
|
|
||||||
em = discord.Embed(title=title, description=description, url=url, color=color)
|
em = discord.Embed(title=title, description=description, url=url, color=color)
|
||||||
|
|
||||||
if rocket and rocket.image_url and rocket.image_url != "Array":
|
if rocket and rocket.image_url and rocket.image_url != "Array":
|
||||||
em.set_image(url=rocket.image_url)
|
em.set_image(url=rocket.image_url)
|
||||||
|
elif launch.pad and launch.pad.map_image:
|
||||||
|
em.set_image(url=launch.pad.map_image)
|
||||||
|
|
||||||
agency = getattr(launch, "agency", None)
|
agency = getattr(launch, "agency", None)
|
||||||
if agency is not None:
|
if agency is not None:
|
||||||
@ -89,6 +90,18 @@ class LaunchLib(commands.Cog):
|
|||||||
data = mission.get(f[0], None)
|
data = mission.get(f[0], None)
|
||||||
if data is not None and data:
|
if data is not None and data:
|
||||||
em.add_field(name=f[1], value=data)
|
em.add_field(name=f[1], value=data)
|
||||||
|
if launch.pad:
|
||||||
|
location_url = getattr(launch.pad, "map_url", None)
|
||||||
|
pad_name = getattr(launch.pad, "name", None)
|
||||||
|
|
||||||
|
if pad_name is not None:
|
||||||
|
if location_url is not None:
|
||||||
|
location_url = re.sub(
|
||||||
|
"[^a-zA-Z0-9/:.'+\"°?=,-]", "", location_url
|
||||||
|
) # Fix bad URLS
|
||||||
|
em.add_field(name="Launch Pad Name", value=f"[{pad_name}]({location_url})")
|
||||||
|
else:
|
||||||
|
em.add_field(name="Launch Pad Name", value=pad_name)
|
||||||
|
|
||||||
if rocket and rocket.family:
|
if rocket and rocket.family:
|
||||||
em.add_field(name="Rocket Family", value=rocket.family)
|
em.add_field(name="Rocket Family", value=rocket.family)
|
||||||
@ -101,11 +114,16 @@ class LaunchLib(commands.Cog):
|
|||||||
|
|
||||||
@commands.group()
|
@commands.group()
|
||||||
async def launchlib(self, ctx: commands.Context):
|
async def launchlib(self, ctx: commands.Context):
|
||||||
if ctx.invoked_subcommand is None:
|
"""Base command for getting launches"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@launchlib.command()
|
@launchlib.command()
|
||||||
async def next(self, ctx: commands.Context, num_launches: int = 1):
|
async def next(self, ctx: commands.Context, num_launches: int = 1):
|
||||||
|
"""
|
||||||
|
Show the next launches
|
||||||
|
|
||||||
|
Use `num_launches` to get more than one.
|
||||||
|
"""
|
||||||
# launches = await api.async_next_launches(num_launches)
|
# launches = await api.async_next_launches(num_launches)
|
||||||
# loop = asyncio.get_running_loop()
|
# loop = asyncio.get_running_loop()
|
||||||
#
|
#
|
||||||
@ -115,6 +133,8 @@ class LaunchLib(commands.Cog):
|
|||||||
#
|
#
|
||||||
launches = await self.api.async_fetch_launch(num=num_launches)
|
launches = await self.api.async_fetch_launch(num=num_launches)
|
||||||
|
|
||||||
|
# log.debug(str(launches))
|
||||||
|
|
||||||
async with ctx.typing():
|
async with ctx.typing():
|
||||||
for x, launch in enumerate(launches):
|
for x, launch in enumerate(launches):
|
||||||
if x >= num_launches:
|
if x >= num_launches:
|
||||||
|
@ -25,8 +25,7 @@ class Leaver(Cog):
|
|||||||
@checks.mod_or_permissions(administrator=True)
|
@checks.mod_or_permissions(administrator=True)
|
||||||
async def leaverset(self, ctx):
|
async def leaverset(self, ctx):
|
||||||
"""Adjust leaver settings"""
|
"""Adjust leaver settings"""
|
||||||
if ctx.invoked_subcommand is None:
|
pass
|
||||||
pass
|
|
||||||
|
|
||||||
@leaverset.command()
|
@leaverset.command()
|
||||||
async def channel(self, ctx: Context):
|
async def channel(self, ctx: Context):
|
||||||
@ -57,5 +56,3 @@ class Leaver(Cog):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
await channel.send(out)
|
await channel.send(out)
|
||||||
else:
|
|
||||||
pass
|
|
||||||
|
@ -49,7 +49,11 @@ class LoveCalculator(Cog):
|
|||||||
|
|
||||||
result_image = soup_object.find("img", class_="result__image").get("src")
|
result_image = soup_object.find("img", class_="result__image").get("src")
|
||||||
|
|
||||||
result_text = soup_object.find("div", class_="result-text").get_text()
|
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 = " ".join(result_text.split())
|
result_text = " ".join(result_text.split())
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -60,14 +64,11 @@ class LoveCalculator(Cog):
|
|||||||
else:
|
else:
|
||||||
emoji = "💔"
|
emoji = "💔"
|
||||||
title = f"Dr. Love says that the love percentage for {x} and {y} is: {emoji} {description} {emoji}"
|
title = f"Dr. Love says that the love percentage for {x} and {y} is: {emoji} {description} {emoji}"
|
||||||
except:
|
except (TypeError, ValueError):
|
||||||
title = "Dr. Love has left a note for you."
|
title = "Dr. Love has left a note for you."
|
||||||
|
|
||||||
em = discord.Embed(
|
em = discord.Embed(
|
||||||
title=title,
|
title=title, description=result_text, color=discord.Color.red(), url=url
|
||||||
description=result_text,
|
|
||||||
color=discord.Color.red(),
|
|
||||||
url=f"https://www.lovecalculator.com/{result_image}",
|
|
||||||
)
|
)
|
||||||
|
em.set_image(url=f"https://www.lovecalculator.com/{result_image}")
|
||||||
await ctx.send(embed=em)
|
await ctx.send(embed=em)
|
||||||
|
@ -45,14 +45,12 @@ class LastSeen(Cog):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_date_time(s):
|
def get_date_time(s):
|
||||||
d = dateutil.parser.parse(s)
|
return dateutil.parser.parse(s)
|
||||||
return d
|
|
||||||
|
|
||||||
@commands.group(aliases=["setlseen"], name="lseenset")
|
@commands.group(aliases=["setlseen"], name="lseenset")
|
||||||
async def lset(self, ctx: commands.Context):
|
async def lset(self, ctx: commands.Context):
|
||||||
"""Change settings for lseen"""
|
"""Change settings for lseen"""
|
||||||
if ctx.invoked_subcommand is None:
|
pass
|
||||||
pass
|
|
||||||
|
|
||||||
@lset.command(name="toggle")
|
@lset.command(name="toggle")
|
||||||
async def lset_toggle(self, ctx: commands.Context):
|
async def lset_toggle(self, ctx: commands.Context):
|
||||||
@ -83,7 +81,7 @@ class LastSeen(Cog):
|
|||||||
# description="{} was last seen at this date and time".format(member.display_name),
|
# description="{} was last seen at this date and time".format(member.display_name),
|
||||||
# timestamp=last_seen)
|
# timestamp=last_seen)
|
||||||
|
|
||||||
embed = discord.Embed(timestamp=last_seen)
|
embed = discord.Embed(timestamp=last_seen, color=await self.bot.get_embed_color(ctx))
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
@commands.Cog.listener()
|
@commands.Cog.listener()
|
||||||
|
@ -8,9 +8,7 @@ from redbot.core.data_manager import cog_data_path
|
|||||||
|
|
||||||
|
|
||||||
class Nudity(commands.Cog):
|
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):
|
def __init__(self, bot: Red):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
@ -111,9 +111,8 @@ async def _withdraw_points(gardener: Gardener, amount):
|
|||||||
|
|
||||||
if (gardener.points - amount) < 0:
|
if (gardener.points - amount) < 0:
|
||||||
return False
|
return False
|
||||||
else:
|
gardener.points -= amount
|
||||||
gardener.points -= amount
|
return True
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class PlantTycoon(commands.Cog):
|
class PlantTycoon(commands.Cog):
|
||||||
@ -245,11 +244,9 @@ class PlantTycoon(commands.Cog):
|
|||||||
await self._load_plants_products()
|
await self._load_plants_products()
|
||||||
|
|
||||||
modifiers = sum(
|
modifiers = sum(
|
||||||
[
|
self.products[product]["modifier"]
|
||||||
self.products[product]["modifier"]
|
for product in gardener.products
|
||||||
for product in gardener.products
|
if gardener.products[product] > 0
|
||||||
if gardener.products[product] > 0
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
degradation = (
|
degradation = (
|
||||||
@ -290,38 +287,31 @@ class PlantTycoon(commands.Cog):
|
|||||||
product = product.lower()
|
product = product.lower()
|
||||||
product_category = product_category.lower()
|
product_category = product_category.lower()
|
||||||
if product in self.products and self.products[product]["category"] == product_category:
|
if product in self.products and self.products[product]["category"] == product_category:
|
||||||
if product in gardener.products:
|
if product in gardener.products and gardener.products[product] > 0:
|
||||||
if gardener.products[product] > 0:
|
gardener.current["health"] += self.products[product]["health"]
|
||||||
gardener.current["health"] += self.products[product]["health"]
|
gardener.products[product] -= 1
|
||||||
gardener.products[product] -= 1
|
if gardener.products[product] == 0:
|
||||||
if gardener.products[product] == 0:
|
del gardener.products[product.lower()]
|
||||||
del gardener.products[product.lower()]
|
if product_category == "fertilizer":
|
||||||
if product_category == "water":
|
emoji = ":poop:"
|
||||||
emoji = ":sweat_drops:"
|
elif product_category == "water":
|
||||||
elif product_category == "fertilizer":
|
emoji = ":sweat_drops:"
|
||||||
emoji = ":poop:"
|
else:
|
||||||
# elif product_category == "tool":
|
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:
|
else:
|
||||||
emoji = ":scissors:"
|
damage_msg = "You gave too much of {}.".format(product)
|
||||||
message = "Your plant got some health back! {}".format(emoji)
|
message = "{} Your plant lost some health. :wilted_rose:".format(damage_msg)
|
||||||
if gardener.current["health"] > gardener.current["threshold"]:
|
gardener.points += self.defaults["points"]["add_health"]
|
||||||
gardener.current["health"] -= self.products[product]["damage"]
|
await gardener.save_gardener()
|
||||||
if product_category == "tool":
|
elif product in gardener.products or product_category != "tool":
|
||||||
damage_msg = "You used {} too many times!".format(product)
|
message = "You have no {}. Go buy some!".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:
|
else:
|
||||||
if product_category == "tool":
|
message = "You don't have a {}. Go buy one!".format(product)
|
||||||
message = "You don't have a {}. Go buy one!".format(product)
|
|
||||||
else:
|
|
||||||
message = "You have no {}. Go buy some!".format(product)
|
|
||||||
else:
|
else:
|
||||||
message = "Are you sure you are using {}?".format(product_category)
|
message = "Are you sure you are using {}?".format(product_category)
|
||||||
|
|
||||||
@ -412,24 +402,18 @@ class PlantTycoon(commands.Cog):
|
|||||||
gardener.current = plant
|
gardener.current = plant
|
||||||
await gardener.save_gardener()
|
await gardener.save_gardener()
|
||||||
|
|
||||||
em = discord.Embed(description=message, color=discord.Color.green())
|
|
||||||
else:
|
else:
|
||||||
plant = gardener.current
|
plant = gardener.current
|
||||||
message = "You're already growing {} **{}**, silly.".format(
|
message = "You're already growing {} **{}**, silly.".format(
|
||||||
plant["article"], plant["name"]
|
plant["article"], plant["name"]
|
||||||
)
|
)
|
||||||
em = discord.Embed(description=message, color=discord.Color.green())
|
em = discord.Embed(description=message, color=discord.Color.green())
|
||||||
|
|
||||||
await ctx.send(embed=em)
|
await ctx.send(embed=em)
|
||||||
|
|
||||||
@_gardening.command(name="profile")
|
@_gardening.command(name="profile")
|
||||||
async def _profile(self, ctx: commands.Context, *, member: discord.Member = None):
|
async def _profile(self, ctx: commands.Context, *, member: discord.Member = None):
|
||||||
"""Check your gardening profile."""
|
"""Check your gardening profile."""
|
||||||
if member is not None:
|
author = member if member is not None else ctx.author
|
||||||
author = member
|
|
||||||
else:
|
|
||||||
author = ctx.author
|
|
||||||
|
|
||||||
gardener = await self._gardener(author)
|
gardener = await self._gardener(author)
|
||||||
try:
|
try:
|
||||||
await self._apply_degradation(gardener)
|
await self._apply_degradation(gardener)
|
||||||
@ -440,9 +424,7 @@ class PlantTycoon(commands.Cog):
|
|||||||
avatar = author.avatar_url if author.avatar else author.default_avatar_url
|
avatar = author.avatar_url if author.avatar else author.default_avatar_url
|
||||||
em.set_author(name="Gardening profile of {}".format(author.name), icon_url=avatar)
|
em.set_author(name="Gardening profile of {}".format(author.name), icon_url=avatar)
|
||||||
em.add_field(name="**Thneeds**", value=str(gardener.points))
|
em.add_field(name="**Thneeds**", value=str(gardener.points))
|
||||||
if not gardener.current:
|
if gardener.current:
|
||||||
em.add_field(name="**Currently growing**", value="None")
|
|
||||||
else:
|
|
||||||
em.set_thumbnail(url=gardener.current["image"])
|
em.set_thumbnail(url=gardener.current["image"])
|
||||||
em.add_field(
|
em.add_field(
|
||||||
name="**Currently growing**",
|
name="**Currently growing**",
|
||||||
@ -450,16 +432,15 @@ class PlantTycoon(commands.Cog):
|
|||||||
gardener.current["name"], gardener.current["health"]
|
gardener.current["name"], gardener.current["health"]
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
em.add_field(name="**Currently growing**", value="None")
|
||||||
if not gardener.badges:
|
if not gardener.badges:
|
||||||
em.add_field(name="**Badges**", value="None")
|
em.add_field(name="**Badges**", value="None")
|
||||||
else:
|
else:
|
||||||
badges = ""
|
badges = "".join("{}\n".format(badge.capitalize()) for badge in gardener.badges)
|
||||||
for badge in gardener.badges:
|
|
||||||
badges += "{}\n".format(badge.capitalize())
|
|
||||||
em.add_field(name="**Badges**", value=badges)
|
em.add_field(name="**Badges**", value=badges)
|
||||||
if not gardener.products:
|
if gardener.products:
|
||||||
em.add_field(name="**Products**", value="None")
|
|
||||||
else:
|
|
||||||
products = ""
|
products = ""
|
||||||
for product_name, product_data in gardener.products.items():
|
for product_name, product_data in gardener.products.items():
|
||||||
if self.products[product_name] is None:
|
if self.products[product_name] is None:
|
||||||
@ -470,6 +451,8 @@ class PlantTycoon(commands.Cog):
|
|||||||
self.products[product_name]["modifier"],
|
self.products[product_name]["modifier"],
|
||||||
)
|
)
|
||||||
em.add_field(name="**Products**", value=products)
|
em.add_field(name="**Products**", value=products)
|
||||||
|
else:
|
||||||
|
em.add_field(name="**Products**", value="None")
|
||||||
if gardener.current:
|
if gardener.current:
|
||||||
degradation = await self._degradation(gardener)
|
degradation = await self._degradation(gardener)
|
||||||
die_in = await _die_in(gardener, degradation)
|
die_in = await _die_in(gardener, degradation)
|
||||||
@ -600,7 +583,6 @@ class PlantTycoon(commands.Cog):
|
|||||||
self.products[pd]["category"],
|
self.products[pd]["category"],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
await ctx.send(embed=em)
|
|
||||||
else:
|
else:
|
||||||
if amount <= 0:
|
if amount <= 0:
|
||||||
message = "Invalid amount! Must be greater than 1"
|
message = "Invalid amount! Must be greater than 1"
|
||||||
@ -629,7 +611,8 @@ class PlantTycoon(commands.Cog):
|
|||||||
else:
|
else:
|
||||||
message = "I don't have this product."
|
message = "I don't have this product."
|
||||||
em = discord.Embed(description=message, color=discord.Color.green())
|
em = discord.Embed(description=message, color=discord.Color.green())
|
||||||
await ctx.send(embed=em)
|
|
||||||
|
await ctx.send(embed=em)
|
||||||
|
|
||||||
@_gardening.command(name="convert")
|
@_gardening.command(name="convert")
|
||||||
async def _convert(self, ctx: commands.Context, amount: int):
|
async def _convert(self, ctx: commands.Context, amount: int):
|
||||||
@ -663,8 +646,7 @@ class PlantTycoon(commands.Cog):
|
|||||||
else:
|
else:
|
||||||
gardener.current = {}
|
gardener.current = {}
|
||||||
message = "You successfully shovelled your plant out."
|
message = "You successfully shovelled your plant out."
|
||||||
if gardener.points < 0:
|
gardener.points = max(gardener.points, 0)
|
||||||
gardener.points = 0
|
|
||||||
await gardener.save_gardener()
|
await gardener.save_gardener()
|
||||||
|
|
||||||
em = discord.Embed(description=message, color=discord.Color.dark_grey())
|
em = discord.Embed(description=message, color=discord.Color.dark_grey())
|
||||||
@ -681,12 +663,12 @@ class PlantTycoon(commands.Cog):
|
|||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
# Couldn't DM the degradation
|
# Couldn't DM the degradation
|
||||||
await ctx.send("ERROR\nYou blocked me, didn't you?")
|
await ctx.send("ERROR\nYou blocked me, didn't you?")
|
||||||
product = "water"
|
|
||||||
product_category = "water"
|
|
||||||
if not gardener.current:
|
if not gardener.current:
|
||||||
message = "You're currently not growing a plant."
|
message = "You're currently not growing a plant."
|
||||||
await _send_message(channel, message)
|
await _send_message(channel, message)
|
||||||
else:
|
else:
|
||||||
|
product = "water"
|
||||||
|
product_category = "water"
|
||||||
await self._add_health(channel, gardener, product, product_category)
|
await self._add_health(channel, gardener, product, product_category)
|
||||||
|
|
||||||
@commands.command(name="fertilize")
|
@commands.command(name="fertilize")
|
||||||
@ -700,11 +682,11 @@ class PlantTycoon(commands.Cog):
|
|||||||
await ctx.send("ERROR\nYou blocked me, didn't you?")
|
await ctx.send("ERROR\nYou blocked me, didn't you?")
|
||||||
channel = ctx.channel
|
channel = ctx.channel
|
||||||
product = fertilizer
|
product = fertilizer
|
||||||
product_category = "fertilizer"
|
|
||||||
if not gardener.current:
|
if not gardener.current:
|
||||||
message = "You're currently not growing a plant."
|
message = "You're currently not growing a plant."
|
||||||
await _send_message(channel, message)
|
await _send_message(channel, message)
|
||||||
else:
|
else:
|
||||||
|
product_category = "fertilizer"
|
||||||
await self._add_health(channel, gardener, product, product_category)
|
await self._add_health(channel, gardener, product, product_category)
|
||||||
|
|
||||||
@commands.command(name="prune")
|
@commands.command(name="prune")
|
||||||
@ -717,12 +699,12 @@ class PlantTycoon(commands.Cog):
|
|||||||
# Couldn't DM the degradation
|
# Couldn't DM the degradation
|
||||||
await ctx.send("ERROR\nYou blocked me, didn't you?")
|
await ctx.send("ERROR\nYou blocked me, didn't you?")
|
||||||
channel = ctx.channel
|
channel = ctx.channel
|
||||||
product = "pruner"
|
|
||||||
product_category = "tool"
|
|
||||||
if not gardener.current:
|
if not gardener.current:
|
||||||
message = "You're currently not growing a plant."
|
message = "You're currently not growing a plant."
|
||||||
await _send_message(channel, message)
|
await _send_message(channel, message)
|
||||||
else:
|
else:
|
||||||
|
product = "pruner"
|
||||||
|
product_category = "tool"
|
||||||
await self._add_health(channel, gardener, product, product_category)
|
await self._add_health(channel, gardener, product, product_category)
|
||||||
|
|
||||||
# async def check_degradation(self):
|
# async def check_degradation(self):
|
||||||
@ -793,7 +775,7 @@ class PlantTycoon(commands.Cog):
|
|||||||
pass
|
pass
|
||||||
await asyncio.sleep(self.defaults["timers"]["notification"] * 60)
|
await asyncio.sleep(self.defaults["timers"]["notification"] * 60)
|
||||||
|
|
||||||
def __unload(self):
|
def cog_unload(self):
|
||||||
self.completion_task.cancel()
|
self.completion_task.cancel()
|
||||||
# self.degradation_task.cancel()
|
# self.degradation_task.cancel()
|
||||||
self.notification_task.cancel()
|
self.notification_task.cancel()
|
||||||
|
@ -67,8 +67,10 @@ class QRInvite(Cog):
|
|||||||
|
|
||||||
extension = pathlib.Path(image_url).parts[-1].replace(".", "?").split("?")[1]
|
extension = pathlib.Path(image_url).parts[-1].replace(".", "?").split("?")[1]
|
||||||
|
|
||||||
|
save_as_name = f"{ctx.guild.id}-{ctx.author.id}"
|
||||||
|
|
||||||
path: pathlib.Path = cog_data_path(self)
|
path: pathlib.Path = cog_data_path(self)
|
||||||
image_path = path / (ctx.guild.icon + "." + extension)
|
image_path = path / f"{save_as_name}.{extension}"
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get(image_url) as response:
|
async with session.get(image_url) as response:
|
||||||
image = await response.read()
|
image = await response.read()
|
||||||
@ -77,27 +79,29 @@ class QRInvite(Cog):
|
|||||||
file.write(image)
|
file.write(image)
|
||||||
|
|
||||||
if extension == "webp":
|
if extension == "webp":
|
||||||
new_path = convert_webp_to_png(str(image_path))
|
new_image_path = convert_webp_to_png(str(image_path))
|
||||||
elif extension == "gif":
|
elif extension == "gif":
|
||||||
await ctx.maybe_send_embed("gif is not supported yet, stay tuned")
|
await ctx.maybe_send_embed("gif is not supported yet, stay tuned")
|
||||||
return
|
return
|
||||||
elif extension == "png":
|
elif extension == "png":
|
||||||
new_path = str(image_path)
|
new_image_path = str(image_path)
|
||||||
|
elif extension == "jpg":
|
||||||
|
new_image_path = convert_jpg_to_png(str(image_path))
|
||||||
else:
|
else:
|
||||||
await ctx.maybe_send_embed(f"{extension} is not supported yet, stay tuned")
|
await ctx.maybe_send_embed(f"{extension} is not supported yet, stay tuned")
|
||||||
return
|
return
|
||||||
|
|
||||||
myqr.run(
|
myqr.run(
|
||||||
invite,
|
invite,
|
||||||
picture=new_path,
|
picture=new_image_path,
|
||||||
save_name=ctx.guild.icon + "_qrcode.png",
|
save_name=f"{save_as_name}_qrcode.png",
|
||||||
save_dir=str(cog_data_path(self)),
|
save_dir=str(cog_data_path(self)),
|
||||||
colorized=colorized,
|
colorized=colorized,
|
||||||
)
|
)
|
||||||
|
|
||||||
png_path: pathlib.Path = path / (ctx.guild.icon + "_qrcode.png")
|
png_path: pathlib.Path = path / f"{save_as_name}_qrcode.png"
|
||||||
with png_path.open("rb") as png_fp:
|
# with png_path.open("rb") as png_fp:
|
||||||
await ctx.send(file=discord.File(png_fp.read(), "qrcode.png"))
|
await ctx.send(file=discord.File(png_path, "qrcode.png"))
|
||||||
|
|
||||||
|
|
||||||
def convert_webp_to_png(path):
|
def convert_webp_to_png(path):
|
||||||
@ -110,3 +114,10 @@ def convert_webp_to_png(path):
|
|||||||
new_path = path.replace(".webp", ".png")
|
new_path = path.replace(".webp", ".png")
|
||||||
im.save(new_path, transparency=255)
|
im.save(new_path, transparency=255)
|
||||||
return new_path
|
return new_path
|
||||||
|
|
||||||
|
|
||||||
|
def convert_jpg_to_png(path):
|
||||||
|
im = Image.open(path)
|
||||||
|
new_path = path.replace(".jpg", ".png")
|
||||||
|
im.save(new_path)
|
||||||
|
return new_path
|
||||||
|
@ -97,9 +97,7 @@ class ReactRestrict(Cog):
|
|||||||
"""
|
"""
|
||||||
current_combos = await self.combo_list()
|
current_combos = await self.combo_list()
|
||||||
|
|
||||||
to_keep = [
|
to_keep = [c for c in current_combos if c.message_id != message_id or c.role_id != role.id]
|
||||||
c for c in current_combos if not (c.message_id == message_id and c.role_id == role.id)
|
|
||||||
]
|
|
||||||
|
|
||||||
if to_keep != current_combos:
|
if to_keep != current_combos:
|
||||||
await self.set_combo_list(to_keep)
|
await self.set_combo_list(to_keep)
|
||||||
@ -210,8 +208,7 @@ class ReactRestrict(Cog):
|
|||||||
"""
|
"""
|
||||||
Base command for this cog. Check help for the commands list.
|
Base command for this cog. Check help for the commands list.
|
||||||
"""
|
"""
|
||||||
if ctx.invoked_subcommand is None:
|
pass
|
||||||
pass
|
|
||||||
|
|
||||||
@reactrestrict.command()
|
@reactrestrict.command()
|
||||||
async def add(self, ctx: commands.Context, message_id: int, *, role: discord.Role):
|
async def add(self, ctx: commands.Context, message_id: int, *, role: discord.Role):
|
||||||
|
@ -32,6 +32,7 @@ class RecyclingPlant(Cog):
|
|||||||
|
|
||||||
x = 0
|
x = 0
|
||||||
reward = 0
|
reward = 0
|
||||||
|
timeoutcount = 0
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
"{0} has signed up for a shift at the Recycling Plant! Type ``exit`` to terminate it early.".format(
|
"{0} has signed up for a shift at the Recycling Plant! Type ``exit`` to terminate it early.".format(
|
||||||
ctx.author.display_name
|
ctx.author.display_name
|
||||||
@ -53,14 +54,25 @@ class RecyclingPlant(Cog):
|
|||||||
return m.author == ctx.author and m.channel == ctx.channel
|
return m.author == ctx.author and m.channel == ctx.channel
|
||||||
|
|
||||||
try:
|
try:
|
||||||
answer = await self.bot.wait_for("message", timeout=120, check=check)
|
answer = await self.bot.wait_for("message", timeout=20, check=check)
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
answer = None
|
answer = None
|
||||||
|
|
||||||
if answer is None:
|
if answer is None:
|
||||||
await ctx.send(
|
if timeoutcount == 2:
|
||||||
"``{}`` fell down the conveyor belt to be sorted again!".format(used["object"])
|
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
|
||||||
elif answer.content.lower().strip() == used["action"]:
|
elif answer.content.lower().strip() == used["action"]:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
"Congratulations! You put ``{}`` down the correct chute! (**+50**)".format(
|
"Congratulations! You put ``{}`` down the correct chute! (**+50**)".format(
|
||||||
|
@ -69,13 +69,12 @@ class RPSLS(Cog):
|
|||||||
|
|
||||||
def get_emote(self, choice):
|
def get_emote(self, choice):
|
||||||
if choice == "rock":
|
if choice == "rock":
|
||||||
emote = ":moyai:"
|
return ":moyai:"
|
||||||
elif choice == "spock":
|
elif choice == "spock":
|
||||||
emote = ":vulcan:"
|
return ":vulcan:"
|
||||||
elif choice == "paper":
|
elif choice == "paper":
|
||||||
emote = ":page_facing_up:"
|
return ":page_facing_up:"
|
||||||
elif choice in ["scissors", "lizard"]:
|
elif choice in ["scissors", "lizard"]:
|
||||||
emote = ":{}:".format(choice)
|
return ":{}:".format(choice)
|
||||||
else:
|
else:
|
||||||
emote = None
|
return None
|
||||||
return emote
|
|
||||||
|
@ -177,7 +177,3 @@ class SCP(Cog):
|
|||||||
|
|
||||||
msg = "http://www.scp-wiki.net/log-of-unexplained-locations"
|
msg = "http://www.scp-wiki.net/log-of-unexplained-locations"
|
||||||
await ctx.maybe_send_embed(msg)
|
await ctx.maybe_send_embed(msg)
|
||||||
|
|
||||||
|
|
||||||
def setup(bot):
|
|
||||||
bot.add_cog(SCP(bot))
|
|
||||||
|
@ -6,6 +6,7 @@ import discord
|
|||||||
from redbot.core import Config, checks, commands
|
from redbot.core import Config, checks, commands
|
||||||
from redbot.core.bot import Red
|
from redbot.core.bot import Red
|
||||||
from redbot.core.commands import Cog
|
from redbot.core.commands import Cog
|
||||||
|
from redbot.core.utils.chat_formatting import pagify
|
||||||
|
|
||||||
log = logging.getLogger("red.fox_v3.stealemoji")
|
log = logging.getLogger("red.fox_v3.stealemoji")
|
||||||
# Replaced with discord.Asset.read()
|
# Replaced with discord.Asset.read()
|
||||||
@ -16,16 +17,16 @@ log = logging.getLogger("red.fox_v3.stealemoji")
|
|||||||
|
|
||||||
|
|
||||||
async def check_guild(guild, emoji):
|
async def check_guild(guild, emoji):
|
||||||
if len(guild.emojis) >= 100:
|
if len(guild.emojis) >= 2 * guild.emoji_limit:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if len(guild.emojis) < 50:
|
if len(guild.emojis) < guild.emoji_limit:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if emoji.animated:
|
if emoji.animated:
|
||||||
return sum(e.animated for e in guild.emojis) < 50
|
return sum(e.animated for e in guild.emojis) < guild.emoji_limit
|
||||||
else:
|
else:
|
||||||
return sum(not e.animated for e in guild.emojis) < 50
|
return sum(not e.animated for e in guild.emojis) < guild.emoji_limit
|
||||||
|
|
||||||
|
|
||||||
class StealEmoji(Cog):
|
class StealEmoji(Cog):
|
||||||
@ -50,6 +51,7 @@ class StealEmoji(Cog):
|
|||||||
default_global = {
|
default_global = {
|
||||||
"stolemoji": {},
|
"stolemoji": {},
|
||||||
"guildbanks": [],
|
"guildbanks": [],
|
||||||
|
"autobanked_guilds": [],
|
||||||
"on": False,
|
"on": False,
|
||||||
"notify": 0,
|
"notify": 0,
|
||||||
"autobank": False,
|
"autobank": False,
|
||||||
@ -68,8 +70,7 @@ class StealEmoji(Cog):
|
|||||||
"""
|
"""
|
||||||
Base command for this cog. Check help for the commands list.
|
Base command for this cog. Check help for the commands list.
|
||||||
"""
|
"""
|
||||||
if ctx.invoked_subcommand is None:
|
pass
|
||||||
pass
|
|
||||||
|
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
@stealemoji.command(name="clearemojis")
|
@stealemoji.command(name="clearemojis")
|
||||||
@ -99,7 +100,8 @@ class StealEmoji(Cog):
|
|||||||
await ctx.maybe_send_embed("No stolen emojis yet")
|
await ctx.maybe_send_embed("No stolen emojis yet")
|
||||||
return
|
return
|
||||||
|
|
||||||
await ctx.maybe_send_embed(emoj)
|
for page in pagify(emoj, delims=[" "]):
|
||||||
|
await ctx.maybe_send_embed(page)
|
||||||
|
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
@stealemoji.command(name="notify")
|
@stealemoji.command(name="notify")
|
||||||
@ -145,11 +147,54 @@ class StealEmoji(Cog):
|
|||||||
|
|
||||||
await ctx.maybe_send_embed("AutoBanking is now " + str(not curr_setting))
|
await ctx.maybe_send_embed("AutoBanking is now " + str(not curr_setting))
|
||||||
|
|
||||||
|
@checks.is_owner()
|
||||||
|
@commands.guild_only()
|
||||||
|
@stealemoji.command(name="deleteserver", aliases=["deleteguild"])
|
||||||
|
async def se_deleteserver(self, ctx: commands.Context, guild_id=None):
|
||||||
|
"""Delete servers the bot is the owner of.
|
||||||
|
|
||||||
|
Useful for auto-generated guildbanks."""
|
||||||
|
if guild_id is None:
|
||||||
|
guild = ctx.guild
|
||||||
|
else:
|
||||||
|
guild = await self.bot.get_guild(guild_id)
|
||||||
|
|
||||||
|
if guild is None:
|
||||||
|
await ctx.maybe_send_embed("Failed to get guild, cancelling")
|
||||||
|
return
|
||||||
|
guild: discord.Guild
|
||||||
|
await ctx.maybe_send_embed(
|
||||||
|
f"Will attempt to delete {guild.name} ({guild.id})\n" f"Okay to continue? (yes/no)"
|
||||||
|
)
|
||||||
|
|
||||||
|
def check(m):
|
||||||
|
return m.author == ctx.author and m.channel == ctx.channel
|
||||||
|
|
||||||
|
try:
|
||||||
|
answer = await self.bot.wait_for("message", timeout=120, check=check)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
await ctx.send("Timed out, canceling")
|
||||||
|
return
|
||||||
|
|
||||||
|
if answer.content.upper() not in ["Y", "YES"]:
|
||||||
|
await ctx.maybe_send_embed("Cancelling")
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
await guild.delete()
|
||||||
|
except discord.Forbidden:
|
||||||
|
log.exception("No permission to delete. I'm probably not the guild owner")
|
||||||
|
await ctx.maybe_send_embed("No permission to delete. I'm probably not the guild owner")
|
||||||
|
except discord.HTTPException:
|
||||||
|
log.exception("Unexpected error when deleting guild")
|
||||||
|
await ctx.maybe_send_embed("Unexpected error when deleting guild")
|
||||||
|
else:
|
||||||
|
await self.bot.send_to_owners(f"Guild {guild.name} deleted")
|
||||||
|
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@stealemoji.command(name="bank")
|
@stealemoji.command(name="bank")
|
||||||
async def se_bank(self, ctx):
|
async def se_bank(self, ctx):
|
||||||
"""Add current server as emoji bank"""
|
"""Add or remove current server as emoji bank"""
|
||||||
|
|
||||||
def check(m):
|
def check(m):
|
||||||
return (
|
return (
|
||||||
@ -224,34 +269,36 @@ class StealEmoji(Cog):
|
|||||||
break
|
break
|
||||||
|
|
||||||
if guildbank is None:
|
if guildbank is None:
|
||||||
if await self.config.autobank():
|
if not await self.config.autobank():
|
||||||
try:
|
|
||||||
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
|
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")
|
||||||
|
return
|
||||||
|
async with self.config.guildbanks() as guildbanks:
|
||||||
|
guildbanks.append(guildbank.id)
|
||||||
|
# Track generated guilds for easier deletion
|
||||||
|
async with self.config.autobanked_guilds() as autobanked_guilds:
|
||||||
|
autobanked_guilds.append(guildbank.id)
|
||||||
|
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
|
||||||
|
if guildbank.text_channels:
|
||||||
|
channel = guildbank.text_channels[0]
|
||||||
|
else:
|
||||||
|
# Always hits the else.
|
||||||
|
# Maybe create_guild doesn't return guild object with
|
||||||
|
# the template channel?
|
||||||
|
channel = await guildbank.create_text_channel("invite-channel")
|
||||||
|
invite = await channel.create_invite()
|
||||||
|
|
||||||
|
await self.bot.send_to_owners(invite)
|
||||||
|
log.info(f"Guild created id {guildbank.id}. Invite: {invite}")
|
||||||
# Next, have I saved this emoji before (because uploaded emoji != orignal emoji)
|
# Next, have I saved this emoji before (because uploaded emoji != orignal emoji)
|
||||||
|
|
||||||
if str(emoji.id) in await self.config.stolemoji():
|
if str(emoji.id) in await self.config.stolemoji():
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from redbot.core import Config, checks, commands
|
from redbot.core import Config, checks, commands
|
||||||
@ -19,6 +20,15 @@ async def sleep_till_next_hour():
|
|||||||
await asyncio.sleep((next_hour - datetime.utcnow()).seconds)
|
await asyncio.sleep((next_hour - datetime.utcnow()).seconds)
|
||||||
|
|
||||||
|
|
||||||
|
async def announce_to_channel(channel, results, title):
|
||||||
|
if channel is not None and results:
|
||||||
|
await channel.send(title)
|
||||||
|
for page in pagify(results, shorten_by=50):
|
||||||
|
await channel.send(page)
|
||||||
|
elif results: # Channel is None, log the results
|
||||||
|
log.info(results)
|
||||||
|
|
||||||
|
|
||||||
class Timerole(Cog):
|
class Timerole(Cog):
|
||||||
"""Add roles to users based on time on server"""
|
"""Add roles to users based on time on server"""
|
||||||
|
|
||||||
@ -27,10 +37,15 @@ class Timerole(Cog):
|
|||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.config = Config.get_conf(self, identifier=9811198108111121, force_registration=True)
|
self.config = Config.get_conf(self, identifier=9811198108111121, force_registration=True)
|
||||||
default_global = {}
|
default_global = {}
|
||||||
default_guild = {"announce": None, "roles": {}}
|
default_guild = {"announce": None, "reapply": True, "roles": {}, "skipbots": True}
|
||||||
|
default_rolemember = {"had_role": False, "check_again_time": None}
|
||||||
|
|
||||||
self.config.register_global(**default_global)
|
self.config.register_global(**default_global)
|
||||||
self.config.register_guild(**default_guild)
|
self.config.register_guild(**default_guild)
|
||||||
|
|
||||||
|
self.config.init_custom("RoleMember", 2)
|
||||||
|
self.config.register_custom("RoleMember", **default_rolemember)
|
||||||
|
|
||||||
self.updating = asyncio.create_task(self.check_hour())
|
self.updating = asyncio.create_task(self.check_hour())
|
||||||
|
|
||||||
async def red_delete_data_for_user(self, **kwargs):
|
async def red_delete_data_for_user(self, **kwargs):
|
||||||
@ -49,18 +64,20 @@ class Timerole(Cog):
|
|||||||
|
|
||||||
Useful for troubleshooting the initial setup
|
Useful for troubleshooting the initial setup
|
||||||
"""
|
"""
|
||||||
|
|
||||||
async with ctx.typing():
|
async with ctx.typing():
|
||||||
|
pre_run = datetime.utcnow()
|
||||||
await self.timerole_update()
|
await self.timerole_update()
|
||||||
|
after_run = datetime.utcnow()
|
||||||
await ctx.tick()
|
await ctx.tick()
|
||||||
|
|
||||||
|
await ctx.maybe_send_embed(f"Took {after_run-pre_run} seconds")
|
||||||
|
|
||||||
@commands.group()
|
@commands.group()
|
||||||
@checks.mod_or_permissions(administrator=True)
|
@checks.mod_or_permissions(administrator=True)
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def timerole(self, ctx):
|
async def timerole(self, ctx):
|
||||||
"""Adjust timerole settings"""
|
"""Adjust timerole settings"""
|
||||||
if ctx.invoked_subcommand is None:
|
pass
|
||||||
pass
|
|
||||||
|
|
||||||
@timerole.command()
|
@timerole.command()
|
||||||
async def addrole(
|
async def addrole(
|
||||||
@ -75,6 +92,9 @@ class Timerole(Cog):
|
|||||||
await ctx.maybe_send_embed("Error: Invalid time string.")
|
await ctx.maybe_send_embed("Error: Invalid time string.")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if parsed_time is None:
|
||||||
|
return await ctx.maybe_send_embed("Error: Invalid time string.")
|
||||||
|
|
||||||
days = parsed_time.days
|
days = parsed_time.days
|
||||||
hours = parsed_time.seconds // 60 // 60
|
hours = parsed_time.seconds // 60 // 60
|
||||||
|
|
||||||
@ -84,9 +104,7 @@ class Timerole(Cog):
|
|||||||
|
|
||||||
await self.config.guild(guild).roles.set_raw(role.id, value=to_set)
|
await self.config.guild(guild).roles.set_raw(role.id, value=to_set)
|
||||||
await ctx.maybe_send_embed(
|
await ctx.maybe_send_embed(
|
||||||
"Time Role for {0} set to {1} days and {2} hours until added".format(
|
f"Time Role for {role.name} set to {days} days and {hours} hours until added"
|
||||||
role.name, days, hours
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@timerole.command()
|
@timerole.command()
|
||||||
@ -114,18 +132,35 @@ class Timerole(Cog):
|
|||||||
|
|
||||||
await self.config.guild(guild).roles.set_raw(role.id, value=to_set)
|
await self.config.guild(guild).roles.set_raw(role.id, value=to_set)
|
||||||
await ctx.maybe_send_embed(
|
await ctx.maybe_send_embed(
|
||||||
"Time Role for {0} set to {1} days and {2} hours until removed".format(
|
f"Time Role for {role.name} set to {days} days and {hours} hours until removed"
|
||||||
role.name, days, hours
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@timerole.command()
|
@timerole.command()
|
||||||
async def channel(self, ctx: commands.Context, channel: discord.TextChannel):
|
async def channel(self, ctx: commands.Context, channel: Optional[discord.TextChannel] = None):
|
||||||
"""Sets the announce channel for role adds"""
|
"""Sets the announce channel for role adds"""
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
|
if channel is None:
|
||||||
|
await self.config.guild(guild).announce.clear()
|
||||||
|
await ctx.maybe_send_embed(f"Announce channel has been cleared")
|
||||||
|
else:
|
||||||
|
await self.config.guild(guild).announce.set(channel.id)
|
||||||
|
await ctx.send(f"Announce channel set to {channel.mention}")
|
||||||
|
|
||||||
await self.config.guild(guild).announce.set(channel.id)
|
@timerole.command()
|
||||||
await ctx.send("Announce channel set to {0}".format(channel.mention))
|
async def reapply(self, ctx: commands.Context):
|
||||||
|
"""Toggle reapplying roles if the member loses it somehow. Defaults to True"""
|
||||||
|
guild = ctx.guild
|
||||||
|
current_setting = await self.config.guild(guild).reapply()
|
||||||
|
await self.config.guild(guild).reapply.set(not current_setting)
|
||||||
|
await ctx.maybe_send_embed(f"Reapplying roles is now set to: {not current_setting}")
|
||||||
|
|
||||||
|
@timerole.command()
|
||||||
|
async def skipbots(self, ctx: commands.Context):
|
||||||
|
"""Toggle skipping bots when adding/removing roles. Defaults to True"""
|
||||||
|
guild = ctx.guild
|
||||||
|
current_setting = await self.config.guild(guild).skipbots()
|
||||||
|
await self.config.guild(guild).skipbots.set(not current_setting)
|
||||||
|
await ctx.maybe_send_embed(f"Skipping bots is now set to: {not current_setting}")
|
||||||
|
|
||||||
@timerole.command()
|
@timerole.command()
|
||||||
async def delrole(self, ctx: commands.Context, role: discord.Role):
|
async def delrole(self, ctx: commands.Context, role: discord.Role):
|
||||||
@ -133,7 +168,8 @@ class Timerole(Cog):
|
|||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
|
|
||||||
await self.config.guild(guild).roles.set_raw(role.id, value=None)
|
await self.config.guild(guild).roles.set_raw(role.id, value=None)
|
||||||
await ctx.send("{0} will no longer be applied".format(role.name))
|
await self.config.custom("RoleMember", role.id).clear()
|
||||||
|
await ctx.maybe_send_embed(f"{role.name} will no longer be applied")
|
||||||
|
|
||||||
@timerole.command()
|
@timerole.command()
|
||||||
async def list(self, ctx: commands.Context):
|
async def list(self, ctx: commands.Context):
|
||||||
@ -153,89 +189,208 @@ class Timerole(Cog):
|
|||||||
str(discord.utils.get(guild.roles, id=int(new_id)))
|
str(discord.utils.get(guild.roles, id=int(new_id)))
|
||||||
for new_id in r_data["required"]
|
for new_id in r_data["required"]
|
||||||
]
|
]
|
||||||
out += "{} | {} days | requires: {}\n".format(str(role), r_data["days"], r_roles)
|
out += f"{role} | {r_data['days']} days | requires: {r_roles}\n"
|
||||||
await ctx.maybe_send_embed(out)
|
await ctx.maybe_send_embed(out)
|
||||||
|
|
||||||
async def timerole_update(self):
|
async def timerole_update(self):
|
||||||
async for guild in AsyncIter(self.bot.guilds):
|
utcnow = datetime.utcnow()
|
||||||
addlist = []
|
all_guilds = await self.config.all_guilds()
|
||||||
removelist = []
|
|
||||||
|
|
||||||
role_dict = await self.config.guild(guild).roles()
|
# all_mrs = await self.config.custom("RoleMember").all()
|
||||||
if not any(role_data for role_data in role_dict.values()): # No roles
|
|
||||||
|
# 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
|
continue
|
||||||
|
|
||||||
async for member in AsyncIter(guild.members):
|
add_results = ""
|
||||||
has_roles = [r.id for r in member.roles]
|
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}")
|
||||||
|
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
|
||||||
|
|
||||||
|
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 = [
|
add_roles = [
|
||||||
int(rID)
|
discord.utils.get(guild.roles, id=int(role_id)) for role_id in addlist
|
||||||
for rID, r_data in role_dict.items()
|
|
||||||
if r_data is not None and not r_data["remove"]
|
|
||||||
]
|
]
|
||||||
remove_roles = [
|
remove_roles = [
|
||||||
int(rID)
|
discord.utils.get(guild.roles, id=int(role_id)) for role_id in removelist
|
||||||
for rID, r_data in role_dict.items()
|
|
||||||
if r_data is not None and r_data["remove"]
|
|
||||||
]
|
]
|
||||||
|
|
||||||
check_add_roles = set(add_roles) - set(has_roles)
|
if None in add_roles or None in remove_roles:
|
||||||
check_remove_roles = set(remove_roles) & set(has_roles)
|
log.info(
|
||||||
|
f"Timerole ran into an error with the roles in: {add_roles + remove_roles}"
|
||||||
|
)
|
||||||
|
|
||||||
await self.check_required_and_date(
|
if addlist:
|
||||||
addlist, check_add_roles, has_roles, member, role_dict
|
try:
|
||||||
)
|
await member.add_roles(*add_roles, reason="Timerole", atomic=False)
|
||||||
await self.check_required_and_date(
|
except (discord.Forbidden, discord.NotFound) as e:
|
||||||
removelist, check_remove_roles, has_roles, member, role_dict
|
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
|
||||||
channel = await self.config.guild(guild).announce()
|
channel = await self.config.guild(guild).announce()
|
||||||
if channel is not None:
|
if channel is not None:
|
||||||
channel = guild.get_channel(channel)
|
channel = guild.get_channel(channel)
|
||||||
|
|
||||||
title = "**These members have received the following roles**\n"
|
if add_results:
|
||||||
await self.announce_roles(title, addlist, channel, guild, to_add=True)
|
title = "**These members have received the following roles**\n"
|
||||||
title = "**These members have lost the following roles**\n"
|
await announce_to_channel(channel, add_results, title)
|
||||||
await self.announce_roles(title, removelist, channel, guild, to_add=False)
|
if remove_results:
|
||||||
|
title = "**These members have lost the following roles**\n"
|
||||||
|
await announce_to_channel(channel, remove_results, title)
|
||||||
|
# End
|
||||||
|
|
||||||
async def announce_roles(self, title, role_list, channel, guild, to_add: True):
|
# async def announce_roles(self, title, role_list, channel, guild, to_add: True):
|
||||||
results = ""
|
# results = ""
|
||||||
async for member, role_id in AsyncIter(role_list):
|
# async for member, role_id in AsyncIter(role_list):
|
||||||
role = discord.utils.get(guild.roles, id=role_id)
|
# role = discord.utils.get(guild.roles, id=role_id)
|
||||||
try:
|
# try:
|
||||||
if to_add:
|
# if to_add:
|
||||||
await member.add_roles(role, reason="Timerole")
|
# await member.add_roles(role, reason="Timerole")
|
||||||
else:
|
# else:
|
||||||
await member.remove_roles(role, reason="Timerole")
|
# await member.remove_roles(role, reason="Timerole")
|
||||||
except (discord.Forbidden, discord.NotFound) as e:
|
# except (discord.Forbidden, discord.NotFound) as e:
|
||||||
results += "{} : {} **(Failed)**\n".format(member.display_name, role.name)
|
# results += f"{member.display_name} : {role.name} **(Failed)**\n"
|
||||||
else:
|
# else:
|
||||||
results += "{} : {}\n".format(member.display_name, role.name)
|
# results += f"{member.display_name} : {role.name}\n"
|
||||||
if channel is not None and results:
|
# if channel is not None and results:
|
||||||
await channel.send(title)
|
# await channel.send(title)
|
||||||
for page in pagify(results, shorten_by=50):
|
# for page in pagify(results, shorten_by=50):
|
||||||
await channel.send(page)
|
# await channel.send(page)
|
||||||
elif results: # Channel is None, log the results
|
# elif results: # Channel is None, log the results
|
||||||
log.info(results)
|
# log.info(results)
|
||||||
|
|
||||||
async def check_required_and_date(self, role_list, check_roles, has_roles, member, role_dict):
|
# async def check_required_and_date(self, role_list, check_roles, has_roles, member, role_dict):
|
||||||
async for role_id in AsyncIter(check_roles):
|
# async for role_id in AsyncIter(check_roles):
|
||||||
# Check for required role
|
# # Check for required role
|
||||||
if "required" in role_dict[str(role_id)]:
|
# if "required" in role_dict[str(role_id)]:
|
||||||
if not set(role_dict[str(role_id)]["required"]) & set(has_roles):
|
# if not set(role_dict[str(role_id)]["required"]) & set(has_roles):
|
||||||
# Doesn't have required role
|
# # Doesn't have required role
|
||||||
continue
|
# continue
|
||||||
|
#
|
||||||
if (
|
# if (
|
||||||
member.joined_at
|
# member.joined_at
|
||||||
+ timedelta(
|
# + timedelta(
|
||||||
days=role_dict[str(role_id)]["days"],
|
# days=role_dict[str(role_id)]["days"],
|
||||||
hours=role_dict[str(role_id)].get("hours", 0),
|
# hours=role_dict[str(role_id)].get("hours", 0),
|
||||||
)
|
# )
|
||||||
<= datetime.today()
|
# <= datetime.utcnow()
|
||||||
):
|
# ):
|
||||||
# Qualifies
|
# # Qualifies
|
||||||
role_list.append((member, role_id))
|
# role_list.append((member, role_id))
|
||||||
|
|
||||||
async def check_hour(self):
|
async def check_hour(self):
|
||||||
await sleep_till_next_hour()
|
await sleep_till_next_hour()
|
||||||
|
46
tts/tts.py
46
tts/tts.py
@ -1,11 +1,35 @@
|
|||||||
import io
|
import io
|
||||||
|
import logging
|
||||||
|
from typing import Optional, TYPE_CHECKING
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
|
from discord.ext.commands import BadArgument, Converter
|
||||||
from gtts import gTTS
|
from gtts import gTTS
|
||||||
|
from gtts.lang import _fallback_deprecated_lang, tts_langs
|
||||||
from redbot.core import Config, commands
|
from redbot.core import Config, commands
|
||||||
from redbot.core.bot import Red
|
from redbot.core.bot import Red
|
||||||
from redbot.core.commands import Cog
|
from redbot.core.commands import Cog
|
||||||
|
|
||||||
|
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):
|
class TTS(Cog):
|
||||||
"""
|
"""
|
||||||
@ -18,7 +42,7 @@ class TTS(Cog):
|
|||||||
|
|
||||||
self.config = Config.get_conf(self, identifier=9811198108111121, force_registration=True)
|
self.config = Config.get_conf(self, identifier=9811198108111121, force_registration=True)
|
||||||
default_global = {}
|
default_global = {}
|
||||||
default_guild = {}
|
default_guild = {"language": "en"}
|
||||||
|
|
||||||
self.config.register_global(**default_global)
|
self.config.register_global(**default_global)
|
||||||
self.config.register_guild(**default_guild)
|
self.config.register_guild(**default_guild)
|
||||||
@ -27,13 +51,29 @@ class TTS(Cog):
|
|||||||
"""Nothing to delete"""
|
"""Nothing to delete"""
|
||||||
return
|
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"])
|
@commands.command(aliases=["t2s", "text2"])
|
||||||
async def tts(self, ctx: commands.Context, *, text: str):
|
async def tts(
|
||||||
|
self, ctx: commands.Context, lang: Optional[ISO639Converter] = None, *, text: str
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Send Text to speech messages as an mp3
|
Send Text to speech messages as an mp3
|
||||||
"""
|
"""
|
||||||
|
if lang is None:
|
||||||
|
lang = await self.config.guild(ctx.guild).language()
|
||||||
|
|
||||||
mp3_fp = io.BytesIO()
|
mp3_fp = io.BytesIO()
|
||||||
tts = gTTS(text, lang="en")
|
tts = gTTS(text, lang=lang)
|
||||||
tts.write_to_fp(mp3_fp)
|
tts.write_to_fp(mp3_fp)
|
||||||
mp3_fp.seek(0)
|
mp3_fp.seek(0)
|
||||||
await ctx.send(file=discord.File(mp3_fp, "text.mp3"))
|
await ctx.send(file=discord.File(mp3_fp, "text.mp3"))
|
||||||
|
@ -19,8 +19,7 @@ class Unicode(Cog):
|
|||||||
@commands.group(name="unicode", pass_context=True)
|
@commands.group(name="unicode", pass_context=True)
|
||||||
async def unicode(self, ctx):
|
async def unicode(self, ctx):
|
||||||
"""Encode/Decode a Unicode character."""
|
"""Encode/Decode a Unicode character."""
|
||||||
if ctx.invoked_subcommand is None:
|
pass
|
||||||
pass
|
|
||||||
|
|
||||||
@unicode.command()
|
@unicode.command()
|
||||||
async def decode(self, ctx: commands.Context, character):
|
async def decode(self, ctx: commands.Context, character):
|
||||||
|
@ -71,6 +71,7 @@ W1, W2, W5, W6 = Random Werewolf
|
|||||||
N1 = Benign Neutral
|
N1 = Benign Neutral
|
||||||
|
|
||||||
0001-1112T11W112N2
|
0001-1112T11W112N2
|
||||||
|
which translates to
|
||||||
0,0,0,1,11,12,E1,R1,R1,R1,R2,P2
|
0,0,0,1,11,12,E1,R1,R1,R1,R2,P2
|
||||||
|
|
||||||
pre-letter = exact role position
|
pre-letter = exact role position
|
||||||
@ -89,7 +90,7 @@ async def parse_code(code, game):
|
|||||||
if len(built) < digits:
|
if len(built) < digits:
|
||||||
built += c
|
built += c
|
||||||
|
|
||||||
if built == "T" or built == "W" or built == "N":
|
if built in ["T", "W", "N"]:
|
||||||
# Random Towns
|
# Random Towns
|
||||||
category = built
|
category = built
|
||||||
built = ""
|
built = ""
|
||||||
@ -115,8 +116,6 @@ async def parse_code(code, game):
|
|||||||
options = [role for role in ROLE_LIST if 10 + idx in role.category]
|
options = [role for role in ROLE_LIST if 10 + idx in role.category]
|
||||||
elif category == "N":
|
elif category == "N":
|
||||||
options = [role for role in ROLE_LIST if 20 + idx in role.category]
|
options = [role for role in ROLE_LIST if 20 + idx in role.category]
|
||||||
pass
|
|
||||||
|
|
||||||
if not options:
|
if not options:
|
||||||
raise IndexError("No Match Found")
|
raise IndexError("No Match Found")
|
||||||
|
|
||||||
@ -129,11 +128,8 @@ async def parse_code(code, game):
|
|||||||
|
|
||||||
async def encode(role_list, rand_roles):
|
async def encode(role_list, rand_roles):
|
||||||
"""Convert role list to code"""
|
"""Convert role list to code"""
|
||||||
out_code = ""
|
|
||||||
|
|
||||||
digit_sort = sorted(role for role in role_list if role < 10)
|
digit_sort = sorted(role for role in role_list if role < 10)
|
||||||
for role in digit_sort:
|
out_code = "".join(str(role) for role in digit_sort)
|
||||||
out_code += str(role)
|
|
||||||
|
|
||||||
digit_sort = sorted(role for role in role_list if 10 <= role < 100)
|
digit_sort = sorted(role for role in role_list if 10 <= role < 100)
|
||||||
if digit_sort:
|
if digit_sort:
|
||||||
|
@ -526,9 +526,10 @@ class Game:
|
|||||||
|
|
||||||
async def _notify(self, event_name, **kwargs):
|
async def _notify(self, event_name, **kwargs):
|
||||||
for i in range(1, 7): # action guide 1-6 (0 is no action)
|
for i in range(1, 7): # action guide 1-6 (0 is no action)
|
||||||
tasks = []
|
tasks = [
|
||||||
for event in self.listeners.get(event_name, {}).get(i, []):
|
asyncio.create_task(event(**kwargs))
|
||||||
tasks.append(asyncio.create_task(event(**kwargs)))
|
for event in self.listeners.get(event_name, {}).get(i, [])
|
||||||
|
]
|
||||||
|
|
||||||
# Run same-priority task simultaneously
|
# Run same-priority task simultaneously
|
||||||
await asyncio.gather(*tasks)
|
await asyncio.gather(*tasks)
|
||||||
@ -555,10 +556,7 @@ class Game:
|
|||||||
async def generate_targets(self, channel, with_roles=False):
|
async def generate_targets(self, channel, with_roles=False):
|
||||||
embed = discord.Embed(title="Remaining Players", description="[ID] - [Name]")
|
embed = discord.Embed(title="Remaining Players", description="[ID] - [Name]")
|
||||||
for i, player in enumerate(self.players):
|
for i, player in enumerate(self.players):
|
||||||
if player.alive:
|
status = "" if player.alive else "*[Dead]*-"
|
||||||
status = ""
|
|
||||||
else:
|
|
||||||
status = "*[Dead]*-"
|
|
||||||
if with_roles or not player.alive:
|
if with_roles or not player.alive:
|
||||||
embed.add_field(
|
embed.add_field(
|
||||||
name=f"{i} - {status}{player.member.display_name}",
|
name=f"{i} - {status}{player.member.display_name}",
|
||||||
@ -579,7 +577,7 @@ class Game:
|
|||||||
if channel_id not in self.p_channels:
|
if channel_id not in self.p_channels:
|
||||||
self.p_channels[channel_id] = self.default_secret_channel.copy()
|
self.p_channels[channel_id] = self.default_secret_channel.copy()
|
||||||
|
|
||||||
for x in range(10): # Retry 10 times
|
for _ in range(10): # Retry 10 times
|
||||||
try:
|
try:
|
||||||
await asyncio.sleep(1) # This will have multiple calls
|
await asyncio.sleep(1) # This will have multiple calls
|
||||||
self.p_channels[channel_id]["players"].append(role.player)
|
self.p_channels[channel_id]["players"].append(role.player)
|
||||||
@ -706,9 +704,7 @@ class Game:
|
|||||||
if not self.any_votes_remaining:
|
if not self.any_votes_remaining:
|
||||||
await channel.send("Voting is not allowed right now")
|
await channel.send("Voting is not allowed right now")
|
||||||
return
|
return
|
||||||
elif channel.name in self.p_channels:
|
elif channel.name not in self.p_channels:
|
||||||
pass
|
|
||||||
else:
|
|
||||||
# Not part of the game
|
# Not part of the game
|
||||||
await channel.send("Cannot vote in this channel")
|
await channel.send("Cannot vote in this channel")
|
||||||
return
|
return
|
||||||
@ -757,14 +753,14 @@ class Game:
|
|||||||
await self._at_voted(target)
|
await self._at_voted(target)
|
||||||
|
|
||||||
async def eval_results(self, target, source=None, method=None):
|
async def eval_results(self, target, source=None, method=None):
|
||||||
if method is not None:
|
if method is None:
|
||||||
out = "**{ID}** - " + method
|
|
||||||
return out.format(ID=target.id, target=target.member.display_name)
|
|
||||||
else:
|
|
||||||
return "**{ID}** - {target} the {role} was found dead".format(
|
return "**{ID}** - {target} the {role} was found dead".format(
|
||||||
ID=target.id, target=target.member.display_name, role=await target.role.get_role()
|
ID=target.id, target=target.member.display_name, role=await target.role.get_role()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
out = "**{ID}** - " + method
|
||||||
|
return out.format(ID=target.id, target=target.member.display_name)
|
||||||
|
|
||||||
async def _quit(self, player):
|
async def _quit(self, player):
|
||||||
"""
|
"""
|
||||||
Have player quit the game
|
Have player quit the game
|
||||||
|
@ -72,6 +72,9 @@ class Role(WolfListener):
|
|||||||
self.blocked = False
|
self.blocked = False
|
||||||
self.properties = {} # Extra data for other roles (i.e. arsonist)
|
self.properties = {} # Extra data for other roles (i.e. arsonist)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.__repr__()
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"{self.__class__.__name__}({self.player.__repr__()})"
|
return f"{self.__class__.__name__}({self.player.__repr__()})"
|
||||||
|
|
||||||
@ -86,7 +89,7 @@ class Role(WolfListener):
|
|||||||
|
|
||||||
log.debug(f"Assigned {self} to {player}")
|
log.debug(f"Assigned {self} to {player}")
|
||||||
|
|
||||||
async def get_alignment(self, source=None):
|
async def get_alignment(self, source=None): # TODO: Rework to be "strength" tiers
|
||||||
"""
|
"""
|
||||||
Interaction for powerful access of alignment
|
Interaction for powerful access of alignment
|
||||||
(Village, Werewolf, Other)
|
(Village, Werewolf, Other)
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import List, Union
|
from typing import Optional
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from redbot.core import Config, checks, commands
|
from redbot.core import Config, checks, commands
|
||||||
from redbot.core.bot import Red
|
from redbot.core.bot import Red
|
||||||
from redbot.core.commands import Cog
|
from redbot.core.commands import Cog
|
||||||
from redbot.core.utils import AsyncIter
|
|
||||||
from redbot.core.utils.menus import DEFAULT_CONTROLS, menu
|
from redbot.core.utils.menus import DEFAULT_CONTROLS, menu
|
||||||
|
|
||||||
from werewolf.builder import (
|
from werewolf.builder import (
|
||||||
@ -15,19 +14,11 @@ from werewolf.builder import (
|
|||||||
role_from_id,
|
role_from_id,
|
||||||
role_from_name,
|
role_from_name,
|
||||||
)
|
)
|
||||||
from werewolf.game import Game
|
from werewolf.game import Game, anyone_has_role
|
||||||
|
|
||||||
log = logging.getLogger("red.fox_v3.werewolf")
|
log = logging.getLogger("red.fox_v3.werewolf")
|
||||||
|
|
||||||
|
|
||||||
async def anyone_has_role(
|
|
||||||
member_list: List[discord.Member], role: discord.Role
|
|
||||||
) -> Union[None, discord.Member]:
|
|
||||||
return await AsyncIter(member_list).find(
|
|
||||||
lambda m: AsyncIter(m.roles).find(lambda r: r.id == role.id)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Werewolf(Cog):
|
class Werewolf(Cog):
|
||||||
"""
|
"""
|
||||||
Base to host werewolf on a guild
|
Base to host werewolf on a guild
|
||||||
@ -56,17 +47,19 @@ class Werewolf(Cog):
|
|||||||
"""Nothing to delete"""
|
"""Nothing to delete"""
|
||||||
return
|
return
|
||||||
|
|
||||||
def __unload(self):
|
def cog_unload(self):
|
||||||
log.debug("Unload called")
|
log.debug("Unload called")
|
||||||
for game in self.games.values():
|
for key in self.games.keys():
|
||||||
del game
|
del self.games[key]
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def buildgame(self, ctx: commands.Context):
|
async def buildgame(self, ctx: commands.Context):
|
||||||
"""
|
"""
|
||||||
Create game codes to run custom games.
|
Create game codes to run custom games.
|
||||||
|
|
||||||
Pick the roles or randomized roles you want to include in a game
|
Pick the roles or randomized roles you want to include in a game.
|
||||||
|
|
||||||
|
Note: The same role can be picked more than once.
|
||||||
"""
|
"""
|
||||||
gb = GameBuilder()
|
gb = GameBuilder()
|
||||||
code = await gb.build_game(ctx)
|
code = await gb.build_game(ctx)
|
||||||
@ -82,8 +75,7 @@ class Werewolf(Cog):
|
|||||||
"""
|
"""
|
||||||
Base command to adjust settings. Check help for command list.
|
Base command to adjust settings. Check help for command list.
|
||||||
"""
|
"""
|
||||||
if ctx.invoked_subcommand is None:
|
pass
|
||||||
pass
|
|
||||||
|
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@wwset.command(name="list")
|
@wwset.command(name="list")
|
||||||
@ -92,9 +84,6 @@ class Werewolf(Cog):
|
|||||||
Lists current guild settings
|
Lists current guild settings
|
||||||
"""
|
"""
|
||||||
valid, role, category, channel, log_channel = await self._get_settings(ctx)
|
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(
|
embed = discord.Embed(
|
||||||
title="Current Guild Settings",
|
title="Current Guild Settings",
|
||||||
@ -176,8 +165,7 @@ class Werewolf(Cog):
|
|||||||
"""
|
"""
|
||||||
Base command for this cog. Check help for the commands list.
|
Base command for this cog. Check help for the commands list.
|
||||||
"""
|
"""
|
||||||
if ctx.invoked_subcommand is None:
|
pass
|
||||||
pass
|
|
||||||
|
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@ww.command(name="new")
|
@ww.command(name="new")
|
||||||
@ -263,6 +251,7 @@ class Werewolf(Cog):
|
|||||||
game = await self._get_game(ctx)
|
game = await self._get_game(ctx)
|
||||||
if not game:
|
if not game:
|
||||||
await ctx.maybe_send_embed("No game running, cannot start")
|
await ctx.maybe_send_embed("No game running, cannot start")
|
||||||
|
return
|
||||||
|
|
||||||
if not await game.setup(ctx):
|
if not await game.setup(ctx):
|
||||||
pass # ToDo something?
|
pass # ToDo something?
|
||||||
@ -285,7 +274,8 @@ class Werewolf(Cog):
|
|||||||
|
|
||||||
game = await self._get_game(ctx)
|
game = await self._get_game(ctx)
|
||||||
game.game_over = True
|
game.game_over = True
|
||||||
game.current_action.cancel()
|
if game.current_action:
|
||||||
|
game.current_action.cancel()
|
||||||
await ctx.maybe_send_embed("Game has been stopped")
|
await ctx.maybe_send_embed("Game has been stopped")
|
||||||
|
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@ -356,8 +346,7 @@ class Werewolf(Cog):
|
|||||||
"""
|
"""
|
||||||
Find custom roles by name, alignment, category, or ID
|
Find custom roles by name, alignment, category, or ID
|
||||||
"""
|
"""
|
||||||
if ctx.invoked_subcommand is None or ctx.invoked_subcommand == self.ww_search:
|
pass
|
||||||
pass
|
|
||||||
|
|
||||||
@ww_search.command(name="name")
|
@ww_search.command(name="name")
|
||||||
async def ww_search_name(self, ctx: commands.Context, *, name):
|
async def ww_search_name(self, ctx: commands.Context, *, name):
|
||||||
@ -399,7 +388,7 @@ class Werewolf(Cog):
|
|||||||
else:
|
else:
|
||||||
await ctx.maybe_send_embed("Role ID not found")
|
await ctx.maybe_send_embed("Role ID not found")
|
||||||
|
|
||||||
async def _get_game(self, ctx: commands.Context, game_code=None) -> Union[Game, None]:
|
async def _get_game(self, ctx: commands.Context, game_code=None) -> Optional[Game]:
|
||||||
guild: discord.Guild = getattr(ctx, "guild", None)
|
guild: discord.Guild = getattr(ctx, "guild", None)
|
||||||
|
|
||||||
if guild is None:
|
if guild is None:
|
||||||
@ -426,7 +415,7 @@ class Werewolf(Cog):
|
|||||||
|
|
||||||
return self.games[guild.id]
|
return self.games[guild.id]
|
||||||
|
|
||||||
async def _game_start(self, game):
|
async def _game_start(self, game: Game):
|
||||||
await game.start()
|
await game.start()
|
||||||
|
|
||||||
async def _get_settings(self, ctx):
|
async def _get_settings(self, ctx):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user