mirror of
https://github.com/brandons209/Red-bot-Cogs.git
synced 2024-09-30 09:06:22 +13:00
commit
0371a4c103
8 changed files with 574 additions and 39 deletions
|
@ -127,9 +127,9 @@ class NameChange(commands.Cog):
|
||||||
except discord.HTTPException:
|
except discord.HTTPException:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@commands.command()
|
# @commands.command()
|
||||||
async def test(self, ctx):
|
# async def test(self, ctx):
|
||||||
await self.update_namechanges()
|
# await self.update_namechanges()
|
||||||
|
|
||||||
@commands.group(name="name", invoke_without_command=True)
|
@commands.group(name="name", invoke_without_command=True)
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
|
@ -348,6 +348,7 @@ class NameChange(commands.Cog):
|
||||||
await ctx.send(page)
|
await ctx.send(page)
|
||||||
|
|
||||||
@namechange_user.group(name="optout")
|
@namechange_user.group(name="optout")
|
||||||
|
@checks.admin()
|
||||||
async def namechange_user_optout(self, ctx):
|
async def namechange_user_optout(self, ctx):
|
||||||
"""
|
"""
|
||||||
Opt specific users out from changing their name
|
Opt specific users out from changing their name
|
||||||
|
@ -401,10 +402,26 @@ class NameChange(commands.Cog):
|
||||||
currency_name = await bank.get_currency_name(ctx.guild)
|
currency_name = await bank.get_currency_name(ctx.guild)
|
||||||
|
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
info(f"It costs {current_cost} {currency_name} **per minute** to change someone's name."),
|
info(f"It costs {current_cost} {currency_name} **per minute** to change someone's name."), delete_after=30,
|
||||||
delete_after=30,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@namechange.command(name="optin")
|
||||||
|
async def namechange_optin(self, ctx):
|
||||||
|
"""
|
||||||
|
Opt in or opt out of allowing others to change your name
|
||||||
|
|
||||||
|
If you are already opted in, running the command again will opt you out.
|
||||||
|
"""
|
||||||
|
member = ctx.author
|
||||||
|
async with self.config.guild(ctx.guild).allowed_users() as allowed_users:
|
||||||
|
if member.id not in allowed_users:
|
||||||
|
allowed_users.append(member.id)
|
||||||
|
await ctx.tick()
|
||||||
|
else:
|
||||||
|
allowed_users.remove(member.id)
|
||||||
|
await ctx.tick()
|
||||||
|
await ctx.send(info("You have opted out of allowing others to change your name."), delete_after=30)
|
||||||
|
|
||||||
@namechange.command(name="remove")
|
@namechange.command(name="remove")
|
||||||
@checks.admin()
|
@checks.admin()
|
||||||
async def namechange_remove(self, ctx, *, member: discord.Member):
|
async def namechange_remove(self, ctx, *, member: discord.Member):
|
||||||
|
|
|
@ -37,10 +37,8 @@ async def create_thread(
|
||||||
Raises HTTPException 400 if thread creation fails
|
Raises HTTPException 400 if thread creation fails
|
||||||
"""
|
"""
|
||||||
guild = channel.guild
|
guild = channel.guild
|
||||||
if archive > 4320 and "THREE_DAY_THREAD_ARCHIVE" not in guild.features:
|
if archive > 10080:
|
||||||
archive = 1440
|
archive = 10080
|
||||||
elif archive == 10080 and "SEVEN_DAY_THREAD_ARCHIVE" not in guild.features:
|
|
||||||
archive = 4320
|
|
||||||
|
|
||||||
if thread_type == THREAD_TYPES.PRIVATE_THREAD and "PRIVATE_THREADS" not in guild.features:
|
if thread_type == THREAD_TYPES.PRIVATE_THREAD and "PRIVATE_THREADS" not in guild.features:
|
||||||
raise AttributeError("Your guild requires Level 2 Boost to use private threads.")
|
raise AttributeError("Your guild requires Level 2 Boost to use private threads.")
|
||||||
|
@ -54,11 +52,7 @@ async def create_thread(
|
||||||
}
|
}
|
||||||
reason = "Punish Thread Creation"
|
reason = "Punish Thread Creation"
|
||||||
|
|
||||||
r = Route(
|
r = Route("POST", "/channels/{channel_id}/threads", channel_id=channel.id,)
|
||||||
"POST",
|
|
||||||
"/channels/{channel_id}/threads",
|
|
||||||
channel_id=channel.id,
|
|
||||||
)
|
|
||||||
|
|
||||||
return (await bot.http.request(r, json=fields, reason=reason))["id"]
|
return (await bot.http.request(r, json=fields, reason=reason))["id"]
|
||||||
|
|
||||||
|
@ -73,11 +67,6 @@ async def add_user_thread(bot, channel: int, member: discord.Member):
|
||||||
"""
|
"""
|
||||||
reason = "Punish Add Member"
|
reason = "Punish Add Member"
|
||||||
|
|
||||||
r = Route(
|
r = Route("POST", "/channels/{channel_id}/thread-members/{user_id}", channel_id=channel, user_id=member.id,)
|
||||||
"POST",
|
|
||||||
"/channels/{channel_id}/thread-members/{user_id}",
|
|
||||||
channel_id=channel,
|
|
||||||
user_id=member.id,
|
|
||||||
)
|
|
||||||
|
|
||||||
return await bot.http.request(r, reason=reason)
|
return await bot.http.request(r, reason=reason)
|
||||||
|
|
|
@ -22,19 +22,14 @@ async def create_thread(bot, channel: discord.TextChannel, message: discord.Mess
|
||||||
Raises HTTPException 400 if thread creation fails
|
Raises HTTPException 400 if thread creation fails
|
||||||
"""
|
"""
|
||||||
guild = channel.guild
|
guild = channel.guild
|
||||||
if archive > 4320 and "THREE_DAY_THREAD_ARCHIVE" not in guild.features:
|
if archive > 10080:
|
||||||
archive = 1440
|
archive = 10080
|
||||||
elif archive == 10080 and "SEVEN_DAY_THREAD_ARCHIVE" not in guild.features:
|
|
||||||
archive = 4320
|
|
||||||
|
|
||||||
fields = {"name": name, "auto_archive_duration": archive}
|
fields = {"name": name, "auto_archive_duration": archive}
|
||||||
reason = "Thread Manager"
|
reason = "Thread Manager"
|
||||||
|
|
||||||
r = Route(
|
r = Route(
|
||||||
"POST",
|
"POST", "/channels/{channel_id}/messages/{message_id}/threads", channel_id=channel.id, message_id=message.id,
|
||||||
"/channels/{channel_id}/messages/{message_id}/threads",
|
|
||||||
channel_id=channel.id,
|
|
||||||
message_id=message.id,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return (await bot.http.request(r, json=fields, reason=reason))["id"]
|
return (await bot.http.request(r, json=fields, reason=reason))["id"]
|
||||||
|
@ -50,12 +45,7 @@ async def add_user_thread(bot, channel: int, member: discord.Member):
|
||||||
"""
|
"""
|
||||||
reason = "Thread Manager"
|
reason = "Thread Manager"
|
||||||
|
|
||||||
r = Route(
|
r = Route("POST", "/channels/{channel_id}/thread-members/{user_id}", channel_id=channel, user_id=member.id,)
|
||||||
"POST",
|
|
||||||
"/channels/{channel_id}/thread-members/{user_id}",
|
|
||||||
channel_id=channel,
|
|
||||||
user_id=member.id,
|
|
||||||
)
|
|
||||||
|
|
||||||
return await bot.http.request(r, reason=reason)
|
return await bot.http.request(r, reason=reason)
|
||||||
|
|
||||||
|
@ -73,11 +63,7 @@ async def get_active_threads(bot, guild: discord.Guild):
|
||||||
|
|
||||||
reason = "Thread Manager"
|
reason = "Thread Manager"
|
||||||
|
|
||||||
r = Route(
|
r = Route("GET", "/guilds/{guild_id}/threads/active", guild_id=guild.id,)
|
||||||
"GET",
|
|
||||||
"/guilds/{guild_id}/threads/active",
|
|
||||||
guild_id=guild.id,
|
|
||||||
)
|
|
||||||
|
|
||||||
res = await bot.http.request(r, reason=reason)
|
res = await bot.http.request(r, reason=reason)
|
||||||
|
|
||||||
|
|
7
threadrotate/__init__.py
Normal file
7
threadrotate/__init__.py
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
from .thread_rotate import ThreadRotate
|
||||||
|
|
||||||
|
__red_end_user_data_statement__ = "This doesn't store any user data."
|
||||||
|
|
||||||
|
|
||||||
|
def setup(bot):
|
||||||
|
bot.add_cog(ThreadRotate(bot))
|
101
threadrotate/discord_thread_feature.py
Normal file
101
threadrotate/discord_thread_feature.py
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
# NOTE: this file contains backports or unintroduced features of next versions of dpy (as for 1.7.3)
|
||||||
|
import discord
|
||||||
|
from discord.http import Route
|
||||||
|
|
||||||
|
|
||||||
|
async def create_thread(
|
||||||
|
bot, channel: discord.TextChannel, name: str, archive: int = 1440, message: discord.Message = None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Creates a new thread in the channel from the message
|
||||||
|
|
||||||
|
Args:
|
||||||
|
channel (TextChannel): The channel the thread will be apart of
|
||||||
|
message (Message): The discord message the thread will start with
|
||||||
|
name (str): The name of the thread
|
||||||
|
archive (int): The archive duration. Can be 60, 1440, 4320, and 10080.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: The channel ID of the newly created thread
|
||||||
|
|
||||||
|
Note:
|
||||||
|
The guild must be boosted for longer thread durations then a day. The archive parameter will automatically be scaled down if the feature is not present.
|
||||||
|
|
||||||
|
Raises HTTPException 400 if thread creation fails
|
||||||
|
"""
|
||||||
|
guild = channel.guild
|
||||||
|
if archive > 10080:
|
||||||
|
archive = 10080
|
||||||
|
|
||||||
|
reason = "Thread Rotation"
|
||||||
|
fields = {"name": name, "auto_archive_duration": archive}
|
||||||
|
|
||||||
|
if message is not None:
|
||||||
|
r = Route(
|
||||||
|
"POST",
|
||||||
|
"/channels/{channel_id}/messages/{message_id}/threads",
|
||||||
|
channel_id=channel.id,
|
||||||
|
message_id=message.id,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
fields["type"] = 11
|
||||||
|
r = Route("POST", "/channels/{channel_id}/threads", channel_id=channel.id,)
|
||||||
|
|
||||||
|
return (await bot.http.request(r, json=fields, reason=reason))["id"]
|
||||||
|
|
||||||
|
|
||||||
|
async def send_thread_message(
|
||||||
|
bot, thread_id: int, content: str, mention_roles: list = [],
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Send a message in a thread, allowing pings for roles
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bot (Red): The bot object
|
||||||
|
thread_id (int): ID of the thread
|
||||||
|
content (str): The message to send
|
||||||
|
mention_roles (list, optional): List of role ids to allow mentions. Defaults to [].
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: ID of the new message
|
||||||
|
"""
|
||||||
|
fields = {"content": content, "allowed_mentions": {"roles": mention_roles}}
|
||||||
|
|
||||||
|
r = Route("POST", "/channels/{channel_id}/messages", channel_id=thread_id,)
|
||||||
|
|
||||||
|
return (await bot.http.request(r, json=fields))["id"]
|
||||||
|
|
||||||
|
|
||||||
|
async def add_user_thread(bot, channel: int, member: discord.Member):
|
||||||
|
"""
|
||||||
|
Add a user to a thread
|
||||||
|
|
||||||
|
Args:
|
||||||
|
channel (int): The channel id that represents the thread
|
||||||
|
member (Member): The member to add to the thread
|
||||||
|
"""
|
||||||
|
reason = "Thread Manager"
|
||||||
|
|
||||||
|
r = Route("POST", "/channels/{channel_id}/thread-members/{user_id}", channel_id=channel, user_id=member.id,)
|
||||||
|
|
||||||
|
return await bot.http.request(r, reason=reason)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_active_threads(bot, guild: discord.Guild):
|
||||||
|
"""
|
||||||
|
Get all active threads in the guild
|
||||||
|
|
||||||
|
Args:
|
||||||
|
guild (Guild): The guild to get active threads in
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list(int): List of thread IDs of each actuvate thread
|
||||||
|
"""
|
||||||
|
|
||||||
|
reason = "Thread Manager"
|
||||||
|
|
||||||
|
r = Route("GET", "/guilds/{guild_id}/threads/active", guild_id=guild.id,)
|
||||||
|
|
||||||
|
res = await bot.http.request(r, reason=reason)
|
||||||
|
|
||||||
|
return [t["id"] for t in res["threads"]]
|
11
threadrotate/info.json
Normal file
11
threadrotate/info.json
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"author": [
|
||||||
|
"brandons209"
|
||||||
|
],
|
||||||
|
"install_msg": "Thank you for installing my cog!",
|
||||||
|
"name": "Thread Rotator",
|
||||||
|
"description": "Allows for automatic thread rotation given a list of topics. Useful for roleplaying, general chats, topic discussions, etc.",
|
||||||
|
"tags": [
|
||||||
|
"Threads"
|
||||||
|
]
|
||||||
|
}
|
371
threadrotate/thread_rotate.py
Normal file
371
threadrotate/thread_rotate.py
Normal file
|
@ -0,0 +1,371 @@
|
||||||
|
import asyncio
|
||||||
|
import discord
|
||||||
|
import random
|
||||||
|
from typing import Optional, Literal
|
||||||
|
|
||||||
|
from redbot.core import Config, checks, commands
|
||||||
|
from redbot.core.utils.chat_formatting import *
|
||||||
|
from redbot.core.utils.predicates import MessagePredicate
|
||||||
|
|
||||||
|
from .discord_thread_feature import send_thread_message, create_thread
|
||||||
|
from .time_utils import parse_time, parse_timedelta
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
|
||||||
|
class ThreadRotate(commands.Cog):
|
||||||
|
"""
|
||||||
|
Rotate threads for events, roleplay, etc
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, bot):
|
||||||
|
self.bot = bot
|
||||||
|
self.config = Config.get_conf(self, identifier=45612361654894681623, force_registration=True)
|
||||||
|
|
||||||
|
default_channel = {
|
||||||
|
"topics": {},
|
||||||
|
"topic_threads": {},
|
||||||
|
"ping_roles": [],
|
||||||
|
"rotation_interval": 10080,
|
||||||
|
"rotate_on": None,
|
||||||
|
"last_topic": None,
|
||||||
|
}
|
||||||
|
self.config.register_channel(**default_channel)
|
||||||
|
|
||||||
|
self.task = asyncio.create_task(self.thread_rotation_task())
|
||||||
|
|
||||||
|
def cog_unload(self):
|
||||||
|
if self.task is not None:
|
||||||
|
self.task.cancel()
|
||||||
|
return super().cog_unload()()
|
||||||
|
|
||||||
|
async def thread_rotation_task(self):
|
||||||
|
await self.bot.wait_until_ready()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
for guild in self.bot.guilds:
|
||||||
|
for channel in guild.text_channels:
|
||||||
|
topics = await self.config.channel(channel).topics()
|
||||||
|
if not topics:
|
||||||
|
continue
|
||||||
|
rotate_on = datetime.fromtimestamp(await self.config.channel(channel).rotate_on())
|
||||||
|
if datetime.now() > rotate_on:
|
||||||
|
await self.rotate_thread(channel)
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
await asyncio.sleep(60)
|
||||||
|
|
||||||
|
async def rotate_thread(self, channel: discord.TextChannel):
|
||||||
|
topics = await self.config.channel(channel).topics()
|
||||||
|
topic_threads = await self.config.channel(channel).topic_threads()
|
||||||
|
ping_roles = await self.config.channel(channel).ping_roles()
|
||||||
|
rotation = timedelta(seconds=await self.config.channel(channel).rotation_interval())
|
||||||
|
rotate_on = datetime.fromtimestamp(await self.config.channel(channel).rotate_on())
|
||||||
|
last_topic = await self.config.channel(channel).last_topic()
|
||||||
|
|
||||||
|
# choose new topic
|
||||||
|
# don't want to choose the last topic, so set it's weight to 0 so it is not choosen
|
||||||
|
if last_topic is not None:
|
||||||
|
topics[last_topic] = 0
|
||||||
|
|
||||||
|
new_topic = random.choices(list(topics.keys()), weights=list(topics.values()), k=1)[0]
|
||||||
|
|
||||||
|
# rotate the thread, create new thread, ping roles, etc
|
||||||
|
# ping roles
|
||||||
|
roles = [channel.guild.get_role(r) for r in ping_roles]
|
||||||
|
roles = [r for r in roles if r is not None]
|
||||||
|
|
||||||
|
role_msg = " ".join([r.mention for r in roles])
|
||||||
|
role_msg += f"\n\nHello, a new topic has been set for {channel.mention}: `{new_topic}`"
|
||||||
|
|
||||||
|
# if a thread already exists for the topic, try to send a message to it to unarchive it
|
||||||
|
new_thread_id = None
|
||||||
|
if new_topic in topic_threads:
|
||||||
|
try:
|
||||||
|
await send_thread_message(self.bot, topic_threads[new_topic], role_msg, mention_roles=ping_roles)
|
||||||
|
new_thread_id = topic_threads[new_topic]
|
||||||
|
except discord.HTTPException: # may occur if bot cant unarchive manually archived threads or thread is deleted
|
||||||
|
try:
|
||||||
|
new_thread_id = await create_thread(self.bot, channel, new_topic, archive=10080)
|
||||||
|
except discord.HTTPException:
|
||||||
|
return
|
||||||
|
await send_thread_message(self.bot, new_thread_id, role_msg, mention_roles=ping_roles)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
new_thread_id = await create_thread(self.bot, channel, new_topic, archive=10080)
|
||||||
|
except discord.HTTPException:
|
||||||
|
return
|
||||||
|
await send_thread_message(self.bot, new_thread_id, role_msg, mention_roles=ping_roles)
|
||||||
|
|
||||||
|
# update next rotation
|
||||||
|
async with self.config.channel(channel).topic_threads() as topic_threads:
|
||||||
|
topic_threads[new_topic] = new_thread_id
|
||||||
|
|
||||||
|
await self.config.channel(channel).rotate_on.set(int((rotate_on + rotation).timestamp()))
|
||||||
|
await self.config.channel(channel).last_topic.set(new_topic)
|
||||||
|
|
||||||
|
@commands.group(name="rotate")
|
||||||
|
@commands.guild_only()
|
||||||
|
@checks.admin()
|
||||||
|
async def thread_rotate(self, ctx):
|
||||||
|
"""
|
||||||
|
Manage thread rotations per channel
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@thread_rotate.command(name="manual")
|
||||||
|
async def thread_rotate_manual(self, ctx, channel: discord.TextChannel):
|
||||||
|
"""
|
||||||
|
Manually rotate a thread topic
|
||||||
|
"""
|
||||||
|
current = await self.config.channel(channel).topics()
|
||||||
|
if not current:
|
||||||
|
await ctx.send(error("That channel has not been setup for thread rotation!"), delete_after=30)
|
||||||
|
return
|
||||||
|
|
||||||
|
await self.rotate_thread(channel)
|
||||||
|
await ctx.tick()
|
||||||
|
|
||||||
|
@thread_rotate.command(name="interval")
|
||||||
|
async def thread_rotate_interval(self, ctx, channel: discord.TextChannel, interval: str):
|
||||||
|
"""
|
||||||
|
Modify the rotation interval for a thread rotation
|
||||||
|
|
||||||
|
The channel must of already been setup for thread rotation
|
||||||
|
|
||||||
|
This will apply on the next thread rotation for the channel!
|
||||||
|
"""
|
||||||
|
current = await self.config.channel(channel).topics()
|
||||||
|
if not current:
|
||||||
|
await ctx.send(error("That channel has not been setup for thread rotation!"), delete_after=30)
|
||||||
|
return
|
||||||
|
|
||||||
|
interval = parse_timedelta(interval.strip())
|
||||||
|
if interval is None:
|
||||||
|
await ctx.send(error("Invalid time interval, please try again!"), delete_after=60)
|
||||||
|
return
|
||||||
|
|
||||||
|
await self.config.channel(channel).rotation_interval.set(interval.total_seconds())
|
||||||
|
await ctx.tick()
|
||||||
|
|
||||||
|
@thread_rotate.command(name="roles")
|
||||||
|
async def thread_rotate_roles(self, ctx, channel: discord.TextChannel, *roles: discord.Role):
|
||||||
|
"""
|
||||||
|
Modify the ping roles for a thread rotation
|
||||||
|
|
||||||
|
The channel must of already been setup for thread rotation
|
||||||
|
"""
|
||||||
|
current = await self.config.channel(channel).topics()
|
||||||
|
if not current:
|
||||||
|
await ctx.send(error("That channel has not been setup for thread rotation!"), delete_after=30)
|
||||||
|
return
|
||||||
|
|
||||||
|
await self.config.channel(channel).ping_roles.set([r.id for r in roles])
|
||||||
|
await ctx.tick()
|
||||||
|
|
||||||
|
@thread_rotate.command(name="topics")
|
||||||
|
async def thread_rotate_topics(self, ctx, channel: discord.TextChannel, *, topics: str = None):
|
||||||
|
"""
|
||||||
|
Modify topics for thread rotation.
|
||||||
|
|
||||||
|
The channel must of already been setup for thread rotation
|
||||||
|
"""
|
||||||
|
current = await self.config.channel(channel).topics()
|
||||||
|
if not current:
|
||||||
|
await ctx.send(error("That channel has not been setup for thread rotation!"), delete_after=30)
|
||||||
|
return
|
||||||
|
|
||||||
|
if topics is None:
|
||||||
|
await ctx.send(info(f"{channel.mention}'s topics:"))
|
||||||
|
topic_msg = "Topics:\n"
|
||||||
|
for topic, weight in current.items():
|
||||||
|
topic_msg += f"{topic}: {weight}\n"
|
||||||
|
await ctx.send(box(topic_msg), delete_after=300)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
topics = topics.split("\n")
|
||||||
|
parsed_topics = {}
|
||||||
|
for topic in topics:
|
||||||
|
topic = topic.split(":")
|
||||||
|
try:
|
||||||
|
if len(topic) > 2:
|
||||||
|
parsed_topics[":".join(topic[0:-1])] = float(topic[-1])
|
||||||
|
else:
|
||||||
|
parsed_topics[topic[0]] = float(topic[-1])
|
||||||
|
except:
|
||||||
|
await ctx.send(
|
||||||
|
error(
|
||||||
|
"Please make sure to use the correct format, every topic and weight should be split by a `:` and the weight should be a single decimal value."
|
||||||
|
),
|
||||||
|
delete_after=60,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
await self.config.channel(channel).topics.set(parsed_topics)
|
||||||
|
await ctx.tick()
|
||||||
|
|
||||||
|
@thread_rotate.command(name="setup")
|
||||||
|
async def thread_rotate_setup(self, ctx, channel: discord.TextChannel):
|
||||||
|
"""
|
||||||
|
Interactively setup a thread rotation for a channel
|
||||||
|
"""
|
||||||
|
guild = ctx.guild
|
||||||
|
now = datetime.now()
|
||||||
|
|
||||||
|
await ctx.send(
|
||||||
|
info(
|
||||||
|
"Welcome to the thread rotation setup wizard!\n\nFirst, please specifiy the rotation interval. Rotation intervals can be formatted as follows:\n\t5 minutes\n\t1 minute 30 seconds\n\t1 hour\n\t2 days\n\t30 days\n\t5h30m\n\t(etc)"
|
||||||
|
),
|
||||||
|
delete_after=300,
|
||||||
|
)
|
||||||
|
|
||||||
|
pred = MessagePredicate.same_context(ctx)
|
||||||
|
try:
|
||||||
|
msg = await self.bot.wait_for("message", check=pred, timeout=121)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
await ctx.send(error("Took too long, cancelling setup!"), delete_after=30)
|
||||||
|
return
|
||||||
|
|
||||||
|
interval = parse_timedelta(msg.content.strip())
|
||||||
|
if interval is None:
|
||||||
|
await ctx.send(error("Invalid time interval, please run setup again!"), delete_after=60)
|
||||||
|
return
|
||||||
|
|
||||||
|
await ctx.send(
|
||||||
|
info(
|
||||||
|
"Thank you.\n\nNow, please specify the date and time to start rotation. You can say `now` to start rotation as soon as setup is complete.\n\nValid date formats are:\n\tFebruary 14 at 6pm EDT\n\t2019-04-13 06:43:00 PST\n\t01/20/18 at 21:00:43\n\t(etc)"
|
||||||
|
),
|
||||||
|
delete_after=300,
|
||||||
|
)
|
||||||
|
|
||||||
|
pred = MessagePredicate.same_context(ctx)
|
||||||
|
try:
|
||||||
|
msg = await self.bot.wait_for("message", check=pred, timeout=121)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
await ctx.send(error("Took too long, cancelling setup!"), delete_after=30)
|
||||||
|
return
|
||||||
|
|
||||||
|
if msg.content.strip().lower() == "now":
|
||||||
|
date = datetime.now()
|
||||||
|
else:
|
||||||
|
date = parse_time(msg.content.strip())
|
||||||
|
|
||||||
|
if date is None:
|
||||||
|
await ctx.send(error("Invalid date, please run setup again!"), delete_after=60)
|
||||||
|
return
|
||||||
|
|
||||||
|
if date < now:
|
||||||
|
await ctx.send(
|
||||||
|
error("Invalid date, the date must be in the future! Please run the setup again."), delete_after=60
|
||||||
|
)
|
||||||
|
|
||||||
|
await ctx.send(
|
||||||
|
info(
|
||||||
|
"Great, next step is to list all roles that should be pinged and added to each thread when it rotates.\n\nList each role **seperated by a comma `,`**.\nYou can use role IDs, role mentions, or role names. If you do not want to ping any roles type `next` or `no`."
|
||||||
|
),
|
||||||
|
delete_after=300,
|
||||||
|
)
|
||||||
|
|
||||||
|
pred = MessagePredicate.same_context(ctx)
|
||||||
|
try:
|
||||||
|
msg = await self.bot.wait_for("message", check=pred, timeout=241)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
await ctx.send(error("Took too long, cancelling setup!"), delete_after=30)
|
||||||
|
return
|
||||||
|
|
||||||
|
if msg.content.strip().lower() != "no" and msg.content.strip().lower() != "next":
|
||||||
|
roles = [m.strip().strip("<").strip(">").strip("@").strip("&") for m in msg.content.split(",")]
|
||||||
|
role_objs = []
|
||||||
|
for r in roles:
|
||||||
|
try:
|
||||||
|
role = guild.get_role(int(r))
|
||||||
|
except:
|
||||||
|
role = discord.utils.find(lambda c: c.name == r, guild.roles)
|
||||||
|
if role is None:
|
||||||
|
await ctx.send(error(f"Unknown channel: `{r}`, please run the command again."), delete_after=60)
|
||||||
|
return
|
||||||
|
|
||||||
|
role_objs.append(role)
|
||||||
|
else:
|
||||||
|
role_objs = []
|
||||||
|
|
||||||
|
await ctx.send(
|
||||||
|
info(
|
||||||
|
"Final step is to list the thread topics and their selection weights.\nThe weight is how likely the topic will be choosen.\nA weight of `1` means it will not be choosen more or less than other topics.\nA weight between 0 and 1 means it is that weight times less likely to be choosen, with a weight of 0 meaning it will never be choosen.\nA weight greater than 1 means it will be that times more likely to be choosen.\n\nFor example, a weight of 1.5 means that topic is 1.5 more likely to be choose over the others. A weight of 0.5 means that topic is half as likely to be choosen by others.\n\nPlease use this format for listing the weights:\n"
|
||||||
|
),
|
||||||
|
delete_after=300,
|
||||||
|
)
|
||||||
|
await ctx.send(box("topic name: weight_value\ntopic 2 name: weight_value\ntopic 3 name: weight_value"))
|
||||||
|
|
||||||
|
pred = MessagePredicate.same_context(ctx)
|
||||||
|
try:
|
||||||
|
msg = await self.bot.wait_for("message", check=pred, timeout=301)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
await ctx.send(error("Took too long, cancelling setup!"), delete_after=30)
|
||||||
|
return
|
||||||
|
|
||||||
|
topics = msg.content.split("\n")
|
||||||
|
parsed_topics = {}
|
||||||
|
for topic in topics:
|
||||||
|
topic = topic.split(":")
|
||||||
|
try:
|
||||||
|
if len(topic) > 2:
|
||||||
|
parsed_topics[":".join(topic[0:-1])] = float(topic[-1])
|
||||||
|
else:
|
||||||
|
parsed_topics[topic[0]] = float(topic[-1])
|
||||||
|
|
||||||
|
if float(topic[-1]) < 0:
|
||||||
|
raise ValueError()
|
||||||
|
except:
|
||||||
|
await ctx.send(
|
||||||
|
error(
|
||||||
|
"Please make sure to use the correct format, every topic and weight should be split by a `:` and the weight should be a single decimal value greater than or equal to 0."
|
||||||
|
),
|
||||||
|
delete_after=60,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
topic_msg = "Topics:\n"
|
||||||
|
for topic, weight in parsed_topics.items():
|
||||||
|
topic_msg += f"{topic}: {weight}\n"
|
||||||
|
|
||||||
|
await ctx.send(
|
||||||
|
info(f"Please review the settings for thread rotation on channel {channel.mention}:"), delete_after=300,
|
||||||
|
)
|
||||||
|
await ctx.send(
|
||||||
|
box(
|
||||||
|
f"Rotation interval: {humanize_timedelta(seconds=interval.total_seconds())}\n\nRotation Start: {date}\n\nPing roles: {humanize_list([r.name for r in role_objs])}"
|
||||||
|
),
|
||||||
|
delete_after=300,
|
||||||
|
)
|
||||||
|
await ctx.send(
|
||||||
|
box(topic_msg), delete_after=300,
|
||||||
|
)
|
||||||
|
|
||||||
|
await ctx.send("Type yes to confirm the thread rotation, type no to cancel thread rotation setup.")
|
||||||
|
|
||||||
|
pred = MessagePredicate.yes_or_no(ctx)
|
||||||
|
|
||||||
|
try:
|
||||||
|
msg = await self.bot.wait_for("message", check=pred, timeout=240)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
await ctx.send(error("Took too long, cancelling setup!"), delete_after=30)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not pred.result:
|
||||||
|
await ctx.send(info("Cancelled setup."), delete_after=60)
|
||||||
|
return
|
||||||
|
|
||||||
|
# setup the channel
|
||||||
|
await self.config.channel(channel).topics.set(parsed_topics)
|
||||||
|
await self.config.channel(channel).ping_roles.set([r.id for r in role_objs])
|
||||||
|
await self.config.channel(channel).rotation_interval.set(interval.total_seconds())
|
||||||
|
await self.config.channel(channel).rotate_on.set(int(date.timestamp()))
|
||||||
|
|
||||||
|
await ctx.send(
|
||||||
|
info(f"Thread rotation setup! The first rotation will start at <t:{int(date.timestamp())}>"),
|
||||||
|
delete_after=60,
|
||||||
|
)
|
||||||
|
|
53
threadrotate/time_utils.py
Normal file
53
threadrotate/time_utils.py
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import pytz
|
||||||
|
import re
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Optional
|
||||||
|
from dateutil import parser
|
||||||
|
from dateutil.tz import gettz
|
||||||
|
|
||||||
|
TIME_RE_STRING = r"\s?".join(
|
||||||
|
[
|
||||||
|
r"((?P<years>\d+?)\s?(years?|y))?",
|
||||||
|
r"((?P<weeks>\d+?)\s?(weeks?|w))?",
|
||||||
|
r"((?P<days>\d+?)\s?(days?|d))?",
|
||||||
|
r"((?P<hours>\d+?)\s?(hours?|hrs|hr?))?",
|
||||||
|
r"((?P<minutes>\d+?)\s?(minutes?|mins?|m(?!o)))?", # prevent matching "months"
|
||||||
|
r"((?P<seconds>\d+?)\s?(seconds?|secs?|s))?",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
TIME_RE = re.compile(TIME_RE_STRING, re.I)
|
||||||
|
|
||||||
|
|
||||||
|
def gen_tzinfos():
|
||||||
|
for zone in pytz.common_timezones:
|
||||||
|
try:
|
||||||
|
tzdate = pytz.timezone(zone).localize(datetime.utcnow(), is_dst=None)
|
||||||
|
except pytz.NonExistentTimeError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
tzinfo = gettz(zone)
|
||||||
|
|
||||||
|
if tzinfo:
|
||||||
|
yield tzdate.tzname(), tzinfo
|
||||||
|
|
||||||
|
|
||||||
|
def parse_time(datetimestring: str):
|
||||||
|
tzinfo = dict(gen_tzinfos())
|
||||||
|
ret = parser.parse(datetimestring, tzinfos=tzinfo)
|
||||||
|
if ret.tzinfo is not None:
|
||||||
|
ret = ret.astimezone(str(datetime.now().astimezone().tzinfo))
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def parse_time_naive(datetimestring: str):
|
||||||
|
return parser.parse(datetimestring)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_timedelta(argument: str) -> Optional[timedelta]:
|
||||||
|
matches = TIME_RE.match(argument)
|
||||||
|
if matches:
|
||||||
|
params = {k: int(v) for k, v in matches.groupdict().items() if v}
|
||||||
|
if params:
|
||||||
|
return timedelta(**params)
|
||||||
|
return None
|
Loading…
Reference in a new issue