2020-01-30 21:10:04 +13:00
from __future__ import annotations
import contextlib
import asyncio
import re
import time
from abc import ABCMeta
2021-02-06 21:40:00 +13:00
from typing import AsyncIterator , Tuple , Optional , Union , List , Dict , Literal
2020-01-30 21:10:04 +13:00
import discord
from discord . ext . commands import CogMeta as DPYCogMeta
2023-01-26 09:44:54 +13:00
from redbot . core import checks , commands , bank , modlog
2020-01-30 21:10:04 +13:00
from redbot . core . config import Config
2023-01-26 09:44:54 +13:00
from redbot . core . utils . chat_formatting import (
box ,
pagify ,
warning ,
humanize_list ,
error ,
info ,
)
from redbot . core . utils . predicates import MessagePredicate
from dateutil import parser
from datetime import datetime
2020-01-30 21:10:04 +13:00
from . events import EventMixin
2020-02-09 08:53:01 +13:00
from . exceptions import (
RoleManagementException ,
PermissionOrHierarchyException ,
MissingRequirementsException ,
ConflictingRoleException ,
)
2020-01-30 21:10:04 +13:00
from . massmanager import MassManagementMixin
from . utils import UtilMixin , variation_stripper_re , parse_timedelta , parse_seconds
try :
from redbot . core . commands import GuildContext
except ImportError :
from redbot . core . commands import Context as GuildContext # type: ignore
# This previously used ``(type(commands.Cog), type(ABC))``
# This was changed to be explicit so that mypy
# would be slightly happier about it.
# This does introduce a potential place this
# can break in the future, but this would be an
# Upstream breaking change announced in advance
class CompositeMetaClass ( DPYCogMeta , ABCMeta ) :
"""
This really only exists because of mypy
wanting mixins to be individually valid classes .
"""
pass # MRO is fine on __new__ with super() use
# no need to manually ensure both get handled here.
2020-01-30 21:15:53 +13:00
2020-01-30 21:10:04 +13:00
MIN_SUB_TIME = 3600
SLEEP_TIME = 300
2020-02-09 08:52:28 +13:00
MAX_EMBED = 25
2020-01-30 21:15:53 +13:00
2020-02-09 08:53:01 +13:00
2020-01-30 21:10:04 +13:00
class RoleManagement (
2020-10-01 07:50:29 +13:00
UtilMixin ,
MassManagementMixin ,
EventMixin ,
commands . Cog ,
metaclass = CompositeMetaClass ,
2020-01-30 21:10:04 +13:00
) :
"""
Cog for role management
"""
2023-01-26 09:44:54 +13:00
__author__ = " mikeshardmind(Sinbad), DiscordLiz, Brandons209 "
__version__ = " 325.0.0 "
2020-01-30 21:10:04 +13:00
def format_help_for_context ( self , ctx ) :
pre_processed = super ( ) . format_help_for_context ( ctx )
return f " { pre_processed } \n Cog Version: { self . __version__ } "
def __init__ ( self , bot ) :
self . bot = bot
2023-01-26 09:44:54 +13:00
self . config = Config . get_conf (
self , identifier = 78631113035100160 , force_registration = True
)
self . config . register_global (
handled_variation = False , handled_full_str_emoji = False
)
2020-01-30 21:10:04 +13:00
self . config . register_role (
2020-02-09 08:52:28 +13:00
exclusive_to = { } ,
2020-01-30 21:10:04 +13:00
requires_any = [ ] ,
requires_all = [ ] ,
2020-06-26 10:50:29 +12:00
add_with = [ ] ,
2020-01-30 21:10:04 +13:00
sticky = False ,
self_removable = False ,
self_role = False ,
protected = False ,
cost = 0 ,
subscription = 0 ,
2020-01-30 21:15:53 +13:00
subscribed_users = { } ,
2020-02-02 16:45:15 +13:00
dm_msg = None ,
2023-01-26 09:44:54 +13:00
age_verification = None ,
2020-01-30 21:15:53 +13:00
) # subscribed_users maps str(user.id)-> end time in unix timestamp
2023-01-26 09:44:54 +13:00
self . config . register_member ( roles = [ ] , forbidden = [ ] , birthday = None )
2023-02-02 10:42:00 +13:00
self . config . register_user ( birthday = None )
2020-01-30 21:10:04 +13:00
self . config . init_custom ( " REACTROLE " , 2 )
self . config . register_custom (
" REACTROLE " , roleid = None , channelid = None , guildid = None
) # ID : Message.id, str(React)
2023-01-26 09:44:54 +13:00
self . config . register_guild (
notify_channel = None , s_roles = [ ] , free_roles = [ ] , join_roles = [ ] , age_log = False
)
2020-01-30 21:10:04 +13:00
self . _ready = asyncio . Event ( )
self . _start_task : Optional [ asyncio . Task ] = None
self . loop = asyncio . get_event_loop ( )
self . _sub_task = self . loop . create_task ( self . sub_checker ( ) )
2020-01-31 16:21:49 +13:00
# remove selfrole commands since we are going to override them
self . bot . remove_command ( " selfrole " )
2020-01-30 21:10:04 +13:00
super ( ) . __init__ ( )
def cog_unload ( self ) :
if self . _start_task :
self . _start_task . cancel ( )
if self . _sub_task :
self . _sub_task . cancel ( )
def init ( self ) :
self . _start_task = asyncio . create_task ( self . initialization ( ) )
self . _start_task . add_done_callback ( lambda f : f . result ( ) )
async def initialization ( self ) :
data : Dict [ str , Dict [ str , Dict [ str , Union [ int , bool , List [ int ] ] ] ] ]
await self . bot . wait_until_red_ready ( )
if not await self . config . handled_variation ( ) :
data = await self . config . custom ( " REACTROLE " ) . all ( )
to_adjust = { }
for message_id , emojis_to_data in data . items ( ) :
for emoji_key in emojis_to_data :
new_key , c = variation_stripper_re . subn ( " " , emoji_key )
if c :
to_adjust [ ( message_id , emoji_key ) ] = new_key
for ( message_id , emoji_key ) , new_key in to_adjust . items ( ) :
data [ message_id ] [ new_key ] = data [ message_id ] [ emoji_key ]
data [ message_id ] . pop ( emoji_key , None )
await self . config . custom ( " REACTROLE " ) . set ( data )
await self . config . handled_variation . set ( True )
if not await self . config . handled_full_str_emoji ( ) :
data = await self . config . custom ( " REACTROLE " ) . all ( )
to_adjust = { }
pattern = re . compile ( r " ^(<?a?:)?([A-Za-z0-9_]+):([0-9]+)( \ :?>?)$ " )
# Am not a fan....
for message_id , emojis_to_data in data . items ( ) :
for emoji_key in emojis_to_data :
new_key , c = pattern . subn ( r " \ 3 " , emoji_key )
if c :
to_adjust [ ( message_id , emoji_key ) ] = new_key
for ( message_id , emoji_key ) , new_key in to_adjust . items ( ) :
data [ message_id ] [ new_key ] = data [ message_id ] [ emoji_key ]
data [ message_id ] . pop ( emoji_key , None )
await self . config . custom ( " REACTROLE " ) . set ( data )
await self . config . handled_full_str_emoji . set ( True )
2023-01-26 09:44:54 +13:00
# register casetype for age
age_case = {
" name " : " Date of Birth Added " ,
" default_setting " : True ,
" image " : " 🧓 " ,
" case_str " : " Date of Birth Added " ,
}
try :
await modlog . register_casetypes ( [ age_case ] )
except RuntimeError :
pass
2023-02-02 10:42:00 +13:00
# fix moving birthdays to user config from member config
for guild in self . bot . guilds :
for member in guild . members :
m_age = await self . config . member ( member ) . birthday ( )
u_age = await self . config . user ( member ) . birthday ( )
if m_age and u_age is None :
await self . config . user ( member ) . birthday . set ( m_age )
await self . config . member ( member ) . birthday . set ( None )
2020-01-30 21:10:04 +13:00
self . _ready . set ( )
async def wait_for_ready ( self ) :
await self . _ready . wait ( )
async def cog_before_invoke ( self , ctx ) :
await self . wait_for_ready ( )
if ctx . guild :
await self . maybe_update_guilds ( ctx . guild )
# makes it a bit more readable
async def sub_helper ( self , guild , role , role_data ) :
for user_id in list ( role_data [ " subscribed_users " ] . keys ( ) ) :
end_time = role_data [ " subscribed_users " ] [ user_id ]
now_time = time . time ( )
if end_time < = now_time :
member = guild . get_member ( int ( user_id ) )
2020-01-30 21:15:53 +13:00
if not member : # clean absent members
2020-01-30 21:10:04 +13:00
del role_data [ " subscribed_users " ] [ user_id ]
continue
2020-02-09 08:52:28 +13:00
# make sure they still have the role
if role not in member . roles :
del role_data [ " subscribed_users " ] [ user_id ]
continue
2020-01-30 21:10:04 +13:00
# charge user
2020-05-08 11:51:55 +12:00
raw_cost = await self . config . role ( role ) . cost ( )
cost = await self . get_cost ( member , role )
2020-01-30 21:10:04 +13:00
currency_name = await bank . get_currency_name ( guild )
curr_sub = await self . config . role ( role ) . subscription ( )
2020-05-08 11:51:55 +12:00
if raw_cost == 0 or curr_sub == 0 :
2020-01-30 21:10:04 +13:00
# role is free now or sub is removed, remove stale sub
del role_data [ " subscribed_users " ] [ user_id ]
continue
2020-05-08 11:51:55 +12:00
if cost == 0 :
continue
2020-01-30 21:10:04 +13:00
msg = f " Hello! You are being charged { cost } { currency_name } for your subscription to the { role . name } role in { guild . name } . "
try :
await bank . withdraw_credits ( member , cost )
msg + = f " \n \n No further action is required! You ' ll be charged again in { parse_seconds ( curr_sub ) } . "
role_data [ " subscribed_users " ] [ user_id ] = now_time + curr_sub
2020-01-30 21:15:53 +13:00
except ValueError : # user is poor
2020-01-30 21:10:04 +13:00
msg + = f " \n \n However, you do not have enough { currency_name } to cover the subscription. The role will be removed. "
await self . update_roles_atomically ( who = member , remove = [ role ] )
del role_data [ " subscribed_users " ] [ user_id ]
try :
await member . send ( msg )
except :
# trys to send in system channel, if that fails then
# send message in first channel bot can speak in
channel = guild . system_channel
msg + = f " \n \n { member . mention } make sure to allow receiving DM ' s from server members so I can DM you this message! "
if channel . permissions_for ( channel . guild . me ) . send_messages :
await channel . send ( msg )
else :
for channel in guild . text_channels :
if channel . permissions_for ( channel . guild . me ) . send_messages :
await channel . send ( msg )
break
return role_data
async def sub_checker ( self ) :
await self . wait_for_ready ( )
while True :
await asyncio . sleep ( SLEEP_TIME )
for guild in self . bot . guilds :
async with self . config . guild ( guild ) . s_roles ( ) as s_roles :
for role_id in reversed ( s_roles ) :
role = guild . get_role ( role_id )
2020-01-30 21:15:53 +13:00
if not role : # clean stale subs if role is deleted
2020-01-30 21:10:04 +13:00
s_roles . remove ( role_id )
continue
role_data = await self . config . role ( role ) . all ( )
role_data = await self . sub_helper ( guild , role , role_data )
2023-01-26 09:44:54 +13:00
await self . config . role ( role ) . subscribed_users . set (
role_data [ " subscribed_users " ]
)
2020-01-30 21:10:04 +13:00
if len ( role_data [ " subscribed_users " ] ) == 0 :
s_roles . remove ( role_id )
2020-05-08 11:51:55 +12:00
async def get_cost ( self , member : discord . Member , role : discord . Role ) :
2021-06-15 08:17:13 +12:00
""" Gets cost of a role for a user """
2020-05-08 11:51:55 +12:00
cost = await self . config . role ( role ) . cost ( )
free_roles = await self . config . guild ( member . guild ) . free_roles ( )
for m_role in member . roles :
if m_role . id in free_roles :
return 0
return cost
2023-01-26 09:44:54 +13:00
@commands.guild_only ( )
@commands.bot_has_permissions ( manage_roles = True )
@checks.admin_or_permissions ( manage_roles = True )
@commands.command ( )
async def changeage ( self , ctx : GuildContext , member : discord . Member , birthday : str ) :
"""
Manually update the birthday of a user .
"""
try :
dob = parser . parse ( birthday )
except :
await ctx . send ( error ( " Invalid date format! " ) , delete_after = 30 )
return
if dob . year == datetime . now ( ) . year :
await ctx . send (
error (
f " Invalid date format, please make sure to include your birth year. "
) ,
delete_after = 30 ,
)
return
2023-02-02 10:42:00 +13:00
await self . config . user ( member ) . birthday . set ( dob . strftime ( " % m/ %d / % Y " ) )
2023-01-26 09:44:54 +13:00
await ctx . tick ( )
2020-01-30 21:10:04 +13:00
@commands.guild_only ( )
@commands.bot_has_permissions ( manage_roles = True )
@checks.admin_or_permissions ( manage_roles = True )
@commands.command ( name = " hackrole " )
async def hackrole ( self , ctx : GuildContext , user_id : int , * , role : discord . Role ) :
"""
Puts a stickyrole on someone not in the server .
"""
if not await self . all_are_valid_roles ( ctx , role ) :
2023-01-26 09:44:54 +13:00
return await ctx . maybe_send_embed (
" Can ' t do that. Discord role heirarchy applies here. "
)
2020-01-30 21:10:04 +13:00
if not await self . config . role ( role ) . sticky ( ) :
return await ctx . send ( " This only works on sticky roles. " )
member = ctx . guild . get_member ( user_id )
if member :
try :
await self . update_roles_atomically ( who = member , give = [ role ] )
except PermissionOrHierarchyException :
await ctx . send ( " Can ' t, somehow " )
else :
await ctx . maybe_send_embed ( " They are in the guild...assigned anyway. " )
else :
2023-01-26 09:44:54 +13:00
async with self . config . member_from_ids (
ctx . guild . id , user_id
) . roles ( ) as sticky :
2020-01-30 21:10:04 +13:00
if role . id not in sticky :
sticky . append ( role . id )
await ctx . tick ( )
@checks.is_owner ( )
@commands.command ( name = " rrcleanup " , hidden = True )
async def rolemanagementcleanup ( self , ctx : GuildContext ) :
2021-06-15 08:17:13 +12:00
""" :eyes: """
2020-01-30 21:10:04 +13:00
data = await self . config . custom ( " REACTROLE " ) . all ( )
key_data = { }
for maybe_message_id , maybe_data in data . items ( ) :
try :
message_id = int ( maybe_message_id )
except ValueError :
continue
ex_keys = list ( maybe_data . keys ( ) )
if not ex_keys :
continue
message = None
channel_id = maybe_data [ ex_keys [ 0 ] ] [ " channelid " ]
channel = ctx . bot . get_channel ( channel_id )
if channel :
with contextlib . suppress ( discord . HTTPException ) :
assert isinstance ( channel , discord . TextChannel ) # nosec
message = await channel . fetch_message ( message_id )
if not message :
key_data . update ( { maybe_message_id : ex_keys } )
for mid , keys in key_data . items ( ) :
for k in keys :
await self . config . custom ( " REACTROLE " , mid , k ) . clear ( )
await ctx . tick ( )
@commands.guild_only ( )
@commands.bot_has_permissions ( manage_roles = True )
@checks.admin_or_permissions ( manage_guild = True )
@commands.command ( name = " rolebind " )
async def bind_role_to_reactions (
2020-10-01 07:50:29 +13:00
self ,
ctx : GuildContext ,
role : discord . Role ,
channel : discord . TextChannel ,
msgid : int ,
emoji : str ,
2020-01-30 21:10:04 +13:00
) :
"""
Binds a role to a reaction on a message . . .
The role is only given if the criteria for it are met .
Make sure you configure the other settings for a role in [ p ] roleset
"""
if not await self . all_are_valid_roles ( ctx , role ) :
2023-01-26 09:44:54 +13:00
return await ctx . maybe_send_embed (
" Can ' t do that. Discord role heirarchy applies here. "
)
2020-01-30 21:10:04 +13:00
try :
message = await channel . fetch_message ( msgid )
except discord . HTTPException :
return await ctx . maybe_send_embed ( " No such message " )
_emoji : Optional [ Union [ discord . Emoji , str ] ]
_emoji = discord . utils . find ( lambda e : str ( e ) == emoji , self . bot . emojis )
if _emoji is None :
try :
await ctx . message . add_reaction ( emoji )
except discord . HTTPException :
return await ctx . maybe_send_embed ( " No such emoji " )
else :
_emoji = emoji
eid = self . strip_variations ( emoji )
else :
eid = str ( _emoji . id )
if not any ( str ( r ) == emoji for r in message . reactions ) :
try :
await message . add_reaction ( _emoji )
except discord . HTTPException :
2023-01-26 09:44:54 +13:00
return await ctx . maybe_send_embed (
" Hmm, that message couldn ' t be reacted to "
)
2020-01-30 21:10:04 +13:00
cfg = self . config . custom ( " REACTROLE " , str ( message . id ) , eid )
await cfg . set (
2020-10-01 07:50:29 +13:00
{
" roleid " : role . id ,
" channelid " : message . channel . id ,
" guildid " : role . guild . id ,
}
2020-01-30 21:10:04 +13:00
)
await ctx . send (
f " Remember, the reactions only function according to "
f " the rules set for the roles using ` { ctx . prefix } roleset` " ,
delete_after = 30 ,
)
@commands.guild_only ( )
@commands.bot_has_permissions ( manage_roles = True )
@checks.admin_or_permissions ( manage_guild = True )
@commands.command ( name = " roleunbind " )
2023-01-26 09:44:54 +13:00
async def unbind_role_from_reactions (
self , ctx : commands . Context , role : discord . Role , msgid : int , emoji : str
) :
2020-01-30 21:10:04 +13:00
"""
unbinds a role from a reaction on a message
"""
if not await self . all_are_valid_roles ( ctx , role ) :
2023-01-26 09:44:54 +13:00
return await ctx . maybe_send_embed (
" Can ' t do that. Discord role heirarchy applies here. "
)
2020-01-30 21:10:04 +13:00
2023-01-26 09:44:54 +13:00
await self . config . custom (
" REACTROLE " , f " { msgid } " , self . strip_variations ( emoji )
) . clear ( )
2020-01-30 21:10:04 +13:00
await ctx . tick ( )
@commands.guild_only ( )
@commands.bot_has_permissions ( manage_roles = True )
@checks.admin_or_permissions ( manage_guild = True )
@commands.group ( name = " roleset " , autohelp = True )
async def rgroup ( self , ctx : GuildContext ) :
"""
Settings for role requirements
"""
pass
2023-01-26 09:44:54 +13:00
@rgroup.command ( name = " age " )
async def rg_age_verification ( self , ctx , role : discord . Role , age : int ) :
"""
Add an age verification requirement for a role
Users will have to give the bot their birthdate in order to obtain a role .
An age of 0 will disable age verification .
"""
if age == 0 :
await self . config . role ( role ) . age_verification . set ( None )
elif age < 0 :
await ctx . send ( error ( " Age must be greater than or equal to 0. " ) )
return
else :
await self . config . role ( role ) . age_verification . set ( age )
await ctx . tick ( )
@rgroup.command ( name = " agelog " )
async def rg_age_verification_log ( self , ctx , log : bool ) :
"""
Log age verification to modlog
"""
await self . config . guild ( ctx . guild ) . age_log . set ( log )
await ctx . tick ( )
2020-06-26 10:50:29 +12:00
@rgroup.command ( name = " addwith " )
2023-01-26 09:44:54 +13:00
async def rg_addwith (
self , ctx : GuildContext , add_role : discord . Role , * roles : discord . Role
) :
2020-06-26 10:50:29 +12:00
"""
Sets a list of roles to add to a user when they receive
the role specifed by ` add_role `
Leave roles empty to clear add_with roles .
Roles with spaces in the name should be put in quotes
"""
current = await self . config . role ( add_role ) . add_with ( )
2020-06-26 16:21:16 +12:00
current = [ discord . utils . get ( ctx . guild . roles , id = r ) for r in current ]
2020-06-26 10:50:29 +12:00
await self . config . role ( add_role ) . add_with . set ( [ r . id for r in roles ] )
if not roles and current :
await ctx . send ( f " Add with roles cleared from: ` { humanize_list ( current ) } ` " )
elif not roles and not current :
await ctx . send ( " No roles originally defined. " )
else :
2020-06-26 16:21:16 +12:00
await ctx . send (
f " Add with roles set to ` { humanize_list ( [ r . name for r in roles ] ) } ` from ` { humanize_list ( current ) if current else None } ` "
)
2020-06-26 10:50:29 +12:00
2020-01-30 21:10:04 +13:00
@rgroup.command ( name = " viewreactions " )
async def rg_view_reactions ( self , ctx : GuildContext ) :
"""
View the reactions enabled for the server
"""
# This design is intentional for later extention to view this per role
use_embeds = await ctx . embed_requested ( )
react_roles = " \n " . join (
2023-01-26 09:44:54 +13:00
[
msg
async for msg in self . build_messages_for_react_roles (
* ctx . guild . roles , use_embeds = use_embeds
)
]
2020-01-30 21:10:04 +13:00
)
if not react_roles :
return await ctx . send ( " No react roles bound here. " )
# ctx.send is already going to escape said mentions if any somehow get generated
# should also not be possible to do so without willfully being done by an admin.
color = await ctx . embed_colour ( ) if use_embeds else None
2023-01-26 09:44:54 +13:00
for page in pagify (
react_roles , escape_mass_mentions = False , page_length = 1800 , shorten_by = 0
) :
2020-01-30 21:10:04 +13:00
# unrolling iterative calling of ctx.maybe_send_embed
if use_embeds :
await ctx . send ( embed = discord . Embed ( description = page , color = color ) )
else :
await ctx . send ( page )
2020-02-02 16:45:15 +13:00
@rgroup.command ( name = " dm-message " )
2023-01-26 09:44:54 +13:00
async def rg_dm_message (
self , ctx : GuildContext , role : discord . Role , * , msg : str = None
) :
2020-02-02 16:45:15 +13:00
"""
Set message to DM to user when they obtain the role .
Will send it in the channel they ran the command if DM fails to send .
Run with no message to get the current message of the role .
Set message to message_clear to clear the message for the role .
"""
if not msg :
curr = await self . config . role ( role ) . dm_msg ( )
if not curr :
await ctx . send ( " No message set for that role. " )
else :
await ctx . send ( curr )
return
elif msg . lower ( ) == " message_clear " :
await self . config . role ( role ) . dm_msg . set ( None )
await ctx . tick ( )
return
await self . config . role ( role ) . dm_msg . set ( msg )
await ctx . tick ( )
2020-02-09 08:52:28 +13:00
@rgroup.group ( name = " join " )
async def join_roles ( self , ctx : GuildContext ) :
"""
Set roles to add to users on join .
"""
pass
@join_roles.command ( name = " add " )
async def join_roles_add ( self , ctx : GuildContext , * , role : discord . Role ) :
"""
Add a role to the join list .
"""
async with self . config . guild ( ctx . guild ) . join_roles ( ) as join_roles :
if role . id not in join_roles :
join_roles . append ( role . id )
await ctx . tick ( )
@join_roles.command ( name = " rem " )
async def join_roles_rem ( self , ctx : GuildContext , * , role : discord . Role ) :
"""
Remove a role from the join list .
"""
async with self . config . guild ( ctx . guild ) . join_roles ( ) as join_roles :
try :
join_roles . remove ( role . id )
except :
await ctx . send ( " Role not in join list! " )
return
await ctx . tick ( )
@join_roles.command ( name = " list " )
async def join_roles_list ( self , ctx : GuildContext ) :
"""
List join roles .
"""
roles = await self . config . guild ( ctx . guild ) . join_roles ( )
if not roles :
await ctx . send ( " No roles defined. " )
return
roles = [ ctx . guild . get_role ( role ) for role in roles ]
missing = len ( [ role for role in roles if role is None ] )
2023-01-26 09:44:54 +13:00
roles = [
f " { i + 1 } . { role . name } " for i , role in enumerate ( roles ) if role is not None
]
2020-02-09 08:52:28 +13:00
msg = " \n " . join ( sorted ( roles ) )
msg = pagify ( msg )
for m in msg :
await ctx . send ( box ( m ) )
2020-01-30 21:10:04 +13:00
@rgroup.command ( name = " viewrole " )
async def rg_view_role ( self , ctx : GuildContext , * , role : discord . Role ) :
"""
Views the current settings for a role
"""
rsets = await self . config . role ( role ) . all ( )
output = (
f " This role: \n { ' is ' if rsets [ ' self_role ' ] else ' is not ' } self assignable "
f " \n { ' is ' if rsets [ ' self_removable ' ] else ' is not ' } self removable "
f " \n { ' is ' if rsets [ ' sticky ' ] else ' is not ' } sticky. "
)
if rsets [ " requires_any " ] :
2023-01-26 09:44:54 +13:00
rstring = " , " . join (
r . name for r in ctx . guild . roles if r . id in rsets [ " requires_any " ]
)
2020-01-30 21:10:04 +13:00
output + = f " \n This role requires any of the following roles: { rstring } "
if rsets [ " requires_all " ] :
2023-01-26 09:44:54 +13:00
rstring = " , " . join (
r . name for r in ctx . guild . roles if r . id in rsets [ " requires_all " ]
)
2020-01-30 21:10:04 +13:00
output + = f " \n This role requires all of the following roles: { rstring } "
2020-06-26 10:50:29 +12:00
if rsets [ " add_with " ] :
2023-01-26 09:44:54 +13:00
rstring = " , " . join (
r . name for r in ctx . guild . roles if r . id in rsets [ " add_with " ]
)
2020-06-26 10:50:29 +12:00
output + = f " \n This role when added will also be added with the following roles: { rstring } "
2020-01-30 21:10:04 +13:00
if rsets [ " exclusive_to " ] :
2020-02-09 08:52:28 +13:00
rstring = " "
for group , roles in rsets [ " exclusive_to " ] . items ( ) :
rstring = f " ` { group } `: "
rstring + = " , " . join ( r . name for r in ctx . guild . roles if r . id in roles )
rstring + = " \n "
output + = f " \n This role is mutually exclusive to the following role groups: \n { rstring } "
2020-01-30 21:10:04 +13:00
if rsets [ " cost " ] :
curr = await bank . get_currency_name ( ctx . guild )
cost = rsets [ " cost " ]
output + = f " \n This role costs { cost } { curr } "
else :
output + = " \n This role does not have an associated cost. "
if rsets [ " subscription " ] :
s = rsets [ " subscription " ]
output + = f " \n This role has a subscription time of: { parse_seconds ( s ) } "
2020-02-02 20:13:29 +13:00
if rsets [ " dm_msg " ] :
dm_msg = rsets [ " dm_msg " ]
output + = f " \n DM Message: { box ( dm_msg ) } "
2023-01-26 09:44:54 +13:00
if rsets [ " age_verification " ] :
age = rsets [ " age_verification " ]
output + = f " \n Minimum Age: ` { age } ` "
2020-01-30 21:10:04 +13:00
for page in pagify ( output ) :
await ctx . send ( page )
@rgroup.command ( name = " cost " )
2023-01-26 09:44:54 +13:00
async def make_purchasable (
self , ctx : GuildContext , cost : int , * , role : discord . Role
) :
2020-01-30 21:10:04 +13:00
"""
Makes a role purchasable for a specified cost .
Cost must be a number greater than 0.
A cost of exactly 0 can be used to remove purchasability .
Purchase eligibility still follows other rules including self assignable .
Warning : If these roles are bound to a reaction ,
it will be possible to gain these without paying .
"""
if not await self . all_are_valid_roles ( ctx , role ) :
2023-01-26 09:44:54 +13:00
return await ctx . maybe_send_embed (
" Can ' t do that. Discord role heirarchy applies here. "
)
2020-01-30 21:10:04 +13:00
if cost < 0 :
return await ctx . send_help ( )
await self . config . role ( role ) . cost . set ( cost )
if cost == 0 :
await ctx . send ( f " { role . name } is no longer purchasable. " )
else :
await ctx . send ( f " { role . name } is purchasable for { cost } " )
@rgroup.command ( name = " subscription " )
async def subscription ( self , ctx , role : discord . Role , * , interval : str ) :
"""
Sets a role to be a subscription , must set cost first .
Will charge role ' s cost every interval, and remove the role if they run out of money
Set to 0 to disable
* * __Minimum subscription duration is 1 hour__ * *
Intervals look like :
5 minutes
1 minute 30 seconds
1 hour
2 days
30 days
5 h30m
( etc )
"""
if not await self . all_are_valid_roles ( ctx , role ) :
2023-01-26 09:44:54 +13:00
return await ctx . maybe_send_embed (
" Can ' t do that. Discord role heirarchy applies here. "
)
2020-01-30 21:10:04 +13:00
role_cost = await self . config . role ( role ) . cost ( )
if role_cost == 0 :
2020-06-28 21:14:02 +12:00
await ctx . send ( warning ( " Please set a cost for the role first. " ) )
2020-01-30 21:10:04 +13:00
return
time = parse_timedelta ( interval )
if int ( time . total_seconds ( ) ) == 0 :
await ctx . send ( " Subscription removed. " )
async with self . config . guild ( ctx . guild ) . s_roles ( ) as s :
s . remove ( role . id )
return
elif int ( time . total_seconds ( ) ) < MIN_SUB_TIME :
await ctx . send ( " Subscriptions must be 1 hour or longer. " )
return
await self . config . role ( role ) . subscription . set ( int ( time . total_seconds ( ) ) )
async with self . config . guild ( ctx . guild ) . s_roles ( ) as s :
s . append ( role . id )
await ctx . send ( f " Subscription set to { parse_seconds ( time . total_seconds ( ) ) } . " )
@rgroup.command ( name = " forbid " )
2023-01-26 09:44:54 +13:00
async def forbid_role (
self , ctx : GuildContext , role : discord . Role , * , user : discord . Member
) :
2020-01-30 21:10:04 +13:00
"""
Forbids a user from gaining a specific role .
"""
async with self . config . member ( user ) . forbidden ( ) as fb :
if role . id not in fb :
fb . append ( role . id )
else :
await ctx . send ( " Role was already forbidden " )
await ctx . tick ( )
@rgroup.command ( name = " unforbid " )
2023-01-26 09:44:54 +13:00
async def unforbid_role (
self , ctx : GuildContext , role : discord . Role , * , user : discord . Member
) :
2020-01-30 21:10:04 +13:00
"""
Unforbids a user from gaining a specific role .
"""
async with self . config . member ( user ) . forbidden ( ) as fb :
if role . id in fb :
fb . remove ( role . id )
else :
await ctx . send ( " Role was not forbidden " )
await ctx . tick ( )
@rgroup.command ( name = " exclusive " )
2023-01-26 09:44:54 +13:00
async def set_exclusivity (
self , ctx : GuildContext , group : str , * roles : discord . Role
) :
2020-01-30 21:10:04 +13:00
"""
2020-02-09 08:52:28 +13:00
Set exclusive roles for group
2020-01-30 21:10:04 +13:00
Takes 2 or more roles and sets them as exclusive to eachother
2020-02-09 08:52:28 +13:00
The group can be any name , use spaces for names with spaces .
Groups will show up in role list etc .
2020-01-30 21:10:04 +13:00
"""
_roles = set ( roles )
if len ( _roles ) < 2 :
return await ctx . send ( " You need to provide at least 2 roles " )
for role in _roles :
async with self . config . role ( role ) . exclusive_to ( ) as ex_list :
2020-02-09 08:52:28 +13:00
if group not in ex_list . keys ( ) :
ex_list [ group ] = [ ]
2023-01-26 09:44:54 +13:00
ex_list [ group ] . extend (
[ r . id for r in _roles if r != role and r . id not in ex_list [ group ] ]
)
2020-02-09 08:52:28 +13:00
2020-01-30 21:10:04 +13:00
await ctx . tick ( )
@rgroup.command ( name = " unexclusive " )
2023-01-26 09:44:54 +13:00
async def unset_exclusivity (
self , ctx : GuildContext , group : str , * roles : discord . Role
) :
2020-01-30 21:10:04 +13:00
"""
2020-02-09 08:52:28 +13:00
Remove exclusive roles for group
2020-01-30 21:10:04 +13:00
Takes any number of roles , and removes their exclusivity settings
2020-02-09 08:52:28 +13:00
The group can be any name , use spaces for names with spaces .
If all roles are removed from a group then
2020-01-30 21:10:04 +13:00
"""
_roles = set ( roles )
if not _roles :
return await ctx . send ( " You need to provide at least a role to do this to " )
for role in _roles :
ex_list = await self . config . role ( role ) . exclusive_to ( )
2020-02-09 08:52:28 +13:00
if group not in ex_list . keys ( ) :
continue
2023-01-26 09:44:54 +13:00
ex_list [ group ] = [
idx for idx in ex_list if idx not in [ r . id for r in _roles ]
]
2020-02-09 08:52:28 +13:00
if not ex_list [ group ] :
del ex_list [ group ]
2020-01-30 21:10:04 +13:00
await self . config . role ( role ) . exclusive_to . set ( ex_list )
await ctx . tick ( )
@rgroup.command ( name = " sticky " )
2023-01-26 09:44:54 +13:00
async def setsticky (
self , ctx : GuildContext , role : discord . Role , sticky : bool = None
) :
2020-01-30 21:10:04 +13:00
"""
sets a role as sticky if used without a settings , gets the current ones
"""
if sticky is None :
is_sticky = await self . config . role ( role ) . sticky ( )
2023-01-26 09:44:54 +13:00
return await ctx . send (
" {role} {verb} sticky " . format (
role = role . name , verb = ( " is " if is_sticky else " is not " )
)
)
2020-01-30 21:10:04 +13:00
await self . config . role ( role ) . sticky . set ( sticky )
if sticky :
for m in role . members :
async with self . config . member ( m ) . roles ( ) as rids :
if role . id not in rids :
rids . append ( role . id )
await ctx . tick ( )
@rgroup.command ( name = " requireall " )
async def reqall ( self , ctx : GuildContext , role : discord . Role , * roles : discord . Role ) :
"""
Sets the required roles to gain a role
Takes a role plus zero or more other roles ( as requirements for the first )
"""
rids = [ r . id for r in roles ]
await self . config . role ( role ) . requires_all . set ( rids )
await ctx . tick ( )
@rgroup.command ( name = " requireany " )
async def reqany ( self , ctx : GuildContext , role : discord . Role , * roles : discord . Role ) :
"""
Sets a role to require already having one of another
Takes a role plus zero or more other roles ( as requirements for the first )
"""
rids = [ r . id for r in ( roles or [ ] ) ]
await self . config . role ( role ) . requires_any . set ( rids )
await ctx . tick ( )
@rgroup.command ( name = " selfrem " )
2023-01-26 09:44:54 +13:00
async def selfrem (
self , ctx : GuildContext , role : discord . Role , removable : bool = None
) :
2020-01-30 21:10:04 +13:00
"""
Sets if a role is self - removable ( default False )
use without a setting to view current
"""
if removable is None :
is_removable = await self . config . role ( role ) . self_removable ( )
return await ctx . send (
2023-01-26 09:44:54 +13:00
" {role} {verb} self-removable " . format (
role = role . name , verb = ( " is " if is_removable else " is not " )
)
2020-01-30 21:10:04 +13:00
)
await self . config . role ( role ) . self_removable . set ( removable )
await ctx . tick ( )
@rgroup.command ( name = " selfadd " )
2023-01-26 09:44:54 +13:00
async def selfadd (
self , ctx : GuildContext , role : discord . Role , assignable : bool = None
) :
2020-01-30 21:10:04 +13:00
"""
Sets if a role is self - assignable via command
( default False )
use without a setting to view current
"""
if assignable is None :
is_assignable = await self . config . role ( role ) . self_role ( )
return await ctx . send (
2023-01-26 09:44:54 +13:00
" {role} {verb} self-assignable " . format (
role = role . name , verb = ( " is " if is_assignable else " is not " )
)
2020-01-30 21:10:04 +13:00
)
await self . config . role ( role ) . self_role . set ( assignable )
await ctx . tick ( )
@rgroup.group ( name = " freerole " )
async def free_roles ( self , ctx : GuildContext ) :
"""
Sets roles that bypass costs for purchasing roles in your guild .
"""
pass
@free_roles.command ( name = " add " )
async def free_roles_add ( self , ctx : GuildContext , * , role : discord . Role ) :
"""
Add a role to the free list .
"""
async with self . config . guild ( ctx . guild ) . free_roles ( ) as free_roles :
if role . id not in free_roles :
free_roles . append ( role . id )
await ctx . tick ( )
@free_roles.command ( name = " rem " )
async def free_roles_rem ( self , ctx : GuildContext , * , role : discord . Role ) :
"""
Remove a role from the free list .
"""
async with self . config . guild ( ctx . guild ) . free_roles ( ) as free_roles :
try :
free_roles . remove ( role . id )
except :
await ctx . send ( " Role not in free list! " )
return
await ctx . tick ( )
@free_roles.command ( name = " list " )
async def free_roles_list ( self , ctx : GuildContext ) :
"""
List free roles .
"""
roles = await self . config . guild ( ctx . guild ) . free_roles ( )
if not roles :
await ctx . send ( " No roles defined. " )
return
roles = [ ctx . guild . get_role ( role ) for role in roles ]
missing = len ( [ role for role in roles if role is None ] )
2023-01-26 09:44:54 +13:00
roles = [
f " { i + 1 } . { role . name } " for i , role in enumerate ( roles ) if role is not None
]
2020-01-30 21:10:04 +13:00
msg = " \n " . join ( sorted ( roles ) )
msg = pagify ( msg )
for m in msg :
await ctx . send ( box ( m ) )
@checks.bot_has_permissions ( manage_roles = True )
@commands.guild_only ( )
2020-01-31 16:21:49 +13:00
@commands.group ( name = " selfrole " , autohelp = True )
async def selfrole ( self , ctx : GuildContext ) :
2020-01-30 21:10:04 +13:00
"""
Self assignable role commands
"""
pass
2020-01-31 16:21:49 +13:00
@selfrole.command ( name = " list " )
async def selfrole_list ( self , ctx : GuildContext ) :
2020-01-30 21:10:04 +13:00
"""
Lists the selfroles and any associated costs .
"""
MYPY = False
if MYPY :
# remove this when mypy supports type narrowing from :=
# It's less efficient, so not removing the actual
# implementation below
data : Dict [ discord . Role , tuple ] = { }
for role_id , vals in ( await self . config . all_roles ( ) ) . items ( ) :
role = ctx . guild . get_role ( role_id )
if role and vals [ " self_role " ] :
data [ role ] = vals [ " cost " ]
else :
data = {
2020-02-09 08:52:28 +13:00
role : ( vals [ " cost " ] , vals [ " subscription " ] , vals [ " exclusive_to " ] )
2020-01-30 21:10:04 +13:00
for role_id , vals in ( await self . config . all_roles ( ) ) . items ( )
if ( role := ctx . guild . get_role ( role_id ) ) and vals [ " self_role " ]
}
if not data :
return await ctx . send ( " There aren ' t any self roles here. " )
embed = discord . Embed ( title = " Roles " , colour = ctx . guild . me . colour )
2020-02-09 15:38:49 +13:00
embed . set_footer ( text = " You can only have one role in the same unique group! " )
2020-01-30 21:10:04 +13:00
i = 0
2023-01-26 09:44:54 +13:00
for role , ( cost , sub , ex_groups ) in sorted (
data . items ( ) , key = lambda kv : kv [ 1 ] [ 0 ]
) :
2020-02-09 08:52:28 +13:00
if ex_groups :
groups = humanize_list ( list ( ex_groups . keys ( ) ) )
else :
groups = None
2020-01-30 21:10:04 +13:00
embed . add_field (
name = f " __** { i + 1 } . { role . name } **__ " ,
2020-02-09 08:52:28 +13:00
value = " %s %s %s "
2020-02-09 08:53:01 +13:00
% (
( f " Cost: { cost } " if cost else " Free " ) ,
( f " , every { parse_seconds ( sub ) } " if sub else " " ) ,
( f " \n unique groups: ` { groups } ` " if groups else " " ) ,
) ,
2020-01-30 21:10:04 +13:00
)
i + = 1
2020-02-09 08:52:28 +13:00
if i % MAX_EMBED == 0 :
2020-01-30 21:10:04 +13:00
await ctx . send ( embed = embed )
2020-02-09 15:38:49 +13:00
embed . clear_fields ( )
2020-01-30 21:10:04 +13:00
2020-02-09 15:38:49 +13:00
if i % MAX_EMBED != 0 :
await ctx . send ( embed = embed )
2020-01-30 21:10:04 +13:00
2020-01-31 16:21:49 +13:00
@selfrole.command ( name = " buy " )
async def selfrole_buy ( self , ctx : GuildContext , * , role : discord . Role ) :
2020-01-30 21:10:04 +13:00
"""
Purchase a role
"""
if role in ctx . author . roles :
await ctx . send ( " You already have that role. " )
return
try :
remove = await self . is_self_assign_eligible ( ctx . author , role )
eligible = await self . config . role ( role ) . self_role ( )
cost = await self . config . role ( role ) . cost ( )
subscription = await self . config . role ( role ) . subscription ( )
except PermissionOrHierarchyException :
2023-01-26 09:44:54 +13:00
await ctx . send (
" I cannot assign roles which I can not manage. (Discord Hierarchy) "
)
2020-02-09 08:52:28 +13:00
except MissingRequirementsException as e :
msg = " "
if e . miss_all :
2020-08-02 07:58:46 +12:00
roles = [ r . name for r in ctx . guild . roles if r . id in e . miss_all ]
2020-02-09 08:52:28 +13:00
msg + = f " You need all of these roles in order to get this role: { humanize_list ( roles ) } \n "
if e . miss_any :
2020-08-02 07:58:46 +12:00
roles = [ r . name for r in ctx . guild . roles if r . id in e . miss_any ]
2020-02-09 08:52:28 +13:00
msg + = f " You need one of these roles in order to get this role: { humanize_list ( roles ) } \n "
await ctx . send ( msg )
except ConflictingRoleException as e :
2020-08-02 07:58:46 +12:00
roles = [ r . name for r in ctx . guild . roles if r . id in e . conflicts ]
2020-02-09 08:52:28 +13:00
plural = " are " if len ( roles ) > 1 else " is "
2020-02-09 08:53:01 +13:00
await ctx . send (
f " You have { humanize_list ( roles ) } , which you are not allowed to remove and { plural } exclusive to: { role . name } "
)
2020-01-30 21:10:04 +13:00
else :
if not eligible :
2023-01-26 09:44:54 +13:00
return await ctx . send (
f " You aren ' t allowed to add ` { role } ` to yourself { ctx . author . mention } ! "
)
2020-01-30 21:10:04 +13:00
if not cost :
2023-01-26 09:44:54 +13:00
return await ctx . send (
" This role doesn ' t have a cost. Please try again using `[p]selfrole add`. "
)
if not await self . verify_age ( role , ctx = ctx ) :
return await ctx . send (
" You do not meet the minimum age requiremnt for this role, if you think this is an error please contact a staff member. "
)
2020-01-30 21:10:04 +13:00
free_roles = await self . config . guild ( ctx . guild ) . free_roles ( )
currency_name = await bank . get_currency_name ( ctx . guild )
for m_role in ctx . author . roles :
if m_role . id in free_roles :
2023-01-26 09:44:54 +13:00
await ctx . send (
f " You ' re special, no { currency_name } will be deducted from your account. "
)
2020-05-08 11:51:55 +12:00
cost = 0
# await self.update_roles_atomically(who=ctx.author, give=[role], remove=remove)
# await ctx.tick()
# return
2020-01-30 21:10:04 +13:00
try :
2020-05-08 11:51:55 +12:00
if cost > 0 :
await bank . withdraw_credits ( ctx . author , cost )
2020-01-30 21:10:04 +13:00
except ValueError :
2023-01-26 09:44:54 +13:00
return await ctx . send (
f " You don ' t have enough { currency_name } (Cost: { cost } { currency_name } ) "
)
2020-01-30 21:10:04 +13:00
else :
if subscription > 0 :
2020-05-08 11:51:55 +12:00
if cost > 0 :
2023-01-26 09:44:54 +13:00
await ctx . send (
f " { role . name } will be renewed every { parse_seconds ( subscription ) } "
)
2020-01-30 21:10:04 +13:00
async with self . config . role ( role ) . subscribed_users ( ) as s :
s [ str ( ctx . author . id ) ] = time . time ( ) + subscription
async with self . config . guild ( ctx . guild ) . s_roles ( ) as s :
if role . id not in s :
s . append ( role . id )
2020-02-09 08:52:28 +13:00
if remove :
plural = " s " if len ( remove ) > 1 else " "
2020-02-09 08:53:01 +13:00
await ctx . send (
f " Removed ` { humanize_list ( [ r . name for r in remove ] ) } ` role { plural } since they are exclusive to the role you added. "
)
2023-01-26 09:44:54 +13:00
await self . update_roles_atomically (
who = ctx . author , give = [ role ] , remove = remove
)
2020-02-02 16:45:15 +13:00
await self . dm_user ( ctx , role )
2020-01-30 21:10:04 +13:00
await ctx . tick ( )
2020-01-31 16:21:49 +13:00
@selfrole.command ( name = " add " )
2020-01-30 21:10:04 +13:00
async def sadd ( self , ctx : GuildContext , * , role : discord . Role ) :
"""
Join a role
"""
if role in ctx . author . roles :
await ctx . send ( " You already have that role. " )
return
try :
remove = await self . is_self_assign_eligible ( ctx . author , role )
eligible = await self . config . role ( role ) . self_role ( )
cost = await self . config . role ( role ) . cost ( )
except PermissionOrHierarchyException :
2023-01-26 09:44:54 +13:00
await ctx . send (
" I cannot assign roles which I can not manage. (Discord Hierarchy) "
)
2020-02-09 08:52:28 +13:00
except MissingRequirementsException as e :
msg = " "
if e . miss_all :
2020-08-02 07:58:46 +12:00
roles = [ r . name for r in ctx . guild . roles if r . id in e . miss_all ]
2020-02-09 08:52:28 +13:00
msg + = f " You need all of these roles in order to get this role: { humanize_list ( roles ) } \n "
if e . miss_any :
2020-08-02 07:58:46 +12:00
roles = [ r . name for r in ctx . guild . roles if r . id in e . miss_any ]
2020-02-09 08:52:28 +13:00
msg + = f " You need one of these roles in order to get this role: { humanize_list ( roles ) } \n "
await ctx . send ( msg )
except ConflictingRoleException as e :
2020-08-02 07:58:46 +12:00
roles = [ r . name for r in ctx . guild . roles if r in e . conflicts ]
2020-02-09 08:52:28 +13:00
plural = " are " if len ( roles ) > 1 else " is "
2020-02-09 08:53:01 +13:00
await ctx . send (
f " You have { humanize_list ( roles ) } , which you are not allowed to remove and { plural } exclusive to: { role . name } "
)
2020-01-30 21:10:04 +13:00
else :
if not eligible :
2023-01-26 09:44:54 +13:00
await ctx . send (
f " You aren ' t allowed to add ` { role } ` to yourself { ctx . author . mention } ! "
)
elif not await self . verify_age ( role , ctx = ctx ) :
return await ctx . send (
" You do not meet the minimum age requiremnt for this role, if you think this is an error please contact a staff member. "
)
2020-01-30 21:10:04 +13:00
elif cost :
2020-02-01 13:37:32 +13:00
await ctx . send (
2023-01-26 09:44:54 +13:00
" This role is not free. "
" Please use `[p]selfrole buy` if you would like to purchase it. "
2020-02-01 13:37:32 +13:00
)
2020-01-30 21:10:04 +13:00
else :
2020-02-09 08:52:28 +13:00
if remove :
plural = " s " if len ( remove ) > 1 else " "
2020-02-09 08:53:01 +13:00
await ctx . send (
f " Removed ` { humanize_list ( [ r . name for r in remove ] ) } ` role { plural } since they are exclusive to the role you added. "
)
2023-01-26 09:44:54 +13:00
await self . update_roles_atomically (
who = ctx . author , give = [ role ] , remove = remove
)
2020-02-02 16:45:15 +13:00
await self . dm_user ( ctx , role )
2020-01-30 21:10:04 +13:00
await ctx . tick ( )
2020-01-31 16:21:49 +13:00
@selfrole.command ( name = " remove " )
2020-01-30 21:10:04 +13:00
async def srem ( self , ctx : GuildContext , * , role : discord . Role ) :
"""
leave a role
"""
if role not in ctx . author . roles :
await ctx . send ( " You do not have that role. " )
return
if await self . config . role ( role ) . self_removable ( ) :
await self . update_roles_atomically ( who = ctx . author , remove = [ role ] )
2020-01-30 21:15:53 +13:00
try : # remove subscription, if any
2020-01-30 21:10:04 +13:00
async with self . config . role ( role ) . subscribed_users ( ) as s :
del s [ str ( ctx . author . id ) ]
except :
pass
await ctx . tick ( )
else :
2023-01-26 09:44:54 +13:00
await ctx . send (
f " You aren ' t allowed to remove ` { role } ` from yourself { ctx . author . mention } !` "
)
2020-01-30 21:10:04 +13:00
# Stuff for clean interaction with react role entries
2023-01-26 09:44:54 +13:00
async def build_messages_for_react_roles (
self , * roles : discord . Role , use_embeds = True
) - > AsyncIterator [ str ] :
2020-01-30 21:10:04 +13:00
"""
Builds info .
Info is suitable for passing to embeds if use_embeds is True
"""
linkfmt = (
" [message # {message_id} ](https://discordapp.com/channels/ {guild_id} / {channel_id} / {message_id} ) "
if use_embeds
else " <https://discordapp.com/channels/ {guild_id} / {channel_id} / {message_id} > "
)
for role in roles :
# pylint: disable=E1133
async for message_id , emoji_info , data in self . get_react_role_entries ( role ) :
channel_id = data . get ( " channelid " , None )
if channel_id :
2020-10-01 07:50:29 +13:00
link = linkfmt . format (
guild_id = role . guild . id ,
channel_id = channel_id ,
message_id = message_id ,
)
2020-01-30 21:10:04 +13:00
else :
link = (
2023-01-26 09:44:54 +13:00
f " unknown message with id { message_id } "
f " (use `roleset fixup` to find missing data for this) "
2020-01-30 21:10:04 +13:00
)
emoji : Union [ discord . Emoji , str ]
if emoji_info . isdigit ( ) :
emoji = (
2023-01-26 09:44:54 +13:00
discord . utils . get ( self . bot . emojis , id = int ( emoji_info ) )
or f " A custom enoji with id { emoji_info } "
2020-01-30 21:10:04 +13:00
)
else :
emoji = emoji_info
react_m = f " { role . name } is bound to { emoji } on { link } "
yield react_m
2023-01-26 09:44:54 +13:00
async def verify_age (
self ,
role : discord . Role ,
ctx : GuildContext = None ,
member : discord . Member = None ,
) - > bool :
"""
Verify age of a user .
Returns True if age is successfully verified , false otherwise .
"""
if ctx is not None :
member = ctx . author
guild = member . guild
2023-02-02 10:42:00 +13:00
dob = await self . config . user ( member ) . birthday ( )
2023-01-26 09:44:54 +13:00
min_age = await self . config . role ( role ) . age_verification ( )
age_log = await self . config . guild ( guild ) . age_log ( )
today = datetime . utcnow ( ) . date ( )
if min_age is None :
return True
if dob is not None :
dob = parser . parse ( dob ) . date ( )
age = today . year - dob . year
# check to see if their birthday has passed
dob = dob . replace ( year = today . year )
if dob > today :
# birthday hasn't passed, subtract one from age
age - = 1
return age > min_age
if ctx is not None :
await ctx . send (
info (
" Please check your DMs with me in order to continue getting this role! "
) ,
delete_after = 30 ,
)
# get dob of user
try :
age_msg = f " Hello! In order to get the ` { role } ` role in ` { guild } `, you must provide your **full date of birth** in order to verify your age. Please send it here. "
await member . send ( age_msg )
pred = MessagePredicate . same_context ( user = member )
msg = await self . bot . wait_for ( " message " , check = pred , timeout = 60 )
except discord . Forbidden : # TODO: send message in guild telling user to allow dms?
return False
except discord . HTTPException :
return False
except asyncio . TimeoutError :
await member . send (
error (
f " Took too long, the { role } role has not been added to you in { guild } ! \n Please try again. "
) ,
delete_after = 30 ,
)
return False
try :
dob = parser . parse ( msg . content . strip ( ) )
except :
await member . send (
error (
f " Invalid date format, the { role } role has not been added to you in { guild } ! \n Please try again. "
) ,
delete_after = 30 ,
)
return False
dob = dob . date ( )
if dob . year == today . year :
await member . send (
error (
f " Invalid date format, please make sure to include your birth year, the { role } role has not been added to you in { guild } ! \n Please try again. "
) ,
delete_after = 30 ,
)
return False
dob_str = dob . strftime ( " % m/ %d / % Y " )
await member . send (
f " Thank you! Please check back in ` { guild } ` to confirm you obtained the role. If not, you may not meet the age requirement for the role. "
)
2023-02-02 10:42:00 +13:00
await self . config . user ( member ) . birthday . set ( dob_str )
2023-01-26 09:44:54 +13:00
if age_log :
try :
await modlog . create_case (
self . bot ,
guild ,
datetime . now ( ) ,
" Date of Birth Added " ,
member ,
moderator = member ,
reason = f " Date of birth added for ` { member } `: ` { dob_str } ` \n \n Role: ` { role } ` " ,
)
except : # TODO: warn staff
pass
age = today . year - dob . year
# check to see if their birthday has passed
dob = dob . replace ( year = today . year )
if dob > today :
# birthday hasn't passed, subtract one from age
age - = 1
return age > min_age
2020-02-02 16:45:15 +13:00
async def dm_user ( self , ctx : GuildContext , role : discord . Role ) :
"""
DM user if dm_msg set for role .
"""
dm_msg = await self . config . role ( role ) . dm_msg ( )
if not dm_msg :
return
try :
await ctx . author . send ( dm_msg )
except :
2020-02-02 16:49:46 +13:00
await ctx . send (
f " Hey { ctx . author . mention } , please allow server members to DM you so I can send you messages! Here is the message for this role: "
)
2020-02-02 16:45:15 +13:00
await ctx . send ( dm_msg )
2023-01-26 09:44:54 +13:00
async def get_react_role_entries (
self , role : discord . Role
) - > AsyncIterator [ Tuple [ str , str , dict ] ] :
2020-01-30 21:10:04 +13:00
"""
yields :
str , str , dict
first str : message id
second str : emoji id or unicode codepoint
dict : data from the corresponding :
config . custom ( " REACTROLE " , messageid , emojiid )
"""
# self.config.register_custom(
# "REACTROLE", roleid=None, channelid=None, guildid=None
# ) # ID : Message.id, str(React)
data = await self . config . custom ( " REACTROLE " ) . all ( )
for mid , _outer in data . items ( ) :
if not _outer or not isinstance ( _outer , dict ) :
continue
for em , rdata in _outer . items ( ) :
if rdata and rdata [ " roleid " ] == role . id :
yield ( mid , em , rdata )
2021-02-06 21:40:00 +13:00
async def red_delete_data_for_user (
self ,
* ,
requester : Literal [ " discord_deleted_user " , " owner " , " user " , " user_strict " ] ,
user_id : int ,
) :
2023-02-02 10:42:00 +13:00
await self . config . user_from_id ( user_id ) . clear ( )