-
Notifications
You must be signed in to change notification settings - Fork 0
/
FediRun.py
201 lines (178 loc) · 9.12 KB
/
FediRun.py
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
from ananas import PineappleBot, reply
from bs4 import BeautifulSoup
import requests
from pyxdameraulevenshtein import normalized_damerau_levenshtein_distance as ndld
import zlib
from typing import List
class FediRun(PineappleBot):
@reply
def respond(self, status, user):
username = user["acct"]
# decode the toot into raw text
soup = BeautifulSoup(status["content"], "lxml")
self.log("raw_toot", soup)
# strip mentions
for mention in status["mentions"]:
for a in soup.find_all(href=mention["url"]):
a.extract()
# put all the lines in a list
# if there's no p tag, add one wrapping everything
if not soup.find_all('p'):
body_children = list(soup.body.children)
wrapper_p = soup.new_tag('p')
soup.body.clear()
soup.body.append(wrapper_p)
for child in body_children:
wrapper_p.append(child)
self.log("wrapped_toot", soup)
# replace <br /> tags with a newline
for br in soup.find_all("br"):
br.replace_with('\n')
# then replace consecutive <p> tags with a double newline
lines = [line.text for line in soup.find_all('p')]
lines = '\n\n'.join(lines)
# finally split all the lines up at the newlines we just added
lines = lines.splitlines()
# only one line, abort as there is no code to run
if len(lines) == 1:
return
# the language must be on the first line of the toot
user_language = lines[0].strip()
if not user_language:
self.log('respond', '@{} left off the language'.format(username))
self._send_reply('@{} the language name *must* be on the first line of your toot'.format(username), status)
return
language = user_language.lower()
# check if tio.run accepts this language
if language in self.languages_friendly:
# convert friendly language name to api language name
language = self.languages_friendly[language]
if language not in self.languages:
self.log('respond', '@{} requested unrecognized language {!r}'.format(username, language))
# no match; return a list of close language name matches
lang_list = self._closest_matches(user_language.lower(), self.languages_friendly.keys(), 10, 0.8)
# no close matches found, abort, it's probably just someone talking about the bot
if not lang_list:
return
lang_string = "\n".join(lang_list)
self._send_reply('@{} language {!r} is unknown on https://tio.run\n'.format(username, user_language) +
'Perhaps you wanted one of these?\n\n' +
'{}'.format(lang_string),
status)
return
self.log('respond', '@{} requested to execute some {!r}'.format(username, language))
# the rest of the toot is treated as code
code = '\n'.join(lines[1:])
# send the code off to tio.run
returned, errors = self._tio(language, code)
response = returned
# grab and check the exit code
if int(errors.splitlines()[-1][len("Exit code: ")-1:]) != 0:
self.log('respond', '@{} got a non-zero exit code, tacking on error output'.format(username, language))
# add error output if the exit code was non-zero
response += '\nError: {}'.format(errors)
# if the response is too long, paste.ee it instead
if len(response) + len('@{} '.format(username)) > 500:
paste = self._paste_ee(response, 'TIO {} output for @{}'.format(user_language, username), 0)
response = 'output too long, pasted here: {}'.format(paste)
self._send_reply('@{} {}'.format(username, response), status)
def _tio(self, language, code, user_input=''):
# build our request dictionary
request = [{'command': 'V', 'payload': {'lang': [language.lower()]}},
{'command': 'F', 'payload': {'.code.tio': code}},
{'command': 'F', 'payload': {'.input.tio': user_input}},
{'command': 'RC'}]
# convert the dictionary into the form tio.run accepts
req = b''
for instr in request:
req += instr['command'].encode()
if 'payload' in instr:
[(name, value)] = instr['payload'].items()
req += b'%s\0' % name.encode()
if type(value) == str:
value = value.encode()
req += b'%u\0' % len(value)
if type(value) != bytes:
value = '\0'.join(value).encode() + b'\0'
req += value
req_raw = zlib.compress(req, 9)[2:-4]
# send our request off to tio.run
url = "https://tio.run/cgi-bin/static/b666d85ff48692ae95f24a66f7612256-run/93d25ed21c8d2bb5917e6217ac439d61"
res = requests.post(url, data=req_raw)
# decode the results
res = zlib.decompress(res.content, 31)
delim = res[:16]
ret = res[16:].split(delim)
count = len(ret) >> 1
returned, errors = ret[:count], ret[count:]
returned = [r.decode('utf-8', 'ignore') for r in returned]
errors = [e.decode('utf-8', 'ignore') for e in errors]
return_str = '\n'.join(returned)
error_str = '\n'.join(errors)
return return_str, error_str
def start(self):
# fetch language list from tio.run
self.languages = self._fetch_languages()
# build a mapping of friendly language names to api language names
self.languages_friendly = {d['name'].lower(): l for l, d in self.languages.items()}
# add some defaults for languages with multiple options or nonobvious names
self.languages_friendly['ada'] = 'ada-gnat'
self.languages_friendly['algol'] = 'algol68g'
self.languages_friendly['apl'] = 'apl-dyalog'
self.languages_friendly['c'] = 'c-clang'
self.languages_friendly['c++'] = 'cpp-clang'
self.languages_friendly['cpp'] = 'cpp-clang'
self.languages_friendly['c#'] = 'cs-core'
self.languages_friendly['cobol'] = 'cobol-gnu'
self.languages_friendly['erlang'] = 'erlang-escript'
self.languages_friendly['forth'] = 'forth-gforth'
self.languages_friendly['fortran'] = 'fortran-gfortran'
self.languages_friendly['f#'] = 'fs-core'
self.languages_friendly['java'] = 'java-jdk'
self.languages_friendly['javascript'] = 'javascript-node'
self.languages_friendly['js'] = 'javascript-node'
self.languages_friendly['lisp'] = 'clisp'
self.languages_friendly['objective-c'] = 'objective-c-clang'
self.languages_friendly['pascal'] = 'pascal-fpc'
self.languages_friendly['postscript'] = 'postscript-xpost'
self.languages_friendly['prolog'] = 'prolog-swi'
self.languages_friendly['python'] = 'python3'
self.languages_friendly['scheme'] = 'scheme-chicken'
self.languages_friendly['vb'] = 'vb-core'
self.languages_friendly['vb.net'] = 'vb-core'
self.languages_friendly['visual basic'] = 'vb-core'
self.languages_friendly['visual basic .net'] = 'vb-core'
def _fetch_languages(self):
lang_url = "https://raw.githubusercontent.com/TryItOnline/tryitonline/master/usr/share/tio.run/languages.json"
response = requests.get(lang_url)
if response:
self.log("_fetch_languages", "Loaded language list from TryItOnline")
else:
self.log("_fetch_languages", "Failed to load language list from TryItOnline")
return response.json()
def _closest_matches(self, search: str, word_list: List[str], num_matches: int, threshold: float) -> List[str]:
# measure the Damerau-Levenshtein distance between our target word and a list of possible close matches,
# returning up to num_matches results
similarities = sorted([(ndld(search, word), word) for word in word_list])
close_matches = [word for (diff, word) in similarities if diff <= threshold]
top_n = close_matches[:num_matches]
return top_n
def _paste_ee(self, data: str, description: str, expire: int, raw: bool = False) -> str:
values = {"key": "public",
"description": description,
"paste": data,
"expiration": expire,
"format": "json"}
result = requests.post("http://paste.ee/api", data=values, timeout=10)
if result:
j = result.json()
if j["status"] == "success":
link_type = "raw" if raw else "link"
return j["paste"][link_type]
elif j["status"] == "error":
return ("an error occurred while posting to paste.ee, code: {}, reason: {}"
.format(j["errorcode"], j["error"]))
def _send_reply(self, response, original):
self.mastodon.status_post(response,
in_reply_to_id=original["id"],
visibility=original["visibility"])