Skip to content
4 changes: 2 additions & 2 deletions adminutils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ async def setup_after_ready(bot):
for alias in command.aliases:
if bot.get_command(alias):
command.aliases[command.aliases.index(alias)] = f"a{alias}"
bot.add_cog(cog)
await bot.add_cog(cog)


def setup(bot):
async def setup(bot):
create_task(setup_after_ready(bot))
147 changes: 93 additions & 54 deletions adminutils/adminutils.py
Original file line number Diff line number Diff line change
@@ -1,39 +1,60 @@
import contextlib
import re
from asyncio import TimeoutError as AsyncTimeoutError
from random import choice
from typing import Optional, Union

import aiohttp
import discord
from red_commons.logging import getLogger
from redbot.core import commands
from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils import chat_formatting as chat
from redbot.core.utils.mod import get_audit_reason
from redbot.core.utils.predicates import MessagePredicate

try:
from redbot import json # support of Draper's branch
except ImportError:
import json

_ = Translator("AdminUtils", __file__)

EMOJI_RE = re.compile(r"(<(a)?:[a-zA-Z0-9_]+:([0-9]+)>)")

CHANNEL_REASONS = {
discord.CategoryChannel: _("You are not allowed to edit this category."),
discord.TextChannel: _("You are not allowed to edit this channel."),
discord.VoiceChannel: _("You are not allowed to edit this channel."),
discord.StageChannel: _("You are not allowed to edit this channel."),
}


async def check_regions(ctx):
"""Check if regions list is populated"""
return ctx.cog.regions


@cog_i18n(_)
class AdminUtils(commands.Cog):
"""Useful commands for server administrators."""

__version__ = "2.5.11"
__version__ = "3.0.1-pre"

# noinspection PyMissingConstructor
def __init__(self, bot):
self.bot = bot
self.session = aiohttp.ClientSession(json_serialize=json.dumps)
self.session = aiohttp.ClientSession()
self.log = getLogger("red.fixator10-cogs.adminutils")
self.regions = []

async def cog_load(self):
try:
regions = await self.bot.http.request(discord.http.Route("GET", "/voice/regions"))
self.regions = [region["id"] for region in regions]
except Exception as e:
self.log.warning(
"Unable to get list of rtc_regions. [p]restartvoice command will be unavailable",
exc_info=e,
)

def cog_unload(self):
self.bot.loop.create_task(self.session.close())
async def cog_unload(self):
await self.session.close()

def format_help_for_context(self, ctx: commands.Context) -> str: # Thanks Sinbad!
pre_processed = super().format_help_for_context(ctx)
Expand All @@ -45,20 +66,19 @@ async def red_delete_data_for_user(self, **kwargs):
@staticmethod
def check_channel_permission(
ctx: commands.Context,
channel_or_category: Union[discord.TextChannel, discord.CategoryChannel],
channel_or_category: Union[
discord.TextChannel,
discord.CategoryChannel,
discord.VoiceChannel,
discord.StageChannel,
],
) -> bool:
"""
Check user's permission in a channel, to be sure he can edit it.
"""
mc = channel_or_category.permissions_for(ctx.author).manage_channels
if mc:
if channel_or_category.permissions_for(ctx.author).manage_channels:
return True
reason = (
_("You are not allowed to edit this channel.")
if not isinstance(channel_or_category, discord.CategoryChannel)
else _("You are not allowed to edit in this category.")
)
raise commands.UserFeedbackCheckFailure(reason)
raise commands.UserFeedbackCheckFailure(CHANNEL_REASONS.get(type(channel_or_category)))

