diff --git a/bot.py b/bot.py new file mode 100644 index 0000000..d884f13 --- /dev/null +++ b/bot.py @@ -0,0 +1,813 @@ +"""" +Copyright © twilsonco 2020 +Description: This is a discord bot to manage torrent transfers through the Transmission transmissionrpc python library. + +Version: 1.0 +""" + +import discord +import asyncio +import aiohttp +import json +import subprocess +from discord.ext.commands import Bot +from discord.ext import commands +from platform import python_version +import os +from os.path import expanduser, join +import re +import datetime +import pytz +import platform +import transmissionrpc +import logging + +BOT_PREFIX = 't/' +TOKEN = 'SECRET_TOKEN' +OWNERS = [OWNER_USER_IDS] +BLACKLIST = [USER_IDS] +WHITELIST = [USER_IDS] +CHANNEL_IDS=[CHANNEL_IDS] +LOGO_URL="https://iyanovich.files.wordpress.com/2009/04/transmission-logo.png" + +TSCLIENT_CONFIG={ + 'host': "10.0.1.2", + 'port': 9091, + 'user': "USERNAME", + 'password': "PASSWORD" +} +DRYRUN = False + +logging.basicConfig(format='%(asctime)s %(message)s',filename=join(expanduser("~"),'transmissionbot.log')) + + + +client = Bot(command_prefix=BOT_PREFIX) +TSCLIENT = None + + + +logger = logging.getLogger('transmission_bot') +logger.setLevel(logging.INFO) +DEFAULT_REASON="TransmissionBot" + +# Begin transmissionrpc functions, lovingly taken from https://github.com/leighmacdonald/transmission_scripts + +class TSClient(transmissionrpc.Client): + """ Basic subclass of the standard transmissionrpc client which provides some simple + helper functionality. + """ + + def get_torrents_by(self, sort_by=None, filter_by=None, reverse=False, filter_regex=None, id_list=None): + """This method will call get_torrents and then perform any sorting or filtering + actions requested on the returned torrent set. + + :param sort_by: Sort key which must exist in `Sort.names` to be valid; + :type sort_by: str + :param filter_by: + :type filter_by: str + :param reverse: + :return: Sorted and filter torrent list + :rtype: transmissionrpc.Torrent[] + """ + torrents = self.get_torrents() + if filter_regex: + regex = re.compile(filter_regex, re.IGNORECASE) + torrents = [tor for tor in torrents if regex.search(tor.name)] + if id_list: + torrents = [tor for tor in torrents if tor.id in id_list] + if filter_by: + torrents = filter_torrents_by(torrents, key=getattr(Filter, filter_by)) + if sort_by is None: + if filter_by == "downloading": + sort_by = "speed_down" + elif filter_by == "seeding": + sort_by = "speed_up" + elif filter_by in ["stopped","finished"]: + sort_by = "ratio" + if sort_by: + torrents = sort_torrents_by(torrents, key=getattr(Sort, sort_by), reverse=reverse) + return torrents + +def make_client(): + """ Create a new transmission RPC client + + If you want to parse more than the standard CLI arguments, like when creating a new customized + script, you can append your options to the argument parser. + + :param args: Optional CLI args passed in. + :return: + """ + try: + return TSClient( + TSCLIENT_CONFIG['host'], + port=TSCLIENT_CONFIG['port'], + user=TSCLIENT_CONFIG['user'], + password=TSCLIENT_CONFIG['password'] + ) + except: + return None + +filter_names = ( + "all", + "active", + "downloading", + "seeding", + "stopped", + "finished" +) + +class Filter(object): + """A set of filtering operations that can be used against a list of torrent objects""" + + names = ( + "all", + "active", + "downloading", + "seeding", + "stopped", + "finished" + ) + + @staticmethod + def all(t): + return t + + @staticmethod + def active(t): + return t.rateUpload > 0 or t.rateDownload > 0 + + @staticmethod + def downloading(t): + return t.status == 'downloading' + + @staticmethod + def seeding(t): + return t.status == 'seeding' + + @staticmethod + def stopped(t): + return t.status == 'stopped' + + @staticmethod + def finished(t): + return t.status == 'finished' + + @staticmethod + def lifetime(t): + return t.date_added + + +def filter_torrents_by(torrents, key=Filter.all): + """ + + :param key: + :param torrents: + :return: []transmissionrpc.Torrent + """ + filtered_torrents = [] + for torrent in torrents: + if key(torrent): + filtered_torrents.append(torrent) + return filtered_torrents + +sort_names = ( + "id", + "progress", + "name", + "size", + "ratio", + "speed", + "speed_up", + "speed_down", + "status", + "queue", + "age", + "activity" +) + +class Sort(object): + """ Defines methods for sorting torrent sequences """ + + names = ( + "id", + "progress", + "name", + "size", + "ratio", + "speed", + "speed_up", + "speed_down", + "status", + "queue", + "age", + "activity" + ) + + @staticmethod + def activity(t): + return t.date_active + + @staticmethod + def age(t): + return t.date_added + + @staticmethod + def queue(t): + return t.queue_position + + @staticmethod + def status(t): + return t.status + + @staticmethod + def progress(t): + return t.progress + + @staticmethod + def name(t): + return t.name.lower() + + @staticmethod + def size(t): + return -t.totalSize + + @staticmethod + def id(t): + return t.id + + @staticmethod + def ratio(t): + return t.ratio + + @staticmethod + def speed(t): + return t.rateUpload + t.rateDownload + + @staticmethod + def speed_up(t): + return t.rateUpload + + @staticmethod + def speed_down(t): + return t.rateDownload + + +def sort_torrents_by(torrents, key=Sort.name, reverse=False): + return sorted(torrents, key=key, reverse=reverse) + +# def print_torrent_line(torrent, colourize=True): +# name = torrent.name +# progress = torrent.progress / 100.0 +# print("[{}] [{}] {} {}[{}/{}]{} ra: {} up: {} dn: {} [{}]".format( +# white_on_blk(torrent.id), +# find_tracker(torrent), +# print_pct(torrent) if colourize else name.decode("latin-1"), +# white_on_blk(""), +# red_on_blk("{:.0%}".format(progress)) if progress < 1 else green_on_blk("{:.0%}".format(progress)), +# magenta_on_blk(natural_size(torrent.totalSize)), +# white_on_blk(""), +# red_on_blk(torrent.ratio) if torrent.ratio < 1.0 else green_on_blk(torrent.ratio), +# green_on_blk(natural_size(float(torrent.rateUpload)) + "/s") if torrent.rateUpload else "0.0 kB/s", +# green_on_blk(natural_size(float(torrent.rateDownload)) + "/s") if torrent.rateDownload else "0.0 kB/s", +# yellow_on_blk(torrent.status) +# )) + +def remove_torrent(torrent, reason=DEFAULT_REASON, delete_files=False): + """ Remove a torrent from the client stopping it first if its in a started state. + + :param client: Transmission RPC Client + :type client: transmissionrpc.Client + :param torrent: Torrent instance to remove + :type torrent: transmissionrpc.Torrent + :param reason: Reason for removal + :type reason: str + :param dry_run: Do a dry run without actually running any commands + :type dry_run: bool + :return: + """ + if torrent.status != "stopped": + if not DRYRUN: + TSCLIENT.stop_torrent(torrent.hashString) + if not DRYRUN: + TSCLIENT.remove_torrent(torrent.hashString, delete_data=delete_files) + logger.info("Removed: {} {}\n\tReason: {}\n\tDry run: {}, Delete files: {}".format(torrent.name, torrent.hashString, reason, DRYRUN,delete_files)) + +def remove_torrents(torrents, reason=DEFAULT_REASON, delete_files=False): + """ Remove a torrent from the client stopping it first if its in a started state. + + :param client: Transmission RPC Client + :type client: transmissionrpc.Client + :param torrent: Torrent instance to remove + :type torrent: transmissionrpc.Torrent + :param reason: Reason for removal + :type reason: str + :param dry_run: Do a dry run without actually running any commands + :type dry_run: bool + :return: + """ + for torrent in torrents: + remove_torrent(torrent, reason=reason, delete_files=delete_files) + +def stop_torrents(torrents=[], reason=DEFAULT_REASON): + """ Stop (pause) a list of torrents from the client. + + :param client: Transmission RPC Client + :type client: transmissionrpc.Client + :param torrent: Torrent instance to remove + :type torrent: transmissionrpc.Torrent + :param reason: Reason for removal + :type reason: str + :param dry_run: Do a dry run without actually running any commands + :type dry_run: bool + :return: + """ + for torrent in (torrents if len(torrents) > 0 else TSCLIENT.get_torrents()): + if torrent.status not in ["stopped","finished"]: + if not DRYRUN: + TSCLIENT.stop_torrent(torrent.hashString) + logger.info("Paused: {} {}\n\tReason: {}\n\tDry run: {}".format(torrent.name, torrent.hashString, reason, DRYRUN)) + +def resume_torrents(torrents=[], reason=DEFAULT_REASON): + """ Stop (pause) a list of torrents from the client. + + :param client: Transmission RPC Client + :type client: transmissionrpc.Client + :param torrent: Torrent instance to remove + :type torrent: transmissionrpc.Torrent + :param reason: Reason for removal + :type reason: str + :param dry_run: Do a dry run without actually running any commands + :type dry_run: bool + :return: + """ + for torrent in (torrents if len(torrents) > 0 else TSCLIENT.get_torrents()): + if torrent.status == "stopped": + if not DRYRUN: + TSCLIENT.start_torrent(torrent.hashString) + logger.info("Resumed: {} {}\n\tReason: {}\n\tDry run: {}".format(torrent.name, torrent.hashString, reason, DRYRUN)) + +def add_torrent(torStr): + tor = None + if torStr != "": + tor = TSCLIENT.add_torrent(torStr) + return tor + + +# Begin discord bot functions, adapted from https://github.com/kkrypt0nn/Python-Discord-Bot-Template + +# async def status_task(): +# while True: +# await client.change_presence(activity=discord.Game("{}help".format(BOT_PREFIX))) +# await asyncio.sleep(86400) + +@client.event +async def on_ready(): + global TSCLIENT + TSCLIENT = make_client() + if TSCLIENT is None: + print("Failed to create transmissionrpc client") + else: + # client.loop.create_task(status_task()) + await client.change_presence(activity=discord.Game("{}help".format(BOT_PREFIX))) + print('Logged in as ' + client.user.name) + print("Discord.py API version:", discord.__version__) + print("Python version:", platform.python_version()) + print("Running on:", platform.system(), platform.release(), "(" + os.name + ")") + print('-------------------') + +def humanbytes(B): + 'Return the given bytes as a human friendly KB, MB, GB, or TB string' + B = float(B) + KB = float(1024) + MB = float(KB ** 2) # 1,048,576 + GB = float(KB ** 3) # 1,073,741,824 + TB = float(KB ** 4) # 1,099,511,627,776 + + if B < KB: + return '{0} {1}'.format(B,'B') + elif KB <= B < MB: + return '{0:.2f} kB'.format(B/KB) + elif MB <= B < GB: + return '{0:.2f} MB'.format(B/MB) + elif GB <= B < TB: + return '{0:.2f} GB'.format(B/GB) + elif TB <= B: + return '{0:.2f} TB'.format(B/TB) + +def tobytes(B): + 'Return the number of bytes given by a string (a float followed by a space and the unit of prefix-bytes eg. "21.34 GB")' + numstr = B.lower().split(' ') + KB = (('kilo','kb','kb/s'),float(1024)) + MB = (('mega','mb','mb/s'),float(KB[1] ** 2)) # 1,048,576 + GB = (('giga','gb','gb/s'),float(KB[1] ** 3)) # 1,073,741,824 + TB = (('tera','tb','tb/s'),float(KB[1] ** 4)) # 1,099,511,627,776 + + for prefix in (KB,MB,GB,TB): + if numstr[1] in prefix[0]: + return float(float(numstr[0]) * prefix[1]) + +# check that message author is allowed and message was sent in allowed channel +async def CommandPrecheck(context): + if len(CHANNEL_IDS) > 0 and context.message.channel.id not in CHANNEL_IDS: + await context.message.channel.send("I don't respond to commands in this channel...") + return False + elif context.message.author.id in BLACKLIST or (len(WHITELIST) > 0 and context.message.author.id not in WHITELIST): + await context.message.channel.send("You're not allowed to use this...") + return False + return True + +@client.command(name='add', pass_context=True) +async def add(context, *, content): + if await CommandPrecheck(context): + if content == "": + await context.message.channel.send("Invalid string") + else: + torStr = None + for t in content.strip().split(" "): + await context.message.channel.send('Adding torrent {}\n Please wait...'.format(t)) + tor = add_torrent(t) + if torStr: + torStr += "\n{}" % tor.name + else: + torStr = tor.name + await context.message.channel.send('✅ Added torrent{}:\n{}'.format("s" if len(content.strip().split(" ")) > 1 else "", torStr)) +@client.command(name='a', pass_context=True) +async def a(context, *, content=""): + await add(context, content=content) + +# def torInfo(t): +# states = ('downloading', 'seeding', 'stopped', 'finished','all') +# stateEmoji = {i:j for i,j in zip(states,['🔻','🌱','⏸','🏁','🔁'])} +# +# downStr = humanbytes(t.progress * 0.01 * t.totalSize) +# upStr = "{} (Ratio: {:.2f})".format(humanbytes(t.uploadedEver), t.uploadRatio) +# runTime = +# +# if t.progress < 100.0: +# have = "{} of {} ({:.1f}){}{}".format(downStr,humanbytes(t.totalSize), t.progress, '' if t.haveUnchecked == 0 else ', {} Unverified'.format(humanbytes(t.haveUnchecked)), '' if t.corruptEver == 0 else ', {} Corrupt'.format(humanbytes(t.corruptEver))) +# avail = "{:.1f}%".format(t.desiredAvailable/t.leftUntilDone) +# else: +# have = "{} ({:d}){}{}".format(humanbytes(t.totalSize), t.progress, '' if t.haveUnchecked == 0 else ', {} Unverified'.format(humanbytes(t.haveUnchecked)), '' if t.corruptEver == 0 else ', {} Corrupt'.format(humanbytes(t.corruptEver))) +# avail = "100%" +# +# +# +# embed=discord.Embed(title=t.name,color=0xb51a00) +# +# return embed + +torListLegend="• Legend:\n⬇️download  rate, ⬆️upload  rate, ⚖️seed  ratio, 🔻downloading, 🌱seeding, 🏁finished, ⏸paused\n• Error:\n✅none, ⚠️tracker  warning, ☄️tracker  error, 🔥local  error\n• Info:\n🐢stalled, 🐇running, 🔒private, 🔓public " + +def torList(torrents, author_name="Torrent Transfers",title=None,description=None): + states = ('downloading', 'seeding', 'stopped', 'finished','all') + stateEmoji = {i:j for i,j in zip(states,['🔻','🌱','⏸','🏁','🔁'])} + errorStrs = ['✅','📡⚠️','📡‼️','🖥‼️'] + + def torListLine(t): + down = humanbytes(t.progress * 0.01 * t.totalSize) + out = "{} {} {} {} ".format(stateEmoji[t.status],errorStrs[t.error],'🐢' if t.isStalled else '🐇', '🔒' if t.isPrivate else '🔓') + if t.status == 'downloading': + out += "{}/{} ⬇️ {}/s ⬆️ {}/s ⚖️ {:.2f}".format(down,humanbytes(t.totalSize),humanbytes(t.rateDownload),humanbytes(t.rateUpload),t.uploadRatio) + elif t.status == 'seeding': + out += "{} ⬆️ {}/s ⚖️ {:.2f}".format(humanbytes(t.totalSize),humanbytes(t.rateUpload),t.uploadRatio) + elif t.status == 'stopped': + out += "{}/{} ⚖️ {:.2f}".format(down,humanbytes(t.totalSize),t.uploadRatio) + elif t.status == 'finished': + out += "{} ⚖️ {:.2f}".format(humanbytes(t.totalSize),t.uploadRatio) + + return out + + nameList = ["{}) {:.245}{}".format(t.id,t.name,"..." if len(t.name) > 245 else "") for t in torrents] + valList = [torListLine(t) for t in torrents] + + n = 0 + i = 0 + embeds = [] + if len(torrents) > 0: + while i < len(torrents): + embed=discord.Embed(title=title,description=description,color=0xb51a00) + embed.set_author(name=author_name, icon_url=LOGO_URL) + for j in range(25): + embed.add_field(name=nameList[i],value=valList[i],inline=False) + i += 1 + n += 1 + if n >= 25: + n = 0 + break + if i >= len(torrents): + break + embed.timestamp = datetime.datetime.now(tz=pytz.timezone('America/Denver')) + embeds.append(embed) + + embeds[-1].set_footer(text=torListLegend) + else: + embeds.append(discord.Embed(title=title, description="No matching transfers found!", color=0xb51a00)) + embeds[-1].set_author(name=author_name, icon_url=LOGO_URL) + embeds[-1].timestamp = datetime.datetime.now(tz=pytz.timezone('America/Denver')) + + return embeds + +def torGetListOpsFromStr(listOpStr): + filter_by = None + sort_by = None + splitcontent = listOpStr.split(" ") + + if "--filter" in splitcontent: + ind = splitcontent.index("--filter") + if len(splitcontent) > ind + 1: + filter_by = splitcontent[ind+1] + del splitcontent[ind+1] + del splitcontent[ind] + elif "-f" in splitcontent: + ind = splitcontent.index("-f") + if len(splitcontent) > ind + 1: + filter_by = splitcontent[ind+1] + del splitcontent[ind+1] + del splitcontent[ind] + + if "--sort" in splitcontent: + ind = splitcontent.index("--sort") + if len(splitcontent) > ind + 1: + sort_by = splitcontent[ind+1] + del splitcontent[ind+1] + del splitcontent[ind] + elif "-s" in splitcontent: + ind = splitcontent.index("-s") + if len(splitcontent) > ind + 1: + sort_by = splitcontent[ind+1] + del splitcontent[ind+1] + del splitcontent[ind] + + filter_regex = " ".join(splitcontent).strip() + if filter_regex == "": + filter_regex = None + + if filter_by is not None and filter_by not in filter_names: + return -1, None, None + if sort_by is not None and sort_by not in sort_names: + return None, -1, None + + return filter_by, sort_by, filter_regex + +@client.command(name='list', pass_context=True) +async def list(context, *, content=""): + if await CommandPrecheck(context): + filter_by, sort_by, filter_regex = torGetListOpsFromStr(content) + if filter_by == -1: + await context.message.channel.send("Invalid filter specified. Choose one of {}".format(str(filter_names))) + return + if sort_by == -1: + await context.message.channel.send("Invalid sort specified. Choose one of {}".format(str(sort_names))) + return + + torrents = TSCLIENT.get_torrents_by(sort_by=sort_by, filter_by=filter_by, filter_regex=filter_regex) + + embeds = torList(torrents, title="{} transfer{} matching '`{}`'".format(len(torrents),'' if len(torrents)==1 else 's',content)) + + for e in embeds: + await context.message.channel.send(embed=e) +@client.command(name='l', pass_context=True) +async def l(context, *, content=""): + await list(context, content=content) + +def torSummary(torrents): + states = ('downloading', 'seeding', 'paused', 'finished') + numInState = [len([True for t in torrents if t.status == s]) for s in states] + numTot = len(torrents) + + sumTot = sum([t.totalSize for t in torrents]) + totSize = humanbytes(sumTot) + totUpRate = humanbytes(sum([t.rateUpload for t in torrents])) + totDownRate = humanbytes(sum([t.rateDownload for t in torrents])) + + downList = [t.progress*0.01*t.totalSize for t in torrents] + upList = [t.ratio * j for t,j in zip(torrents,downList)] + + sumDown = sum(downList) + sumUp = sum(upList) + + totDown = humanbytes(sumDown) + totUp = humanbytes(sumUp) + + totRatio = '{:.2f}'.format(sumUp / sumDown) + + totDownRatio = '{:.2f}'.format(sumDown / sumTot * 100.0) + + numTopRatios = min([len(torrents),5]) + topRatios = "• Top {} ratio{}:".format(numTopRatios,"s" if numTopRatios > 0 else "") + sortByRatio = sorted(torrents,key=lambda t:float(t.ratio),reverse=True) + for i in range(numTopRatios): + topRatios += "\n {:.1f} {:.35}{}".format(float(sortByRatio[i].ratio),sortByRatio[i].name,"..." if len(sortByRatio[i].name) > 35 else "") + + embed=discord.Embed(title="🌊 Torrent Summary", description="*React to see list of corresponding transfers*", color=0xb51a00) + embed.set_thumbnail(url=LOGO_URL) + embed.add_field(name="⬇️ {}/s".format(totDownRate), value="⬆️ {}/s".format(totUpRate), inline=False) + embed.add_field(name="⏬ {} of {}".format(totDown,totSize), value="⏫ {} ⚖️ {}".format(totUp,totRatio), inline=False) + embed.add_field(name="🔁 {} transfer{} total".format(numTot,"" if numTot == 0 else "s"), value="🔻 {} 🌱 {} 🏁 {} ⏸ {}".format(numInState[0],numInState[1],numInState[3],numInState[2]), inline=False) + embed.set_footer(text=topRatios+'\n• Legend:\n⬇️total  download  rate, ⬆️total  upload  rate, ⏬total  downloaded, ⏫total  uploaded, 🔻downloading, 🌱seeding, 🏁finished, ⏸paused, ⚖️total  seed  ratio') + embed.timestamp = datetime.datetime.now(tz=pytz.timezone('America/Denver')) + # await context.message.channel.send(embed=embed) + return embed,numInState + +@client.command(name='summary', pass_context=True) +async def summary(context, *, content=""): + if await CommandPrecheck(context): + states = ('downloading', 'seeding', 'paused', 'finished','all') + stateEmoji = ['🔻','🌱','⏸','🏁','🔃'] + + summary=torSummary(TSCLIENT.get_torrents()) + msg = await context.message.channel.send(embed=summary[0]) + for i in range(len(summary[1])): + if summary[1][i] > 0: + await msg.add_reaction(stateEmoji[i]) + await msg.add_reaction(stateEmoji[-1]) + + def check(reaction, user): + return user == context.message.author and str(reaction.emoji) in stateEmoji + + try: + reaction, user = await client.wait_for('reaction_add', timeout=60.0, check=check) + except asyncio.TimeoutError: + pass + else: + cmds = {i:j for i,j in zip(stateEmoji,('--filter downloading', '--filter seeding', '--filter stopped', '--filter finished', ''))} + await list(context, content=cmds[str(reaction.emoji)]) +@client.command(name='s', pass_context=True) +async def s(context, *, content=""): + await summary(context, content=content) + +def strListToList(strList): + if not re.match('^[0-9\,\-]+$', strList): + return False + outList = [] + for seg in strList.strip().split(","): + subseg = seg.split("-") + if len(subseg) == 1 and int(subseg[0]) not in outList: + outList.append(int(subseg[0])) + elif len(subseg) == 2: + subseg = sorted([int(i) for i in subseg]) + outList += range(subseg[0],subseg[1]+1) + if len(outList) == 0: + return False + + return outList + +@client.command(name='modify', pass_context=True) +async def modify(context, *, content=""): + if await CommandPrecheck(context): + allOnly = content.strip() == "" + torrents = [] + if not allOnly: + id_list = strListToList(content) + filter_by = None + sort_by = None + filter_regex = None + if not id_list: + filter_by, sort_by, filter_regex = torGetListOpsFromStr(content) + if filter_by == -1: + await context.message.channel.send("Invalid filter specified. Choose one of {}".format(str(filter_names))) + return + if sort_by == -1: + await context.message.channel.send("Invalid sort specified. Choose one of {}".format(str(sort_names))) + return + # await context.message.channel.send("{} {} {} {}".format()) + # return + torrents = TSCLIENT.get_torrents_by(filter_by=filter_by, sort_by=sort_by, filter_regex=filter_regex, id_list=id_list) + + if len(torrents) > 0: + ops = ["pause","resume","remove","removedelete"] + opNames = ["pause","resume","remove","remove and delete"] + opEmoji = ['⏸','▶️','❌','🗑'] + opStr = "⏸pause ▶️resume ❌remove 🗑remove  and  delete" + embeds = torList(torrents,author_name="Click a reaction to choose modification".format(len(torrents), '' if len(torrents)==1 else 's'),title="{} transfer{} matching '`{}`' will be modified".format(len(torrents), '' if len(torrents)==1 else 's', content)) + embeds[-1].set_footer(text=torListLegend+"\n• Options:\n"+opStr) + embeds[-1].timestamp = datetime.datetime.now(tz=pytz.timezone('America/Denver')) + else: + embed=discord.Embed(title="Modify transfers",color=0xb51a00) + embed.set_author(name="No matching transfers found!", icon_url=LOGO_URL) + embeds = [embed] + else: + ops = ["pauseall","resumeall"] + opNames = ["pause all","resume all"] + opEmoji = ['⏸','▶️'] + opStr = "⏸ pause or ▶️ resume all" + embed=discord.Embed(title="React to choose modification",color=0xb51a00) + embed.set_author(name="All transfers will be affected!", icon_url=LOGO_URL) + embed.set_footer(text=opStr) + embed.timestamp = datetime.datetime.now(tz=pytz.timezone('America/Denver')) + embeds = [embed] + msgs = [await context.message.channel.send(embed=e) for e in embeds] + + if not allOnly and len(torrents) == 0: + return + + for i in opEmoji: + await msgs[-1].add_reaction(i) + + def check(reaction, user): + return user == context.message.author and str(reaction.emoji) in opEmoji + + try: + reaction, user = await client.wait_for('reaction_add', timeout=60.0, check=check) + except asyncio.TimeoutError: + return + else: + cmds = {i:j for i,j in zip(opEmoji,ops)} + cmd = cmds[str(reaction.emoji)] + + doContinue = True + if "remove" in cmds[str(reaction.emoji)]: + embed=discord.Embed(title="Are you sure you wish to remove{} {} transfer{}?".format(' and DELETE' if 'delete' in cmds[str(reaction.emoji)] else '', len(torrents), '' if len(torrents)==1 else 's'),description="**This action is irreversible!**",color=0xb51a00) + embed.set_footer(text="react ✅ to continue or ❌ to cancel") + msg = await context.message.channel.send(embed=embed) + + for i in ['✅','❌']: + await msg.add_reaction(i) + + def check1(reaction, user): + return user == context.message.author and str(reaction.emoji) in ['✅','❌'] + try: + reaction, user = await client.wait_for('reaction_add', timeout=60.0, check=check1) + except asyncio.TimeoutError: + doContinue = False + else: + doContinue = str(reaction.emoji) == '✅' + if doContinue: + if "pause" in cmd: + stop_torrents(torrents) + elif "resume" in cmd: + resume_torrents(torrents) + else: + remove_torrents(torrents,delete_files="delete" in cmd) + + ops = ["pause","resume","remove","removedelete","pauseall","resumeall"] + opNames = ["paused","resumed","removed","removed and deleted","paused","resumed"] + opEmoji = ["⏸","▶️","❌","🗑","⏸","▶️"] + ops = {i:j for i,j in zip(ops,opNames)} + opEmoji = {i:j for i,j in zip(ops,opEmoji)} + await context.message.channel.send("{} Transfer{} {}".format(str(reaction.emoji),'s' if allOnly or len(torrents) > 1 else '', ops[cmd])) + else: + await context.message.channel.send("❌ Cancelled!") +@client.command(name='m', pass_context=True) +async def m(context, *, content=""): + await modify(context, content=content) + +client.remove_command('help') + +@client.command(name='help', description='Help HUD.', brief='HELPOOOO!!!', pass_context=True) +async def help(context, *, content=""): + if await CommandPrecheck(context): + if content != "": + if content in ["l","list"]: + embed = discord.Embed(title='List transfers', color=0xb51a00) + embed.set_author(name="List current transfers with sorting, filtering, and search options", icon_url=LOGO_URL) + embed.add_field(name="Usage", value='`{0}list [--filter FILTER] [--sort SORT] [NAME]`'.format(BOT_PREFIX), inline=False) + embed.add_field(name="Filtering", value='`--filter FILTER` or `-f FILTER`\n`FILTER` is one of `{}`'.format(str(filter_names)), inline=False) + embed.add_field(name="Sorting", value='`--sort SORT` or `-s SORT`\n`SORT` is one of `{}`'.format(str(sort_names)), inline=False) + embed.add_field(name="Searching by name", value='`NAME` is a regular expression used to search transfer names (no enclosing quotes; may contain spaces)', inline=False) + embed.add_field(name="Examples", value="*List all transfers:* `{0}list`\n*Search using phrase 'ubuntu':* `{0}l ubuntu`\n*List downloading transfers:* `{0}l -f downloading`\n*Sort transfers by age:* `{0}list --sort age`".format(BOT_PREFIX), inline=False) + await context.message.channel.send(embed=embed) + elif content in ["a","add"]: + embed = discord.Embed(title='Add transfer', description="If multiple torrents are added, separate them by spaces", color=0xb51a00) + embed.set_author(name="Add one or more specified torrents by magnet link or url to torrent file", icon_url=LOGO_URL) + embed.add_field(name="Usage", value='`{0}add TORRENT_FILE_URL_OR_MAGNET_LINK ...`\n`{0}a TORRENT_FILE_URL_OR_MAGNET_LINK ...`'.format(BOT_PREFIX), inline=False) + embed.add_field(name="Examples", value="*Add download of ubuntu OS:* `{0}add https://releases.ubuntu.com/20.04/ubuntu-20.04.1-desktop-amd64.iso.torrent`".format(BOT_PREFIX), inline=False) + await context.message.channel.send(embed=embed) + elif content in ["m","modify"]: + embed = discord.Embed(title='Modify existing transfer(s)', color=0xb51a00) + embed.set_author(name="Pause, resume, remove, or remove and delete specified transfer(s)", icon_url=LOGO_URL) + embed.add_field(name="Usage", value='`{0}modify [LIST_OPTIONS] [TORRENT_ID_SPECIFIER]`'.format(BOT_PREFIX), inline=False) + embed.add_field(name="Pause or resume ALL transfers", value="Simply run `{0}modify` to pause or resume all existing transfers".format(BOT_PREFIX), inline=False) + embed.add_field(name="By list options", value='`LIST_OPTIONS` is a valid set of options to the `{0}list` command (see `{0}help list` for details)'.format(BOT_PREFIX), inline=False) + embed.add_field(name="By ID specifier", value='`TORRENT_ID_SPECIFIER` is a valid transfer ID specifier—*e.g.* `1,3-5,9` to specify transfers 1, 3, 4, 5, and 9\n*Transfer IDs are the left-most number in the list of transfers (use* `{0}list` *to print full list)*'.format(BOT_PREFIX), inline=False) + embed.add_field(name="Examples", value="`{0}modify`\n`{0}m seinfeld`\n`{0}m 23,34,36-42`\n`{0}m --filter downloading seinfeld`".format(BOT_PREFIX), inline=False) + await context.message.channel.send(embed=embed) + else: + embed = discord.Embed(title='List of commands:', color=0xb51a00) + embed.set_author(name='Transmission Bot: Manage torrent file transfers', icon_url=LOGO_URL) + embed.add_field(name="Print summary of transfers", value="*print summary from all transfers, with followup options to list transfers*\n*ex.* `{0}summary` or `{0}s`".format(BOT_PREFIX), inline=False) + embed.add_field(name="List torrent transfers", value="*list current transfers with sorting, filtering, and search options*\n*ex.* `{0}list [OPTIONS]` or `{0}l [OPTIONS]`".format(BOT_PREFIX), inline=False) + embed.add_field(name="Add new torrent transfers", value="*add one or more specified torrents by magnet link or url to torrent file*\n*ex.* `{0}add TORRENT ...` or `{0}a TORRENT ...`".format(BOT_PREFIX), inline=False) + embed.add_field(name="Modify existing transfers", value="*pause, resume, remove, or remove and delete specified transfers*\n*ex.* `{0}modify [TORRENT]` or `{0}m [TORRENT]`".format(BOT_PREFIX), inline=False) + embed.add_field(name='Help - Gives this menu', value='*with optional details of specified command*\n*ex.* `{0}help` or `{0}help COMMAND`'.format(BOT_PREFIX), inline=False) + + await context.message.channel.send(embed=embed) + +@client.event +async def on_command_error(context, error): + if isinstance(error, commands.CommandOnCooldown): + await context.message.delete() + embed = discord.Embed(title="Error!", description='This command is on a {:.2f}s cooldown'.format(error.retry_after), color=0xb51a00) + message = await context.message.channel.send(embed=embed) + await asyncio.sleep(5) + await message.delete() + elif isinstance(error, commands.CommandNotFound): + await context.message.delete() + embed = discord.Embed(title="Error!", description="I don't know that command!", color=0xb51a00) + message = await context.message.channel.send(embed=embed) + await asyncio.sleep(2) + await help(context) + raise error + +client.run(TOKEN)