Skip to content

Commit 4712893

Browse files
committed
Paginate the help command and snippets
1 parent 2422f25 commit 4712893

File tree

2 files changed

+181
-19
lines changed

2 files changed

+181
-19
lines changed

bot.py

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,23 +24,26 @@
2424

2525
__version__ = '1.3.7'
2626

27-
import discord
28-
from discord.ext import commands
29-
import aiohttp
27+
from contextlib import redirect_stdout
3028
from urllib.parse import urlparse
29+
from copy import deepcopy
3130
import asyncio
3231
import textwrap
32+
import traceback
3333
import datetime
34+
import inspect
35+
import string
3436
import time
3537
import json
36-
import sys
3738
import os
3839
import re
39-
import string
40-
import traceback
4140
import io
42-
import inspect
43-
from contextlib import redirect_stdout
41+
42+
import discord
43+
from discord.ext import commands
44+
from paginator import PaginatorSession
45+
import aiohttp
46+
4447

4548
class Github:
4649
head = 'https://api.github.com/repos/kyb3r/modmail/git/refs/heads/master'
@@ -206,13 +209,12 @@ def overwrites(self, ctx, modrole=None):
206209
def help_embed(self, prefix):
207210
em = discord.Embed(color=0x00FFFF)
208211
em.set_author(name='Mod Mail - Help', icon_url=self.user.avatar_url)
209-
em.description = 'This bot is a python implementation of a "Mod Mail" bot. ' \
210-
'Made by kyb3r and improved by the suggestions of others.'
212+
em.description = 'Here is a list of commands for the bot.'
211213

212214
cmds = f'`{prefix}setup` - Sets up the categories that will be used by the bot.\n' \
213215
f'`{prefix}about` - Shows general information about the bot.\n' \
214216
f'`{prefix}contact` - Allows a moderator to initiate a thread with a given recipient.\n' \
215-
f'`{prefix}reply <message...>` - Sends a message to the current thread\'s recipient.\n' \
217+
f'`{prefix}reply` - Sends a message to the current thread\'s recipient.\n' \
216218
f'`{prefix}close` - Closes the current thread and deletes the channel.\n' \
217219
f'`{prefix}archive` - Closes the thread and moves the channel to archive category.\n' \
218220
f'`{prefix}block` - Blocks a user from using modmail.\n' \
@@ -244,7 +246,7 @@ def help_embed(self, prefix):
244246
em.add_field(name='Custom Mentions', value=mention)
245247
em.add_field(name='Warning', value=warn)
246248
em.add_field(name='Github', value='https://github.com/kyb3r/modmail')
247-
em.set_footer(text=f'Modmail v{__version__} | Star the repository to unlock hidden features! /s')
249+
em.set_footer(text=f'modmail v{__version__} • A star on the repository is appreciated.')
248250

249251
return em
250252

@@ -283,8 +285,16 @@ def uptime(self):
283285
@commands.command()
284286
async def help(self, ctx):
285287
prefix = self.config.get('PREFIX', 'm.')
286-
em = self.help_embed(prefix)
287-
await ctx.send(embed=em)
288+
289+
em1 = self.help_embed(prefix)
290+
em2 = deepcopy(em1)
291+
em1.set_footer(text=f'modmail v{__version__}')
292+
em2.description = None
293+
em2.remove_field(0)
294+
em1._fields = em1._fields[0:1]
295+
296+
session = PaginatorSession(ctx, em1, em2)
297+
await session.run()
288298

289299
@commands.command()
290300
async def about(self, ctx):
@@ -391,16 +401,28 @@ async def setup(self, ctx, *, modrole: discord.Role=None):
391401
@commands.has_permissions(manage_messages=True)
392402
async def _snippets(self, ctx):
393403
'''Returns a list of snippets that are currently set.'''
404+
embeds = []
405+
394406
em = discord.Embed(color=discord.Color.green())
395407
em.set_author(name='Snippets', icon_url=ctx.guild.icon_url)
396-
first_line = 'Here is a list of snippets that are currently configured. '
408+
409+
embeds.append(em)
410+
411+
em.description = 'Here is a list of snippets that are currently configured.'
412+
397413
if not self.snippets:
398-
first_line = 'You dont have any snippets at the moment. '
399-
em.description = first_line + 'You can add snippets by adding config variables in the form' \
400-
' **`SNIPPET_{NAME}`**'
414+
em.color = discord.Color.red()
415+
em.description = 'You dont have any snippets at the moment.'
416+
401417
for name, value in self.snippets.items():
418+
if len(em.fields) == 5:
419+
em = discord.Embed(color=discord.Color.green(), description=em.description)
420+
em.set_author(name='Snippets', icon_url=ctx.guild.icon_url)
421+
embeds.append(em)
402422
em.add_field(name=name, value=value, inline=False)
403-
await ctx.send(embed=em)
423+
424+
session = PaginatorSession(ctx, *embeds)
425+
await session.run()
404426