@commands.command(name="prune")
@commands.guild_only()
Expand Down Expand Up @@ -92,10 +112,8 @@ async def cleanup_users(self, ctx, days: Optional[int] = 1, *roles: discord.Role
).format(to_kick=to_kick, days=days, roles=roles_text if roles else "")
)
)
try:
with contextlib.suppress(AsyncTimeoutError):
await self.bot.wait_for("message", check=pred, timeout=30)
except AsyncTimeoutError:
pass
if ctx.assume_yes or pred.result:
cleanup = await ctx.guild.prune_members(
days=days, reason=get_audit_reason(ctx.author), roles=roles or None
Expand All @@ -113,23 +131,20 @@ async def cleanup_users(self, ctx, days: Optional[int] = 1, *roles: discord.Role

@commands.command()
@commands.guild_only()
@commands.admin_or_permissions(manage_guild=True)
@commands.bot_has_permissions(manage_guild=True)
async def restartvoice(self, ctx: commands.Context):
"""Change server's voice region to random and back
@commands.check(check_regions)
@commands.admin_or_permissions(manage_channels=True)
@commands.bot_has_permissions(manage_channels=True)
async def restartvoice(
self, ctx: commands.Context, channel: Union[discord.VoiceChannel, discord.StageChannel]
):
"""Change voice channel's region to random and back

Useful to reinitate all voice connections"""
current_region = ctx.guild.region
random_region = choice(
[
r
for r in discord.VoiceRegion
if not r.value.startswith("vip") and current_region != r
]
)
await ctx.guild.edit(region=random_region)
await ctx.guild.edit(
region=current_region,
current_region = channel.rtc_region
random_region = choice([r for r in self.regions if current_region != r])
await channel.edit(rtc_region=random_region)
await channel.edit(
rtc_region=current_region,
reason=get_audit_reason(ctx.author, _("Voice restart")),
)
await ctx.tick()
Expand All @@ -142,8 +157,8 @@ async def restartvoice(self, ctx: commands.Context):
async def massmove(
self,
ctx: commands.Context,
from_channel: discord.VoiceChannel,
to_channel: discord.VoiceChannel = None,
from_channel: Union[discord.VoiceChannel, discord.StageChannel],
to_channel: Union[discord.VoiceChannel, discord.StageChannel] = None,
):
"""Move all members from one voice channel to another

Expand Down Expand Up @@ -171,6 +186,7 @@ async def massmove(
continue
await ctx.send(_("Finished moving users. {} members could not be moved.").format(fails))

# TODO: Stickers?
@commands.group()
@commands.guild_only()
@commands.admin_or_permissions(manage_emojis=True)
Expand Down Expand Up @@ -209,8 +225,6 @@ async def emoji_add(self, ctx, name: str, url: str, *roles: discord.Role):
),
),
)
except discord.InvalidArgument:
await ctx.send(chat.error(_("This image type is unsupported, or link is incorrect")))
except discord.HTTPException as e:
await ctx.send(chat.error(_("An error occurred on adding an emoji: {}").format(e)))
else:
Expand Down Expand Up @@ -255,13 +269,6 @@ async def emote_steal(
),
)
await ctx.tick()
except discord.InvalidArgument:
await ctx.send(
_(
"This image type is not supported anymore or Discord returned incorrect data. Try again later."
)
)
return
except discord.HTTPException as e:
await ctx.send(chat.error(_("An error occurred on adding an emoji: {}").format(e)))

Expand Down Expand Up @@ -307,6 +314,7 @@ async def emoji_remove(self, ctx: commands.Context, *, emoji: discord.Emoji):
await emoji.delete(reason=get_audit_reason(ctx.author))
await ctx.tick()

# TODO: Threads?
@commands.group()
@commands.guild_only()
@commands.admin_or_permissions(manage_channels=True)
Expand Down Expand Up @@ -366,8 +374,8 @@ async def channel_create_voice(
Use double quotes if category has spaces

Examples:
`[p]channel add voice "The Zoo" Awesome Channel` will create under the "The Zoo" category.
`[p]channel add voice Awesome Channel` will create under no category, at the top.
`[p]channel add voice "The Zoo" Awesome Channel` will create voice channel under the "The Zoo" category.
`[p]channel add voice Awesome Channel` will create stage channel under no category, at the top.
"""
if category:
self.check_channel_permission(ctx, category)
Expand All @@ -382,11 +390,41 @@ async def channel_create_voice(
else:
await ctx.tick()

@channel_create.command(name="stage")
async def channel_create_stage(
self,
ctx: commands.Context,
category: Optional[discord.CategoryChannel] = None,
*,
name: str,
):
"""Create a stage channel

You can create the channel under a category if passed, else it is created under no category
Use double quotes if category has spaces

Examples:
`[p]channel add voice "The Zoo" Awesome Channel` will create voice channel under the "The Zoo" category.
`[p]channel add voice Awesome Channel` will create stage channel under no category, at the top.
"""
if category:
self.check_channel_permission(ctx, category)
try:
await ctx.guild.create_stage_channel(
name, category=category, reason=get_audit_reason(ctx.author)
)
except discord.Forbidden:
await ctx.send(chat.error(_("I can't create channel in this category")))
except discord.HTTPException as e:
await ctx.send(chat.error(_("I am unable to create a channel: {}").format(e)))
else:
await ctx.tick()

@channel.command(name="rename")
async def channel_rename(
self,
ctx: commands.Context,
channel: Union[discord.TextChannel, discord.VoiceChannel],
channel: Union[discord.TextChannel, discord.VoiceChannel, discord.StageChannel],
*,
name: str,
):
Expand All @@ -409,7 +447,10 @@ async def channel_rename(

@channel.command(name="delete", aliases=["remove"])
async def channel_delete(
self, ctx: commands.Context, *, channel: Union[discord.TextChannel, discord.VoiceChannel]
self,
ctx: commands.Context,
*,
channel: Union[discord.TextChannel, discord.VoiceChannel, discord.StageChannel],
):
"""Remove a channel from server

Expand All @@ -427,10 +468,8 @@ async def channel_delete(
).format(channel=channel.mention)
)
)
try:
with contextlib.suppress(AsyncTimeoutError):
await self.bot.wait_for("message", check=pred, timeout=30)
except AsyncTimeoutError:
pass
if ctx.assume_yes or pred.result:
try:
await channel.delete(reason=get_audit_reason(ctx.author))
Expand Down
3 changes: 1 addition & 2 deletions adminutils/info.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@
"install_msg": "Thanks for install.\nThis cog contains some useful commands for server administrators.",
"short": "Useful commands for server administrators.",
"description": "Useful commands for server administrators.",
"min_bot_version": "3.4.0",
"max_bot_version": "3.4.99",
"min_bot_version": "3.5.0",
"tags": [
"admin",
"emoji",
Expand Down
Loading