performance improvements

* added checks for whether an added torrent uses a private tracker. If so, the command message that added the torrent(s) is deleted (added corresponding configuration option), and a message is printed to the user to remind them to check the private tracker rules regarding sharing of torrent files
* `t/add` output to use embeds
* better error handling and logging
* added `channel.typing()` where appropriate so users know the bot is thinking
* added configuration options for overriding private torrent removal protection for the user that added the transfer
This commit is contained in:
Tim Wilson 2020-09-25 11:39:34 -06:00 committed by GitHub
parent 5e8f0b39cb
commit a67fd9f9fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

159
bot.py
View File

@ -50,7 +50,9 @@ CONFIG = {
"blacklist_user_ids": [], # discord users disallowed to use bot "blacklist_user_ids": [], # discord users disallowed to use bot
"owner_user_ids": [], # discord users given full access "owner_user_ids": [], # discord users given full access
"delete_command_messages": False, # delete command messages from users "delete_command_messages": False, # delete command messages from users
"delete_command_message_private_torrent": True, # deletes command message if that message contains one or more torrent files that use a private tracker
"private_transfers_protected": True, # prevent transfers on private trackers from being removed "private_transfers_protected": True, # prevent transfers on private trackers from being removed
"private_transfer_protection_added_user_override": True, # if true, the user that added a private transfer can remove it regardless of 'private_transfers_protected'
"whitelist_user_can_remove": True, # if true, whitelisted users can remove any transfer "whitelist_user_can_remove": True, # if true, whitelisted users can remove any transfer
"whitelist_user_can_delete": True, # if true, whitelisted users can remove and delete any transfer "whitelist_user_can_delete": True, # if true, whitelisted users can remove and delete any transfer
"whitelist_added_user_remove_delete_override": True, # if true, override both 'whitelist_user_can_remove' and 'whitelist_user_can_delete' allowing whitelisted users to remove and delete transfers they added "whitelist_added_user_remove_delete_override": True, # if true, override both 'whitelist_user_can_remove' and 'whitelist_user_can_delete' allowing whitelisted users to remove and delete transfers they added
@ -119,6 +121,7 @@ logger.setLevel(logging.INFO) # set according to table below. values LESS than t
""" """
Level Numeric value Level Numeric value
__________________________
CRITICAL 50 CRITICAL 50
ERROR 40 ERROR 40
WARNING 30 WARNING 30
@ -662,10 +665,14 @@ def verify_torrents(torrents=[]):
logger.info("Verified: {} {}\n\tDry run: {}".format(torrent.name, torrent.hashString, CONFIG['dryrun'])) logger.info("Verified: {} {}\n\tDry run: {}".format(torrent.name, torrent.hashString, CONFIG['dryrun']))
def add_torrent(torStr): def add_torrent(torStr):
tor = None torrent = None
if not CONFIG['dryrun']:
if torStr != "": if torStr != "":
tor = TSCLIENT.add_torrent(torStr) torrent = TSCLIENT.add_torrent(torStr)
return tor logger.info("Added: {} {}\n\tDry run: {}".format(torrent.name, torrent.hashString, CONFIG['dryrun']))
else:
logger.info("Added: {} \n\tDry run: {}".format(torStr if len(torStr) < 300 else torStr[:200], CONFIG['dryrun']))
return torrent
# Begin discord bot functions, adapted from https://github.com/kkrypt0nn/Python-Discord-Bot-Template # Begin discord bot functions, adapted from https://github.com/kkrypt0nn/Python-Discord-Bot-Template
@ -813,7 +820,7 @@ def prepare_notifications(changedTransfers, states=CONFIG['notification_states']
embeds.append(discord.Embed(title="")) embeds.append(discord.Embed(title=""))
embeds[-1].timestamp = ts embeds[-1].timestamp = ts
if len(nameStr) + len(valStr) + len(v) > 1000: if len(nameStr) + len(valStr) + len(v) > 1000:
embeds[-1].add_field(name=nStr, value=valStr, inline=False) embeds[-1].add_field(name=nameStr, value=valStr, inline=False)
nameStr = "" nameStr = ""
valStr = "" valStr = ""
else: else:
@ -956,7 +963,7 @@ async def loop_notifications():
@client.event @client.event
async def on_ready(): async def on_ready():
global TSCLIENT_CONFIG, CONFIG global TSCLIENT_CONFIG, CONFIG
unlock()
TSCLIENT_CONFIG = CONFIG['tsclient'] TSCLIENT_CONFIG = CONFIG['tsclient']
if not CONFIG: # load from config file if not CONFIG: # load from config file
CONFIG = load_json(path=CONFIG_JSON) CONFIG = load_json(path=CONFIG_JSON)
@ -1116,8 +1123,17 @@ def message_has_torrent_file(message):
return True return True
return False return False
def commaListToParagraphForm(l):
outStr = ''
if len(l) > 0:
outStr += ('' if len(l <= 2) else ', ').join(l[:-1])
outStr += ('{} and '.format('' if len(l) <= 2 else ',') if len(l) > 1 else '') + str(l[-1])
return outStr
async def add(message, content = ""): async def add(message, content = ""):
if await CommandPrecheck(message): if await CommandPrecheck(message):
async with message.channel.typing():
torFileList = [] torFileList = []
for f in message.attachments: for f in message.attachments:
if len(f.filename) > 8 and f.filename[-8:].lower() == ".torrent": if len(f.filename) > 8 and f.filename[-8:].lower() == ".torrent":
@ -1135,20 +1151,25 @@ async def add(message, content = ""):
pass pass
torStr = [] torStr = []
for t in torFileList: torIDs = []
for i,t in enumerate(torFileList):
# await message.channel.send('Adding torrent from file: {}\n Please wait...'.format(t["name"])) # await message.channel.send('Adding torrent from file: {}\n Please wait...'.format(t["name"]))
try: try:
tor = add_torrent(t["content"]) tor = add_torrent(t["content"])
if tor: if tor:
logger.info("User {} ({}) added torrent from file: {} ({})".format(message.author.name, message.author.id, tor.name, tor.hashString))
lock() lock()
TORRENT_ADDED_USERS[tor.hashString] = message.author.id TORRENT_ADDED_USERS[tor.hashString] = message.author.id
unlock() unlock()
logger.info("User {} ({}) added torrent from file {}: {} ({})".format(message.author.name, message.author.id, t["name"], tor.name, tor.hashString))
# if tor.isPrivate:
# privateTransfers.append(len(privateTransfers))
logger.debug("Added to TORRENT_ADDED_USERS") logger.debug("Added to TORRENT_ADDED_USERS")
except: torStr.append("💽 {}".format(tor.name))
await message.channel.send('‼️ Error communicating with Transmission ‼️') torIDs.append(tor.id)
return elif CONFIG['dryrun']:
torStr.append("📄 {}".format(tor.name)) torStr.append("💽added file dryrun: {}".format(t["name"]))
except Exception as e:
logger.warning("Exception when adding torrent from file: {}".format(e))
for t in content.strip().split(" "): for t in content.strip().split(" "):
if len(t) > 5: if len(t) > 5:
@ -1156,24 +1177,72 @@ async def add(message, content = ""):
try: try:
tor = add_torrent(t) tor = add_torrent(t)
if tor: if tor:
logger.info("User {} ({}) added torrent from URL: {} ({})".format(message.author.name, message.author.id, tor.name, tor.hashString))
lock() lock()
TORRENT_ADDED_USERS[tor.hashString] = message.author.id TORRENT_ADDED_USERS[tor.hashString] = message.author.id
unlock() unlock()
logger.info("User {} ({}) added torrent from URL: {} ({})".format(message.author.name, message.author.id, tor.name, tor.hashString))
# if tor.isPrivate:
# privateTransfers.append(len(privateTransfers))
logger.debug("Added to TORRENT_ADDED_USERS") logger.debug("Added to TORRENT_ADDED_USERS")
except:
await message.channel.send('‼️ Error communicating with Transmission ‼️')
return
torStr.append("🧲 {}".format(tor.name)) torStr.append("🧲 {}".format(tor.name))
torIDs.append(tor.id)
except Exception as e:
logger.warning("Exception when adding torrent from URL: {}".format(e))
if len(torStr) > 0: if len(torStr) > 0:
await message.channel.send('🟢 Added torrent{}:\n{}'.format("s" if len(torStr) > 1 else "", '\n'.join(torStr))) embeds = []
if len('\n'.join(torStr)) > 2000:
embeds.append(discord.Embed(title='🟢 Added torrents'))
descStr = torStr[0]
for t in torStr[1:]:
if len(descStr) + len(t) < 2000:
descStr += '\n{}'.format(t)
else:
embeds[-1].description = descStr
embeds.append(discord.Embed(title='🟢 Added torrents'))
descStr = t
else:
embeds = [discord.Embed(title='🟢 Added torrent{}'.format("s" if len(torStr) > 1 else ""), description='\n'.join(torStr), color=0xb51a00)]
privateTransfers = []
if not CONFIG['dryrun']:
logger.debug("Checking for private transfers amidst the {} new torrents".format(len(torStr)))
privateCheckSuccess = False
for i in range(5):
try:
newTorrents = TSCLIENT.get_torrents_by(id_list=torIDs)
logger.debug("Fetched {} transfers from transmission corresponding to the {} transfer IDs recorded".format(len(newTorrents),len(torIDs)))
for tor in newTorrents:
logger.debug("Checking private status of added transfer {}: {}".format(i+1, tor.name))
if tor.isPrivate:
privateTransfers.append(torIDs.index(tor.id))
logger.debug("Transfer is private")
privateCheckSuccess = True
logger.debug("Successfully checked for private tranfers: {} found".format(len(privateTransfers)))
break
except AttributeError as e:
logger.debug("Attribute error when checking for private status of added torrent(s): {}".format(e))
except Exception as e:
logger.warning("Exception when checking for private status of added torrent(s): {}".format(e))
asyncio.sleep(0.2)
if len(privateTransfers) > 0 or CONFIG['dryrun']:
if len(privateTransfers) > 0 and CONFIG['delete_command_message_private_torrent']:
try:
await message.delete()
except Exception as e:
logger.warning("Exception when removing command message used to add private torrent(s): {}".format(e))
embeds[-1].set_footer(text="\n🔐  One or more added torrents are using a private tracker, which may prohibit running the same transfer from multiple locations. Ensure that you're not breaking any private tracker rules.{}".format('' if CONFIG['dryrun'] else "\n(I erased the command message to prevent any unintentional sharing of torrent files)"))
for e in embeds:
await message.channel.send(embed=e)
else: else:
await message.channel.send('🚫 No torrents added!') await message.channel.send('🚫 No torrents added!')
@client.command(name='add', aliases=['a'], pass_context=True) @client.command(name='add', aliases=['a'], pass_context=True)
async def add_cmd(context, *, content = ""): async def add_cmd(context, *, content = ""):
try:
await add(context.message, content=content) await add(context.message, content=content)
except Exception as e:
logger.warning("Exception when adding torrent(s): {}".format(e))
# def torInfo(t): # def torInfo(t):
# states = ('downloading', 'seeding', 'stopped', 'finished','all') # states = ('downloading', 'seeding', 'stopped', 'finished','all')
@ -1289,6 +1358,7 @@ def torSummary(torrents, repeat_msg_key=None, show_repeat=True):
async def summary(message, content="", repeat_msg_key=None): async def summary(message, content="", repeat_msg_key=None):
global REPEAT_MSGS global REPEAT_MSGS
if await CommandPrecheck(message): if await CommandPrecheck(message):
async with message.channel.typing():
if not repeat_msg_key: if not repeat_msg_key:
if len(REPEAT_MSGS) == 0: if len(REPEAT_MSGS) == 0:
reload_client() reload_client()
@ -1453,7 +1523,10 @@ async def summary(message, content="", repeat_msg_key=None):
@client.command(name='summary',aliases=['s'], pass_context=True) @client.command(name='summary',aliases=['s'], pass_context=True)
async def summary_cmd(context, *, content="", repeat_msg_key=None): async def summary_cmd(context, *, content="", repeat_msg_key=None):
try:
await summary(context.message, content, repeat_msg_key=repeat_msg_key) await summary(context.message, content, repeat_msg_key=repeat_msg_key)
except Exception as e:
logger.warning("Exception in t/summary: {}".format(e))
def strListToList(strList): def strListToList(strList):
if not re.match('^[0-9\,\-]+$', strList): if not re.match('^[0-9\,\-]+$', strList):
@ -1647,6 +1720,7 @@ async def repeat_command(command, message, content="", msg_list=[]):
async def list_transfers(message, content="", repeat_msg_key=None): async def list_transfers(message, content="", repeat_msg_key=None):
global REPEAT_MSGS global REPEAT_MSGS
if await CommandPrecheck(message): if await CommandPrecheck(message):
async with message.channel.typing():
id_list = strListToList(content) id_list = strListToList(content)
filter_by = None filter_by = None
sort_by = None sort_by = None
@ -1801,10 +1875,14 @@ async def list_transfers(message, content="", repeat_msg_key=None):
@client.command(name='list', aliases=['l'], pass_context=True) @client.command(name='list', aliases=['l'], pass_context=True)
async def list_transfers_cmd(context, *, content="", repeat_msg_key=None): async def list_transfers_cmd(context, *, content="", repeat_msg_key=None):
try:
await list_transfers(context.message, content=content, repeat_msg_key=repeat_msg_key) await list_transfers(context.message, content=content, repeat_msg_key=repeat_msg_key)
except Exception as e:
logger.warning("Exception in t/list: {}".format(e))
async def modify(message, content=""): async def modify(message, content=""):
if await CommandPrecheck(message): if await CommandPrecheck(message):
async with message.channel.typing():
allOnly = content.strip() == "" allOnly = content.strip() == ""
torrents = [] torrents = []
if not allOnly: if not allOnly:
@ -1894,8 +1972,16 @@ async def modify(message, content=""):
if CONFIG['private_transfers_protected']: if CONFIG['private_transfers_protected']:
removeTorrents = [t for t in torrents if not t.isPrivate] removeTorrents = [t for t in torrents if not t.isPrivate]
if len(removeTorrents) != len(torrents): if len(removeTorrents) != len(torrents):
if CONFIG['private_transfer_protection_added_user_override']:
oldTorrents = load_json(path=TORRENT_JSON)
removeTorrents = [t for t in torrents if not t.isPrivate or ((t.hashString in oldTorrents and oldTorrents[t.hashString]['added_user'] == message.author.id) or (t.hashString in TORRENT_ADDED_USERS and TORRENT_ADDED_USERS[t.hashString] == message.author.id))]
if len(removeTorrents) != len(torrents):
torrents = removeTorrents
footerPrepend = "(I'm not allowed to remove private transfers unless they were added by you, but those you added and the public ones)\n"
else:
torrents = removeTorrents torrents = removeTorrents
footerPrepend = "(I'm not allowed to remove private transfers, but I'll do the public ones)\n" footerPrepend = "(I'm not allowed to remove private transfers, but I'll do the public ones)\n"
if "delete" in cmds[str(reaction.emoji)] and not CONFIG['whitelist_user_can_delete'] and message.author.id not in CONFIG['owner_user_ids']: if "delete" in cmds[str(reaction.emoji)] and not CONFIG['whitelist_user_can_delete'] and message.author.id not in CONFIG['owner_user_ids']:
# user may not be allowed to perform this operation. Check if they added any transfers, and whether the added_user_override is enabled. # user may not be allowed to perform this operation. Check if they added any transfers, and whether the added_user_override is enabled.
if CONFIG['whitelist_added_user_remove_delete_override']: if CONFIG['whitelist_added_user_remove_delete_override']:
@ -1954,11 +2040,13 @@ async def modify(message, content=""):
else: else:
doContinue = str(reaction.emoji) == '' doContinue = str(reaction.emoji) == ''
if doContinue: if doContinue:
async with message.channel.typing():
await message.channel.send("{} Trying to {} transfer{}, please wait...".format(str(reaction.emoji), cmdName, 's' if allOnly or len(torrents) > 1 else '')) await message.channel.send("{} Trying to {} transfer{}, please wait...".format(str(reaction.emoji), cmdName, 's' if allOnly or len(torrents) > 1 else ''))
try:
if "pause" in cmd: if "pause" in cmd:
stop_torrents(torrents) stop_torrents(torrents)
elif "resume" in cmd: elif "resume" in cmd:
resume_torrents(torrents) resume_torrents(torrents, start_all=("all" in cmd))
elif "verify" in cmd: elif "verify" in cmd:
verify_torrents(torrents) verify_torrents(torrents)
else: else:
@ -1974,6 +2062,9 @@ async def modify(message, content=""):
if msg2 is not None: if msg2 is not None:
await message_clear_reactions(msg2, message) await message_clear_reactions(msg2, message)
return return
except Exception as e:
await message.channel.send("A problem occurred trying to modify transfer(s). You may need to try again... Sorry!".format(str(reaction.emoji), cmdName, 's' if allOnly or len(torrents) > 1 else ''))
logger.warning("Exception in t/modify running command '{}': {}".format(cmd,e))
else: else:
await message.channel.send("❌ Cancelled!") await message.channel.send("❌ Cancelled!")
await message_clear_reactions(msg, message) await message_clear_reactions(msg, message)
@ -2003,9 +2094,18 @@ async def modify(message, content=""):
doContinue = True doContinue = True
if "remove" in cmds[str(reaction.emoji)]: if "remove" in cmds[str(reaction.emoji)]:
footerPrepend = "" footerPrepend = ""
if CONFIG['private_transfers_protected']:
removeTorrents = [t for t in torrents if not t.isPrivate]
if CONFIG['private_transfers_protected']: if CONFIG['private_transfers_protected']:
removeTorrents = [t for t in torrents if not t.isPrivate] removeTorrents = [t for t in torrents if not t.isPrivate]
if len(removeTorrents) != len(torrents): if len(removeTorrents) != len(torrents):
if CONFIG['private_transfer_protection_added_user_override']:
oldTorrents = load_json(path=TORRENT_JSON)
removeTorrents = [t for t in torrents if not t.isPrivate or ((t.hashString in oldTorrents and oldTorrents[t.hashString]['added_user'] == message.author.id) or (t.hashString in TORRENT_ADDED_USERS and TORRENT_ADDED_USERS[t.hashString] == message.author.id))]
if len(removeTorrents) != len(torrents):
torrents = removeTorrents
footerPrepend = "(I'm not allowed to remove private transfers unless they were added by you, but those you added and the public ones)\n"
else:
torrents = removeTorrents torrents = removeTorrents
footerPrepend = "(I'm not allowed to remove private transfers, but I'll do the public ones)\n" footerPrepend = "(I'm not allowed to remove private transfers, but I'll do the public ones)\n"
if "delete" in cmds[str(reaction.emoji)] and not CONFIG['whitelist_user_can_delete'] and message.author.id not in CONFIG['owner_user_ids']: if "delete" in cmds[str(reaction.emoji)] and not CONFIG['whitelist_user_can_delete'] and message.author.id not in CONFIG['owner_user_ids']:
@ -2066,18 +2166,18 @@ async def modify(message, content=""):
else: else:
doContinue = str(reaction.emoji) == '' doContinue = str(reaction.emoji) == ''
if doContinue: if doContinue:
async with message.channel.typing():
await message.channel.send("{} Trying to {} transfer{}, please wait...".format(str(reaction.emoji), cmdName, 's' if allOnly or len(torrents) > 1 else '')) await message.channel.send("{} Trying to {} transfer{}, please wait...".format(str(reaction.emoji), cmdName, 's' if allOnly or len(torrents) > 1 else ''))
try:
if "pause" in cmd: if "pause" in cmd:
stop_torrents(torrents) stop_torrents(torrents)
elif "resume" in cmd: elif "resume" in cmd:
resume_torrents(torrents) resume_torrents(torrents, start_all=("all" in cmd))
elif "verify" in cmd: elif "verify" in cmd:
verify_torrents(torrents) verify_torrents(torrents)
else: else:
remove_torrents(torrents,delete_files="delete" in cmd) remove_torrents(torrents,delete_files="delete" in cmd)
ops = ["pause","resume","remove","removedelete","pauseall","resumeall","verify"] ops = ["pause","resume","remove","removedelete","pauseall","resumeall","verify"]
opNames = ["paused","resumed","removed","removed and deleted","paused","resumed","queued for verification"] opNames = ["paused","resumed","removed","removed and deleted","paused","resumed","queued for verification"]
opEmoji = ["","▶️","","🗑","","▶️","🔬"] opEmoji = ["","▶️","","🗑","","▶️","🔬"]
@ -2088,6 +2188,9 @@ async def modify(message, content=""):
if msg2 is not None: if msg2 is not None:
await message_clear_reactions(msg2, message) await message_clear_reactions(msg2, message)
return return
except Exception as e:
await message.channel.send("A problem occurred trying to modify transfer(s). You may need to try again... Sorry!".format(str(reaction.emoji), cmdName, 's' if allOnly or len(torrents) > 1 else ''))
logger.warning("Exception in t/modify running command '{}': {}".format(cmd,e))
else: else:
await message.channel.send("❌ Cancelled!") await message.channel.send("❌ Cancelled!")
await message_clear_reactions(msg, message) await message_clear_reactions(msg, message)
@ -2099,7 +2202,11 @@ async def modify(message, content=""):
@client.command(name='modify', aliases=['m'], pass_context=True) @client.command(name='modify', aliases=['m'], pass_context=True)
async def modify_cmd(context, *, content=""): async def modify_cmd(context, *, content=""):
try:
await modify(context.message, content=content) await modify(context.message, content=content)
except Exception as e:
logger.warning("Exception in t/modify: {}".format(e))
async def toggle_compact_out(message): async def toggle_compact_out(message):
global OUTPUT_MODE global OUTPUT_MODE
@ -2190,6 +2297,18 @@ async def toggle_notifications(message):
async def toggle_notifications_cmd(context): async def toggle_notifications_cmd(context):
await toggle_notifications(context.message) await toggle_notifications(context.message)
async def toggle_dryrun(message):
global CONFIG
CONFIG['dryrun'] = not CONFIG['dryrun']
await message.channel.send("Toggled dryrun to {}".format(CONFIG['dryrun']))
return
@client.command(name='dryrun', pass_context=True)
async def toggle_dryrun_cmd(context):
if CommandPrecheck(context.message, whitelist=CONFIG['owner_user_ids']):
await toggle_dryrun(context.message)
@client.event @client.event
async def on_message(message): async def on_message(message):
if message.author.id == client.user.id: if message.author.id == client.user.id: