KiTTY, un bot Discord qui est un petit chat :) Il est basé sur une ancienne version du bot Red, sous Python 3.6 et qui a des fonctionnalités bien sympatiques !
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

launcher.py 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  1. from __future__ import print_function
  2. import os
  3. import sys
  4. import subprocess
  5. try: # Older Pythons lack this
  6. import urllib.request # We'll let them reach the Python
  7. from importlib.util import find_spec # check anyway
  8. except ImportError:
  9. pass
  10. import platform
  11. import webbrowser
  12. import hashlib
  13. import argparse
  14. import shutil
  15. import stat
  16. import time
  17. try:
  18. import pip
  19. except ImportError:
  20. pip = None
  21. REQS_DIR = "lib"
  22. sys.path.insert(0, REQS_DIR)
  23. REQS_TXT = "requirements.txt"
  24. REQS_NO_AUDIO_TXT = "requirements_no_audio.txt"
  25. FFMPEG_BUILDS_URL = "https://ffmpeg.zeranoe.com/builds/"
  26. INTRO = ("==========================\n"
  27. "KiTTY Launcher - Installer\n"
  28. "==========================\n")
  29. IS_WINDOWS = os.name == "nt"
  30. IS_MAC = sys.platform == "darwin"
  31. IS_64BIT = platform.machine().endswith("64")
  32. INTERACTIVE_MODE = not len(sys.argv) > 1 # CLI flags = non-interactive
  33. PYTHON_OK = sys.version_info >= (3, 5)
  34. FFMPEG_FILES = {
  35. "ffmpeg.exe" : "e0d60f7c0d27ad9d7472ddf13e78dc89",
  36. "ffplay.exe" : "d100abe8281cbcc3e6aebe550c675e09",
  37. "ffprobe.exe" : "0e84b782c0346a98434ed476e937764f"
  38. }
  39. def parse_cli_arguments():
  40. parser = argparse.ArgumentParser(description="KiTTY launcher")
  41. parser.add_argument("--start", "-s",
  42. help="Starts KiTTY",
  43. action="store_true")
  44. parser.add_argument("--auto-restart",
  45. help="Autorestarts Red in case of issues",
  46. action="store_true")
  47. parser.add_argument("--update-reqs",
  48. help="Updates requirements (w/ audio)",
  49. action="store_true")
  50. parser.add_argument("--update-reqs-no-audio",
  51. help="Updates requirements (w/o audio)",
  52. action="store_true")
  53. parser.add_argument("--repair",
  54. help="Issues a git reset --hard",
  55. action="store_true")
  56. return parser.parse_args()
  57. def install_reqs(audio):
  58. remove_reqs_readonly()
  59. interpreter = sys.executable
  60. if interpreter is None:
  61. print("Python interpreter not found.")
  62. return
  63. txt = REQS_TXT if audio else REQS_NO_AUDIO_TXT
  64. args = [
  65. interpreter, "-m",
  66. "pip", "install",
  67. "--upgrade",
  68. "--target", REQS_DIR,
  69. "-r", txt
  70. ]
  71. if IS_MAC: # --target is a problem on Homebrew. See PR #552
  72. args.remove("--target")
  73. args.remove(REQS_DIR)
  74. code = subprocess.call(args)
  75. if code == 0:
  76. print("\nRequirements setup completed.")
  77. else:
  78. print("\nAn error occurred and the requirements setup might "
  79. "not be completed. Consult the docs.\n")
  80. def update_pip():
  81. interpreter = sys.executable
  82. if interpreter is None:
  83. print("Python interpreter not found.")
  84. return
  85. args = [
  86. interpreter, "-m",
  87. "pip", "install",
  88. "--upgrade", "pip"
  89. ]
  90. code = subprocess.call(args)
  91. if code == 0:
  92. print("\nPip has been updated.")
  93. else:
  94. print("\nAn error occurred and pip might not have been updated.")
  95. def reset_red(reqs=False, data=False, cogs=False, git_reset=False):
  96. if reqs:
  97. try:
  98. shutil.rmtree(REQS_DIR, onerror=remove_readonly)
  99. print("Installed local packages have been wiped.")
  100. except FileNotFoundError:
  101. pass
  102. except Exception as e:
  103. print("An error occurred when trying to remove installed "
  104. "requirements: {}".format(e))
  105. if data:
  106. try:
  107. shutil.rmtree("data", onerror=remove_readonly)
  108. print("'data' folder has been wiped.")
  109. except FileNotFoundError:
  110. pass
  111. except Exception as e:
  112. print("An error occurred when trying to remove the 'data' folder: "
  113. "{}".format(e))
  114. if cogs:
  115. try:
  116. shutil.rmtree("cogs", onerror=remove_readonly)
  117. print("'cogs' folder has been wiped.")
  118. except FileNotFoundError:
  119. pass
  120. except Exception as e:
  121. print("An error occurred when trying to remove the 'cogs' folder: "
  122. "{}".format(e))
  123. if git_reset:
  124. code = subprocess.call(("git", "reset", "--hard"))
  125. if code == 0:
  126. print("Red has been restored to the last local commit.")
  127. else:
  128. print("The repair has failed.")
  129. def download_ffmpeg(bitness):
  130. clear_screen()
  131. repo = "https://github.com/Twentysix26/Red-DiscordBot/raw/master/"
  132. verified = []
  133. if bitness == "32bit":
  134. print("Please download 'ffmpeg 32bit static' from the page that "
  135. "is about to open.\nOnce done, open the 'bin' folder located "
  136. "inside the zip.\nThere should be 3 files: ffmpeg.exe, "
  137. "ffplay.exe, ffprobe.exe.\nPut all three of them into the "
  138. "bot's main folder.")
  139. time.sleep(4)
  140. webbrowser.open(FFMPEG_BUILDS_URL)
  141. return
  142. for filename in FFMPEG_FILES:
  143. if os.path.isfile(filename):
  144. print("{} already present. Verifying integrity... "
  145. "".format(filename), end="")
  146. _hash = calculate_md5(filename)
  147. if _hash == FFMPEG_FILES[filename]:
  148. verified.append(filename)
  149. print("Ok")
  150. continue
  151. else:
  152. print("Hash mismatch. Redownloading.")
  153. print("Downloading {}... Please wait.".format(filename))
  154. with urllib.request.urlopen(repo + filename) as data:
  155. with open(filename, "wb") as f:
  156. f.write(data.read())
  157. print("Download completed.")
  158. for filename, _hash in FFMPEG_FILES.items():
  159. if filename in verified:
  160. continue
  161. print("Verifying {}... ".format(filename), end="")
  162. if not calculate_md5(filename) != _hash:
  163. print("Passed.")
  164. else:
  165. print("Hash mismatch. Please redownload.")
  166. print("\nAll files have been downloaded.")
  167. def verify_requirements():
  168. sys.path_importer_cache = {} # I don't know if the cache reset has any
  169. basic = find_spec("discord") # side effect. Without it, the lib folder
  170. audio = find_spec("nacl") # wouldn't be seen if it didn't exist
  171. if not basic: # when the launcher was started
  172. return None
  173. elif not audio:
  174. return False
  175. else:
  176. return True
  177. def is_git_installed():
  178. try:
  179. subprocess.call(["git", "--version"], stdout=subprocess.DEVNULL,
  180. stdin =subprocess.DEVNULL,
  181. stderr=subprocess.DEVNULL)
  182. except FileNotFoundError:
  183. return False
  184. else:
  185. return True
  186. def requirements_menu():
  187. clear_screen()
  188. while True:
  189. print(INTRO)
  190. print("Main requirements:\n")
  191. print("1. Install basic + audio requirements (recommended)")
  192. print("2. Install basic requirements")
  193. if IS_WINDOWS:
  194. print("\nffmpeg (required for audio):")
  195. print("3. Install ffmpeg 32bit")
  196. if IS_64BIT:
  197. print("4. Install ffmpeg 64bit (recommended on Windows 64bit)")
  198. print("\n0. Go back")
  199. choice = user_choice()
  200. if choice == "1":
  201. install_reqs(audio=True)
  202. wait()
  203. elif choice == "2":
  204. install_reqs(audio=False)
  205. wait()
  206. elif choice == "3" and IS_WINDOWS:
  207. download_ffmpeg(bitness="32bit")
  208. wait()
  209. elif choice == "4" and (IS_WINDOWS and IS_64BIT):
  210. download_ffmpeg(bitness="64bit")
  211. wait()
  212. elif choice == "0":
  213. break
  214. clear_screen()
  215. def maintenance_menu():
  216. clear_screen()
  217. while True:
  218. print(INTRO)
  219. print("Maintenance:\n")
  220. print("1. Repair KiTTY (discards code changes, keeps data intact)")
  221. print("2. Wipe 'data' folder (all settings, cogs' data...)")
  222. print("3. Wipe 'lib' folder (all local requirements / local installed"
  223. " python packages)")
  224. print("4. Factory reset")
  225. print("\n0. Go back")
  226. choice = user_choice()
  227. if choice == "1":
  228. print("Any code modification you have made will be lost. Data/"
  229. "non-default cogs will be left intact. Are you sure?")
  230. if user_pick_yes_no():
  231. reset_red(git_reset=True)
  232. wait()
  233. elif choice == "2":
  234. print("Are you sure? This will wipe the 'data' folder, which "
  235. "contains all your settings and cogs' data.\nThe 'cogs' "
  236. "folder, however, will be left intact.")
  237. if user_pick_yes_no():
  238. reset_red(data=True)
  239. wait()
  240. elif choice == "3":
  241. reset_red(reqs=True)
  242. wait()
  243. elif choice == "4":
  244. print("Are you sure? This will wipe ALL your Red's installation "
  245. "data.\nYou'll lose all your settings, cogs and any "
  246. "modification you have made.\nThere is no going back.")
  247. if user_pick_yes_no():
  248. reset_red(reqs=True, data=True, cogs=True, git_reset=True)
  249. wait()
  250. elif choice == "0":
  251. break
  252. clear_screen()
  253. def run_red(autorestart):
  254. interpreter = sys.executable
  255. if interpreter is None: # This should never happen
  256. raise RuntimeError("Couldn't find Python's interpreter")
  257. if verify_requirements() is None:
  258. print("You don't have the requirements to start Red. "
  259. "Install them from the launcher.")
  260. if not INTERACTIVE_MODE:
  261. exit(1)
  262. cmd = (interpreter, "kitty.py")
  263. while True:
  264. try:
  265. code = subprocess.call(cmd)
  266. except KeyboardInterrupt:
  267. code = 0
  268. break
  269. else:
  270. if code == 0:
  271. break
  272. elif code == 26:
  273. print("Restarting KiTTY...")
  274. continue
  275. else:
  276. if not autorestart:
  277. break
  278. print("KiTTY has been terminated. Exit code: %d" % code)
  279. if INTERACTIVE_MODE:
  280. wait()
  281. def clear_screen():
  282. if IS_WINDOWS:
  283. os.system("cls")
  284. else:
  285. os.system("clear")
  286. def wait():
  287. if INTERACTIVE_MODE:
  288. input("Press enter to continue.")
  289. def user_choice():
  290. return input("> ").lower().strip()
  291. def user_pick_yes_no():
  292. choice = None
  293. yes = ("yes", "y")
  294. no = ("no", "n")
  295. while choice not in yes and choice not in no:
  296. choice = input("Yes/No > ").lower().strip()
  297. return choice in yes
  298. def remove_readonly(func, path, excinfo):
  299. os.chmod(path, 0o755)
  300. func(path)
  301. def remove_reqs_readonly():
  302. """Workaround for issue #569"""
  303. if not os.path.isdir(REQS_DIR):
  304. return
  305. os.chmod(REQS_DIR, 0o755)
  306. for root, dirs, files in os.walk(REQS_DIR):
  307. for d in dirs:
  308. os.chmod(os.path.join(root, d), 0o755)
  309. for f in files:
  310. os.chmod(os.path.join(root, f), 0o755)
  311. def calculate_md5(filename):
  312. hash_md5 = hashlib.md5()
  313. with open(filename, "rb") as f:
  314. for chunk in iter(lambda: f.read(4096), b""):
  315. hash_md5.update(chunk)
  316. return hash_md5.hexdigest()
  317. def create_fast_start_scripts():
  318. """Creates scripts for fast boot of Red without going
  319. through the launcher"""
  320. interpreter = sys.executable
  321. if not interpreter:
  322. return
  323. call = "\"{}\" launcher.py".format(interpreter)
  324. start_red = "{} --start".format(call)
  325. start_red_autorestart = "{} --start --auto-restart".format(call)
  326. modified = False
  327. if IS_WINDOWS:
  328. ccd = "pushd %~dp0\n"
  329. pause = "\npause"
  330. ext = ".bat"
  331. else:
  332. ccd = 'cd "$(dirname "$0")"\n'
  333. pause = "\nread -rsp $'Press enter to continue...\\n'"
  334. if not IS_MAC:
  335. ext = ".sh"
  336. else:
  337. ext = ".command"
  338. start_red = ccd + start_red + pause
  339. start_red_autorestart = ccd + start_red_autorestart + pause
  340. files = {
  341. "start_red" + ext : start_red,
  342. "start_red_autorestart" + ext : start_red_autorestart
  343. }
  344. if not IS_WINDOWS:
  345. files["start_launcher" + ext] = ccd + call
  346. for filename, content in files.items():
  347. if not os.path.isfile(filename):
  348. print("Creating {}... (fast start scripts)".format(filename))
  349. modified = True
  350. with open(filename, "w") as f:
  351. f.write(content)
  352. if not IS_WINDOWS and modified: # Let's make them executable on Unix
  353. for script in files:
  354. st = os.stat(script)
  355. os.chmod(script, st.st_mode | stat.S_IEXEC)
  356. def main():
  357. print("Verifying git installation...")
  358. has_git = is_git_installed()
  359. is_git_installation = os.path.isdir(".git")
  360. if IS_WINDOWS:
  361. os.system("TITLE KiTTY Launcher")
  362. clear_screen()
  363. try:
  364. create_fast_start_scripts()
  365. except Exception as e:
  366. print("Failed making fast start scripts: {}\n".format(e))
  367. while True:
  368. print(INTRO)
  369. if not has_git:
  370. print("WARNING: Git not found. This means that it's either not "
  371. "installed or not in the PATH environment variable like "
  372. "requested in the guide.\n")
  373. print("1. Run KiTTY /w autorestart in case of issues")
  374. print("2. Run KiTTY")
  375. print("3. Update")
  376. print("4. Install requirements")
  377. print("5. Maintenance (repair, reset...)")
  378. print("\n0. Quit")
  379. choice = user_choice()
  380. if choice == "1":
  381. run_red(autorestart=True)
  382. elif choice == "2":
  383. run_red(autorestart=False)
  384. elif choice == "3":
  385. print('zuck this')
  386. elif choice == "4":
  387. requirements_menu()
  388. elif choice == "5":
  389. maintenance_menu()
  390. elif choice == "0":
  391. break
  392. clear_screen()
  393. args = parse_cli_arguments()
  394. if __name__ == '__main__':
  395. abspath = os.path.abspath(__file__)
  396. dirname = os.path.dirname(abspath)
  397. # Sets current directory to the script's
  398. os.chdir(dirname)
  399. if not PYTHON_OK:
  400. print("Red needs Python 3.5 or superior. Install the required "
  401. "version.\nPress enter to continue.")
  402. if INTERACTIVE_MODE:
  403. wait()
  404. exit(1)
  405. if pip is None:
  406. print("KiTTY cannot work without the pip module. Please make sure to "
  407. "install Python without unchecking any option during the setup")
  408. wait()
  409. exit(1)
  410. if args.repair:
  411. reset_red(git_reset=True)
  412. if args.update_reqs:
  413. install_reqs(audio=True)
  414. elif args.update_reqs_no_audio:
  415. install_reqs(audio=False)
  416. if INTERACTIVE_MODE:
  417. main()
  418. elif args.start:
  419. print("Starting KiTTY...")
  420. run_red(autorestart=args.auto_restart)