[Invites] Initial commit
This commit is contained in:
5
invites/__init__.py
Normal file
5
invites/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from .invites import Invites
|
||||
|
||||
|
||||
def setup(bot):
|
||||
bot.add_cog(Invites(bot))
|
||||
10
invites/info.json
Normal file
10
invites/info.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"author": ["aikaterna"],
|
||||
"description": "Invite count display and leaderboard.",
|
||||
"install_msg": "Thanks for installing. Use `[p]invites` to get started.",
|
||||
"short": "Invite count display and leaderboard.",
|
||||
"tags": ["invites"],
|
||||
"permissions": ["administrator", "embed_links"],
|
||||
"type": "COG"
|
||||
}
|
||||
|
||||
282
invites/invites.py
Normal file
282
invites/invites.py
Normal file
@@ -0,0 +1,282 @@
|
||||
from __future__ import annotations
|
||||
import discord
|
||||
from datetime import datetime
|
||||
import re
|
||||
from typing import List, Callable
|
||||
from redbot.core import commands, checks, Config
|
||||
from redbot.core.utils import chat_formatting as cf
|
||||
from redbot.vendored.discord.ext import menus
|
||||
|
||||
OLD_CODE_RE = re.compile("^[0-9a-zA-Z]{16}$")
|
||||
CODE_RE = re.compile("^[0-9a-zA-Z]{6}$")
|
||||
|
||||
FAILURE_MSG = "That invite doesn't seem to be valid."
|
||||
PERM_MSG = "I need the Administrator permission on this guild to view invite information."
|
||||
|
||||
|
||||
class Invites(commands.Cog):
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
self.config = Config.get_conf(self, 2713371001, force_registration=True)
|
||||
|
||||
default_guild = {"pinned_invites": []}
|
||||
|
||||
self.config.register_guild(**default_guild)
|
||||
|
||||
@commands.guild_only()
|
||||
@commands.group()
|
||||
async def invites(self, ctx):
|
||||
"""Invite information."""
|
||||
pass
|
||||
|
||||
@commands.max_concurrency(1, commands.BucketType.user)
|
||||
@invites.command()
|
||||
async def show(self, ctx, invite_code_or_url=None):
|
||||
"""Show the stats for an invite, or show all invites."""
|
||||
if not ctx.me.permissions_in(ctx.channel).administrator:
|
||||
return await self._send_embed(ctx, PERM_MSG)
|
||||
|
||||
if not invite_code_or_url:
|
||||
pages = MenuPages(await ctx.guild.invites())
|
||||
await self._menu(ctx, pages)
|
||||
else:
|
||||
invite_code = await self._find_invite_code(invite_code_or_url)
|
||||
if not invite_code:
|
||||
return await self._send_embed(ctx, msg)
|
||||
|
||||
@invites.command()
|
||||
async def leaderboard(self, ctx, list_all_invites=False):
|
||||
"""List pinned invites or all invites in a leaderboard style."""
|
||||
if not ctx.me.permissions_in(ctx.channel).administrator:
|
||||
return await self._send_embed(ctx, PERM_MSG)
|
||||
|
||||
if not list_all_invites:
|
||||
pinned_invites = await self.config.guild(ctx.guild).pinned_invites()
|
||||
if not pinned_invites:
|
||||
return await self._send_embed(ctx, "No invites are pinned, or there are no invites to display.")
|
||||
else:
|
||||
pinned_invites = await ctx.guild.invites()
|
||||
invite_info = ""
|
||||
for i, invite_code_or_object in enumerate(pinned_invites):
|
||||
if not list_all_invites:
|
||||
inv_object = await self._get_invite_from_code(ctx, invite_code_or_object)
|
||||
else:
|
||||
inv_object = invite_code_or_object
|
||||
max_uses = await self.get_invite_max_uses(ctx, inv_object)
|
||||
inv_details = f"{i+1}. {inv_object.url} [ {inv_object.uses} uses / {max_uses} max ]\n"
|
||||
invite_info += inv_details
|
||||
embed = discord.Embed(title=f"Invite Usage for {ctx.guild.name}", description=invite_info)
|
||||
if not list_all_invites:
|
||||
embed.set_footer(text="Only displaying pinned invites.")
|
||||
else:
|
||||
embed.set_footer(text="Displaying all invites.")
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@invites.command(aliases=["listpinned"])
|
||||
async def listpin(self, ctx):
|
||||
"""List pinned invites."""
|
||||
pinned_invites = await self.config.guild(ctx.guild).pinned_invites()
|
||||
invite_list = "None." if len(pinned_invites) == 0 else "\n".join(pinned_invites)
|
||||
await self._send_embed(ctx, "Pinned Invites", invite_list)
|
||||
|
||||
@invites.command()
|
||||
async def pin(self, ctx, invite_code_or_url: str):
|
||||
"""Pin an invite to the leaderboard."""
|
||||
if not ctx.me.permissions_in(ctx.channel).administrator:
|
||||
return await self._send_embed(ctx, PERM_MSG)
|
||||
|
||||
invite_code = await self._find_invite_code(invite_code_or_url)
|
||||
invite_code = await self._check_invite_code(ctx, invite_code)
|
||||
if not invite_code:
|
||||
return await self._send_embed(ctx, FAILURE_MSG)
|
||||
|
||||
existing_pins = await self.config.guild(ctx.guild).pinned_invites()
|
||||
if invite_code not in existing_pins:
|
||||
existing_pins.append(invite_code)
|
||||
await self.config.guild(ctx.guild).pinned_invites.set(existing_pins)
|
||||
await self._send_embed(ctx, f"{invite_code} was added to the pinned list.")
|
||||
else:
|
||||
await self._send_embed(ctx, f"{invite_code} is already in the pinned list.")
|
||||
|
||||
@invites.command()
|
||||
async def unpin(self, ctx, invite_code_or_url: str):
|
||||
"""Unpin an invite from the leaderboard."""
|
||||
invite_code = await self._find_invite_code(invite_code_or_url)
|
||||
if not invite_code:
|
||||
return await self._send_embed(ctx, FAILURE_MSG)
|
||||
|
||||
pinned_invites = await self.config.guild(ctx.guild).pinned_invites()
|
||||
if invite_code in pinned_invites:
|
||||
pinned_invites.remove(invite_code)
|
||||
else:
|
||||
return await self._send_embed(ctx, f"{invite_code} isn't in the pinned list.")
|
||||
await self.config.guild(ctx.guild).pinned_invites.set(pinned_invites)
|
||||
await self._send_embed(ctx, f"{invite_code} was removed from the pinned list.")
|
||||
|
||||
@invites.command(hidden=True)
|
||||
async def version(self, ctx):
|
||||
"""Invites version."""
|
||||
await self._send_embed(ctx, self.__version__)
|
||||
|
||||
@staticmethod
|
||||
async def _check_invite_code(ctx, invite_code):
|
||||
for invite in await ctx.guild.invites():
|
||||
if invite.code == invite_code:
|
||||
return invite_code
|
||||
else:
|
||||
continue
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
async def _find_invite_code(invite_code_or_url: str):
|
||||
invite_match = re.fullmatch(OLD_CODE_RE, invite_code_or_url) or re.fullmatch(CODE_RE, invite_code_or_url)
|
||||
if invite_match:
|
||||
return invite_code_or_url
|
||||
else:
|
||||
sep = invite_code_or_url.rfind("/")
|
||||
if sep:
|
||||
try:
|
||||
invite_code = invite_code_or_url.rsplit("/", 1)[1]
|
||||
return invite_code
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
async def _get_invite_from_code(ctx, invite_code):
|
||||
for invite in await ctx.guild.invites():
|
||||
if invite.code == invite_code:
|
||||
return invite
|
||||
else:
|
||||
continue
|
||||
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
async def get_invite_max_uses(self, ctx, invite_object):
|
||||
if invite_object.max_uses == 0:
|
||||
return "\N{INFINITY}"
|
||||
else:
|
||||
return invite_object.max_uses
|
||||
|
||||
async def _menu(self, ctx, pages):
|
||||
# `wait` in this function is whether the menus wait for completion.
|
||||
# An example of this is with concurrency:
|
||||
# If max_concurrency's wait arg is False (the default):
|
||||
# This function's wait being False will not follow the expected max_concurrency behaviour
|
||||
# This function's wait being True will follow the expected max_concurrency behaviour
|
||||
await MenuActions(source=pages, delete_message_after=False, clear_reactions_after=True, timeout=180).start(
|
||||
ctx, wait=True
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def _send_embed(ctx, title: str = None, description: str = None):
|
||||
title = "\N{ZERO WIDTH SPACE}" if title is None else title
|
||||
embed = discord.Embed()
|
||||
embed.title = title
|
||||
if description:
|
||||
embed.description = description
|
||||
embed.colour = await ctx.embed_colour()
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
|
||||
class MenuPages(menus.ListPageSource):
|
||||
def __init__(self, methods: List[discord.Invite]):
|
||||
super().__init__(methods, per_page=1)
|
||||
|
||||
async def format_page(self, menu: MenuActions, invite: discord.Invite) -> discord.Embed:
|
||||
# Use the menu to generate pages as they are needed instead of giving it a list of
|
||||
# already-generated embeds.
|
||||
embed = discord.Embed(title=f"Invites for {menu.ctx.guild.name}")
|
||||
max_uses = await Invites.get_invite_max_uses(menu.ctx, invite)
|
||||
msg = f"{cf.bold(invite.url)}\n\n"
|
||||
msg += f"Uses: {invite.uses}/{max_uses}\n"
|
||||
msg += f"Target Channel: {invite.channel.mention}\n"
|
||||
msg += f"Created by: {invite.inviter.mention}\n"
|
||||
msg += f"Created at: {invite.created_at.strftime('%m-%d-%Y @ %H:%M:%S UTC')}\n"
|
||||
if invite.temporary:
|
||||
msg += "Temporary invite\n"
|
||||
if invite.max_age == 0:
|
||||
max_age = f""
|
||||
else:
|
||||
max_age = f"Max age: {self._dynamic_time(int(invite.max_age))}"
|
||||
msg += f"{max_age}"
|
||||
embed.description = msg
|
||||
|
||||
return embed
|
||||
|
||||
@staticmethod
|
||||
def _dynamic_time(time):
|
||||
m, s = divmod(time, 60)
|
||||
h, m = divmod(m, 60)
|
||||
d, h = divmod(h, 24)
|
||||
|
||||
if d > 0:
|
||||
msg = "{0}d {1}h"
|
||||
elif d == 0 and h > 0:
|
||||
msg = "{1}h {2}m"
|
||||
elif d == 0 and h == 0 and m > 0:
|
||||
msg = "{2}m {3}s"
|
||||
elif d == 0 and h == 0 and m == 0 and s > 0:
|
||||
msg = "{3}s"
|
||||
else:
|
||||
msg = ""
|
||||
return msg.format(d, h, m, s)
|
||||
|
||||
|
||||
class MenuActions(menus.MenuPages, inherit_buttons=False):
|
||||
def reaction_check(self, payload):
|
||||
"""The function that is used to check whether the payload should be processed.
|
||||
This is passed to :meth:`discord.ext.commands.Bot.wait_for <Bot.wait_for>`.
|
||||
|
||||
There should be no reason to override this function for most users.
|
||||
This is done this way in this cog to let a bot owner operate the menu
|
||||
along with the original command invoker.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
payload: :class:`discord.RawReactionActionEvent`
|
||||
The payload to check.
|
||||
|
||||
Returns
|
||||
---------
|
||||
:class:`bool`
|
||||
Whether the payload should be processed.
|
||||
"""
|
||||
if payload.message_id != self.message.id:
|
||||
return False
|
||||
if payload.user_id not in (*self.bot.owner_ids, self._author_id):
|
||||
return False
|
||||
|
||||
return payload.emoji in self.buttons
|
||||
|
||||
async def show_checked_page(self, page_number: int) -> None:
|
||||
# This is a custom impl of show_checked_page that allows looping around back to the
|
||||
# beginning of the page stack when at the end and using next, or looping to the end
|
||||
# when at the beginning page and using prev.
|
||||
max_pages = self._source.get_max_pages()
|
||||
try:
|
||||
if max_pages is None:
|
||||
await self.show_page(page_number)
|
||||
elif page_number >= max_pages:
|
||||
await self.show_page(0)
|
||||
elif page_number < 0:
|
||||
await self.show_page(max_pages - 1)
|
||||
elif max_pages > page_number >= 0:
|
||||
await self.show_page(page_number)
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
@menus.button("\N{UP-POINTING RED TRIANGLE}", position=menus.First(1))
|
||||
async def prev(self, payload: discord.RawReactionActionEvent):
|
||||
await self.show_checked_page(self.current_page - 1)
|
||||
|
||||
@menus.button("\N{DOWN-POINTING RED TRIANGLE}", position=menus.First(2))
|
||||
async def next(self, payload: discord.RawReactionActionEvent):
|
||||
await self.show_checked_page(self.current_page + 1)
|
||||
|
||||
@menus.button("\N{CROSS MARK}", position=menus.Last(0))
|
||||
async def close_menu(self, payload: discord.RawReactionActionEvent) -> None:
|
||||
self.stop()
|
||||
Reference in New Issue
Block a user