[Invites] Initial commit

This commit is contained in:
aikaterna
2020-07-23 20:25:21 -04:00
parent f7ba8d10bb
commit bc5a387f96
4 changed files with 299 additions and 0 deletions

5
invites/__init__.py Normal file
View File

@@ -0,0 +1,5 @@
from .invites import Invites
def setup(bot):
bot.add_cog(Invites(bot))

10
invites/info.json Normal file
View 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
View 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()