eval.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504
  1. # ©️ Dan Gazizullin, 2021-2023
  2. # This file is a part of Hikka Userbot
  3. # 🌐 https://github.com/hikariatama/Hikka
  4. # You can redistribute it and/or modify it under the terms of the GNU AGPLv3
  5. # 🔑 https://www.gnu.org/licenses/agpl-3.0.html
  6. import contextlib
  7. import itertools
  8. import os
  9. import subprocess
  10. import sys
  11. import tempfile
  12. import typing
  13. from types import ModuleType
  14. import hikkatl
  15. from hikkatl.errors.rpcerrorlist import MessageIdInvalidError
  16. from hikkatl.sessions import StringSession
  17. from hikkatl.tl.types import Message
  18. from meval import meval
  19. from .. import loader, main, utils
  20. from ..log import HikkaException
  21. class Brainfuck:
  22. def __init__(self, memory_size: int = 30000):
  23. if memory_size < 0:
  24. raise ValueError("memory size cannot be negative")
  25. self._data = [0] * memory_size
  26. self.out = ""
  27. self.error = None
  28. @property
  29. def data(self):
  30. return self._data
  31. def run(self, code: str) -> str:
  32. self.out = ""
  33. had_error = self._eval(code)
  34. if had_error:
  35. return ""
  36. self._interpret(code)
  37. return self.out
  38. def _report_error(
  39. self,
  40. message: str,
  41. line: typing.Optional[int] = None,
  42. column: typing.Optional[int] = None,
  43. ):
  44. self.error = (
  45. message + f" at line {line}, column {column}"
  46. if line is not None and column is not None
  47. else ""
  48. )
  49. def _eval(self, source: str):
  50. line = col = 0
  51. stk = []
  52. loop_open = False
  53. for c in source:
  54. if c == "[":
  55. if loop_open:
  56. self._report_error("unexpected token '['", line, col)
  57. return True
  58. loop_open = True
  59. stk.append("[")
  60. elif c == "]":
  61. loop_open = False
  62. if len(stk) == 0:
  63. self._report_error("unexpected token ']'", line, col)
  64. return True
  65. stk.pop()
  66. elif c == "\n":
  67. line += 1
  68. col = -1
  69. col += 1
  70. if len(stk) != 0:
  71. self._report_error("unmatched brackets")
  72. return True
  73. return False
  74. def _interpret(self, source: str):
  75. line = col = ptr = current = 0
  76. while current < len(source):
  77. if source[current] == ">":
  78. if ptr == (len(self.data) - 1):
  79. self._report_error("pointer out of range", line, col)
  80. return True
  81. ptr += 1
  82. elif source[current] == "<":
  83. if ptr == 0:
  84. self._report_error("pointer out of range", line, col)
  85. return True
  86. ptr -= 1
  87. elif source[current] == "+":
  88. if self.data[ptr] >= 2**32:
  89. self._report_error("cell overflow")
  90. return True
  91. self.data[ptr] += 1
  92. elif source[current] == "-":
  93. if self.data[ptr] == 0:
  94. self._report_error("cell underflow")
  95. return True
  96. self.data[ptr] -= 1
  97. elif source[current] == ".":
  98. self.out += chr(self.data[ptr])
  99. elif source[current] == "[":
  100. if self.data[ptr] == 0:
  101. while source[current] != "]":
  102. current += 1
  103. elif source[current] == "]":
  104. if self.data[ptr] != 0:
  105. while source[current] != "[":
  106. current -= 1
  107. elif source[current] == "\n":
  108. line += 1
  109. col = -1
  110. col += 1
  111. current += 1
  112. return False
  113. @loader.tds
  114. class Evaluator(loader.Module):
  115. """Evaluates code in various languages"""
  116. strings = {"name": "Evaluator"}
  117. @loader.command(alias="eval")
  118. async def e(self, message: Message):
  119. try:
  120. result = await meval(
  121. utils.get_args_raw(message),
  122. globals(),
  123. **await self.getattrs(message),
  124. )
  125. except Exception:
  126. item = HikkaException.from_exc_info(*sys.exc_info())
  127. await utils.answer(
  128. message,
  129. self.strings("err").format(
  130. "4985626654563894116",
  131. "python",
  132. utils.escape_html(utils.get_args_raw(message)),
  133. "error",
  134. self.censor((
  135. "\n".join(item.full_stack.splitlines()[:-1])
  136. + "\n\n"
  137. + "🚫 "
  138. + item.full_stack.splitlines()[-1]
  139. )),
  140. ),
  141. )
  142. return
  143. if callable(getattr(result, "stringify", None)):
  144. with contextlib.suppress(Exception):
  145. result = str(result.stringify())
  146. with contextlib.suppress(MessageIdInvalidError):
  147. await utils.answer(
  148. message,
  149. self.strings("eval").format(
  150. "4985626654563894116",
  151. "python",
  152. utils.escape_html(utils.get_args_raw(message)),
  153. "python",
  154. utils.escape_html(self.censor(str(result))),
  155. ),
  156. )
  157. @loader.command()
  158. async def ecpp(self, message: Message, c: bool = False):
  159. try:
  160. subprocess.check_output(
  161. ["gcc" if c else "g++", "--version"],
  162. stderr=subprocess.STDOUT,
  163. )
  164. except Exception:
  165. await utils.answer(
  166. message,
  167. self.strings("no_compiler").format(
  168. "4986046904228905931" if c else "4985844035743646190",
  169. "C (gcc)" if c else "C++ (g++)",
  170. ),
  171. )
  172. return
  173. code = utils.get_args_raw(message)
  174. message = await utils.answer(message, self.strings("compiling"))
  175. error = False
  176. with tempfile.TemporaryDirectory() as tmpdir:
  177. file = os.path.join(tmpdir, "code.cpp")
  178. with open(file, "w") as f:
  179. f.write(code)
  180. try:
  181. result = subprocess.check_output(
  182. ["gcc" if c else "g++", "-o", "code", "code.cpp"],
  183. cwd=tmpdir,
  184. stderr=subprocess.STDOUT,
  185. ).decode()
  186. except subprocess.CalledProcessError as e:
  187. result = e.output.decode()
  188. error = True
  189. if not result:
  190. try:
  191. result = subprocess.check_output(
  192. ["./code"],
  193. cwd=tmpdir,
  194. stderr=subprocess.STDOUT,
  195. ).decode()
  196. except subprocess.CalledProcessError as e:
  197. result = e.output.decode()
  198. error = True
  199. with contextlib.suppress(MessageIdInvalidError):
  200. await utils.answer(
  201. message,
  202. self.strings("err" if error else "eval").format(
  203. "4986046904228905931" if c else "4985844035743646190",
  204. "c" if c else "cpp",
  205. utils.escape_html(code),
  206. "error" if error else "output",
  207. utils.escape_html(result),
  208. ),
  209. )
  210. @loader.command()
  211. async def ec(self, message: Message):
  212. await self.ecpp(message, c=True)
  213. @loader.command()
  214. async def enode(self, message: Message):
  215. try:
  216. subprocess.check_output(
  217. ["node", "--version"],
  218. stderr=subprocess.STDOUT,
  219. )
  220. except Exception:
  221. await utils.answer(
  222. message,
  223. self.strings("no_compiler").format(
  224. "4985643941807260310",
  225. "Node.js",
  226. ),
  227. )
  228. return
  229. code = utils.get_args_raw(message)
  230. error = False
  231. with tempfile.TemporaryDirectory() as tmpdir:
  232. file = os.path.join(tmpdir, "code.js")
  233. with open(file, "w") as f:
  234. f.write(code)
  235. try:
  236. result = subprocess.check_output(
  237. ["node", "code.js"],
  238. cwd=tmpdir,
  239. stderr=subprocess.STDOUT,
  240. ).decode()
  241. except subprocess.CalledProcessError as e:
  242. result = e.output.decode()
  243. error = True
  244. with contextlib.suppress(MessageIdInvalidError):
  245. await utils.answer(
  246. message,
  247. self.strings("err" if error else "eval").format(
  248. "4985643941807260310",
  249. "javascript",
  250. utils.escape_html(code),
  251. "error" if error else "output",
  252. utils.escape_html(result),
  253. ),
  254. )
  255. @loader.command()
  256. async def ephp(self, message: Message):
  257. try:
  258. subprocess.check_output(
  259. ["php", "--version"],
  260. stderr=subprocess.STDOUT,
  261. )
  262. except Exception:
  263. await utils.answer(
  264. message,
  265. self.strings("no_compiler").format(
  266. "4985815079074136919",
  267. "PHP",
  268. ),
  269. )
  270. return
  271. code = utils.get_args_raw(message)
  272. error = False
  273. with tempfile.TemporaryDirectory() as tmpdir:
  274. file = os.path.join(tmpdir, "code.php")
  275. with open(file, "w") as f:
  276. f.write(f"<?php {code} ?>")
  277. try:
  278. result = subprocess.check_output(
  279. ["php", "code.php"],
  280. cwd=tmpdir,
  281. stderr=subprocess.STDOUT,
  282. ).decode()
  283. except subprocess.CalledProcessError as e:
  284. result = e.output.decode()
  285. error = True
  286. with contextlib.suppress(MessageIdInvalidError):
  287. await utils.answer(
  288. message,
  289. self.strings("err" if error else "eval").format(
  290. "4985815079074136919",
  291. "php",
  292. utils.escape_html(code),
  293. "error" if error else "output",
  294. utils.escape_html(result),
  295. ),
  296. )
  297. @loader.command()
  298. async def eruby(self, message: Message):
  299. try:
  300. subprocess.check_output(
  301. ["ruby", "--version"],
  302. stderr=subprocess.STDOUT,
  303. )
  304. except Exception:
  305. await utils.answer(
  306. message,
  307. self.strings("no_compiler").format(
  308. "4985760855112024628",
  309. "Ruby",
  310. ),
  311. )
  312. return
  313. code = utils.get_args_raw(message)
  314. error = False
  315. with tempfile.TemporaryDirectory() as tmpdir:
  316. file = os.path.join(tmpdir, "code.rb")
  317. with open(file, "w") as f:
  318. f.write(code)
  319. try:
  320. result = subprocess.check_output(
  321. ["ruby", "code.rb"],
  322. cwd=tmpdir,
  323. stderr=subprocess.STDOUT,
  324. ).decode()
  325. except subprocess.CalledProcessError as e:
  326. result = e.output.decode()
  327. error = True
  328. with contextlib.suppress(MessageIdInvalidError):
  329. await utils.answer(
  330. message,
  331. self.strings("err" if error else "eval").format(
  332. "4985760855112024628",
  333. "ruby",
  334. utils.escape_html(code),
  335. "error" if error else "output",
  336. utils.escape_html(result),
  337. ),
  338. )
  339. @loader.command()
  340. async def ebf(self, message: Message):
  341. code = utils.get_args_raw(message)
  342. if "-debug" in code:
  343. code = code.replace("-debug", "")
  344. code = code.replace(" ", "")
  345. debug = True
  346. else:
  347. debug = False
  348. error = False
  349. bf = Brainfuck()
  350. result = bf.run(code)
  351. if bf.error:
  352. result = bf.error
  353. error = True
  354. if not result:
  355. result = "<empty>"
  356. if debug:
  357. result += "\n\n" + " | ".join(map(str, filter(lambda x: x, bf.data)))
  358. with contextlib.suppress(MessageIdInvalidError):
  359. await utils.answer(
  360. message,
  361. self.strings("err" if error else "eval").format(
  362. "5474256197542486673",
  363. "brainfuck",
  364. utils.escape_html(code),
  365. "error" if error else "output",
  366. utils.escape_html(result),
  367. ),
  368. )
  369. def censor(self, ret: str) -> str:
  370. ret = ret.replace(str(self._client.hikka_me.phone), "&lt;phone&gt;")
  371. if redis := os.environ.get("REDIS_URL") or main.get_config_key("redis_uri"):
  372. ret = ret.replace(redis, f'redis://{"*" * 26}')
  373. if db := os.environ.get("DATABASE_URL") or main.get_config_key("db_uri"):
  374. ret = ret.replace(db, f'postgresql://{"*" * 26}')
  375. if btoken := self._db.get("hikka.inline", "bot_token", False):
  376. ret = ret.replace(
  377. btoken,
  378. f'{btoken.split(":")[0]}:{"*" * 26}',
  379. )
  380. if htoken := self.lookup("loader").get("token", False):
  381. ret = ret.replace(htoken, f'eugeo_{"*" * 26}')
  382. ret = ret.replace(
  383. StringSession.save(self._client.session),
  384. "StringSession(**************************)",
  385. )
  386. return ret
  387. async def getattrs(self, message: Message) -> dict:
  388. reply = await message.get_reply_message()
  389. return {
  390. "message": message,
  391. "client": self._client,
  392. "reply": reply,
  393. "r": reply,
  394. **self.get_sub(hikkatl.tl.types),
  395. **self.get_sub(hikkatl.tl.functions),
  396. "event": message,
  397. "chat": message.to_id,
  398. "hikkatl": hikkatl,
  399. "telethon": hikkatl,
  400. "utils": utils,
  401. "main": main,
  402. "loader": loader,
  403. "f": hikkatl.tl.functions,
  404. "c": self._client,
  405. "m": message,
  406. "lookup": self.lookup,
  407. "self": self,
  408. "db": self.db,
  409. }
  410. def get_sub(self, obj: typing.Any, _depth: int = 1) -> dict:
  411. """Get all callable capitalised objects in an object recursively, ignoring _*"""
  412. return {
  413. **dict(
  414. filter(
  415. lambda x: x[0][0] != "_"
  416. and x[0][0].upper() == x[0][0]
  417. and callable(x[1]),
  418. obj.__dict__.items(),
  419. )
  420. ),
  421. **dict(
  422. itertools.chain.from_iterable([
  423. self.get_sub(y[1], _depth + 1).items()
  424. for y in filter(
  425. lambda x: x[0][0] != "_"
  426. and isinstance(x[1], ModuleType)
  427. and x[1] != obj
  428. and x[1].__package__.rsplit(".", _depth)[0] == "hikkatl.tl",
  429. obj.__dict__.items(),
  430. )
  431. ])
  432. ),
  433. }