[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

View File

@@ -27,6 +27,8 @@ imgwelcome - Welcome users to your server(s) with an image. The repo can be foun
inspirobot - Fetch "inspirational" messages from inspirobot.me with [p]inspireme.
invites - Display invites that are available on the server and the information those invites contain. The bot must have the administrator permission granted on the guild to be able to use this cog.
luigipoker - Play the Luigi Poker minigame from New Super Mario Brothers. Ported from the v2 version written by themario30.
noflippedtables - A v3 port of irdumb's v2 cog with a little extra surprise included. Unflip all the tables.

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()