eval.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494
  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. utils.escape_html(utils.get_args_raw(message)),
  132. self.censor(
  133. (
  134. "\n".join(item.full_stack.splitlines()[:-1])
  135. + "\n\n"
  136. + "🚫 "
  137. + item.full_stack.splitlines()[-1]
  138. )
  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. utils.escape_html(utils.get_args_raw(message)),
  152. utils.escape_html(self.censor(str(result))),
  153. ),
  154. )
  155. @loader.command()
  156. async def ecpp(self, message: Message, c: bool = False):
  157. try:
  158. subprocess.check_output(
  159. ["gcc" if c else "g++", "--version"],
  160. stderr=subprocess.STDOUT,
  161. )
  162. except Exception:
  163. await utils.answer(
  164. message,
  165. self.strings("no_compiler").format(
  166. "4986046904228905931" if c else "4985844035743646190",
  167. "C (gcc)" if c else "C++ (g++)",
  168. ),
  169. )
  170. return
  171. code = utils.get_args_raw(message)
  172. message = await utils.answer(message, self.strings("compiling"))
  173. error = False
  174. with tempfile.TemporaryDirectory() as tmpdir:
  175. file = os.path.join(tmpdir, "code.cpp")
  176. with open(file, "w") as f:
  177. f.write(code)
  178. try:
  179. result = subprocess.check_output(
  180. ["gcc" if c else "g++", "-o", "code", "code.cpp"],
  181. cwd=tmpdir,
  182. stderr=subprocess.STDOUT,
  183. ).decode()
  184. except subprocess.CalledProcessError as e:
  185. result = e.output.decode()
  186. error = True
  187. if not result:
  188. try:
  189. result = subprocess.check_output(
  190. ["./code"],
  191. cwd=tmpdir,
  192. stderr=subprocess.STDOUT,
  193. ).decode()
  194. except subprocess.CalledProcessError as e:
  195. result = e.output.decode()
  196. error = True
  197. with contextlib.suppress(MessageIdInvalidError):
  198. await utils.answer(
  199. message,
  200. self.strings("err" if error else "eval").format(
  201. "4986046904228905931" if c else "4985844035743646190",
  202. utils.escape_html(code),
  203. f"<code>{utils.escape_html(result)}</code>",
  204. ),
  205. )
  206. @loader.command()
  207. async def ec(self, message: Message):
  208. await self.ecpp(message, c=True)
  209. @loader.command()
  210. async def enode(self, message: Message):
  211. try:
  212. subprocess.check_output(
  213. ["node", "--version"],
  214. stderr=subprocess.STDOUT,
  215. )
  216. except Exception:
  217. await utils.answer(
  218. message,
  219. self.strings("no_compiler").format(
  220. "4985643941807260310",
  221. "Node.js",
  222. ),
  223. )
  224. return
  225. code = utils.get_args_raw(message)
  226. error = False
  227. with tempfile.TemporaryDirectory() as tmpdir:
  228. file = os.path.join(tmpdir, "code.js")
  229. with open(file, "w") as f:
  230. f.write(code)
  231. try:
  232. result = subprocess.check_output(
  233. ["node", "code.js"],
  234. cwd=tmpdir,
  235. stderr=subprocess.STDOUT,
  236. ).decode()
  237. except subprocess.CalledProcessError as e:
  238. result = e.output.decode()
  239. error = True
  240. with contextlib.suppress(MessageIdInvalidError):
  241. await utils.answer(
  242. message,
  243. self.strings("err" if error else "eval").format(
  244. "4985643941807260310",
  245. utils.escape_html(code),
  246. f"<code>{utils.escape_html(result)}</code>",
  247. ),
  248. )
  249. @loader.command()
  250. async def ephp(self, message: Message):
  251. try:
  252. subprocess.check_output(
  253. ["php", "--version"],
  254. stderr=subprocess.STDOUT,
  255. )
  256. except Exception:
  257. await utils.answer(
  258. message,
  259. self.strings("no_compiler").format(
  260. "4985815079074136919",
  261. "PHP",
  262. ),
  263. )
  264. return
  265. code = utils.get_args_raw(message)
  266. error = False
  267. with tempfile.TemporaryDirectory() as tmpdir:
  268. file = os.path.join(tmpdir, "code.php")
  269. with open(file, "w") as f:
  270. f.write(f"<?php {code} ?>")
  271. try:
  272. result = subprocess.check_output(
  273. ["php", "code.php"],
  274. cwd=tmpdir,
  275. stderr=subprocess.STDOUT,
  276. ).decode()
  277. except subprocess.CalledProcessError as e:
  278. result = e.output.decode()
  279. error = True
  280. with contextlib.suppress(MessageIdInvalidError):
  281. await utils.answer(
  282. message,
  283. self.strings("err" if error else "eval").format(
  284. "4985815079074136919",
  285. utils.escape_html(code),
  286. f"<code>{utils.escape_html(result)}</code>",
  287. ),
  288. )
  289. @loader.command()
  290. async def eruby(self, message: Message):
  291. try:
  292. subprocess.check_output(
  293. ["ruby", "--version"],
  294. stderr=subprocess.STDOUT,
  295. )
  296. except Exception:
  297. await utils.answer(
  298. message,
  299. self.strings("no_compiler").format(
  300. "4985760855112024628",
  301. "Ruby",
  302. ),
  303. )
  304. return
  305. code = utils.get_args_raw(message)
  306. error = False
  307. with tempfile.TemporaryDirectory() as tmpdir:
  308. file = os.path.join(tmpdir, "code.rb")
  309. with open(file, "w") as f:
  310. f.write(code)
  311. try:
  312. result = subprocess.check_output(
  313. ["ruby", "code.rb"],
  314. cwd=tmpdir,
  315. stderr=subprocess.STDOUT,
  316. ).decode()
  317. except subprocess.CalledProcessError as e:
  318. result = e.output.decode()
  319. error = True
  320. with contextlib.suppress(MessageIdInvalidError):
  321. await utils.answer(
  322. message,
  323. self.strings("err" if error else "eval").format(
  324. "4985760855112024628",
  325. utils.escape_html(code),
  326. f"<code>{utils.escape_html(result)}</code>",
  327. ),
  328. )
  329. @loader.command()
  330. async def ebf(self, message: Message):
  331. code = utils.get_args_raw(message)
  332. if "-debug" in code:
  333. code = code.replace("-debug", "")
  334. code = code.replace(" ", "")
  335. debug = True
  336. else:
  337. debug = False
  338. error = False
  339. bf = Brainfuck()
  340. result = bf.run(code)
  341. if bf.error:
  342. result = bf.error
  343. error = True
  344. if not result:
  345. result = "<empty>"
  346. if debug:
  347. result += "\n\n" + " | ".join(map(str, filter(lambda x: x, bf.data)))
  348. with contextlib.suppress(MessageIdInvalidError):
  349. await utils.answer(
  350. message,
  351. self.strings("err" if error else "eval").format(
  352. "5474256197542486673",
  353. utils.escape_html(code),
  354. f"<code>{utils.escape_html(result)}</code>",
  355. ),
  356. )
  357. def censor(self, ret: str) -> str:
  358. ret = ret.replace(str(self._client.hikka_me.phone), "&lt;phone&gt;")
  359. if redis := os.environ.get("REDIS_URL") or main.get_config_key("redis_uri"):
  360. ret = ret.replace(redis, f'redis://{"*" * 26}')
  361. if db := os.environ.get("DATABASE_URL") or main.get_config_key("db_uri"):
  362. ret = ret.replace(db, f'postgresql://{"*" * 26}')
  363. if btoken := self._db.get("hikka.inline", "bot_token", False):
  364. ret = ret.replace(
  365. btoken,
  366. f'{btoken.split(":")[0]}:{"*" * 26}',
  367. )
  368. if htoken := self.lookup("loader").get("token", False):
  369. ret = ret.replace(htoken, f'eugeo_{"*" * 26}')
  370. ret = ret.replace(
  371. StringSession.save(self._client.session),
  372. "StringSession(**************************)",
  373. )
  374. return ret
  375. async def getattrs(self, message: Message) -> dict:
  376. reply = await message.get_reply_message()
  377. return {
  378. "message": message,
  379. "client": self._client,
  380. "reply": reply,
  381. "r": reply,
  382. **self.get_sub(hikkatl.tl.types),
  383. **self.get_sub(hikkatl.tl.functions),
  384. "event": message,
  385. "chat": message.to_id,
  386. "hikkatl": hikkatl,
  387. "telethon": hikkatl,
  388. "utils": utils,
  389. "main": main,
  390. "loader": loader,
  391. "f": hikkatl.tl.functions,
  392. "c": self._client,
  393. "m": message,
  394. "lookup": self.lookup,
  395. "self": self,
  396. "db": self.db,
  397. }
  398. def get_sub(self, obj: typing.Any, _depth: int = 1) -> dict:
  399. """Get all callable capitalised objects in an object recursively, ignoring _*"""
  400. return {
  401. **dict(
  402. filter(
  403. lambda x: x[0][0] != "_"
  404. and x[0][0].upper() == x[0][0]
  405. and callable(x[1]),
  406. obj.__dict__.items(),
  407. )
  408. ),
  409. **dict(
  410. itertools.chain.from_iterable(
  411. [
  412. self.get_sub(y[1], _depth + 1).items()
  413. for y in filter(
  414. lambda x: x[0][0] != "_"
  415. and isinstance(x[1], ModuleType)
  416. and x[1] != obj
  417. and x[1].__package__.rsplit(".", _depth)[0] == "hikkatl.tl",
  418. obj.__dict__.items(),
  419. )
  420. ]
  421. )
  422. ),
  423. }