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 !
Não pode escolher mais do que 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
Este repositório está arquivado. Pode ver ficheiros e cloná-lo, mas não pode fazer envios ou lançar questões ou pedidos de integração.

710 linhas
27 KiB

  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)