405427
@commands.command()
406428
@commands.has_permissions(administrator=True)

paginator.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import discord
2+
from discord.ext import commands
3+
from collections import OrderedDict
4+
import asyncio
5+
import inspect
6+
7+
class PaginatorSession:
8+
'''
9+
Class that interactively paginates a set of embeds
10+
11+
Parameters
12+
------------
13+
ctx: Context
14+
The context of the command.
15+
timeout:
16+
How long to wait for before the session closes
17+
embeds: List[discord.Embed]
18+
A list of entries to paginate.
19+
20+
Methods
21+
-------
22+
add_page:
23+
Add an embed to paginate
24+
run:
25+
Run the interactive session
26+
close:
27+
Forcefully destroy a session
28+
'''
29+
def __init__(self, ctx, *embeds, **options):
30+
self.ctx = ctx
31+
self.timeout = options.get('timeout', 60)
32+
self.embeds = embeds
33+
self.running = False
34+
self.base = None
35+
self.current = 0
36+
self.reaction_map = {
37+
'⏮': self.first_page,
38+
'◀': self.previous_page,
39+
'▶': self.next_page,
40+
'⏭': self.last_page,
41+
# '⏹': self.close
42+
}
43+
if options.get('edit_footer', True) and len(self.embeds) > 1:
44+
for i, em in enumerate(self.embeds):
45+
footer_text = f'Page {i+1} of {len(self.embeds)}'
46+
em.footer.text = options.get('footer_text', em.footer.text)
47+
if em.footer.text:
48+
footer_text = footer_text + ' • ' + em.footer.text
49+
em.set_footer(text=footer_text, icon_url=em.footer.icon_url)
50+
51+
52+
def add_page(self, embed):
53+
if isinstance(embed, discord.Embed):
54+
self.embeds.append(embed)
55+
else:
56+
raise TypeError('Page must be an Embed object.')
57+
58+
async def create_base(self, embed):
59+
self.base = await self.ctx.send(embed=embed)
60+
61+
if len(self.embeds) == 1:
62+
self.running = False
63+
return
64+
65+
self.running = True
66+
for reaction in self.reaction_map.keys():
67+
if len(self.embeds) == 2 and reaction in '⏮⏭':
68+
continue
69+
await self.base.add_reaction(reaction)
70+
71+
async def show_page(self, index: int):
72+
if not 0 <= index < len(self.embeds):
73+
return
74+
75+
self.current = index
76+
page = self.embeds[index]
77+
78+
if self.running:
79+
await self.base.edit(embed=page)
80+
else:
81+
await self.create_base(page)
82+
83+
def react_check(self, reaction, user):
84+
if user.id != self.ctx.author.id:
85+
return False
86+
if reaction.message.id != self.base.id:
87+
return False
88+
if reaction.emoji in self.reaction_map.keys():
89+
return True
90+
91+
async def run(self):
92+
if not self.running:
93+
await self.show_page(0)
94+
while self.running:
95+
try:
96+
reaction, user = await self.ctx.bot.wait_for('reaction_add', check=self.react_check, timeout=self.timeout)
97+
except asyncio.TimeoutError:
98+
self.paginating = False
99+
await self.close(delete=False)
100+
else:
101+
action = self.reaction_map.get(reaction.emoji)
102+
await action()
103+
try:
104+
await self.base.remove_reaction(reaction, user)
105+
except:
106+
pass
107+
108+
109+
def previous_page(self):
110+
'''Go to the previous page.'''
111+
return self.show_page(self.current-1)
112+
113+
def next_page(self):
114+
'''Go to the next page'''
115+
return self.show_page(self.current+1)
116+
117+
async def close(self, delete=True):
118+
'''Delete this embed.'''
119+
self.running = False
120+
121+
try:
122+
await self.ctx.message.add_reaction('✅')
123+
except:
124+
pass
125+
126+
if delete:
127+
return await self.base.delete()
128+
129+
try:
130+
await self.base.clear_reactions()
131+
except:
132+
pass
133+
134+
def first_page(self):
135+
'''Go to immediately to the first page'''
136+
return self.show_page(0)
137+
138+
def last_page(self):
139+
'''Go to immediately to the last page'''
140+
return self.show_page(len(self.embeds)-1)

0 commit comments

Comments
 (0)