· 6 years ago · Jan 31, 2020, 03:44 PM
1from discord.ext import commands
2import fuzzy
3import asyncio
4import discord
5import re
6import zlib
7import io
8import os
9import lxml.etree as etree
10from collections import Counter
11
12
13
14class SphinxObjectFileReader:
15 # Inspired by Sphinx's InventoryFileReader
16 BUFSIZE = 16 * 1024
17
18 def __init__(self, buffer):
19 self.stream = io.BytesIO(buffer)
20
21 def readline(self):
22 return self.stream.readline().decode('utf-8')
23
24 def skipline(self):
25 self.stream.readline()
26
27 def read_compressed_chunks(self):
28 decompressor = zlib.decompressobj()
29 while True:
30 chunk = self.stream.read(self.BUFSIZE)
31 if len(chunk) == 0:
32 break
33 yield decompressor.decompress(chunk)
34 yield decompressor.flush()
35
36class BotUser(commands.Converter):
37 async def convert(self, ctx, argument):
38 if not argument.isdigit():
39 raise commands.BadArgument('Not a valid bot user ID.')
40 try:
41 user = await ctx.bot.fetch_user(argument)
42 except discord.NotFound:
43 raise commands.BadArgument('Bot user not found (404).')
44 except discord.HTTPException as e:
45 raise commands.BadArgument(f'Error fetching bot user: {e}')
46 else:
47 if not user.bot:
48 raise commands.BadArgument('This is not a bot.')
49 return user
50
51
52
53class API(commands.Cog):
54 """Discord API exclusive things."""
55
56 def __init__(self, bot):
57 self.bot = bot
58 self.issue = re.compile(r'##(?P<number>[0-9]+)')
59 self._recently_blocked = set()
60
61
62 def parse_object_inv(self, stream, url):
63 # key: URL
64 # n.b.: key doesn't have `discord` or `discord.ext.commands` namespaces
65 result = {}
66
67 # first line is version info
68 inv_version = stream.readline().rstrip()
69
70 if inv_version != '# Sphinx inventory version 2':
71 raise RuntimeError('Invalid objects.inv file version.')
72
73 # next line is "# Project: <name>"
74 # then after that is "# Version: <version>"
75 projname = stream.readline().rstrip()[11:]
76
77
78 # next line says if it's a zlib header
79 line = stream.readline()
80 if 'zlib' not in line:
81 raise RuntimeError('Invalid objects.inv file, not z-lib compatible.')
82
83 # This code mostly comes from the Sphinx repository.
84 entry_regex = re.compile(r'(?x)(.+?)\s+(\S*:\S*)\s+(-?\d+)\s+(\S+)\s+(.*)')
85 for line in stream.read_compressed_lines():
86 match = entry_regex.match(line.rstrip())
87 if not match:
88 continue
89
90 name, directive, location, dispname = match.groups()
91 domain, _, subdirective = directive.partition(':')
92 if directive == 'py:module' and name in result:
93 # From the Sphinx Repository:
94 # due to a bug in 1.1 and below,
95 # two inventory entries are created
96 # for Python modules, and the first
97 # one is correct
98 continue
99
100 # Most documentation pages have a label
101 if directive == 'std:doc':
102 subdirective = 'label'
103
104 if location.endswith('$'):
105 location = location[:-1] + name
106
107 key = name if dispname == '-' else dispname
108 prefix = f'{subdirective}:' if domain == 'std' else ''
109
110 if projname == 'discord.py':
111 key = key.replace('discord.ext.commands.', '').replace('discord.', '')
112
113 result[f'{prefix}{key}'] = os.path.join(url, location)
114
115 return result
116
117 async def build_rtfm_lookup_table(self, page_types):
118 cache = {}
119 for key, page in page_types.items():
120 sub = cache[key] = {}
121 async with self.bot.session.get(page + '/objects.inv') as resp:
122 if resp.status != 200:
123 raise RuntimeError('Cannot build rtfm lookup table, try again later.')
124
125 stream = SphinxObjectFileReader(await resp.read())
126 cache[key] = self.parse_object_inv(stream, page)
127
128 self._rtfm_cache = cache
129
130
131 async def do_rtfm(self, ctx, key, obj):
132 page_types = {
133 'latest': 'https://discordpy.readthedocs.io/en/latest',
134 'python': 'https://docs.python.org/3',
135 }
136
137 if obj is None:
138 await ctx.send(page_types[key])
139 return
140
141 if not hasattr(self, '_rtfm_cache'):
142 await ctx.trigger_typing()
143 await self.build_rtfm_lookup_table(page_types)
144
145 obj = re.sub(r'^(?:discord\.(?:ext\.)?)?(?:commands\.)?(.+)', r'\1', obj)
146
147 if key.startswith('latest'):
148 # point the abc.Messageable types properly:
149 q = obj.lower()
150 for name in dir(discord.abc.Messageable):
151 if name[0] == '_':
152 continue
153 if q == name:
154 obj = f'abc.Messageable.{name}'
155 break
156
157 cache = list(self._rtfm_cache[key].items())
158
159
160 matches = fuzzy.finder(obj, cache, key=lambda t: t[0], lazy=False)[:8]
161
162 e = discord.Embed(colour=discord.Colour.blurple())
163 if len(matches) == 0:
164 return await ctx.send('Could not find anything. Sorry.')
165
166 e.description = '\n'.join(f'[`{key}`]({url})' for key, url in matches)
167 await ctx.send(embed=e)
168
169
170
171
172 @commands.group(aliases=['rtfd'], invoke_without_command=True)
173 async def rtfm(self, ctx, *, obj: str = None,member: discord.Member = None):
174 """Gives you a documentation link for a discord.py entity.
175 Events, objects, and functions are all supported through a
176 a cruddy fuzzy algorithm.
177 """
178 await self.do_rtfm(ctx, 'latest', obj)
179
180 @rtfm.command(name='python', aliases=['py'])
181 async def rtfm_python(self, ctx, *, obj: str = None):
182 """Gives you a documentation link for a Python entity."""
183 await self.do_rtfm(ctx, 'python', obj)
184
185
186
187
188def setup(bot):
189 bot.add_cog(API(bot))
190 print("API cog has been loaded")