-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathembedshell.py
More file actions
376 lines (307 loc) · 13.4 KB
/
Copy pathembedshell.py
File metadata and controls
376 lines (307 loc) · 13.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
# Based off repl from RDanny by Danny.
# Will fail if the bot does not have embed permissions.
# Embed designed and implemented by taciturasa.
# Released under the MIT License.
#
# The MIT License (MIT)
#
# Copyright (c) 2015 Rapptz
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
import discord
from discord.ext import commands
import collections
import aiohttp
import inspect
import traceback
from contextlib import redirect_stdout
import io
class EmbedShell():
def __init__(self, bot):
self.bot = bot
self.repl_sessions = {}
self.repl_embeds = {}
self.aioclient = aiohttp.ClientSession()
def cleanup_code(self, content):
'''Automatically removes code blocks from the code.'''
# remove ```py\n```
if content.startswith('```') and content.endswith('```'):
return '\n'.join(content.split('\n')[1:-1])
# remove `foo`
return content.strip('` \n')
def get_syntax_error(self, err):
'''Returns SyntaxError formatted for repl reply.'''
return '```py\n{0.text}{1:>{0.offset}}\n{2}: {0}```'.format(
err,
'^',
type(err).__name__)
async def post_to_hastebin(self, string):
'''Posts a string to hastebin.'''
url = "https://hastebin.com/documents"
data = string.encode('utf-8')
async with self.aioclient.post(url=url, data=data) as haste_response:
print(haste_response)
haste_key = (await haste_response.json())['key']
haste_url = f"http://hastebin.com/{haste_key}"
return haste_url
@commands.group(name='shell',
aliases=['ipython', 'repl',
'longexec', 'core', 'overkill'],
pass_context=True,
invoke_without_command=True)
async def repl(self, ctx, *, name: str=None):
'''Head on impact with an interactive python shell.'''
# TODO Minimize local variables
# TODO Minimize branches
session = ctx.message.channel.id
embed = discord.Embed(
description="_Enter code to execute or evaluate. "
"`exit()` or `quit` to exit._")
embed.set_author(
name="Interactive Python Shell",
icon_url="https://upload.wikimedia.org/wikipedia/commons/thumb"
"/c/c3/Python-logo-notext.svg/1024px-Python-logo-notext.svg.png")
embed.set_footer(text="Based on RDanny's repl command by Danny.")
if name is not None:
embed.title = name.strip(" ")
history = collections.OrderedDict()
variables = {
'ctx': ctx,
'bot': self.bot,
'message': ctx.message,
'server': ctx.message.server,
'channel': ctx.message.channel,
'author': ctx.message.author,
'discord': discord,
'twitter': self.twitter,
'_': None
}
if session in self.repl_sessions:
error_embed = discord.Embed(
color=15746887,
description="**Error**: "
"_Shell is already running in channel._")
await self.bot.say(embed=error_embed)
return
shell = await self.bot.say(embed=embed)
self.repl_sessions[session] = shell
self.repl_embeds[shell] = embed
while True:
response = await self.bot.wait_for_message(
author=ctx.message.author,
channel=ctx.message.channel,
check=lambda m: m.content.startswith('`'))
cleaned = self.cleanup_code(response.content)
shell = self.repl_sessions[session]
# Regular Bot Method
# try:
# await self.bot.get_message(
# ctx.message.channel,
# self.repl_sessions[session].id)
# except discord.NotFound:
# new_shell = await self.bot.say(embed=self.repl_embeds[shell])
# self.repl_sessions[session] = new_shell
#
# embed = self.repl_embeds[shell]
# del self.repl_embeds[shell]
# self.repl_embeds[new_shell] = embed
#
# shell = self.repl_sessions[session]
shell_check = discord.utils.get(
self.bot.messages,
id=self.repl_sessions[session].id)
# Self Bot Method
if shell_check is None:
new_shell = await self.bot.say(embed=self.repl_embeds[shell])
self.repl_sessions[session] = new_shell
embed = self.repl_embeds[shell]
del self.repl_embeds[shell]
self.repl_embeds[new_shell] = embed
shell = self.repl_sessions[session]
del shell_check
try:
await self.bot.delete_message(response)
except discord.Forbidden:
pass
if len(self.repl_embeds[shell].fields) >= 7:
self.repl_embeds[shell].remove_field(0)
if cleaned in ('quit', 'exit', 'exit()'):
self.repl_embeds[shell].color = 16426522
if self.repl_embeds[shell].title is not discord.Embed.Empty:
history_string = "History for {}\n\n\n".format(
self.repl_embeds[shell].title)
else:
history_string = "History for latest session\n\n\n"
for item in history.keys():
history_string += ">>> {}\n{}\n\n".format(
item,
history[item])
haste_url = await self.post_to_hastebin(history_string)
return_msg = "[`Leaving shell session. "\
"History hosted on hastebin.`]({})".format(
haste_url)
self.repl_embeds[shell].add_field(
name="`>>> {}`".format(cleaned),
value=return_msg,
inline=False)
await self.bot.edit_message(
self.repl_sessions[session],
embed=self.repl_embeds[shell])
del self.repl_embeds[shell]
del self.repl_sessions[session]
return
executor = exec
if cleaned.count('\n') == 0:
# single statement, potentially 'eval'
try:
code = compile(cleaned, '<repl session>', 'eval')
except SyntaxError:
pass
else:
executor = eval
if executor is exec:
try:
code = compile(cleaned, '<repl session>', 'exec')
except SyntaxError as err:
self.repl_embeds[shell].color = 15746887
return_msg = self.get_syntax_error(err)
history[cleaned] = return_msg
if len(cleaned) > 800:
cleaned = "<Too big to be printed>"
if len(return_msg) > 800:
haste_url = await self.post_to_hastebin(return_msg)
return_msg = "[`SyntaxError too big to be printed. "\
"Hosted on hastebin.`]({})".format(
haste_url)
self.repl_embeds[shell].add_field(
name="`>>> {}`".format(cleaned),
value=return_msg,
inline=False)
await self.bot.edit_message(
self.repl_sessions[session],
embed=self.repl_embeds[shell])
continue
variables['message'] = response
fmt = None
stdout = io.StringIO()
try:
with redirect_stdout(stdout):
result = executor(code, variables)
if inspect.isawaitable(result):
result = await result
except Exception as err:
self.repl_embeds[shell].color = 15746887
value = stdout.getvalue()
fmt = '```py\n{}{}\n```'.format(
value,
traceback.format_exc())
else:
self.repl_embeds[shell].color = 4437377
value = stdout.getvalue()
if result is not None:
fmt = '```py\n{}{}\n```'.format(
value,
result)
variables['_'] = result
elif value:
fmt = '```py\n{}\n```'.format(value)
history[cleaned] = fmt
if len(cleaned) > 800:
cleaned = "<Too big to be printed>"
try:
if fmt is not None:
if len(fmt) >= 800:
haste_url = await self.post_to_hastebin(fmt)
self.repl_embeds[shell].add_field(
name="`>>> {}`".format(cleaned),
value="[`Content too big to be printed. "
"Hosted on hastebin.`]({})".format(
haste_url),
inline=False)
await self.bot.edit_message(
self.repl_sessions[session],
embed=self.repl_embeds[shell])
else:
self.repl_embeds[shell].add_field(
name="`>>> {}`".format(cleaned),
value=fmt,
inline=False)
await self.bot.edit_message(
self.repl_sessions[session],
embed=self.repl_embeds[shell])
else:
self.repl_embeds[shell].add_field(
name="`>>> {}`".format(cleaned),
value="`Empty response, assumed successful.`",
inline=False)
await self.bot.edit_message(
self.repl_sessions[session],
embed=self.repl_embeds[shell])
except discord.Forbidden:
pass
except discord.HTTPException as err:
error_embed = discord.Embed(
color=15746887,
description='**Error**: _{}_'.format(err))
await self.bot.say(embed=error_embed)
@repl.command(name='jump',
aliases=['hop', 'pull', 'recenter', 'whereditgo'],
pass_context=True)
async def _repljump(self, ctx):
'''Brings the shell back down so you can see it again.'''
session = ctx.message.channel.id
if session not in self.repl_sessions:
error_embed = discord.Embed(
color=15746887,
description="**Error**: _No shell running in channel._")
await self.bot.say(embed=error_embed)
return
shell = self.repl_sessions[session]
embed = self.repl_embeds[shell]
await self.bot.delete_message(ctx.message)
try:
await self.bot.delete_message(shell)
except discord.errors.NotFound:
pass
new_shell = await self.bot.say(embed=embed)
self.repl_sessions[session] = new_shell
del self.repl_embeds[shell]
self.repl_embeds[new_shell] = embed
@repl.command(name='clear',
aliases=['clean', 'purge', 'cleanup',
'ohfuckme', 'deletthis'],
pass_context=True)
async def _replclear(self, ctx):
'''Clears the fields of the shell and resets the color.'''
session = ctx.message.channel.id
if session not in self.repl_sessions:
error_embed = discord.Embed(
color=15746887,
description="**Error**: _No shell running in channel._")
await self.bot.say(embed=error_embed)
return
shell = self.repl_sessions[session]
self.repl_embeds[shell].color = discord.Color.default()
self.repl_embeds[shell].clear_fields()
await self.bot.delete_message(ctx.message)
await self.bot.edit_message(
shell,
embed=self.repl_embeds[shell])
def setup(bot):
bot.add_cog(EmbedShell(bot))