configurator.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. # Friendly Telegram (telegram userbot)
  2. # Copyright (C) 2018-2021 The Authors
  3. # This program is free software: you can redistribute it and/or modify
  4. # it under the terms of the GNU Affero General Public License as published by
  5. # the Free Software Foundation, either version 3 of the License, or
  6. # (at your option) any later version.
  7. # This program is distributed in the hope that it will be useful,
  8. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. # GNU Affero General Public License for more details.
  11. # You should have received a copy of the GNU Affero General Public License
  12. # along with this program. If not, see <https://www.gnu.org/licenses/>.
  13. import ast
  14. import inspect
  15. import locale
  16. import os
  17. import string
  18. import sys
  19. import time
  20. from dialog import Dialog, ExecutableNotFound
  21. from . import main, utils
  22. def _safe_input(*args, **kwargs):
  23. """Try to invoke input(*), print an error message if an EOFError or OSError occurs)"""
  24. try:
  25. return input(*args, **kwargs)
  26. except (EOFError, OSError):
  27. raise
  28. except KeyboardInterrupt:
  29. print()
  30. return None
  31. class TDialog:
  32. """Reimplementation of dialog.Dialog without external dependencies"""
  33. OK = True
  34. NOT_OK = False
  35. def __init__(self):
  36. self._title = ""
  37. # Similar interface to pythondialog
  38. def menu(self, title, choices):
  39. """Print a menu and get a choice"""
  40. print(self._title)
  41. print()
  42. print()
  43. print(title)
  44. print()
  45. biggest = max(len(k) for k, d in choices)
  46. for i, (k, v) in enumerate(choices, 1):
  47. print(
  48. f" {str(i)}. {k}"
  49. + " " * (biggest + 2 - len(k))
  50. + (v.replace("\n", "...\n "))
  51. )
  52. while True:
  53. inp = _safe_input(
  54. "Please enter your selection as a number, or 0 to cancel: "
  55. )
  56. if inp is None:
  57. inp = 0
  58. try:
  59. inp = int(inp)
  60. if inp == 0:
  61. return self.NOT_OK, "Cancelled"
  62. return self.OK, choices[inp - 1][0]
  63. except (ValueError, IndexError):
  64. pass
  65. def inputbox(self, query):
  66. """Get a text input of the query"""
  67. print(self._title)
  68. print()
  69. print()
  70. print(query)
  71. print()
  72. inp = _safe_input("Please enter your response, or type nothing to cancel: ")
  73. if inp == "" or inp is None:
  74. return self.NOT_OK, "Cancelled"
  75. return self.OK, inp
  76. def msgbox(self, msg):
  77. """Print some info"""
  78. print(self._title)
  79. print()
  80. print()
  81. print(msg)
  82. return self.OK
  83. def set_background_title(self, title):
  84. """Set the internal variable"""
  85. self._title = title
  86. def yesno(self, question):
  87. """Ask yes or no, default to no"""
  88. print(self._title)
  89. print()
  90. return (
  91. self.OK
  92. if (_safe_input(f"{question} (y/N): ") or "").lower() == "y"
  93. else self.NOT_OK
  94. )
  95. TITLE = ""
  96. if sys.stdout.isatty():
  97. try:
  98. DIALOG = Dialog(dialog="dialog", autowidgetsize=True)
  99. locale.setlocale(locale.LC_ALL, "")
  100. except (ExecutableNotFound, locale.Error):
  101. # Fall back to a terminal based configurator.
  102. DIALOG = TDialog()
  103. else:
  104. DIALOG = TDialog()
  105. MODULES = None
  106. DB = None # eww... meh.
  107. # pylint: disable=W0603
  108. def validate_value(value):
  109. """Convert string to literal or return string"""
  110. try:
  111. return ast.literal_eval(value)
  112. except (ValueError, SyntaxError):
  113. return value
  114. def modules_config():
  115. """Show menu of all modules and allow user to enter one"""
  116. code, tag = DIALOG.menu(
  117. "Modules",
  118. choices=[
  119. (module.name, inspect.cleandoc(getattr(module, "__doc__", None) or ""))
  120. for module in MODULES.modules
  121. if getattr(module, "config", {})
  122. ],
  123. )
  124. if code == DIALOG.OK:
  125. for mod in MODULES.modules:
  126. if mod.name == tag:
  127. # Match
  128. while not module_config(mod):
  129. time.sleep(0.05)
  130. return modules_config()
  131. return None
  132. def module_config(mod):
  133. """Show menu for specific module and allow user to set config items"""
  134. choices = [
  135. (key, getattr(mod.config, "getdoc", lambda k: "Undocumented key")(key))
  136. for key in getattr(mod, "config", {}).keys()
  137. ]
  138. code, tag = DIALOG.menu(
  139. "Module configuration for {}".format(mod.name), choices=choices
  140. )
  141. if code == DIALOG.OK:
  142. code, value = DIALOG.inputbox(tag)
  143. if code == DIALOG.OK:
  144. DB.setdefault(mod.__class__.__name__, {}).setdefault("__config__", {})[
  145. tag
  146. ] = validate_value(value)
  147. DIALOG.msgbox("Config value set successfully")
  148. return False
  149. return True
  150. def run(database, data_root, phone, init, mods):
  151. """Launch configurator"""
  152. global DB, MODULES, TITLE
  153. DB = database
  154. MODULES = mods
  155. TITLE = "Userbot Configuration for {}"
  156. TITLE = TITLE.format(phone)
  157. DIALOG.set_background_title(TITLE)
  158. while main_config(init, data_root):
  159. time.sleep(0.05)
  160. return DB
  161. def api_config(data_root):
  162. """Request API config from user and set"""
  163. code, hash_value = DIALOG.inputbox("Enter your API Hash")
  164. if code == DIALOG.OK:
  165. if len(hash_value) != 32 or any(
  166. it not in string.hexdigits for it in hash_value
  167. ):
  168. DIALOG.msgbox("Invalid hash")
  169. return
  170. code, id_value = DIALOG.inputbox("Enter your API ID")
  171. if not id_value or any(it not in string.digits for it in id_value):
  172. DIALOG.msgbox("Invalid ID")
  173. return
  174. with open(
  175. os.path.join(
  176. data_root or os.path.dirname(utils.get_base_dir()), "api_token.txt"
  177. ),
  178. "w",
  179. ) as file:
  180. file.write(id_value + "\n" + hash_value)
  181. DIALOG.msgbox("API Token and ID set.")
  182. def logging_config():
  183. """Ask the user to choose a loglevel and save it"""
  184. code, tag = DIALOG.menu(
  185. "Log Level",
  186. choices=[
  187. ("50", "CRITICAL"),
  188. ("40", "ERROR"),
  189. ("30", "WARNING"),
  190. ("20", "INFO"),
  191. ("10", "DEBUG"),
  192. ("0", "ALL"),
  193. ],
  194. )
  195. if code == DIALOG.OK:
  196. DB.setdefault(main.__name__, {})["loglevel"] = int(tag)
  197. def factory_reset_check():
  198. """Make sure the user wants to factory reset"""
  199. global DB
  200. if (
  201. DIALOG.yesno(
  202. "Do you REALLY want to erase ALL userbot data stored in Telegram cloud?\n"
  203. "Your existing Telegram chats will not be affected."
  204. )
  205. == DIALOG.OK
  206. ):
  207. DB = None
  208. def main_config(init, data_root):
  209. """Main menu"""
  210. if init:
  211. return api_config(data_root)
  212. choices = [
  213. ("API Token and ID", "Configure API Token and ID"),
  214. ("Modules", "Modular configuration"),
  215. ("Logging", "Configure debug output"),
  216. ("Factory reset", "Removes all userbot data stored in Telegram cloud"),
  217. ]
  218. code, tag = DIALOG.menu("Main Menu", choices=choices)
  219. if code != DIALOG.OK:
  220. return False
  221. if tag == "Modules":
  222. modules_config()
  223. if tag == "API Token and ID":
  224. api_config(data_root)
  225. if tag == "Logging":
  226. logging_config()
  227. if tag == "Factory reset":
  228. factory_reset_check()
  229. return False
  230. return True