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 !
Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.
To repozytorium jest zarchiwizowane. Możesz wyświetlać pliki i je sklonować, ale nie możesz do niego przepychać zmian lub otwierać zgłoszeń/Pull Requestów.

710 wiersze
27KB

  1. from discord.ext import commands
  2. from cogs.utils.dataIO import dataIO
  3. from cogs.utils import checks
  4. from cogs.utils.chat_formatting import pagify, box
  5. from __main__ import send_cmd_help, set_cog
  6. import os
  7. from subprocess import run as sp_run, PIPE
  8. import shutil
  9. from asyncio import as_completed
  10. from setuptools import distutils
  11. import discord
  12. from functools import partial
  13. from concurrent.futures import ThreadPoolExecutor
  14. from time import time
  15. from importlib.util import find_spec
  16. from copy import deepcopy
  17. NUM_THREADS = 4
  18. REPO_NONEX = 0x1
  19. REPO_CLONE = 0x2
  20. REPO_SAME = 0x4
  21. REPOS_LIST = "https://twentysix26.github.io/Red-Docs/red_cog_approved_repos/"
  22. WINDOWS_OS = os.name == 'nt'
  23. DISCLAIMER = ("You're about to add a 3rd party repository. The creator of Red, KiTTY,"
  24. " and its community have no responsibility for any potential "
  25. "damage that the content of 3rd party repositories might cause."
  26. "\nBy typing 'I agree' you declare to have read and understand "
  27. "the above message. This message won't be shown again until the"
  28. " next reboot.")
  29. class UpdateError(Exception):
  30. pass
  31. class CloningError(UpdateError):
  32. pass
  33. class RequirementFail(UpdateError):
  34. pass
  35. class Downloader:
  36. """Cog downloader/installer."""
  37. def __init__(self, bot):
  38. self.bot = bot
  39. self.disclaimer_accepted = False
  40. self.path = os.path.join("data", "downloader")
  41. self.file_path = os.path.join(self.path, "repos.json")
  42. # {name:{url,cog1:{installed},cog1:{installed}}}
  43. self.repos = dataIO.load_json(self.file_path)
  44. self.executor = ThreadPoolExecutor(NUM_THREADS)
  45. self._do_first_run()
  46. def save_repos(self):
  47. dataIO.save_json(self.file_path, self.repos)
  48. @commands.group(pass_context=True)
  49. @checks.is_owner()
  50. async def cog(self, ctx):
  51. """Additional cogs management"""
  52. if ctx.invoked_subcommand is None:
  53. await send_cmd_help(ctx)
  54. @cog.group(pass_context=True)
  55. async def repo(self, ctx):
  56. """Repo management commands"""
  57. if ctx.invoked_subcommand is None or \
  58. isinstance(ctx.invoked_subcommand, commands.Group):
  59. await send_cmd_help(ctx)
  60. return
  61. @repo.command(name="add", pass_context=True)
  62. async def _repo_add(self, ctx, repo_name: str, repo_url: str):
  63. """Adds repo to available repo lists
  64. Warning: Adding 3RD Party Repositories is at your own
  65. Risk."""
  66. if not self.disclaimer_accepted:
  67. await self.bot.say(DISCLAIMER)
  68. answer = await self.bot.wait_for_message(timeout=30,
  69. author=ctx.message.author)
  70. if answer is None:
  71. await self.bot.say('Not adding repo.')
  72. return
  73. elif "i agree" not in answer.content.lower():
  74. await self.bot.say('Not adding repo.')
  75. return
  76. else:
  77. self.disclaimer_accepted = True
  78. self.repos[repo_name] = {}
  79. self.repos[repo_name]['url'] = repo_url
  80. try:
  81. self.update_repo(repo_name)
  82. except CloningError:
  83. await self.bot.say("That repository link doesn't seem to be "
  84. "valid.")
  85. del self.repos[repo_name]
  86. return
  87. except FileNotFoundError:
  88. error_message = ("I couldn't find git. The downloader needs it "
  89. "for it to properly work.")
  90. if WINDOWS_OS:
  91. error_message += ("\nIf you just installed it you may need "
  92. "a reboot for it to be seen into the PATH "
  93. "environment variable.")
  94. await self.bot.say(error_message)
  95. return
  96. self.populate_list(repo_name)
  97. self.save_repos()
  98. data = self.get_info_data(repo_name)
  99. if data:
  100. msg = data.get("INSTALL_MSG")
  101. if msg:
  102. await self.bot.say(msg[:2000])
  103. await self.bot.say("Repo '{}' added.".format(repo_name))
  104. @repo.command(name="remove")
  105. async def _repo_del(self, repo_name: str):
  106. """Removes repo from repo list. COGS ARE NOT REMOVED."""
  107. def remove_readonly(func, path, excinfo):
  108. os.chmod(path, 0o755)
  109. func(path)
  110. if repo_name not in self.repos:
  111. await self.bot.say("That repo doesn't exist.")
  112. return
  113. del self.repos[repo_name]
  114. try:
  115. shutil.rmtree(os.path.join(self.path, repo_name), onerror=remove_readonly)
  116. except FileNotFoundError:
  117. pass
  118. self.save_repos()
  119. await self.bot.say("Repo '{}' removed.".format(repo_name))
  120. @cog.command(name="list")
  121. async def _send_list(self, repo_name=None):
  122. """Lists installable cogs
  123. Repositories list:
  124. https://twentysix26.github.io/Red-Docs/red_cog_approved_repos/"""
  125. retlist = []
  126. if repo_name and repo_name in self.repos:
  127. msg = "Available cogs:\n"
  128. for cog in sorted(self.repos[repo_name].keys()):
  129. if 'url' == cog:
  130. continue
  131. data = self.get_info_data(repo_name, cog)
  132. if data and data.get("HIDDEN") is True:
  133. continue
  134. if data:
  135. retlist.append([cog, data.get("SHORT", "")])
  136. else:
  137. retlist.append([cog, ''])
  138. else:
  139. if self.repos:
  140. msg = "Available repos:\n"
  141. for repo_name in sorted(self.repos.keys()):
  142. data = self.get_info_data(repo_name)
  143. if data:
  144. retlist.append([repo_name, data.get("SHORT", "")])
  145. else:
  146. retlist.append([repo_name, ""])
  147. else:
  148. await self.bot.say("You haven't added a repository yet.\n"
  149. "Start now! {}".format(REPOS_LIST))
  150. return
  151. col_width = max(len(row[0]) for row in retlist) + 2
  152. for row in retlist:
  153. msg += "\t" + "".join(word.ljust(col_width) for word in row) + "\n"
  154. msg += "\nRepositories list: {}".format(REPOS_LIST)
  155. for page in pagify(msg, delims=['\n'], shorten_by=8):
  156. await self.bot.say(box(page))
  157. @cog.command()
  158. async def info(self, repo_name: str, cog: str=None):
  159. """Shows info about the specified cog"""
  160. if cog is not None:
  161. cogs = self.list_cogs(repo_name)
  162. if cog in cogs:
  163. data = self.get_info_data(repo_name, cog)
  164. if data:
  165. msg = "{} by {}\n\n".format(cog, data["AUTHOR"])
  166. msg += data["NAME"] + "\n\n" + data["DESCRIPTION"]
  167. await self.bot.say(box(msg))
  168. else:
  169. await self.bot.say("The specified cog has no info file.")
  170. else:
  171. await self.bot.say("That cog doesn't exist."
  172. " Use cog list to see the full list.")
  173. else:
  174. data = self.get_info_data(repo_name)
  175. if data is None:
  176. await self.bot.say("That repo does not exist or the"
  177. " information file is missing for that repo"
  178. ".")
  179. return
  180. name = data.get("NAME", None)
  181. name = repo_name if name is None else name
  182. author = data.get("AUTHOR", "Unknown")
  183. desc = data.get("DESCRIPTION", "")
  184. msg = ("```{} by {}```\n\n{}".format(name, author, desc))
  185. await self.bot.say(msg)
  186. @cog.command(hidden=True)
  187. async def search(self, *terms: str):
  188. """Search installable cogs"""
  189. pass # TO DO
  190. @cog.command(pass_context=True)
  191. async def update(self, ctx):
  192. """Updates cogs"""
  193. tasknum = 0
  194. num_repos = len(self.repos)
  195. min_dt = 0.5
  196. burst_inc = 0.1/(NUM_THREADS)
  197. touch_n = tasknum
  198. touch_t = time()
  199. def regulate(touch_t, touch_n):
  200. dt = time() - touch_t
  201. if dt + burst_inc*(touch_n) > min_dt:
  202. touch_n = 0
  203. touch_t = time()
  204. return True, touch_t, touch_n
  205. return False, touch_t, touch_n + 1
  206. tasks = []
  207. for r in self.repos:
  208. task = partial(self.update_repo, r)
  209. task = self.bot.loop.run_in_executor(self.executor, task)
  210. tasks.append(task)
  211. base_msg = "Downloading updated cogs, please wait... "
  212. status = ' %d/%d repos updated' % (tasknum, num_repos)
  213. msg = await self.bot.say(base_msg + status)
  214. updated_cogs = []
  215. new_cogs = []
  216. deleted_cogs = []
  217. failed_cogs = []
  218. error_repos = {}
  219. installed_updated_cogs = []
  220. for f in as_completed(tasks):
  221. tasknum += 1
  222. try:
  223. name, updates, oldhash = await f
  224. if updates:
  225. if type(updates) is dict:
  226. for k, l in updates.items():
  227. tl = [(name, c, oldhash) for c in l]
  228. if k == 'A':
  229. new_cogs.extend(tl)
  230. elif k == 'D':
  231. deleted_cogs.extend(tl)
  232. elif k == 'M':
  233. updated_cogs.extend(tl)
  234. except UpdateError as e:
  235. name, what = e.args
  236. error_repos[name] = what
  237. edit, touch_t, touch_n = regulate(touch_t, touch_n)
  238. if edit:
  239. status = ' %d/%d repos updated' % (tasknum, num_repos)
  240. msg = await self._robust_edit(msg, base_msg + status)
  241. status = 'done. '
  242. for t in updated_cogs:
  243. repo, cog, _ = t
  244. if self.repos[repo][cog]['INSTALLED']:
  245. try:
  246. await self.install(repo, cog,
  247. no_install_on_reqs_fail=False)
  248. except RequirementFail:
  249. failed_cogs.append(t)
  250. else:
  251. installed_updated_cogs.append(t)
  252. for t in updated_cogs.copy():
  253. if t in failed_cogs:
  254. updated_cogs.remove(t)
  255. if not any(self.repos[repo][cog]['INSTALLED'] for
  256. repo, cog, _ in updated_cogs):
  257. status += ' No updates to apply. '
  258. if new_cogs:
  259. status += '\nNew cogs: ' \
  260. + ', '.join('%s/%s' % c[:2] for c in new_cogs) + '.'
  261. if deleted_cogs:
  262. status += '\nDeleted cogs: ' \
  263. + ', '.join('%s/%s' % c[:2] for c in deleted_cogs) + '.'
  264. if updated_cogs:
  265. status += '\nUpdated cogs: ' \
  266. + ', '.join('%s/%s' % c[:2] for c in updated_cogs) + '.'
  267. if failed_cogs:
  268. status += '\nCogs that got new requirements which have ' + \
  269. 'failed to install: ' + \
  270. ', '.join('%s/%s' % c[:2] for c in failed_cogs) + '.'
  271. if error_repos:
  272. status += '\nThe following repos failed to update: '
  273. for n, what in error_repos.items():
  274. status += '\n%s: %s' % (n, what)
  275. msg = await self._robust_edit(msg, base_msg + status)
  276. if not installed_updated_cogs:
  277. return
  278. patchnote_lang = 'Prolog'
  279. shorten_by = 8 + len(patchnote_lang)
  280. for note in self.patch_notes_handler(installed_updated_cogs):
  281. if note is None:
  282. continue
  283. for page in pagify(note, delims=['\n'], shorten_by=shorten_by):
  284. await self.bot.say(box(page, patchnote_lang))
  285. await self.bot.say("Cogs updated. Reload updated cogs? (yes/no)")
  286. answer = await self.bot.wait_for_message(timeout=15,
  287. author=ctx.message.author)
  288. if answer is None:
  289. await self.bot.say("Ok then, you can reload cogs with"
  290. " `{}reload <cog_name>`".format(ctx.prefix))
  291. elif answer.content.lower().strip() == "yes":
  292. registry = dataIO.load_json(os.path.join("data", "red", "cogs.json"))
  293. update_list = []
  294. fail_list = []
  295. for repo, cog, _ in installed_updated_cogs:
  296. if not registry.get('cogs.' + cog, False):
  297. continue
  298. try:
  299. self.bot.unload_extension("cogs." + cog)
  300. self.bot.load_extension("cogs." + cog)
  301. update_list.append(cog)
  302. except:
  303. fail_list.append(cog)
  304. msg = 'Done.'
  305. if update_list:
  306. msg += " The following cogs were reloaded: "\
  307. + ', '.join(update_list) + "\n"
  308. if fail_list:
  309. msg += " The following cogs failed to reload: "\
  310. + ', '.join(fail_list)
  311. await self.bot.say(msg)
  312. else:
  313. await self.bot.say("Ok then, you can reload cogs with"
  314. " `{}reload <cog_name>`".format(ctx.prefix))
  315. def patch_notes_handler(self, repo_cog_hash_pairs):
  316. for repo, cog, oldhash in repo_cog_hash_pairs:
  317. repo_path = os.path.join('data', 'downloader', repo)
  318. cogfile = os.path.join(cog, cog + ".py")
  319. cmd = ["git", "-C", repo_path, "log", "--relative-date",
  320. "--reverse", oldhash + '..', cogfile
  321. ]
  322. try:
  323. log = sp_run(cmd, stdout=PIPE).stdout.decode().strip()
  324. yield self.format_patch(repo, cog, log)
  325. except:
  326. pass
  327. @cog.command(pass_context=True)
  328. async def uninstall(self, ctx, repo_name, cog):
  329. """Uninstalls a cog"""
  330. if repo_name not in self.repos:
  331. await self.bot.say("That repo doesn't exist.")
  332. return
  333. if cog not in self.repos[repo_name]:
  334. await self.bot.say("That cog isn't available from that repo.")
  335. return
  336. set_cog("cogs." + cog, False)
  337. self.repos[repo_name][cog]['INSTALLED'] = False
  338. self.save_repos()
  339. os.remove(os.path.join("cogs", cog + ".py"))
  340. owner = self.bot.get_cog('Owner')
  341. await owner.unload.callback(owner, cog_name=cog)
  342. await self.bot.say("Cog successfully uninstalled.")
  343. @cog.command(name="install", pass_context=True)
  344. async def _install(self, ctx, repo_name: str, cog: str):
  345. """Installs specified cog"""
  346. if repo_name not in self.repos:
  347. await self.bot.say("That repo doesn't exist.")
  348. return
  349. if cog not in self.repos[repo_name]:
  350. await self.bot.say("That cog isn't available from that repo.")
  351. return
  352. data = self.get_info_data(repo_name, cog)
  353. try:
  354. install_cog = await self.install(repo_name, cog, notify_reqs=True)
  355. except RequirementFail:
  356. await self.bot.say("That cog has requirements that I could not "
  357. "install. Check the console for more "
  358. "informations.")
  359. return
  360. if data is not None:
  361. install_msg = data.get("INSTALL_MSG", None)
  362. if install_msg:
  363. await self.bot.say(install_msg[:2000])
  364. if install_cog:
  365. await self.bot.say("Installation completed. Load it now? (yes/no)")
  366. answer = await self.bot.wait_for_message(timeout=15,
  367. author=ctx.message.author)
  368. if answer is None:
  369. await self.bot.say("Ok then, you can load it with"
  370. " `{}load {}`".format(ctx.prefix, cog))
  371. elif answer.content.lower().strip() == "yes":
  372. set_cog("cogs." + cog, True)
  373. owner = self.bot.get_cog('Owner')
  374. await owner.load.callback(owner, cog_name=cog)
  375. else:
  376. await self.bot.say("Ok then, you can load it with"
  377. " `{}load {}`".format(ctx.prefix, cog))
  378. elif install_cog is False:
  379. await self.bot.say("Invalid cog. Installation aborted.")
  380. else:
  381. await self.bot.say("That cog doesn't exist. Use cog list to see"
  382. " the full list.")
  383. async def install(self, repo_name, cog, *, notify_reqs=False,
  384. no_install_on_reqs_fail=True):
  385. # 'no_install_on_reqs_fail' will make the cog get installed anyway
  386. # on requirements installation fail. This is necessary because due to
  387. # how 'cog update' works right now, the user would have no way to
  388. # reupdate the cog if the update fails, since 'cog update' only
  389. # updates the cogs that get a new commit.
  390. # This is not a great way to deal with the problem and a cog update
  391. # rework would probably be the best course of action.
  392. reqs_failed = False
  393. if cog.endswith('.py'):
  394. cog = cog[:-3]
  395. path = self.repos[repo_name][cog]['file']
  396. cog_folder_path = self.repos[repo_name][cog]['folder']
  397. cog_data_path = os.path.join(cog_folder_path, 'data')
  398. data = self.get_info_data(repo_name, cog)
  399. if data is not None:
  400. requirements = data.get("REQUIREMENTS", [])
  401. requirements = [r for r in requirements
  402. if not self.is_lib_installed(r)]
  403. if requirements and notify_reqs:
  404. await self.bot.say("Installing cog's requirements...")
  405. for requirement in requirements:
  406. if not self.is_lib_installed(requirement):
  407. success = await self.bot.pip_install(requirement)
  408. if not success:
  409. if no_install_on_reqs_fail:
  410. raise RequirementFail()
  411. else:
  412. reqs_failed = True
  413. to_path = os.path.join("cogs", cog + ".py")
  414. print("Copying {}...".format(cog))
  415. shutil.copy(path, to_path)
  416. if os.path.exists(cog_data_path):
  417. print("Copying {}'s data folder...".format(cog))
  418. distutils.dir_util.copy_tree(cog_data_path,
  419. os.path.join('data', cog))
  420. self.repos[repo_name][cog]['INSTALLED'] = True
  421. self.save_repos()
  422. if not reqs_failed:
  423. return True
  424. else:
  425. raise RequirementFail()
  426. def get_info_data(self, repo_name, cog=None):
  427. if cog is not None:
  428. cogs = self.list_cogs(repo_name)
  429. if cog in cogs:
  430. info_file = os.path.join(cogs[cog].get('folder'), "info.json")
  431. if os.path.isfile(info_file):
  432. try:
  433. data = dataIO.load_json(info_file)
  434. except:
  435. return None
  436. return data
  437. else:
  438. repo_info = os.path.join(self.path, repo_name, 'info.json')
  439. if os.path.isfile(repo_info):
  440. try:
  441. data = dataIO.load_json(repo_info)
  442. return data
  443. except:
  444. return None
  445. return None
  446. def list_cogs(self, repo_name):
  447. valid_cogs = {}
  448. repo_path = os.path.join(self.path, repo_name)
  449. folders = [f for f in os.listdir(repo_path)
  450. if os.path.isdir(os.path.join(repo_path, f))]
  451. legacy_path = os.path.join(repo_path, "cogs")
  452. legacy_folders = []
  453. if os.path.exists(legacy_path):
  454. for f in os.listdir(legacy_path):
  455. if os.path.isdir(os.path.join(legacy_path, f)):
  456. legacy_folders.append(os.path.join("cogs", f))
  457. folders = folders + legacy_folders
  458. for f in folders:
  459. cog_folder_path = os.path.join(self.path, repo_name, f)
  460. cog_folder = os.path.basename(cog_folder_path)
  461. for cog in os.listdir(cog_folder_path):
  462. cog_path = os.path.join(cog_folder_path, cog)
  463. if os.path.isfile(cog_path) and cog_folder == cog[:-3]:
  464. valid_cogs[cog[:-3]] = {'folder': cog_folder_path,
  465. 'file': cog_path}
  466. return valid_cogs
  467. def get_dir_name(self, url):
  468. splitted = url.split("/")
  469. git_name = splitted[-1]
  470. return git_name[:-4]
  471. def is_lib_installed(self, name):
  472. return bool(find_spec(name))
  473. def _do_first_run(self):
  474. save = False
  475. repos_copy = deepcopy(self.repos)
  476. # Issue 725
  477. for repo in repos_copy:
  478. for cog in repos_copy[repo]:
  479. cog_data = repos_copy[repo][cog]
  480. if isinstance(cog_data, str): # ... url field
  481. continue
  482. for k, v in cog_data.items():
  483. if k in ("file", "folder"):
  484. repos_copy[repo][cog][k] = os.path.normpath(cog_data[k])
  485. if self.repos != repos_copy:
  486. self.repos = repos_copy
  487. save = True
  488. invalid = []
  489. for repo in self.repos:
  490. broken = 'url' in self.repos[repo] and len(self.repos[repo]) == 1
  491. if broken:
  492. save = True
  493. try:
  494. self.update_repo(repo)
  495. self.populate_list(repo)
  496. except CloningError:
  497. invalid.append(repo)
  498. continue
  499. except Exception as e:
  500. print(e) # TODO: Proper logging
  501. continue
  502. for repo in invalid:
  503. del self.repos[repo]
  504. if save:
  505. self.save_repos()
  506. def populate_list(self, name):
  507. valid_cogs = self.list_cogs(name)
  508. new = set(valid_cogs.keys())
  509. old = set(self.repos[name].keys())
  510. for cog in new - old:
  511. self.repos[name][cog] = valid_cogs.get(cog, {})
  512. self.repos[name][cog]['INSTALLED'] = False
  513. for cog in new & old:
  514. self.repos[name][cog].update(valid_cogs[cog])
  515. for cog in old - new:
  516. if cog != 'url':
  517. del self.repos[name][cog]
  518. def update_repo(self, name):
  519. def run(*args, **kwargs):
  520. env = os.environ.copy()
  521. env['GIT_TERMINAL_PROMPT'] = '0'
  522. kwargs['env'] = env
  523. return sp_run(*args, **kwargs)
  524. try:
  525. dd = self.path
  526. if name not in self.repos:
  527. raise UpdateError("Repo does not exist in data, wtf")
  528. folder = os.path.join(dd, name)
  529. # Make sure we don't git reset the Red folder on accident
  530. if not os.path.exists(os.path.join(folder, '.git')):
  531. #if os.path.exists(folder):
  532. #shutil.rmtree(folder)
  533. url = self.repos[name].get('url')
  534. if not url:
  535. raise UpdateError("Need to clone but no URL set")
  536. branch = None
  537. if "@" in url: # Specific branch
  538. url, branch = url.rsplit("@", maxsplit=1)
  539. if branch is None:
  540. p = run(["git", "clone", url, folder])
  541. else:
  542. p = run(["git", "clone", "-b", branch, url, folder])
  543. if p.returncode != 0:
  544. raise CloningError()
  545. self.populate_list(name)
  546. return name, REPO_CLONE, None
  547. else:
  548. rpbcmd = ["git", "-C", folder, "rev-parse", "--abbrev-ref", "HEAD"]
  549. p = run(rpbcmd, stdout=PIPE)
  550. branch = p.stdout.decode().strip()
  551. rpcmd = ["git", "-C", folder, "rev-parse", branch]
  552. p = run(["git", "-C", folder, "reset", "--hard",
  553. "origin/%s" % branch, "-q"])
  554. if p.returncode != 0:
  555. raise UpdateError("Error resetting to origin/%s" % branch)
  556. p = run(rpcmd, stdout=PIPE)
  557. if p.returncode != 0:
  558. raise UpdateError("Unable to determine old commit hash")
  559. oldhash = p.stdout.decode().strip()
  560. p = run(["git", "-C", folder, "pull", "-q", "--ff-only"])
  561. if p.returncode != 0:
  562. raise UpdateError("Error pulling updates")
  563. p = run(rpcmd, stdout=PIPE)
  564. if p.returncode != 0:
  565. raise UpdateError("Unable to determine new commit hash")
  566. newhash = p.stdout.decode().strip()
  567. if oldhash == newhash:
  568. return name, REPO_SAME, None
  569. else:
  570. self.populate_list(name)
  571. self.save_repos()
  572. ret = {}
  573. cmd = ['git', '-C', folder, 'diff', '--no-commit-id',
  574. '--name-status', oldhash, newhash]
  575. p = run(cmd, stdout=PIPE)
  576. if p.returncode != 0:
  577. raise UpdateError("Error in git diff")
  578. changed = p.stdout.strip().decode().split('\n')
  579. for f in changed:
  580. if not f.endswith('.py'):
  581. continue
  582. status, _, cogpath = f.partition('\t')
  583. split = os.path.split(cogpath)
  584. cogdir, cogname = split[-2:]
  585. cogname = cogname[:-3] # strip .py
  586. if len(split) != 2 or cogdir != cogname:
  587. continue
  588. if status not in ret:
  589. ret[status] = []
  590. ret[status].append(cogname)
  591. return name, ret, oldhash
  592. except CloningError as e:
  593. raise CloningError(name, *e.args) from None
  594. except UpdateError as e:
  595. raise UpdateError(name, *e.args) from None
  596. async def _robust_edit(self, msg, text):
  597. try:
  598. msg = await self.bot.edit_message(msg, text)
  599. except discord.errors.NotFound:
  600. msg = await self.bot.send_message(msg.channel, text)
  601. except:
  602. raise
  603. return msg
  604. @staticmethod
  605. def format_patch(repo, cog, log):
  606. header = "Patch Notes for %s/%s" % (repo, cog)
  607. line = "=" * len(header)
  608. if log:
  609. return '\n'.join((header, line, log))
  610. def check_folders():
  611. if not os.path.exists(os.path.join("data", "downloader")):
  612. print('Making repo downloads folder...')
  613. os.mkdir(os.path.join("data", "downloader"))
  614. def check_files():
  615. f = os.path.join("data", "downloader", "repos.json")
  616. if not dataIO.is_valid_json(f):
  617. print("Creating default data/downloader/repos.json")
  618. dataIO.save_json(f, {})
  619. def setup(bot):
  620. check_folders()
  621. check_files()
  622. n = Downloader(bot)
  623. bot.add_cog(n)