2021-09-03 16:25:34 +12:00
from redbot . core import commands , Config , checks
from redbot . core . commands import Context , Cog
from redbot . core . utils . chat_formatting import *
2022-01-17 12:28:45 +13:00
from redbot . core . utils . menus import menu , DEFAULT_CONTROLS
2021-09-03 16:25:34 +12:00
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<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 )
class Subscriber ( commands . Cog ) :
"""
2021-12-07 10:43:05 +13:00
Automates subscriptions to roles to make donators and other roles easier to manage .
2021-09-03 16:25:34 +12:00
"""
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 " <t: { int ( end_date . timestamp ( ) ) } > " ,
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 )
2022-01-17 12:28:45 +13:00
if rm_members :
for mem in rm_members :
members . remove ( mem )
await self . config . guild ( guild ) . subscribers . set ( members )
2021-09-03 16:25:34 +12:00
# 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 } `. \n The subscription will end on <t: { int ( end_time . timestamp ( ) ) } > "
)
)
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 ( )
2022-01-17 12:28:45 +13:00
@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 } `: <t: { int ( end_date ) } > \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 )
2021-09-03 16:25:34 +12:00
@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 <t: { int ( end_date . timestamp ( ) ) } > " )
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 (
2022-01-17 12:31:45 +13:00
self ,
* ,
requester : Literal [ " discord_deleted_user " , " owner " , " user " , " user_strict " ] ,
user_id : int ,
2021-09-03 16:25:34 +12:00
) :
pass