numerous bug fixes, additions, and edits. new cog: nitroemoji

This commit is contained in:
brandons209 2020-02-08 14:52:28 -05:00
parent c478215cbc
commit e2dd8fed2a
13 changed files with 591 additions and 49 deletions

View file

@ -48,6 +48,10 @@ More admin commands that provide various functionality.
- List all users with a role/roles quickly and easily.
#### Nitro Emoji
Allows nitro boosters to add one emoji to your server. Log's additions and removal of custom emojis to a channel. Can turn this off to stop more people from adding, but those who added can remove their emoji.
#### Pony
Search derpibooru for pony images. Ported from [Alzarath](https://github.com/Alzarath/Booru-Cogs).
**Features:**
@ -75,7 +79,6 @@ Modified from [Sinbad](https://github.com/mikeshardmind/SinbadCogs).
- Adds in subscription based roles which renew every customized interval.
- Allows settings messages through DM to users who obtain a specific role (such as role info).
- Renames srole to selfrole and removes Red's default selfrole.
**In progress:**
- Makes listing roles a bit prettier.
- Allow setting roles to automatically add on guild join.
- Enhance exclusive roles, allow setting custom role groups where the bot enforces only one role to be on a user at a time, even if it isn't a selfrole. The bot will automatically remove the old role if a new role from the same group is added. Also lists name of role group in list command to make it clearer.

View file

@ -154,10 +154,10 @@ class ActivityLogger(commands.Cog):
for guild in guilds:
self.cache[guild.id] = self.default_guild.copy()
if not channel_data:
guilds = self.bot.guilds
for guild in guilds:
for channel in guild.channels:
guilds = self.bot.guilds
for guild in guilds:
for channel in guild.channels:
if not channel.id in self.cache.keys():
self.cache[channel.id] = self.default_channel.copy()
for guild in self.bot.guilds:

19
disable/info.json Normal file
View file

@ -0,0 +1,19 @@
{
"author": [
"Brandons209"
],
"bot_version": [
3,
3,
0
],
"description": "Small cog that allows disabling all bot commands for everyone except Admins/owner in a guild. Nice for things like bot setup. You can also set the error message when a person trys using a command when its disabled.",
"hidden": false,
"install_msg": "Thank you for using this cog!",
"requirements": [],
"short": "Disable all commands for everyone except admins.",
"tags": [
"brandons209",
"redbot"
]
}

View file

@ -1,7 +1,5 @@
from . import core
from cog_shared.sinbad_libs import extra_setup
@extra_setup
def setup(bot):
bot.add_cog(core.EconomyTrickle(bot))

View file

@ -4,6 +4,8 @@ from redbot.core import commands
configable_guild_defaults = {
"interval": 5,
"fail_rate": 0.2,
"decay_rate": 0.5,
"level_xp_base": 100,
"xp_lv_increase": 50,
"maximum_level": None,
@ -56,4 +58,12 @@ def settings_converter(user_input: str) -> dict:
except AssertionError:
raise commands.BadArgument(f"{value} must be a non-negative integer value or `null`")
for value in ("fail_rate", "decay_rate"):
if value in args:
try:
assert args[value] >= 0
assert args[value] <= 1
except AssertionError:
raise commands.BadArgument(f"{value} must be a decimal value between 0 and 1.")
return args

View file

@ -4,6 +4,7 @@ import contextlib
from typing import no_type_check, Union
from datetime import datetime, timedelta
from collections import defaultdict
import random
import discord
from redbot.core import commands, checks
@ -19,7 +20,7 @@ class EconomyTrickle(commands.Cog):
Automatic Economy gains for active users
"""
__version__ = "2.0.2"
__version__ = "2.1.0"
def __init__(self, bot, *args, **kwargs):
super().__init__(*args, **kwargs)
@ -30,6 +31,7 @@ class EconomyTrickle(commands.Cog):
mode="blacklist",
blacklist=[],
whitelist=[],
min_voice_members=2,
**configable_guild_defaults,
# custom_level_table={}, # TODO
)
@ -55,21 +57,24 @@ class EconomyTrickle(commands.Cog):
while self is self.bot.get_cog(self.__class__.__name__):
await asyncio.sleep(60)
now = datetime.utcnow()
data = await self.config.all_guilds()
for g in self.bot.guilds:
if g.id in data and data[g.id]["active"]:
minutes[g] += 1
if minutes[g] % data[g.id]["interval"]:
if minutes[g] % data[g.id]["interval"] == 0:
minutes[g] = 0
print(f"processing...{minutes[g]}, {data[g.id]['interval']}")
now = datetime.utcnow()
tsk = self.bot.loop.create_task(self.do_rewards_for(g, now, data[g.id]))
self.extra_tasks.append(tsk)
async def do_rewards_for(self, guild: discord.Guild, now: datetime, data: dict):
after = now - timedelta(minutes=data["interval"])
after = now - timedelta(minutes=data["interval"], seconds=10)
print(f"after: {after}")
voice_mem = await self.config.guild(guild).min_voice_members()
if data["mode"] == "blacklist":
def mpred(m: discord.Message):
@ -77,7 +82,7 @@ class EconomyTrickle(commands.Cog):
def vpred(mem: discord.Member):
with contextlib.suppress(AttributeError):
return mem.voice.channel.id not in data["blacklist"] and not mem.bot
return len(mem.voice.channel.members) > voice_mem and mem.voice.channel.id not in data["blacklist"] and not mem.bot
else:
@ -86,16 +91,42 @@ class EconomyTrickle(commands.Cog):
def vpred(mem: discord.Member):
with contextlib.suppress(AttributeError):
return mem.voice.channel.id in data["whitelist"] and not mem.bot
return len(mem.voice.channel.members) > voice_mem and mem.voice.channel.id in data["whitelist"] and not mem.bot
has_active_message = set(self.recordhandler.get_active_for_guild(guild=guild, after=after, message_check=mpred))
is_active_voice = {m for m in guild.members if vpred(m)}
print(f"active: {[m.name for m in has_active_message]}")
is_active = has_active_message | is_active_voice
for member in is_active:
# take exp away from inactive users
for member in guild.members:
if member in is_active:
continue
# loose exp per interval
xp = min(data["xp_per_interval"] * data["decay_rate"], 1)
xp = await self.config.member(member).xp() - xp
xp = max(xp, 0)
await self.config.member(member).xp.set(xp)
# update level on these users
level, next_needed = 0, data["level_xp_base"]
while xp >= next_needed:
level += 1
xp -= next_needed
next_needed += data["xp_lv_increase"]
if data["maximum_level"] is not None:
level = min(data["maximum_level"], level)
await self.config.member(member).level.set(level)
for member in is_active:
# failed for this member, skip
if data["fail_rate"] > random.random():
continue
# xp processing first
xp = data["xp_per_interval"]
if member in has_active_message:
@ -155,15 +186,17 @@ class EconomyTrickle(commands.Cog):
@ect.command()
@no_type_check
async def setstuff(self, ctx, *, data: settings_converter):
async def setstuff(self, ctx, *, data: settings_converter = None):
"""
Set other variables
format for this (and defaults):
format *example* for this (and defaults):
```yaml
bonus_per_level: 5
econ_per_interval: 20
fail_rate: 0.2
decay_rate: 0.5
extra_message_xp: 0
extra_voice_xp: 0
interval: 5
@ -174,6 +207,17 @@ class EconomyTrickle(commands.Cog):
xp_per_interval: 10
```
"""
if not data:
data = await self.config.guild(ctx.guild).all()
keys = list(configable_guild_defaults.keys())
msg = "Current data: (run `help trickleset setstuff to set`)\n```yaml\n"
for key in keys:
msg += f"{key}: {data[key]}\n"
msg += "```"
await ctx.send(msg)
return
for k, v in data.items():
await self.config.guild(ctx.guild).get_attr(k).set(v)
await ctx.tick()
@ -191,6 +235,23 @@ class EconomyTrickle(commands.Cog):
await self.config.guild(ctx.guild).mode.set(mode)
await ctx.tick()
@ect.command(name="voice")
async def rset_voicemem(self, ctx, min_voice_members: int = 0):
"""
Minimum number of voice members needed to count as active.
If users are in a voice chat, only trickle if there are at least min_voice_members
in there.
"""
if min_voice_members < 1:
curr = await self.config.guild(ctx.guild).min_voice_members()
await ctx.send(f"Current: {curr}")
return
await self.config.guild(ctx.guild).min_voice_members.set(min_voice_members)
await ctx.tick()
@ect.command(name="addchan")
async def rset_add_chan(self, ctx, *channels: Union[discord.TextChannel, discord.VoiceChannel]):
"""

View file

@ -47,7 +47,7 @@ class MoreAdmin(commands.Cog):
self.bot = bot
self.config = Config.get_conf(self, identifier=213438438248, force_registration=True)
default_guild = {"user_count_channel": None, "sus_user_channel": None, "sus_user_threshold": None}
default_guild = {"user_count_channel": None, "sus_user_channel": None, "sus_user_threshold": None, "prefixes": []}
default_role = {"addable": []} # role ids who can add this role
self.config.register_role(**default_role)
@ -58,6 +58,11 @@ class MoreAdmin(commands.Cog):
async def initialize(self):
await self.register_casetypes()
for guild in self.bot.guilds:
async with self.config.guild(guild).prefixes() as prefixes:
if not prefixes:
curr = await self.bot.get_valid_prefixes()
prefixes.extend(curr)
def cog_unload(self):
self.user_task.cancel()
@ -65,19 +70,18 @@ class MoreAdmin(commands.Cog):
@staticmethod
async def register_casetypes():
# register mod case
punish_case = {
purge_case = {
"name": "Purge",
"default_setting": True,
"image": "\N{WOMANS BOOTS}",
"case_str": "Purge",
}
try:
await modlog.register_casetype(**punish_case)
await modlog.register_casetype(**purge_case)
except RuntimeError:
pass
@staticmethod
async def find_last_message(guild: discord.Guild, role: discord.Role):
async def find_last_message(self, guild: discord.Guild, role: discord.Role, include_bot_commands: bool):
"""
Finds last message of EVERY user with role in a guild.
**WARNING VERY SLOW AND COSTLY OPERATION!**
@ -86,12 +90,21 @@ class MoreAdmin(commands.Cog):
"""
last_msgs = {}
text_channels = [channel for channel in guild.channels if isinstance(channel, discord.TextChannel)]
prefixes = await self.config.guild(guild).prefixes()
for channel in text_channels:
async for message in channel.history(limit=None):
if isinstance(message.author, discord.Member) and role in message.author.roles:
if message.author.id not in last_msgs.keys():
# prefix check
skip = False
if include_bot_commands:
for prefix in prefixes:
if message.content and prefix == message.content[:len(prefix)]:
skip = True
break
if message.author.id not in last_msgs.keys() and not skip:
last_msgs[message.author.id] = message
else:
elif not skip:
curr_last = last_msgs[message.author.id]
if message.created_at > curr_last.created_at:
last_msgs[message.author.id] = message
@ -209,6 +222,26 @@ class MoreAdmin(commands.Cog):
await self.config.guild(ctx.guild).sus_user_threshold.set(int(threshold.total_seconds()))
await ctx.tick()
@adminset.command(name="prefixes")
async def adminset_prefixes(self, ctx, *, prefixes: str = None):
"""
Set prefixes for bot commands to check for when purging.
Seperate prefixes with spaces.
Used for purge command.
"""
if not prefixes:
prefixes = await self.config.guild(ctx.guild).prefixes()
curr = [f"`{p}`" for p in prefixes]
await ctx.send("Current Prefixes: " + humanize_list(curr))
return
prefixes = [p for p in prefixes.split(" ")]
await self.config.guild(ctx.guild).prefixes.set(prefixes)
prefixes = [f"`{p}`" for p in prefixes]
await ctx.send("Prefixes set to: " + humanize_list(prefixes))
@adminset.command(name="addable")
async def adminset_addable(self, ctx, role: discord.Role, *, role_list: str = None):
"""
@ -358,7 +391,7 @@ class MoreAdmin(commands.Cog):
@commands.command(name="purge")
@checks.admin_or_permissions(administrator=True)
@checks.bot_has_permissions(kick_members=True)
async def purge(self, ctx, role: discord.Role, check_messages: bool = True, *, threshold: str = None):
async def purge(self, ctx, role: discord.Role, check_messages: bool = True, include_bot_commands: bool = False, *, threshold: str = None):
"""
Purge inactive users with role.
@ -368,6 +401,9 @@ class MoreAdmin(commands.Cog):
If check_messages is yes/true/1 then purging is dictated by the user's last message.
If check_messages is no/false/0 then purging is dictated by the user's join date.
If checking last message and bot is yes/true/1 then the bot won't count bot include_bot_commands as a valid last message for purge.
**Make sure to set prefixes with [p]adminset**
Threshold should be an interval.
Intervals look like:
@ -389,7 +425,7 @@ class MoreAdmin(commands.Cog):
errored = []
start_time = time.time()
if check_messages:
last_msgs = await self.find_last_message(guild, role)
last_msgs = await self.find_last_message(guild, role, include_bot_commands)
for member in guild.members:
if role in member.roles:

6
nitroemoji/__init__.py Normal file
View file

@ -0,0 +1,6 @@
from .nitroemoji import NitroEmoji
async def setup(bot):
n = NitroEmoji(bot)
await n.initialize()
bot.add_cog(n)

21
nitroemoji/info.json Normal file
View file

@ -0,0 +1,21 @@
{
"author": [
"Brandons209"
],
"bot_version": [
3,
3,
0
],
"description": "Allow nitro boosters to add an emoji to the server, with channel logging.",
"hidden": false,
"install_msg": "Thank you for using this cog! Please make sure to set the log channel using `[p]nitroset`",
"requirements": ["aiohttp"],
"short": "Allow boosters to have a server emoji.",
"tags": [
"brandons209",
"nitro",
"boost",
"emoji"
]
}

257
nitroemoji/nitroemoji.py Normal file
View file

@ -0,0 +1,257 @@
from redbot.core.utils.chat_formatting import *
from redbot.core import Config, checks, commands, bank
from redbot.core.data_manager import cog_data_path
import discord
import aiohttp
import PIL
import os
class NitroEmoji(commands.Cog):
"""
Reward nitro boosters with a custom emoji.
"""
def __init__(self, bot):
self.bot = bot
self.config = Config.get_conf(self, identifier=123859659843, force_registration=True)
default_guild = {
"channel": None,
"disabled": False
}
default_member = {
"emojis": []
}
self.config.register_guild(**default_guild)
self.config.register_member(**default_member)
async def initialize(self):
for member in self.bot.get_all_members():
async with self.config.member(member).emojis() as data:
to_remove = []
for e in data:
emoji = self.find_emoji(member.guild, e)
# clean removed emojis if bot is down
if not emoji:
to_remove.append(e)
for r in to_remove:
data.remove(r)
@staticmethod
def get_boosts(member: discord.Member):
# this will return number of boosts once api supports it
return member.premium_since is not None
def find_emoji(self, guild, name):
emoji = self.bot.get_emoji(name)
# find by name:
if not emoji:
emoji = discord.utils.get(guild.emojis, name=name)
return emoji
async def add_emoji(self, member, name, attachment_or_url, reason=None):
path = str(cog_data_path(cog_instance=self))
path = os.path.join(path, str(member.id) + name)
if isinstance(attachment_or_url, discord.Attachment):
await attachment_or_url.save(path)
elif isinstance(attachment_or_url, str):
async with aiohttp.ClientSession(loop=self.bot.loop) as session:
async with session.get(attachment_or_url) as r:
if r.status == 200:
with open(path, "wb") as f:
f.write(await r.read())
# verify image
im = PIL.Image.open(path)
im.verify()
# upload emoji
with open(path, "rb") as f:
emoji = await member.guild.create_custom_emoji(name=name, image=f.read())
os.remove(path)
async with self.config.member(member).emojis() as e:
e.append(emoji.id)
channel = await self.config.guild(member.guild).channel()
channel = member.guild.get_channel(channel)
if not channel:
return
embed = discord.Embed(title="Custom Emoji Added", colour=member.colour)
embed.set_footer(text="User ID:{}".format(member.id))
embed.set_author(name=str(member), url=emoji.url)
embed.set_thumbnail(url=emoji.url)
embed.set_author(name=str(member))
if reason:
embed.add_field(name="Reason", value=reason)
await channel.send(embed=embed)
async def del_emoji(self, guild, member, emoji=None, reason=None):
channel = await self.config.guild(member.guild).channel()
channel = member.guild.get_channel(channel)
if channel:
embed = discord.Embed(title="Custom Emoji Removed", colour=member.colour)
embed.set_footer(text="User ID:{}".format(member.id))
embed.set_author(name=str(member), url=emoji.url)
embed.set_thumbnail(url=emoji.url)
embed.set_author(name=str(member))
if reason:
embed.add_field(name="Reason", value=reason)
await channel.send(embed=embed)
if emoji:
await emoji.delete()
async with self.config.member(member).emojis() as e:
e.remove(emoji.id)
@commands.group(name="nitroset")
@commands.guild_only()
@checks.admin()
async def nitroset(self, ctx):
"""Manage nitro emoji settings."""
pass
@nitroset.command(name="channel")
async def nitroset_channel(self, ctx, channel: discord.TextChannel):
"""
Set the channel to log nitro events.
Logs boosts, unboosts, and added emojis.
"""
await self.config.guild(ctx.guild).channel.set(channel.id)
await ctx.tick()
@nitroset.command(name="disable")
async def nitroset_disable(self, ctx, *, on_off: bool = None):
"""
Disable users from adding more emojis.
Users can still remove and list their own emojis.
"""
if on_off is None:
curr = await self.config.guild(ctx.guild).disabled()
msg = "enabled" if not curr else "disabled"
await ctx.send(f"Nitro emojis is {msg}.")
return
await self.config.guild(ctx.guild).disabled.set(on_off)
await ctx.tick()
@commands.group(name="nitroemoji")
@checks.bot_has_permissions(manage_emojis=True)
async def nitroemoji(self, ctx):
"""
Manage your emojis if you boosted the server.
"""
pass
@nitroemoji.command(name="add")
async def nitroemoji_add(self, ctx, name: str, *, url: str = None):
"""
Add an emoji to the server, if you boosted.
Can only add an emoji for every boost you have in the server.
"""
disabled = await self.config.guild(ctx.guild).disabled()
if disabled:
await ctx.send("Sorry, adding emojis is currently disabled right now.")
return
curr = await self.config.member(ctx.author).emojis()
boosts = self.get_boosts(ctx.author)
# TODO: add in checking for multiple emojis once supported
if boosts and not curr:
try:
if url:
emoji = await self.add_emoji(ctx.author, name, url)
else:
emoji = await self.add_emoji(ctx.author, name, ctx.message.attachments[0])
await ctx.tick()
except discord.errors.HTTPException as e:
await ctx.send(e.text)
except PIL.UnidentifiedImageError:
await ctx.send("That is not a valid picture! Pictures must be in PNG, JPEG, or GIF format.")
except:
await ctx.send("Something went wrong, make sure to add a valid picture (PNG, JPG, or GIF) of the right size (256KB) and a valid name.")
return
elif not boosts:
await ctx.send("Sorry, you need to be a nitro booster to add an emoji!")
elif curr:
await ctx.send("You already have a custom emoji, please delete it first before adding another one.")
@nitroemoji.command(name="rem")
async def nitroemoji_rem(self, ctx, name: str):
"""
Remove an emoji to the server, if you boosted.
"""
curr = await self.config.member(ctx.author).emojis()
emoji = self.find_emoji(ctx.guild, name)
if emoji:
if emoji.id in curr:
await self.del_emoji(ctx.guild, ctx.author, emoji=emoji, reason="Removed by user.")
await ctx.tick()
else:
await ctx.send("That isn't your custom emoji.")
else:
await ctx.send(warning("Emoji not found."))
@nitroemoji.command(name="list")
async def nitroemoji_list(self, ctx):
"""
List your custom emojis in the server
"""
curr = await self.config.member(ctx.author).emojis()
msg = ""
if curr:
msg += "Current emojis:\n"
for emoji in curr:
emoji = self.find_emoji(ctx.guild, emoji)
if not emoji:
continue
msg += f"{emoji.url}\n"
else:
msg += "You have no custom emojis."
for page in pagify(msg):
await ctx.send(page)
@commands.Cog.listener()
async def on_member_update(self, before, after):
# check if they stopped boosting
if before.premium_since != after.premium_since and after.premium_since is None:
emojis = await self.config.member(after).emojis()
for emoji in emojis:
emoji = self.find_emoji(after.guild, emoji)
if not emoji:
continue
await self.del_emoji(after.guild, after, emoji=emoji, reason="Stopped boosting.")
@commands.Cog.listener()
async def on_guild_emojis_update(self, guild, before, after):
b_e = set(before)
a_e = set(after)
diff = b_e - a_e
if diff:
for e in diff:
for member in guild.premium_subscribers:
curr = await self.config.member(member).emojis()
if e.id in curr:
curr.remove(e.id)
await self.config.member(member).emojis.set(curr)
await self.del_emoji(guild, member, emoji=e, reason="Manually deleted by admin.")
break

View file

@ -11,10 +11,10 @@ import discord
from discord.ext.commands import CogMeta as DPYCogMeta
from redbot.core import checks, commands, bank
from redbot.core.config import Config
from redbot.core.utils.chat_formatting import box, pagify, warning
from redbot.core.utils.chat_formatting import box, pagify, warning, humanize_list
from .events import EventMixin
from .exceptions import RoleManagementException, PermissionOrHierarchyException
from .exceptions import RoleManagementException, PermissionOrHierarchyException, MissingRequirementsException, ConflictingRoleException
from .massmanager import MassManagementMixin
from .utils import UtilMixin, variation_stripper_re, parse_timedelta, parse_seconds
@ -41,7 +41,7 @@ class CompositeMetaClass(DPYCogMeta, ABCMeta):
MIN_SUB_TIME = 3600
SLEEP_TIME = 300
MAX_EMBED = 25
class RoleManagement(
UtilMixin, MassManagementMixin, EventMixin, commands.Cog, metaclass=CompositeMetaClass,
@ -62,7 +62,7 @@ class RoleManagement(
self.config = Config.get_conf(self, identifier=78631113035100160, force_registration=True)
self.config.register_global(handled_variation=False, handled_full_str_emoji=False)
self.config.register_role(
exclusive_to=[],
exclusive_to={},
requires_any=[],
requires_all=[],
sticky=False,
@ -79,7 +79,7 @@ class RoleManagement(
self.config.register_custom(
"REACTROLE", roleid=None, channelid=None, guildid=None
) # ID : Message.id, str(React)
self.config.register_guild(notify_channel=None, s_roles=[], free_roles=[])
self.config.register_guild(notify_channel=None, s_roles=[], free_roles=[], join_roles=[])
self._ready = asyncio.Event()
self._start_task: Optional[asyncio.Task] = None
self.loop = asyncio.get_event_loop()
@ -155,6 +155,10 @@ class RoleManagement(
if not member: # clean absent members
del role_data["subscribed_users"][user_id]
continue
# make sure they still have the role
if role not in member.roles:
del role_data["subscribed_users"][user_id]
continue
# charge user
cost = await self.config.role(role).cost()
currency_name = await bank.get_currency_name(guild)
@ -406,6 +410,56 @@ class RoleManagement(
await self.config.role(role).dm_msg.set(msg)
await ctx.tick()
@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])
roles = [f"{i+1}.{role.name}" for i, role in enumerate(roles) if role is not None]
msg = "\n".join(sorted(roles))
msg = pagify(msg)
for m in msg:
await ctx.send(box(m))
@rgroup.command(name="viewrole")
async def rg_view_role(self, ctx: GuildContext, *, role: discord.Role):
"""
@ -426,8 +480,12 @@ class RoleManagement(
rstring = ", ".join(r.name for r in ctx.guild.roles if r.id in rsets["requires_all"])
output += f"\nThis role requires all of the following roles: {rstring}"
if rsets["exclusive_to"]:
rstring = ", ".join(r.name for r in ctx.guild.roles if r.id in rsets["exclusive_to"])
output += f"\nThis role is mutually exclusive to the following roles: {rstring}"
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"\nThis role is mutually exclusive to the following role groups:\n{rstring}"
if rsets["cost"]:
curr = await bank.get_currency_name(ctx.guild)
cost = rsets["cost"]
@ -535,9 +593,13 @@ class RoleManagement(
await ctx.tick()
@rgroup.command(name="exclusive")
async def set_exclusivity(self, ctx: GuildContext, *roles: discord.Role):
async def set_exclusivity(self, ctx: GuildContext, group: str, *roles: discord.Role):
"""
Set exclusive roles for group
Takes 2 or more roles and sets them as exclusive to eachother
The group can be any name, use spaces for names with spaces.
Groups will show up in role list etc.
"""
_roles = set(roles)
@ -547,13 +609,20 @@ class RoleManagement(
for role in _roles:
async with self.config.role(role).exclusive_to() as ex_list:
ex_list.extend([r.id for r in _roles if r != role and r.id not in ex_list])
if group not in ex_list.keys():
ex_list[group] = []
ex_list[group].extend([r.id for r in _roles if r != role and r.id not in ex_list[group]])
await ctx.tick()
@rgroup.command(name="unexclusive")
async def unset_exclusivity(self, ctx: GuildContext, *roles: discord.Role):
async def unset_exclusivity(self, ctx: GuildContext, group: str, *roles: discord.Role):
"""
Remove exclusive roles for group
Takes any number of roles, and removes their exclusivity settings
The group can be any name, use spaces for names with spaces.
If all roles are removed from a group then
"""
_roles = set(roles)
@ -563,7 +632,11 @@ class RoleManagement(
for role in _roles:
ex_list = await self.config.role(role).exclusive_to()
ex_list = [idx for idx in ex_list if idx not in [r.id for r in _roles]]
if group not in ex_list.keys():
continue
ex_list[group] = [idx for idx in ex_list if idx not in [r.id for r in _roles]]
if not ex_list[group]:
del ex_list[group]
await self.config.role(role).exclusive_to.set(ex_list)
await ctx.tick()
@ -586,7 +659,6 @@ class RoleManagement(
await ctx.tick()
# TODO set roles who don't need to pay for roles
@rgroup.command(name="requireall")
async def reqall(self, ctx: GuildContext, role: discord.Role, *roles: discord.Role):
"""
@ -724,7 +796,7 @@ class RoleManagement(
data[role] = vals["cost"]
else:
data = {
role: (vals["cost"], vals["subscription"])
role: (vals["cost"], vals["subscription"], vals["exclusive_to"])
for role_id, vals in (await self.config.all_roles()).items()
if (role := ctx.guild.get_role(role_id)) and vals["self_role"]
}
@ -734,15 +806,20 @@ class RoleManagement(
embed = discord.Embed(title="Roles", colour=ctx.guild.me.colour)
i = 0
for role, (cost, sub) in sorted(data.items(), key=lambda kv: kv[1]):
for role, (cost, sub, ex_groups) in sorted(data.items(), key=lambda kv: kv[1][0]):
if ex_groups:
groups = humanize_list(list(ex_groups.keys()))
else:
groups = None
embed.add_field(
name=f"__**{i+1}. {role.name}**__",
value="%s%s"
% ((f"Cost: {cost}" if cost else "Free"), (f", every {parse_seconds(sub)}" if sub else "")),
value="%s%s%s"
% ((f"Cost: {cost}" if cost else "Free"), (f", every {parse_seconds(sub)}" if sub else ""), (f"\nunique groups: `{groups}`" if groups else ""))
)
i += 1
if i % 25 == 0:
if i % MAX_EMBED == 0:
await ctx.send(embed=embed)
embed.set_footer(text="You can only have one role in the same unique group!")
await ctx.send(embed=embed)
@ -759,10 +836,21 @@ class RoleManagement(
eligible = await self.config.role(role).self_role()
cost = await self.config.role(role).cost()
subscription = await self.config.role(role).subscription()
except RoleManagementException:
return
except PermissionOrHierarchyException:
await ctx.send("I cannot assign roles which I can not manage. (Discord Hierarchy)")
except MissingRequirementsException as e:
msg = ""
if e.miss_all:
roles = [r for r in ctx.guild.roles if r in e.miss_all]
msg += f"You need all of these roles in order to get this role: {humanize_list(roles)}\n"
if e.miss_any:
roles = [r for r in ctx.guild.roles if r in e.miss_any]
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:
roles = [r for r in ctx.guild.roles if r in e.conflicts]
plural = "are" if len(roles) > 1 else "is"
await ctx.send(f"You have {humanize_list(roles)}, which you are not allowed to remove and {plural} exclusive to: {role.name}")
else:
if not eligible:
return await ctx.send(f"You aren't allowed to add `{role}` to yourself {ctx.author.mention}!")
@ -792,6 +880,9 @@ class RoleManagement(
if role.id not in s:
s.append(role.id)
if remove:
plural = "s" if len(remove) > 1 else ""
await ctx.send(f"Removed `{humanize_list([r.name for r in remove])}` role{plural} since they are exclusive to the role you added.")
await self.update_roles_atomically(who=ctx.author, give=[role], remove=remove)
await self.dm_user(ctx, role)
await ctx.tick()
@ -808,10 +899,22 @@ class RoleManagement(
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 RoleManagementException:
return
except PermissionOrHierarchyException:
await ctx.send("I cannot assign roles which I can not manage. (Discord Hierarchy)")
except MissingRequirementsException as e:
msg = ""
if e.miss_all:
roles = [r for r in ctx.guild.roles if r in e.miss_all]
msg += f"You need all of these roles in order to get this role: {humanize_list(roles)}\n"
if e.miss_any:
roles = [r for r in ctx.guild.roles if r in e.miss_any]
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:
print(e.conflicts)
roles = [r for r in ctx.guild.roles if r in e.conflicts]
plural = "are" if len(roles) > 1 else "is"
await ctx.send(f"You have {humanize_list(roles)}, which you are not allowed to remove and {plural} exclusive to: {role.name}")
else:
if not eligible:
await ctx.send(f"You aren't allowed to add `{role}` to yourself {ctx.author.mention}!")
@ -821,6 +924,9 @@ class RoleManagement(
"This role is not free. " "Please use `[p]selfrole buy` if you would like to purchase it."
)
else:
if remove:
plural = "s" if len(remove) > 1 else ""
await ctx.send(f"Removed `{humanize_list([r.name for r in remove])}` role{plural} since they are exclusive to the role you added.")
await self.update_roles_atomically(who=ctx.author, give=[role], remove=remove)
await self.dm_user(ctx, role)
await ctx.tick()

View file

@ -51,6 +51,17 @@ class EventMixin(MixinMeta):
lost, gained = lost - gained, gained - lost
sym_diff = lost | gained
# check if new member roles are exclusive to others.
ex = []
for r in gained:
ex_groups = (await self.config.role_from_id(r).exclusive_to()).values()
for ex_roles in ex_groups:
ex.extend(ex_roles)
to_remove = [r for r in after.roles if r.id in ex]
if to_remove:
await after.remove_roles(*to_remove, reason="conflict with exclusive roles")
for r in sym_diff:
if not await self.config.role_from_id(r).sticky():
lost.discard(r)
@ -83,6 +94,17 @@ class EventMixin(MixinMeta):
to_add = [r for r in to_add if r < guild.me.top_role]
await member.add_roles(*to_add)
# join roles
async with self.config.guild(guild).join_roles() as join_roles:
to_add: List[discord.Role] = []
for role_id in join_roles:
role = discord.utils.get(guild.roles, id=role_id)
if role:
to_add.append(role)
if to_add:
to_add = [r for r in to_add if r < guild.me.top_role]
await member.add_roles(*to_add)
@commands.Cog.listener()
async def on_raw_reaction_add(self, payload: discord.raw_models.RawReactionActionEvent):
await self.wait_for_ready()

View file

@ -170,7 +170,10 @@ class UtilMixin(MixinMeta):
"""
data = await self.config.all_roles()
ex = data.get(role.id, {}).get("exclusive_to", [])
ex_data = data.get(role.id, {}).get("exclusive_to", {}).values()
ex = []
for ex_roles in ex_data:
ex.extend(ex_roles)
conflicts: List[discord.Role] = [r for r in who.roles if r.id in ex]
for r in conflicts: