mirror of
https://github.com/brandons209/Red-bot-Cogs.git
synced 2024-04-28 01:23:26 +12:00
add threadrotate
This commit is contained in:
parent
794fb786c4
commit
a4c6cad1dc
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))
|
126
threadrotate/discord_thread_feature.py
Normal file
126
threadrotate/discord_thread_feature.py
Normal file
|
@ -0,0 +1,126 @@
|
|||
# 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 > 4320 and "THREE_DAY_THREAD_ARCHIVE" not in guild.features:
|
||||
archive = 1440
|
||||
elif archive == 10080 and "SEVEN_DAY_THREAD_ARCHIVE" not in guild.features:
|
||||
archive = 4320
|
||||
|
||||
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:
|
||||
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": {"parse": ["roles"], "roles": mention_roles}}
|
||||
|
||||
r = Route(
|
||||
"POST",
|
||||
"/channels/{channel_id}/nessages",
|
||||
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"]]
|
8
threadrotate/info.json
Normal file
8
threadrotate/info.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"author" : ["brandons209"],
|
||||
"install_msg" : "Thank you for installing my cog!",
|
||||
"name" : "Thread Manager",
|
||||
"short" : "Manage access to threads for users.",
|
||||
"description" : "Allows a more finer management of users and threads.",
|
||||
"tags" : ["Threads"]
|
||||
}
|
131
threadrotate/thread_rotate.py
Normal file
131
threadrotate/thread_rotate.py
Normal file
|
@ -0,0 +1,131 @@
|
|||
import asyncio
|
||||
import discord
|
||||
import random
|
||||
from datetime import datetime, timedelta
|
||||
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 *
|
||||
from .time_utils import *
|
||||
|
||||
|
||||
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": {},
|
||||
"ping_roles": [],
|
||||
"rotation_interval": 10080,
|
||||
"rotate_on": None,
|
||||
"last_topic": None,
|
||||
}
|
||||
self.config.register_channel(**default_channel)
|
||||
|
||||
@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="setup")
|
||||
async def thread_rotate_setup(self, ctx, channel: discord.TextChannel):
|
||||
"""
|
||||
Interactively setup a thread rotation for a channel
|
||||
"""
|
||||
guild = ctx.guild
|
||||
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)"
|
||||
)
|
||||
)
|
||||
|
||||
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)"
|
||||
)
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
date = parse_time(msg.content.strip())
|
||||
if date is None:
|
||||
await ctx.send(error("Invalid date, please run setup again!"), delete_after=60)
|
||||
return
|
||||
|
||||
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."
|
||||
)
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
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."))
|
||||
return
|
||||
|
||||
role_objs.append(channel)
|
||||
|
||||
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"
|
||||
)
|
||||
)
|
||||
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 = {}
|
||||
# TODO error checking
|
||||
for topic in topics:
|
||||
topic = topic.split(":")
|
||||
parsed_topics[topic[0]] = float(topic[1])
|
||||
|
||||
await ctx.send(info(f"Please review the settings for thread rotation on channel {channel.mention}"))
|
55
threadrotate/time_utils.py
Normal file
55
threadrotate/time_utils.py
Normal file
|
@ -0,0 +1,55 @@
|
|||
import pytz
|
||||
import re
|
||||
from datetime import datetime as dt
|
||||
from typing import Optional
|
||||
from dateutil import parser
|
||||
from dateutil.tz import gettz
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
TIME_RE_STRING = r"\s?".join(
|
||||
[
|
||||
r"((?P<years>\d+?)\s?(years?|y))?",
|
||||
r"((?P<months>\d+?)\s?(months?|mt))?",
|
||||
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(dt.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(pytz.utc)
|
||||
return ret
|
||||
|
||||
|
||||
def parse_time_naive(datetimestring: str):
|
||||
return parser.parse(datetimestring)
|
||||
|
||||
|
||||
def parse_timedelta(argument: str) -> Optional[relativedelta]:
|
||||
matches = TIME_RE.match(argument)
|
||||
if matches:
|
||||
params = {k: int(v) for k, v in matches.groupdict().items() if v}
|
||||
if params:
|
||||
return relativedelta(**params)
|
||||
return None
|
Loading…
Reference in a new issue