hikka_backup.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  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 asyncio
  7. import contextlib
  8. import datetime
  9. import io
  10. import json
  11. import logging
  12. import os
  13. import time
  14. import zipfile
  15. from pathlib import Path
  16. from hikkatl.tl.types import Message
  17. from .. import loader, utils
  18. from ..inline.types import BotInlineCall
  19. logger = logging.getLogger(__name__)
  20. @loader.tds
  21. class HikkaBackupMod(loader.Module):
  22. """Handles database and modules backups"""
  23. strings = {"name": "HikkaBackup"}
  24. async def client_ready(self):
  25. if not self.get("period"):
  26. await self.inline.bot.send_photo(
  27. self.tg_id,
  28. photo="https://github.com/hikariatama/assets/raw/master/unit_alpha.png",
  29. caption=self.strings("period"),
  30. reply_markup=self.inline.generate_markup(
  31. utils.chunks(
  32. [
  33. {
  34. "text": f"🕰 {i} h",
  35. "callback": self._set_backup_period,
  36. "args": (i,),
  37. }
  38. for i in [1, 2, 4, 6, 8, 12, 24, 48, 168]
  39. ],
  40. 3,
  41. )
  42. + [
  43. [
  44. {
  45. "text": "🚫 Never",
  46. "callback": self._set_backup_period,
  47. "args": (0,),
  48. }
  49. ]
  50. ]
  51. ),
  52. )
  53. self._backup_channel, _ = await utils.asset_channel(
  54. self._client,
  55. "hikka-backups",
  56. "📼 Your database backups will appear here",
  57. silent=True,
  58. archive=True,
  59. avatar="https://github.com/hikariatama/assets/raw/master/hikka-backups.png",
  60. _folder="hikka",
  61. )
  62. async def _set_backup_period(self, call: BotInlineCall, value: int):
  63. if not value:
  64. self.set("period", "disabled")
  65. await call.answer(self.strings("never"), show_alert=True)
  66. await call.delete()
  67. return
  68. self.set("period", value * 60 * 60)
  69. self.set("last_backup", round(time.time()))
  70. await call.answer(self.strings("saved"), show_alert=True)
  71. await call.delete()
  72. @loader.command()
  73. async def set_backup_period(self, message: Message):
  74. if (
  75. not (args := utils.get_args_raw(message))
  76. or not args.isdigit()
  77. or int(args) not in range(200)
  78. ):
  79. await utils.answer(message, self.strings("invalid_args"))
  80. return
  81. if not int(args):
  82. self.set("period", "disabled")
  83. await utils.answer(message, f"<b>{self.strings('never')}</b>")
  84. return
  85. period = int(args) * 60 * 60
  86. self.set("period", period)
  87. self.set("last_backup", round(time.time()))
  88. await utils.answer(message, f"<b>{self.strings('saved')}</b>")
  89. @loader.loop(interval=1, autostart=True)
  90. async def handler(self):
  91. try:
  92. if self.get("period") == "disabled":
  93. raise loader.StopLoop
  94. if not self.get("period"):
  95. await asyncio.sleep(3)
  96. return
  97. if not self.get("last_backup"):
  98. self.set("last_backup", round(time.time()))
  99. await asyncio.sleep(self.get("period"))
  100. return
  101. await asyncio.sleep(
  102. self.get("last_backup") + self.get("period") - time.time()
  103. )
  104. backup = io.BytesIO(json.dumps(self._db).encode())
  105. backup.name = (
  106. f"hikka-db-backup-{datetime.datetime.now():%d-%m-%Y-%H-%M}.json"
  107. )
  108. await self._client.send_file(self._backup_channel, backup)
  109. self.set("last_backup", round(time.time()))
  110. except loader.StopLoop:
  111. raise
  112. except Exception:
  113. logger.exception("HikkaBackup failed")
  114. await asyncio.sleep(60)
  115. @loader.command()
  116. async def backupdb(self, message: Message):
  117. txt = io.BytesIO(json.dumps(self._db).encode())
  118. txt.name = f"db-backup-{datetime.datetime.now():%d-%m-%Y-%H-%M}.json"
  119. await self._client.send_file(
  120. "me",
  121. txt,
  122. caption=self.strings("backup_caption").format(
  123. prefix=utils.escape_html(self.get_prefix())
  124. ),
  125. )
  126. await utils.answer(message, self.strings("backup_sent"))
  127. @loader.command()
  128. async def restoredb(self, message: Message):
  129. if not (reply := await message.get_reply_message()) or not reply.media:
  130. await utils.answer(
  131. message,
  132. self.strings("reply_to_file"),
  133. )
  134. return
  135. file = await reply.download_media(bytes)
  136. decoded_text = json.loads(file.decode())
  137. with contextlib.suppress(KeyError):
  138. decoded_text["hikka.inline"].pop("bot_token")
  139. if not self._db.process_db_autofix(decoded_text):
  140. raise RuntimeError("Attempted to restore broken database")
  141. self._db.clear()
  142. self._db.update(**decoded_text)
  143. self._db.save()
  144. await utils.answer(message, self.strings("db_restored"))
  145. await self.invoke("restart", "-f", peer=message.peer_id)
  146. @loader.command()
  147. async def backupmods(self, message: Message):
  148. mods_quantity = len(self.lookup("Loader").get("loaded_modules", {}))
  149. result = io.BytesIO()
  150. result.name = "mods.zip"
  151. db_mods = json.dumps(self.lookup("Loader").get("loaded_modules", {})).encode()
  152. with zipfile.ZipFile(result, "w", zipfile.ZIP_DEFLATED) as zipf:
  153. for root, _, files in os.walk(loader.LOADED_MODULES_DIR):
  154. for file in files:
  155. if file.endswith(f"{self.tg_id}.py"):
  156. with open(os.path.join(root, file), "rb") as f:
  157. zipf.writestr(file, f.read())
  158. mods_quantity += 1
  159. zipf.writestr("db_mods.json", db_mods)
  160. archive = io.BytesIO(result.getvalue())
  161. archive.name = f"mods-{datetime.datetime.now():%d-%m-%Y-%H-%M}.zip"
  162. await utils.answer_file(
  163. message,
  164. archive,
  165. caption=self.strings("modules_backup").format(
  166. mods_quantity,
  167. utils.escape_html(self.get_prefix()),
  168. ),
  169. )
  170. @loader.command()
  171. async def restoremods(self, message: Message):
  172. if not (reply := await message.get_reply_message()) or not reply.media:
  173. await utils.answer(message, self.strings("reply_to_file"))
  174. return
  175. file = await reply.download_media(bytes)
  176. try:
  177. decoded_text = json.loads(file.decode())
  178. except Exception:
  179. try:
  180. file = io.BytesIO(file)
  181. file.name = "mods.zip"
  182. with zipfile.ZipFile(file) as zf:
  183. with zf.open("db_mods.json", "r") as modules:
  184. db_mods = json.loads(modules.read().decode())
  185. if isinstance(db_mods, dict) and all(
  186. (
  187. isinstance(key, str)
  188. and isinstance(value, str)
  189. and utils.check_url(value)
  190. )
  191. for key, value in db_mods.items()
  192. ):
  193. self.lookup("Loader").set("loaded_modules", db_mods)
  194. for name in zf.namelist():
  195. if name == "db_mods.json":
  196. continue
  197. path = loader.LOADED_MODULES_PATH / Path(name).name
  198. with zf.open(name, "r") as module:
  199. path.write_bytes(module.read())
  200. except Exception:
  201. logger.exception("Unable to restore modules")
  202. await utils.answer(message, self.strings("reply_to_file"))
  203. return
  204. else:
  205. if not isinstance(decoded_text, dict) or not all(
  206. isinstance(key, str) and isinstance(value, str)
  207. for key, value in decoded_text.items()
  208. ):
  209. raise RuntimeError("Invalid backup")
  210. self.lookup("Loader").set("loaded_modules", decoded_text)
  211. await utils.answer(message, self.strings("mods_restored"))
  212. await self.invoke("restart", "-f", peer=message.peer_id)