from redbot.core import commands, Config, checks from redbot.core.commands import Context, Cog from redbot.core.utils.chat_formatting import * from redbot.core.utils.menus import menu, DEFAULT_CONTROLS import discord import re import asyncio from dateutil.relativedelta import relativedelta from datetime import datetime, timezone from typing import Literal, Optional TIME_RE_STRING = r"\s?".join( [ r"((?P\d+?)\s?(years?|y))?", r"((?P\d+?)\s?(months?|mt))?", r"((?P\d+?)\s?(weeks?|w))?", r"((?P\d+?)\s?(days?|d))?", r"((?P\d+?)\s?(hours?|hrs|hr?))?", r"((?P\d+?)\s?(minutes?|mins?|m(?!o)))?", # prevent matching "months" r"((?P\d+?)\s?(seconds?|secs?|s))?", ] ) TIME_RE = re.compile(TIME_RE_STRING, re.I) class Subscriber(commands.Cog): """ Automates subscriptions to roles to make donators and other roles easier to manage. """ def __init__(self, bot): self.bot = bot self.config = Config.get_conf(self, identifier=74572674632164, force_registration=True) default_guild = { "dm_message": "Hello {member}! Just a friendly reminder your subscription to `{role}` will end on {end_date}. Please contact the staff to renew your role.", "subscribers": [], "reminder_time": "3 days", } # maps role id (str) -> end date in unix timestamp default_member = {"roles": {}, "reminded": {}} self.config.register_guild(**default_guild) self.config.register_member(**default_member) self.task = asyncio.create_task(self.initialize()) @staticmethod 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 def cog_unload(self): self.task.cancel() async def initialize(self): await self.bot.wait_until_ready() _guilds = [g for g in self.bot.guilds if g.large and not (g.chunked or g.unavailable)] await self.bot.request_offline_members(*_guilds) while True: now = datetime.now(tz=timezone.utc) for guild in self.bot.guilds: members = await self.config.guild(guild).subscribers() remind_time = self.parse_timedelta(await self.config.guild(guild).reminder_time()) dm = await self.config.guild(guild).dm_message() rm_members = [] for member in members: member = guild.get_member(member) if not member: rm_members.append(member) continue roles = await self.config.member(member).roles() reminders = await self.config.member(member).reminded() to_remove = [] for role, end_date in roles.items(): role = guild.get_role(int(role)) end_date = datetime.fromtimestamp(end_date).astimezone(tz=timezone.utc) if not role: continue if now > end_date: try: await member.remove_roles(role) except discord.Forbidden: continue try: await member.send( info( f"Your subscription to the role `{role}` in `{guild}` has expired and been removed." ) ) except: pass to_remove.append(str(role.id)) del reminders[str(role.id)] elif (now + remind_time) > end_date and not reminders[str(role.id)]: dm = dm.format( role=role, end_date=f"", member=member.mention, guild=guild, ) try: await member.send(f"**Role Expiration Notice for {guild}**\n\n{dm}") reminders[str(role.id)] = True except: pass elif (now + remind_time) < end_date: reminders[str(role.id)] = False await self.config.member(member).reminded.set(reminders) if to_remove: for role in to_remove: del roles[role] await self.config.member(member).roles.set(roles) if rm_members: for mem in rm_members: members.remove(mem) await self.config.guild(guild).subscribers.set(members) # sleep for 30 minutes await asyncio.sleep(1800) # await asyncio.sleep(15) @commands.group(name="subset") @checks.admin_or_permissions(administrator=True) @commands.guild_only() async def subset(self, ctx): """ Set subscription settings """ pass @subset.command(name="message") async def subset_message(self, ctx, *, msg: str = None): """ Sets the reminder message sent to users when their subscription is about to end. Leave message as blank to view the current DM message. You can use these values below to represent the role, end date, etc automatically: - {role} will be replaced with the name of the role - {member} will be replaced with the member's username - {guild} will be replaced with the name of the guild - {end_date} will be replaced with the date and time the subscription will end """ if msg is None: curr = await self.config.guild(ctx.guild).dm_message() await ctx.send(box(curr)) return await self.config.guild(ctx.guild).dm_message.set(msg) await ctx.tick() @subset.command(name="reminder") async def subset_reminder(self, ctx, *, interval: str): """ Set the time before the end of a user's subscription to remind them. Intervals can be: - 5 minutes - 1 minute 30 seconds - 1 hour - 2 days - 30 days - 5 months - 2 years (etc) """ if not self.parse_timedelta(interval): await ctx.send(error("The interval is invalid, please try again.")) return await self.config.guild(ctx.guild).reminder_time.set(interval) await ctx.tick() @commands.command(name="subadd") @checks.admin_or_permissions(administrator=True) @checks.bot_has_permissions(manage_roles=True) @commands.guild_only() async def subadd(self, ctx, role: discord.Role, member: discord.Member, *, duration: str): """ Add a role and subscription to a member for the specified duration. """ now = datetime.now(tz=timezone.utc) duration = self.parse_timedelta(duration) if not duration: await ctx.send(error("The duration is invalid, please try again.")) return end_time = now + duration async with self.config.member(member).roles() as roles: if str(role.id) not in roles: try: await member.add_roles(role) except discord.Forbidden: await ctx.send( error( "I do not have permission to add this role, make sure the role is lower in the hierarchy then my top role." ) ) return # role is converted to string since redbot will do it, so make it explict its a string roles[str(role.id)] = end_time.timestamp() else: await ctx.send( error( "The user is already subscribed to this role, please renew their subscription instead using `subrenew`." ) ) return async with self.config.guild(ctx.guild).subscribers() as subs: if member.id not in subs: subs.append(member.id) async with self.config.member(member).reminded() as reminded: reminded[str(role.id)] = False try: await member.send( info( f"You have been subscribed to the role `{role}` in `{ctx.guild}`.\nThe subscription will end on " ) ) except: pass await ctx.tick() @commands.command(name="subrem") @checks.admin_or_permissions(administrator=True) @checks.bot_has_permissions(manage_roles=True) @commands.guild_only() async def subrem(self, ctx, role: discord.Role, member: discord.Member): """ Manually remove a subscribed role from a member. """ async with self.config.member(member).roles() as roles: if str(role.id) in roles: try: await member.remove_roles(role) except discord.Forbidden: await ctx.send( error( "I do not have permission to remove this role, make sure the role is lower in the hierarchy then my top role." ) ) return del roles[str(role.id)] else: await ctx.send(error("The user is not subscribed to this role.")) return if not roles: async with self.config.guild(ctx.guild).subscribers() as subs: subs.remove(member.id) async with self.config.member(member).reminded() as reminded: del reminded[str(role.id)] try: await member.send( info(f"Your subscription to the role `{role}` in `{ctx.guild}` has been manually removed.") ) except: pass await ctx.tick() @commands.command(name="subrenew") @checks.admin_or_permissions(administrator=True) @commands.guild_only() async def subrenew(self, ctx, role: discord.Role, member: discord.Member, *, duration: str): """ Renew's a user's role subscription for the specified duration. """ now = datetime.now(tz=timezone.utc) duration = self.parse_timedelta(duration) if not duration: await ctx.send(error("The duration is invalid, please try again.")) return end_time = now + duration async with self.config.member(member).roles() as roles: if str(role.id) in roles: # role is converted to string since redbot will do it, so make it explict its a string roles[str(role.id)] = end_time.timestamp() else: await ctx.send(error("The user is not subscribed to this role.")) return await ctx.tick() @commands.command(name="subviewall") @checks.admin_or_permissions(administrator=True) @commands.guild_only() async def subview_all(self, ctx): """ View all subscriptions in the server """ members = await self.config.guild(ctx.guild).subscribers() if not members: await ctx.send(error("No users have subscriptions in your server."), delete_after=60) return msg = "" for member in members: member = ctx.guild.get_member(member) if not member: continue msg += f"{member.mention}:\n" roles = await self.config.member(member).roles() for role, end_date in roles.items(): role = ctx.guild.get_role(int(role)) if not role: continue msg += f"\t- `@{role.name}`: \n" msg += "\n" pages = list(pagify(msg, page_length=1700, delims=["\n"], priority=True)) pages = [f"{page}\n\n-----------------\n**Page {i+1} of {len(pages)}**" for i, page in enumerate(pages)] if not pages: # should never happen await ctx.send( error( "There are subscribed users, but I couldn't get any of their information. Please contact the bot developer for help." ), delete_after=60, ) else: await menu(ctx, pages, DEFAULT_CONTROLS) @commands.command(name="subview") @commands.guild_only() async def subview(self, ctx): """ View your current subscriptions """ member = ctx.author roles = await self.config.member(member).roles() if not roles: await ctx.send(info("You are not subscribed to any roles!")) return embeds = [] embed = discord.Embed(title=f"Subscribed Roles", colour=member.colour) cnt = 0 for role, end_date in roles.items(): end_date = datetime.fromtimestamp(end_date).astimezone(tz=timezone.utc) embed.add_field(name=str(ctx.guild.get_role(int(role))), value=f"Ends on ") cnt += 1 # to avoid embed limits if cnt > 25: embeds.append(embed) embed = discord.Embed(title=f"Subscribed Roles", colour=member.colour) cnt = 0 embeds.append(embed) for embed in embeds: await ctx.send(embed=embed) async def red_delete_data_for_user( self, *, requester: Literal["discord_deleted_user", "owner", "user", "user_strict"], user_id: int, ): pass