bugfixes and features

I forgot most of them; will list in next commit
This commit is contained in:
Tim Wilson 2020-10-20 15:55:18 -06:00 committed by GitHub
parent 1aa9a1d3be
commit 097ff37405
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

395
bot.py
View File

@ -18,6 +18,7 @@ from platform import python_version
import os import os
import sys import sys
from os.path import expanduser, join, exists, isdir, isfile from os.path import expanduser, join, exists, isdir, isfile
import shutil
import re import re
import datetime import datetime
import pytz import pytz
@ -25,6 +26,7 @@ import platform
import secrets import secrets
import transmissionrpc import transmissionrpc
import logging import logging
from logging import handlers
import base64 import base64
import random import random
from enum import Enum from enum import Enum
@ -39,91 +41,90 @@ Bot configuration:
2. run the bot, which will make a config.json file containing this configuration info 2. run the bot, which will make a config.json file containing this configuration info
3. comment or remove the configuration below, as the config.json will be used instead (this also makes updating to new versions easier) 3. comment or remove the configuration below, as the config.json will be used instead (this also makes updating to new versions easier)
""" """
CONFIG = { # CONFIG = {
"tsclient": { # information for transmission remote web gui # "tsclient": { # information for transmission remote web gui
'host': "192.168.0.2", # 'host': "192.168.0.2",
'port': 9091, # 'port': 9091,
'user': "USERNAME", # 'user': "USERNAME",
'password': "PASSWORD" # 'password': "PASSWORD"
}, # },
"whitelist_user_ids": [], # discord users allowed to use bot # "whitelist_user_ids": [], # discord users allowed to use bot
"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
"DM_compact_output_user_ids": [], # DO NOT EDIT MANUALLY! if a user id is in this list, that user will get compact output via DM (changed by t/compact command) # "DM_compact_output_user_ids": [], # DO NOT EDIT MANUALLY! if a user id is in this list, that user will get compact output via DM (changed by t/compact command)
"reaction_wait_timeout": 7200, # seconds the bot should wait for a reaction to be clicked by a user # "reaction_wait_timeout": 7200, # seconds the bot should wait for a reaction to be clicked by a user
"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 # "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' # "private_transfer_protection_added_user_override": True, # if true, the user that added a private transfer can remove it regardless of 'private_transfers_protected'
"private_transfer_protection_bot_owner_override": False, # similar to 'private_transfer_protection_added_user_override', but allows bot owners to delete private transfers # "private_transfer_protection_bot_owner_override": False, # similar to 'private_transfer_protection_added_user_override', but allows bot owners to delete private transfers
"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
"bot_prefix": "t/", # bot command prefix # "bot_prefix": "t/", # bot command prefix
"bot_token": "BOT_TOKEN", # bot token # "bot_token": "BOT_TOKEN", # bot token
"dryrun": False, # if true, no changes are actually applied to transfers # "dryrun": False, # if true, no changes are actually applied to transfers
"listen_channel_ids": [], # channels in which to listen for commands # "listen_channel_ids": [], # channels in which to listen for commands
"listen_all_channels": False, # if true, listen for commands in all text channels # "listen_all_channels": False, # if true, listen for commands in all text channels
"listen_DMs": True, # listen for commands via DM to the bot # "listen_DMs": True, # listen for commands via DM to the bot
"logo_url": "https://iyanovich.files.wordpress.com/2009/04/transmission-logo.png", # URL to logo that appears in some output # "logo_url": "https://iyanovich.files.wordpress.com/2009/04/transmission-logo.png", # URL to logo that appears in some output
"notification_channel_id": 'NOTIFICATION_CHANNEL_ID', # channel to which in-channel notificatations will be posted # "notification_channel_id": 0, # id of channel to which in-channel notificatations will be posted
"notification_enabled": True, # if False, in-channel and DM notifications are disabled # "notification_enabled": True, # if False, in-channel and DM notifications are disabled
"notification_enabled_in_channel": True, # if False, in-channel notifications are disabled, but DM notifications will still work # "notification_enabled_in_channel": True, # if False, in-channel notifications are disabled, but DM notifications will still work
"notification_freq": 300, # number of seconds between checking transfers and posting notifications # "notification_freq": 300, # number of seconds between checking transfers and posting notifications
"notification_reaction_check_factor": 2, # determines how long DM notification subscription reactions will be monitored on in-channel and DM notifications; they're monitored for (notification_reaction_check_factor X notification_freq) seconds # "notification_DM_opt_out_user_ids": [], # DON'T MODIFY (used by bot to record users that have opted out of receiving DM notifications)
"notification_DM_opt_out_user_ids": [], # DON'T MODIFY (used by bot to record users that have opted out of receiving DM notifications) # "notification_states":{ # determines the types of transfer state changes that are reported in notifications...
"notification_states":{ # determines the types of transfer state changes that are reported in notifications... # "in_channel": # ...for in-channel notifications, (this is the full list of potential state changes)
"in_channel": # ...for in-channel notifications, (this is the full list of potential state changes) # [
[ # "new",
"new", # "removed",
"removed", # "error",
"error", # "downloaded",
"downloaded", # "stalled",
"stalled", # "unstalled",
"unstalled", # "finished",
"finished", # "stopped",
"stopped", # "started"
"started" # ],
], # "notified_users": # ...DM notifications for users that opted in to DM notifications for transfer(s)
"notified_users": # ...DM notifications for users that opted in to DM notifications for transfer(s) # [
[ # "removed",
"removed", # "error",
"error", # "downloaded",
"downloaded", # "stalled",
"stalled", # "unstalled",
"unstalled", # "finished",
"finished", # "stopped",
"stopped", # "started"
"started" # ],
], # "added_user":# ...and DM notifications to users that added transfers
"added_user":# ...and DM notifications to users that added transfers # [
[ # "removed",
"removed", # "error",
"error", # "downloaded",
"downloaded", # "stalled",
"stalled", # "unstalled",
"unstalled", # "finished",
"finished", # "stopped",
"stopped", # "started"
"started" # ]
] # },
}, # "repeat_cancel_verbose": True, # if true, print message when auto-update is canceled for a message
"repeat_cancel_verbose": True, # if true, print message when auto-update is canceled for a message # "repeat_freq": 2, # number of seconds between updating an auto-update message
"repeat_freq": 2, # number of seconds between updating an auto-update message # "repeat_freq_DM_by_user_ids": {}, # use t/repeatfreq to set autoupdate frequency over DM on a per-user basis
"repeat_freq_DM_by_user_ids": {}, # use t/repeatfreq to set autoupdate frequency over DM on a per-user basis # "repeat_timeout_DM_by_user_ids": {}, # same but for autoupdate timeout
"repeat_timeout_DM_by_user_ids": {}, # same but for autoupdate timeout # "repeat_timeout": 3600, # number of seconds before an auto-update message times out
"repeat_timeout": 3600, # number of seconds before an auto-update message times out # "repeat_timeout_verbose": True, # if true, print message when auto-update message times out and stops updating
"repeat_timeout_verbose": True, # if true, print message when auto-update message times out # "summary_num_top_ratio": 5 # number of top seed-ratio transfers to show at the bottom of the summary output
"summary_num_top_ratio": 5 # number of top seed-ratio transfers to show at the bottom of the summary output # }
}
TSCLIENT_CONFIG = None TSCLIENT_CONFIG = None
# logging.basicConfig(format='%(asctime)s %(message)s',filename=join(expanduser("~"),'ts_scripts.log')) # logging.basicConfig(format='%(asctime)s %(message)s',filename=join(expanduser("~"),'ts_scripts.log'))
logName = join(CONFIG_DIR,'transmissionbot.log')
logging.basicConfig(format='%(asctime)s %(message)s',filename=join(CONFIG_DIR,'transmissionbot.log')) logging.basicConfig(format='%(asctime)s %(message)s',filename=join(CONFIG_DIR,'transmissionbot.log'))
logger = logging.getLogger('transmission_bot') logger = logging.getLogger('transmission_bot')
logger.setLevel(logging.INFO) # set according to table below. values LESS than the set value will be ignored logger.setLevel(logging.DEBUG) # set according to table below. Events with values LESS than the set value will not be logged
""" """
Level Numeric value Level Numeric value
__________________________ __________________________
@ -135,6 +136,14 @@ DEBUG 10
NOTSET 0 NOTSET 0
""" """
fh = logging.handlers.RotatingFileHandler(logName, backupCount=5)
if os.path.isfile(logName): # log already exists, roll over!
fh.doRollover()
fmt = logging.Formatter('%(asctime)s [%(threadName)14s:%(filename)8s:%(lineno)5s - %(funcName)20s()] %(levelname)8s: %(message)s')
fh.setFormatter(fmt)
logger.addHandler(fh)
# END USER CONFIGURATION # END USER CONFIGURATION
# for storing config and transfer list # for storing config and transfer list
@ -208,9 +217,23 @@ def generate_json(json_data=None, path=None, overwrite=False):
return False return False
if not exists(os.path.dirname(path)): if not exists(os.path.dirname(path)):
mkdir_p(os.path.dirname(path)) mkdir_p(os.path.dirname(path))
with open(path, 'w') as cf: try:
lock() lock()
cf.write(dumps(json_data, sort_keys=True, indent=4, separators=(',', ': '))) if exists(path):
# first backup the existing file
shutil.copy2(path,"{}.bak".format(path))
try:
with open(path, 'w') as cf:
cf.write(dumps(json_data, sort_keys=True, indent=4, separators=(',', ': ')))
except Exception as e:
logger.error("Exception when writing JSON file {}, reverting to backup: {}".format(path,e))
shutil.move("{}.bak".format(path), path)
else:
with open(path, 'w') as cf:
cf.write(dumps(json_data, sort_keys=True, indent=4, separators=(',', ': ')))
except Exception as e:
logger.fatal("Exception when writing JSON file: {}".format(e))
finally:
unlock() unlock()
return True return True
@ -407,10 +430,11 @@ def make_client():
:param args: Optional CLI args passed in. :param args: Optional CLI args passed in.
:return: :return:
""" """
logger.debug("Making new TSClient")
global MAKE_CLIENT_FAILED global MAKE_CLIENT_FAILED
tsclient = None tsclient = None
lock()
try: try:
lock()
tsclient = TSClient( tsclient = TSClient(
TSCLIENT_CONFIG['host'], TSCLIENT_CONFIG['host'],
port=TSCLIENT_CONFIG['port'], port=TSCLIENT_CONFIG['port'],
@ -418,6 +442,7 @@ def make_client():
password=TSCLIENT_CONFIG['password'] password=TSCLIENT_CONFIG['password']
) )
MAKE_CLIENT_FAILED = False MAKE_CLIENT_FAILED = False
logger.debug("Made new TSClient")
except Exception as e: except Exception as e:
logger.error("Failed to make TS client: {}".format(e)) logger.error("Failed to make TS client: {}".format(e))
MAKE_CLIENT_FAILED = True MAKE_CLIENT_FAILED = True
@ -705,18 +730,22 @@ def check_for_transfer_changes():
# 'progress':t.progress # 'progress':t.progress
# } # }
# } # }
lock()
curTorrents = {t.hashString:{ try:
'name':t.name, lock()
'error':t.error, curTorrents = {t.hashString:{
'errorString':t.errorString, 'name':t.name,
'status':t.status, 'error':t.error,
'isStalled':t.isStalled, 'errorString':t.errorString,
'progress':t.progress, 'status':t.status,
'added_user':None if t.hashString not in TORRENT_ADDED_USERS else TORRENT_ADDED_USERS[t.hashString], 'isStalled':t.isStalled,
'notified_users':[] if t.hashString not in TORRENT_NOTIFIED_USERS else TORRENT_NOTIFIED_USERS[t.hashString], 'progress':t.progress,
'optout_users':[] if t.hashString not in TORRENT_OPTOUT_USERS else TORRENT_OPTOUT_USERS[t.hashString] 'added_user':None if t.hashString not in TORRENT_ADDED_USERS else TORRENT_ADDED_USERS[t.hashString],
} for t in torrents} 'notified_users':[] if t.hashString not in TORRENT_NOTIFIED_USERS else TORRENT_NOTIFIED_USERS[t.hashString],
'optout_users':[] if t.hashString not in TORRENT_OPTOUT_USERS else TORRENT_OPTOUT_USERS[t.hashString]
} for t in torrents}
finally:
unlock()
if exists(TORRENT_JSON): if exists(TORRENT_JSON):
oldTorrents = load_json(path=TORRENT_JSON) oldTorrents = load_json(path=TORRENT_JSON)
if len(curTorrents) > 0 and len(oldTorrents) > 0 and len(next(iter(curTorrents.values()))) != len(next(iter(oldTorrents.values()))): if len(curTorrents) > 0 and len(oldTorrents) > 0 and len(next(iter(curTorrents.values()))) != len(next(iter(oldTorrents.values()))):
@ -740,20 +769,25 @@ def check_for_transfer_changes():
# logger.debug("Removing {} ({}) from 'optout_users' for {} ({})".format(user.name, u, t['name'], h)) # logger.debug("Removing {} ({}) from 'optout_users' for {} ({})".format(user.name, u, t['name'], h))
# curTorrents[h]['optout_users'].remove(u) # curTorrents[h]['optout_users'].remove(u)
# logger.debug("new 'optout_users' for {} ({}): {}".format(t['name'], h, str(curTorrents[h]['optout_users']))) # logger.debug("new 'optout_users' for {} ({}): {}".format(t['name'], h, str(curTorrents[h]['optout_users'])))
try:
TORRENT_NOTIFIED_USERS = {} lock()
TORRENT_ADDED_USERS = {} TORRENT_NOTIFIED_USERS = {}
TORRENT_OPTOUT_USERS = {} TORRENT_ADDED_USERS = {}
unlock() TORRENT_OPTOUT_USERS = {}
finally:
unlock()
generate_json(json_data=curTorrents, path=TORRENT_JSON, overwrite=True) generate_json(json_data=curTorrents, path=TORRENT_JSON, overwrite=True)
else: else:
TORRENT_NOTIFIED_USERS = {} try:
TORRENT_ADDED_USERS = {} lock()
TORRENT_OPTOUT_USERS = {} TORRENT_NOTIFIED_USERS = {}
unlock() TORRENT_ADDED_USERS = {}
TORRENT_OPTOUT_USERS = {}
finally:
unlock()
generate_json(json_data=curTorrents, path=TORRENT_JSON, overwrite=True) generate_json(json_data=curTorrents, path=TORRENT_JSON, overwrite=True)
return None return None
# print("before checking") # print("before checking")
# get lists of different transfer changes # get lists of different transfer changes
@ -772,7 +806,7 @@ def check_for_transfer_changes():
# print("done checking for changes") # print("done checking for changes")
# DEBUG grab a few random transfers for each type, vary the number to see if multiple embeds works # DEBUG grab a few random transfers for each type, vary the number to see if multiple embeds works
# print(str(oldTorrents)) # print(str(oldTorrents))
# numTransfers = 3 # numTransfers = 3
@ -785,7 +819,7 @@ def check_for_transfer_changes():
# newTransfers = {h:t for h,t in random.sample(curTorrents.items(),numTransfers)} # newTransfers = {h:t for h,t in random.sample(curTorrents.items(),numTransfers)}
# print(str(errorTransfers)) # print(str(errorTransfers))
# print("done applying debug changes") # print("done applying debug changes")
return { return {
'new':{'name':"🟢 {0} new transfer{1}", 'data':newTransfers}, 'new':{'name':"🟢 {0} new transfer{1}", 'data':newTransfers},
'removed':{'name':"❌ {0} removed transfer{1}", 'data':removedTransfers}, 'removed':{'name':"❌ {0} removed transfer{1}", 'data':removedTransfers},
@ -798,7 +832,7 @@ def check_for_transfer_changes():
'started':{'name':"▶️ {0} transfer{1} resumed", 'data':startedTransfers} 'started':{'name':"▶️ {0} transfer{1} resumed", 'data':startedTransfers}
} }
def prepare_notifications(changedTransfers, states=CONFIG['notification_states']['in_channel']): def prepare_notifications(changedTransfers, states=["removed", "error", "downloaded", "stalled", "unstalled", "finished", "stopped", "started"]):
nTotal = sum([len(d['data']) for s,d in changedTransfers.items() if s in states]) if changedTransfers is not None else 0 nTotal = sum([len(d['data']) for s,d in changedTransfers.items() if s in states]) if changedTransfers is not None else 0
torrents = {} torrents = {}
if nTotal > 0: if nTotal > 0:
@ -840,7 +874,7 @@ def prepare_notifications(changedTransfers, states=CONFIG['notification_states']
return None, nTotal, torrents return None, nTotal, torrents
async def check_notification_reactions(message, is_text_channel, torrents, starttime=datetime.datetime.now()): async def check_notification_reactions(message, is_text_channel, torrents, starttime=datetime.datetime.now()):
if (datetime.datetime.now() - starttime).total_seconds() >= CONFIG['notification_freq'] * CONFIG['notification_reaction_check_factor']: if (datetime.datetime.now() - starttime).total_seconds() >= CONFIG['reaction_wait_timeout']:
if is_text_channel: if is_text_channel:
await message.clear_reactions() await message.clear_reactions()
return return
@ -849,7 +883,7 @@ async def check_notification_reactions(message, is_text_channel, torrents, start
return user.id in CONFIG['whitelist_user_ids'] and reaction.message.id == message.id and (str(reaction.emoji) == '🔕' or (str(reaction.emoji) == '🔔' and is_text_channel)) return user.id in CONFIG['whitelist_user_ids'] and reaction.message.id == message.id and (str(reaction.emoji) == '🔕' or (str(reaction.emoji) == '🔔' and is_text_channel))
try: try:
reaction, user = await client.wait_for('reaction_add', timeout=CONFIG['notification_freq'], check=check) reaction, user = await client.wait_for('reaction_add', timeout=CONFIG['reaction_wait_timeout'], check=check)
except asyncio.TimeoutError: except asyncio.TimeoutError:
return await check_notification_reactions(message, is_text_channel, torrents, starttime=starttime) return await check_notification_reactions(message, is_text_channel, torrents, starttime=starttime)
else: else:
@ -874,13 +908,13 @@ async def check_notification_reactions(message, is_text_channel, torrents, start
return await check_notification_reactions(message, is_text_channel, torrents, starttime=starttime) return await check_notification_reactions(message, is_text_channel, torrents, starttime=starttime)
async def run_notifications(): async def run_notifications():
if CONFIG['notification_enabled']: if CONFIG['notification_enabled'] and CONFIG['notification_channel_id'] > 0:
# get all changes # get all changes
logger.debug("Running notification check") logger.debug("Running notification check")
changedTransfers = check_for_transfer_changes() changedTransfers = check_for_transfer_changes()
nTotal = sum([len(d['data']) for d in changedTransfers.values()]) if changedTransfers is not None else 0 nTotal = sum([len(d['data']) for d in changedTransfers.values()]) if changedTransfers is not None else 0
if nTotal > 0: if nTotal > 0:
addReactions = (sum([len(d['data']) for k,d in changedTransfers.items() if k != "removed"]) > 0)
# first in_channel notifications # first in_channel notifications
if CONFIG['notification_enabled_in_channel']: if CONFIG['notification_enabled_in_channel']:
embeds, n, torrents = prepare_notifications(changedTransfers, CONFIG['notification_states']['in_channel']) embeds, n, torrents = prepare_notifications(changedTransfers, CONFIG['notification_states']['in_channel'])
@ -889,8 +923,9 @@ async def run_notifications():
if n > 0: if n > 0:
ch = client.get_channel(CONFIG['notification_channel_id']) ch = client.get_channel(CONFIG['notification_channel_id'])
msgs = [await ch.send(embed=e) for e in embeds] msgs = [await ch.send(embed=e) for e in embeds]
[await msgs[-1].add_reaction(s) for s in ['🔔','🔕']] if addReactions:
asyncio.create_task(check_notification_reactions(msgs[-1], True, torrents, datetime.datetime.now())) [await msgs[-1].add_reaction(s) for s in ['🔔','🔕']]
asyncio.create_task(check_notification_reactions(msgs[-1], True, torrents, datetime.datetime.now()))
# Now notify the users # Now notify the users
@ -906,7 +941,7 @@ async def run_notifications():
if s in CONFIG['notification_states']['added_user']: if s in CONFIG['notification_states']['added_user']:
for h,t in d['data'].items(): for h,t in d['data'].items():
logger.debug("Checking transfer: {} ({})".format(str(t), h)) logger.debug("Checking transfer: {} ({})".format(str(t), h))
if t['added_user'] is not None and t['added_user'] not in t['optout_users']: if t['added_user'] is not None and t['added_user'] not in t['optout_users'] and t['added_user'] not in CONFIG['notification_DM_opt_out_user_ids']:
u = t['added_user'] u = t['added_user']
if u in addedUserChangedTransfers: if u in addedUserChangedTransfers:
if s in addedUserChangedTransfers[u]: if s in addedUserChangedTransfers[u]:
@ -940,15 +975,17 @@ async def run_notifications():
embeds[-1].set_author(name="Activity for transfer{} you added".format('' if n == 1 else 's')) embeds[-1].set_author(name="Activity for transfer{} you added".format('' if n == 1 else 's'))
user = client.get_user(u) user = client.get_user(u)
msgs = [await user.send(embed=e) for e in embeds] msgs = [await user.send(embed=e) for e in embeds]
await msgs[-1].add_reaction('🔕') if addReactions:
asyncio.create_task(check_notification_reactions(msgs[-1], False, torrents, datetime.datetime.now())) await msgs[-1].add_reaction('🔕')
asyncio.create_task(check_notification_reactions(msgs[-1], False, torrents, datetime.datetime.now()))
for u,transfers in notifiedUserChangedTransfers.items(): for u,transfers in notifiedUserChangedTransfers.items():
logger.debug("Sending notified_user notificaions for user {}".format(u)) logger.debug("Sending notified_user notificaions for user {}".format(u))
embeds, n, torrents = prepare_notifications(transfers, CONFIG['notification_states']['notified_users']) embeds, n, torrents = prepare_notifications(transfers, CONFIG['notification_states']['notified_users'])
if n > 0: if n > 0:
user = client.get_user(u) user = client.get_user(u)
msgs = [await user.send(embed=e) for e in embeds] msgs = [await user.send(embed=e) for e in embeds]
await msgs[-1].add_reaction('🔕') if addReactions:
await msgs[-1].add_reaction('🔕')
asyncio.create_task(check_notification_reactions(msgs[-1], False, torrents, datetime.datetime.now())) asyncio.create_task(check_notification_reactions(msgs[-1], False, torrents, datetime.datetime.now()))
else: else:
logger.debug("No changed transfers...") logger.debug("No changed transfers...")
@ -1188,9 +1225,13 @@ async def add(message, content = ""):
try: try:
tor = add_torrent(t["content"]) tor = add_torrent(t["content"])
if tor: if tor:
lock() try:
TORRENT_ADDED_USERS[tor.hashString] = message.author.id lock()
unlock() TORRENT_ADDED_USERS[tor.hashString] = message.author.id
except Exception as e:
logger.fatal("Error adding user to 'TORRENT_ADDED_USERS' for new transfer: {}".format(e))
finally:
unlock()
logger.info("User {} ({}) added torrent from file {}: {} ({})".format(message.author.name, message.author.id, t["name"], tor.name, tor.hashString)) logger.info("User {} ({}) added torrent from file {}: {} ({})".format(message.author.name, message.author.id, t["name"], tor.name, tor.hashString))
# if tor.isPrivate: # if tor.isPrivate:
# privateTransfers.append(len(privateTransfers)) # privateTransfers.append(len(privateTransfers))
@ -1208,9 +1249,13 @@ async def add(message, content = ""):
try: try:
tor = add_torrent(t) tor = add_torrent(t)
if tor: if tor:
lock() try:
TORRENT_ADDED_USERS[tor.hashString] = message.author.id lock()
unlock() TORRENT_ADDED_USERS[tor.hashString] = message.author.id
except Exception as e:
logger.fatal("Error adding user to 'TORRENT_ADDED_USERS' for new transfer: {}".format(e))
finally:
unlock()
logger.info("User {} ({}) added torrent from URL: {} ({})".format(message.author.name, message.author.id, tor.name, tor.hashString)) logger.info("User {} ({}) added torrent from URL: {} ({})".format(message.author.name, message.author.id, tor.name, tor.hashString))
# if tor.isPrivate: # if tor.isPrivate:
# privateTransfers.append(len(privateTransfers)) # privateTransfers.append(len(privateTransfers))
@ -2461,7 +2506,7 @@ async def modify_cmd(context, *, content=""):
logger.warning("Exception in t/modify: {}".format(e)) logger.warning("Exception in t/modify: {}".format(e))
async def toggle_compact_out(message): async def toggle_compact_out(message, content=""):
global OUTPUT_MODE, CONFIG global OUTPUT_MODE, CONFIG
if isDM(message): if isDM(message):
if message.author.id in CONFIG['DM_compact_output_user_ids']: if message.author.id in CONFIG['DM_compact_output_user_ids']:
@ -2503,11 +2548,11 @@ async def LegendGetEmbed(embed_data=None):
embed.add_field(name="Error", value=joinChar.join(["✅—none","—tracker  warning","🌐—tracker  error","🖥—local  error"]), inline=not isCompact) embed.add_field(name="Error", value=joinChar.join(["✅—none","—tracker  warning","🌐—tracker  error","🖥—local  error"]), inline=not isCompact)
embed.add_field(name="Activity📈", value=joinChar.join(["🐢—stalled","🐇—active","🚀—running (rate>0)"]), inline=not isCompact) embed.add_field(name="Activity📈", value=joinChar.join(["🐢—stalled","🐇—active","🚀—running (rate>0)"]), inline=not isCompact)
embed.add_field(name="Tracker📡", value=joinChar.join(["🔐—private","🔓—public"]), inline=not isCompact) embed.add_field(name="Tracker📡", value=joinChar.join(["🔐—private","🔓—public"]), inline=not isCompact)
embed.add_field(name="Messages💬", value=joinChar.join(["🔄—auto-update message","❎—cancel auto-update","🖨—reprint at bottom", "🧾—summarize listed transfers"]), inline=not isCompact) embed.add_field(name="Messages💬", value=joinChar.join(["🔄—auto-update message","❎—cancel auto-update","🖨—reprint at bottom", "📱 *or* 💻—switch output format to mobile/desktop", "🧾—summarize listed transfers"]), inline=not isCompact)
embed.add_field(name="Notifications📣", value=joinChar.join(["🔔—enable","🔕—disable"]), inline=not isCompact) embed.add_field(name="Notifications📣", value=joinChar.join(["🔔—enable","🔕—disable"]), inline=not isCompact)
return embed return embed
async def legend(message): async def legend(message, content=""):
if await CommandPrecheck(message): if await CommandPrecheck(message):
await message.channel.send(embed=await LegendGetEmbed()) await message.channel.send(embed=await LegendGetEmbed())
return return
@ -2605,15 +2650,16 @@ async def set_repeat_timeout(message, content=CONFIG['repeat_timeout']):
async def set_repeat_timeout_cmd(context, content=""): async def set_repeat_timeout_cmd(context, content=""):
await set_repeat_timeout(context.message, content.strip()) await set_repeat_timeout(context.message, content.strip())
async def toggle_notifications(message):
async def toggle_notifications(message, content=""):
global CONFIG global CONFIG
if isDM(message) and await CommandPrecheck(message): if isDM(message) and await CommandPrecheck(message):
if message.author.id in CONFIG['notification_DM_opt_out_user_ids']: if message.author.id in CONFIG['notification_DM_opt_out_user_ids']:
CONFIG['notification_DM_opt_out_user_ids'].remove(message.author.id) CONFIG['notification_DM_opt_out_user_ids'].remove(message.author.id)
await message.channel.send('🔕DM notifications disabled') await message.channel.send('🔔DM notifications enabled')
else: else:
CONFIG['notification_DM_opt_out_user_ids'].append(message.author.id) CONFIG['notification_DM_opt_out_user_ids'].append(message.author.id)
await message.channel.send('🔔DM notifications enabled') await message.channel.send('🔕DM notifications disabled')
generate_json(json_data=CONFIG, path=CONFIG_JSON, overwrite=True) generate_json(json_data=CONFIG, path=CONFIG_JSON, overwrite=True)
elif await CommandPrecheck(message, whitelist=CONFIG['owner_user_ids']): elif await CommandPrecheck(message, whitelist=CONFIG['owner_user_ids']):
if CONFIG['notification_enabled_in_channel']: if CONFIG['notification_enabled_in_channel']:
@ -2629,7 +2675,7 @@ 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): async def toggle_dryrun(message, content=""):
global CONFIG global CONFIG
CONFIG['dryrun'] = not CONFIG['dryrun'] CONFIG['dryrun'] = not CONFIG['dryrun']
await message.channel.send("Toggled dryrun to {}".format(CONFIG['dryrun'])) await message.channel.send("Toggled dryrun to {}".format(CONFIG['dryrun']))
@ -2648,24 +2694,21 @@ async def on_message(message):
if message_has_torrent_file(message): if message_has_torrent_file(message):
await add(message, content=message.content) await add(message, content=message.content)
if isDM(message): # dm only if isDM(message): # dm only
if len(message.content) >= len("summary") and "summary" == message.content[:len("summary")]: contentLower = message.content.lower()
await summary(message) c = message.content
elif len(message.content) >= len("list") and "list" in message.content[:len("list")]: for k,v in dmCommands.items():
await list_transfers(message, content=message.content[len("list"):].strip()) for ai in [k] + v['alias']:
elif len(message.content) >= len("add") and "add" in message.content[:len("add")]: a = ai
await add(message, content=message.content[len("add"):].strip()) cl = contentLower
elif len(message.content) >= len("modify") and "modify" in message.content[:len("modify")]: if len(ai) == 1:
await modify(message, content=message.content[len("modify"):].strip()) a += ' '
elif len(message.content) >= len("legend") and "legend" in message.content[:len("legend")]: if len(c) == 1:
await legend(message) cl += ' '
elif len(message.content) >= len("help") and "help" in message.content[:len("help")]: c += ' '
await help(message, content=message.content[len("help"):].strip()) if len(cl) >= len(a) and a == cl[:len(a)]:
elif len(message.content) >= len("notifications") and "notifications" in message.content[:len("notifications")]: await v['cmd'](message, content=c[len(a):].strip())
await toggle_notifications(message) return
elif len(message.content) >= len("compact") and "compact" in message.content[:len("compact")]: await client.process_commands(message)
await toggle_compact_out(message)
else:
await client.process_commands(message)
elif not message.guild: # group dm only elif not message.guild: # group dm only
# do stuff here # # do stuff here #
pass pass
@ -2675,7 +2718,7 @@ async def on_message(message):
client.remove_command('help') client.remove_command('help')
async def help(message, content="", compact_output=(OUTPUT_MODE == OutputMode.MOBILE)): async def print_help(message, content="", compact_output=(OUTPUT_MODE == OutputMode.MOBILE)):
if await CommandPrecheck(message): if await CommandPrecheck(message):
if content != "": if content != "":
if content in ["l","list"]: if content in ["l","list"]:
@ -2689,13 +2732,14 @@ async def help(message, content="", compact_output=(OUTPUT_MODE == OutputMode.MO
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)*\n*Either TORRENT_ID_SPECIFIER or NAME can be specified, but not both*'.format(CONFIG['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)*\n*Either TORRENT_ID_SPECIFIER or NAME can be specified, but not both*'.format(CONFIG['bot_prefix']), 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="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*List 10 most recently added transfers (sort transfers by age and specify number):* `{0}list --sort age -N 10`".format(CONFIG['bot_prefix']), 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*List 10 most recently added transfers (sort transfers by age and specify number):* `{0}list --sort age -N 10`".format(CONFIG['bot_prefix']), inline=False)
await message.channel.send(embed=embed) # await message.channel.send(embed=embed)
elif content in ["a","add"]: elif content in ["a","add"]:
embed = discord.Embed(title='Add transfer', description="If multiple torrents are added, separate them by spaces", color=0xb51a00) 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, url to torrent file, or by attaching a torrent file", icon_url=CONFIG['logo_url']) embed.set_author(name="Add one or more specified torrents by magnet link, url to torrent file, or by attaching a torrent file", icon_url=CONFIG['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(CONFIG['bot_prefix']), inline=False) embed.add_field(name="Usage", value='`{0}add TORRENT_FILE_URL_OR_MAGNET_LINK ...`\n`{0}a TORRENT_FILE_URL_OR_MAGNET_LINK ...`'.format(CONFIG['bot_prefix']), inline=False)
embed.add_field(name="Examples", value="*Add download of Linux Ubuntu using link to torrent file:* `{0}add https://releases.ubuntu.com/20.04/ubuntu-20.04.1-desktop-amd64.iso.torrent`\n*Add download of ubuntu using the actual `.torrent` file:* Select the `.torrent` file as an attachmend in Discord, then enter `t/a` as the caption".format(CONFIG['bot_prefix']), inline=False) embed.add_field(name="Notes", value='*You can add transfers by uploading a torrent file without having to type anything, i.e. no command necessary, just upload it to TransmissionBot\'s channel or via DM*', inline=False)
await message.channel.send(embed=embed) embed.add_field(name="Examples", value="*Add download of Linux Ubuntu using link to torrent file:* `{0}add https://releases.ubuntu.com/20.04/ubuntu-20.04.1-desktop-amd64.iso.torrent`\n*Add download of ubuntu using the actual `.torrent` file:* Select the `.torrent` file as an attachmend in Discord and send, no `{0}add` needed!".format(CONFIG['bot_prefix']), inline=False)
# await message.channel.send(embed=embed)
elif content in ["m","modify"]: elif content in ["m","modify"]:
embed = discord.Embed(title='Modify existing transfer(s)', color=0xb51a00) 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=CONFIG['logo_url']) embed.set_author(name="Pause, resume, remove, or remove and delete specified transfer(s)", icon_url=CONFIG['logo_url'])
@ -2703,25 +2747,25 @@ async def help(message, content="", compact_output=(OUTPUT_MODE == OutputMode.MO
embed.add_field(name="Pause or resume ALL transfers", value="Simply run `{0}modify` to pause or resume all existing transfers".format(CONFIG['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(CONFIG['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(CONFIG['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(CONFIG['bot_prefix']), inline=False)
embed.add_field(name="Examples", value="`{0}modify`\n`{0}m ubuntu`\n`{0}m 23,34,36-42`\n`{0}m --filter downloading ubuntu`".format(CONFIG['bot_prefix']), inline=False) embed.add_field(name="Examples", value="`{0}modify`\n`{0}m ubuntu`\n`{0}m 23,34,36-42`\n`{0}m --filter downloading ubuntu`".format(CONFIG['bot_prefix']), inline=False)
await message.channel.send(embed=embed) # await message.channel.send(embed=embed)
elif content in ["s","summary"]: elif content in ["s","summary"]:
embed = discord.Embed(title="Print summary of transfers", color=0xb51a00) embed = discord.Embed(title="Print summary of transfers", color=0xb51a00)
embed.set_author(name="Print summary of active transfer information", icon_url=CONFIG['logo_url']) embed.set_author(name="Print summary of active transfer information", icon_url=CONFIG['logo_url'])
embed.add_field(name="Usage", value='`{0}summary [LIST_OPTIONS]`'.format(CONFIG['bot_prefix']), inline=False) embed.add_field(name="Usage", value='`{0}summary [LIST_OPTIONS]`'.format(CONFIG['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(CONFIG['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(CONFIG['bot_prefix']), inline=False)
embed.add_field(name="Examples", value="`{0}summary`\n`{0}s --filter private`\n`{0}s 23,34,36-42`\n`{0}s --filter downloading ubuntu`".format(CONFIG['bot_prefix']), inline=False) embed.add_field(name="Examples", value="`{0}summary`\n`{0}s --filter private`\n`{0}s 23,34,36-42`\n`{0}s --filter downloading ubuntu`".format(CONFIG['bot_prefix']), inline=False)
await message.channel.send(embed=embed) # await message.channel.send(embed=embed)
elif content in ["config"]: elif content in ["config"]:
embed = discord.Embed(title="Configuration", color=0xb51a00) embed = discord.Embed(title="Configuration", color=0xb51a00)
embed.set_author(name="Configure bot options", icon_url=CONFIG['logo_url']) embed.set_author(name="Configure bot options", icon_url=CONFIG['logo_url'])
embed.add_field(name='Toggle output style', value='*toggle between desktop (default), mobile (narrow), or smart selection of output style*\n*ex.* `{0}compact` or `{0}c`'.format(CONFIG['bot_prefix']), inline=False) embed.add_field(name='Toggle output style', value='*toggle between desktop (default), mobile (narrow), or smart selection of output style*\n*ex.* `{0}compact` or `{0}c`'.format(CONFIG['bot_prefix']), inline=False)
embed.add_field(name='Toggle notifications', value='*toggle notifications regarding transfer state changes to be checked every {1} (can be changed in config file)*\n*ex.* `{0}notifications` or `{0}n`'.format(CONFIG['bot_prefix'], humantime(CONFIG['notification_freq'],compact_output=False)), inline=False) embed.add_field(name='Toggle notifications', value='*toggle notifications regarding transfer state changes to be checked every {1} (can be changed in config file)*\n*ex.* `{0}notifications` or `{0}n`'.format(CONFIG['bot_prefix'], humantime(CONFIG['notification_freq'],compact_output=False)), inline=False)
embed.add_field(name='Set auto-update message frequency and timeout', value='**Frequency:** *Use* `{0}set-repeat-freq NUM_SECONDS` *to set the repeat frequency of auto-update messages (*`NUM_SECONDS`*must be greater than 0, leave blank to revert to default of {1})*\n**Timeout:** *Use* `{0}set-repeat-timeout NUM_SECONDS` *to set the amount of time an auto-repeat message will repeat until it quits automatically (times out) (*`NUM_SECONDS` *must be greater or equal to 0. Set to 0 for no timeout. Leave blank to revert to default of {2})*'.format(CONFIG['bot_prefix'], humantime(CONFIG['repeat_freq'],compact_output=False),humantime(CONFIG['repeat_timeout'],compact_output=False)), inline=False) embed.add_field(name='Set auto-update message frequency and timeout', value='**Frequency:** *Use* `{0}set-repeat-freq NUM_SECONDS` *or* `{0}freq NUM_SECONDS`*to set the repeat frequency of auto-update messages (*`NUM_SECONDS`*must be greater than 0, leave blank to revert to default of {1})*\n**Timeout:** *Use* `{0}set-repeat-timeout NUM_SECONDS` *or* `{0}timeout NUM_SECONDS` *to set the amount of time an auto-repeat message will repeat until it quits automatically (times out) (*`NUM_SECONDS` *must be greater or equal to 0. Set to 0 for no timeout. Leave blank to revert to default of {2})*'.format(CONFIG['bot_prefix'], humantime(CONFIG['repeat_freq'],compact_output=False),humantime(CONFIG['repeat_timeout'],compact_output=False)), inline=False)
await message.channel.send(embed=embed) # await message.channel.send(embed=embed)
else: else:
embed = discord.Embed(title='List of commands:', color=0xb51a00) embed = discord.Embed(title='List of commands:', description='Send commands in-channel or directly to me via DM.', color=0xb51a00)
embed.set_author(name='Transmission Bot: Manage torrent file transfers', icon_url=CONFIG['logo_url']) embed.set_author(name='Transmission Bot: Manage torrent file transfers', icon_url=CONFIG['logo_url'])
embed.add_field(name="Add new torrent transfers", value="*add one or more specified torrents by magnet link, url to torrent file, or by attaching a torrent file*\n*ex.* `{0}add TORRENT ...` or `{0}a TORRENT ...`".format(CONFIG['bot_prefix']), inline=False) embed.add_field(name="Add new torrent transfers", value="*add one or more specified torrents by magnet link, url to torrent file (in which case you don't need to use a command), or by attaching a torrent file*\n*ex.* `{0}add TORRENT ...` or `{0}a TORRENT ...`".format(CONFIG['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 [LIST_OPTIONS]` or `{0}m [LIST_OPTIONS]`".format(CONFIG['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 [LIST_OPTIONS]` or `{0}m [LIST_OPTIONS]`".format(CONFIG['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 [LIST_OPTIONS]` or `{0}l [LIST_OPTIONS]`".format(CONFIG['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 [LIST_OPTIONS]` or `{0}l [LIST_OPTIONS]`".format(CONFIG['bot_prefix']), inline=False)
embed.add_field(name="Print summary of transfers", value="*print summary for specified transfers, with followup options to list subsets of those transfers*\n*ex.* `{0}summary [LIST_OPTIONS]` or `{0}s [LIST_OPTIONS]`".format(CONFIG['bot_prefix']), inline=False) embed.add_field(name="Print summary of transfers", value="*print summary for specified transfers, with followup options to list subsets of those transfers*\n*ex.* `{0}summary [LIST_OPTIONS]` or `{0}s [LIST_OPTIONS]`".format(CONFIG['bot_prefix']), inline=False)
@ -2736,12 +2780,28 @@ async def help(message, content="", compact_output=(OUTPUT_MODE == OutputMode.MO
# embed.add_field(name=legendEmbed.title, value='', inline=False) # embed.add_field(name=legendEmbed.title, value='', inline=False)
# for f in legendEmbed.fields: # for f in legendEmbed.fields:
# embed.add_field(name=f.name, value=f.value, inline=f.inline) # embed.add_field(name=f.name, value=f.value, inline=f.inline)
if not isDM(message):
try:
await message.author.send(embed=embed)
await message.channel.send('Hi {}, I sent you a DM with the help information'.format(message.author.display_name))
except:
await message.channel.send(embed=embed)
else:
await message.channel.send(embed=embed) await message.channel.send(embed=embed)
@client.command(name='help', description='Help HUD.', brief='HELPOOOO!!!', pass_context=True) @client.command(name='help', description='Help HUD.', brief='HELPOOOO!!!', pass_context=True)
async def help_cmd(context, *, content=""): async def help_cmd(context, *, content=""):
await help(context.message, content) await print_help(context.message, content)
@client.command(name='test', pass_context=True)
async def test(context, *, content=""):
if await CommandPrecheck(context.message, whitelist=CONFIG['owner_user_ids']):
user = context.message.author
await user.send("test message")
await context.message.channel.send("Hey {}, I sent you a message!".format(user.display_name))
pass
return
@client.event @client.event
async def on_command_error(context, error): async def on_command_error(context, error):
@ -2777,7 +2837,7 @@ async def on_command_error(context, error):
return return
if isinstance(error, commands.UserInputError): if isinstance(error, commands.UserInputError):
await context.send("Invalid input.") await context.send("Invalid input.")
await help(context) await print_help(context)
return return
if isinstance(error, commands.NoPrivateMessage): if isinstance(error, commands.NoPrivateMessage):
try: try:
@ -2812,4 +2872,17 @@ async def on_command_error(context, error):
await help_cmd(context) await help_cmd(context)
raise error raise error
dmCommands = {
'summary': {'alias':['sum','s'], 'cmd':summary},
'list': {'alias':['ls','l'], 'cmd':list_transfers},
'legend': {'alias':[], 'cmd':legend},
'add': {'alias':['a'], 'cmd':add},
'modify': {'alias':['mod','m'], 'cmd':modify},
'help': {'alias':[], 'cmd':print_help},
'compact': {'alias':['c'], 'cmd':toggle_compact_out},
'notifications': {'alias':['n'], 'cmd':toggle_notifications},
'set-repeat-timeout': {'alias':['timeout'], 'cmd':set_repeat_timeout},
'set-repeat-freq': {'alias':['freq'], 'cmd':set_repeat_freq}
}
client.run(CONFIG['bot_token']) client.run(CONFIG['bot_token'])