Skip to content

Instantly share code, notes, and snippets.

@tiberiosantos
Created February 24, 2016 03:28
Show Gist options
  • Save tiberiosantos/c0e1d2f46993a8e6bb12 to your computer and use it in GitHub Desktop.
Save tiberiosantos/c0e1d2f46993a8e6bb12 to your computer and use it in GitHub Desktop.
Port of the xdccq addon from jmoiron for Hexchat IRC
#!/usr/bin/env python
#
# written by jmoiron, <http://dev.jmoiron.net/xchat/>
# ported to hexchat by tiberio, <tiberiosantos.github.io>
# licensed under the GNU GPL v2
# a copy of the license can be found:
# http://www.gnu.org/licenses/gpl.txt
# version 0.7:
# - initial port of plugin
# version 0.6:
# - added 'step' to pack list
# - special thanks to Jan Malakhovski
# for the patch for this feature
class HexChat(object):
"""Fake hexchat object. For testing via command line."""
def __nonzero__(self):
"""Make this boolean False:
>>> bool(HexChat())
False
"""
return False
def prnt(self, s):
s = s.replace('\00302', '')
s = s.replace('\00303', '')
s = s.replace('\002', '')
s = s.replace('\003', '')
print s
command=prnt
def __getattr__(self, name):
def lambda_(*x, **y):
print '%s: %s, %s' % (name, x, y)
return lambda_
try: import hexchat
except ImportError: hexchat = HexChat()
import sys
import re
from exceptions import ValueError
__module_name__ = "xdccq"
__module_version__ = "0.6"
__module_description__ = "Xdcc Queue"
# to enable some debugging output, set to True
__debugging__ = False
colors = {
'black' : '01', 'dkblue' : '02', 'dkgreen' : '03', 'red' : '04',
'brown' : '05', 'purple' : '06', 'orange' : '07', 'yellow' : '08',
'ltgreen' : '09', 'aqua' : '10', 'ltblue' : '11', 'blue' : '10',
'violet' : '13', 'grey' : '14', 'ltgrey' : '15', 'white' : '16'
}
# for americans
colors['gray'], colors['ltgray'] = colors['grey'], colors['ltgrey']
if __debugging__:
import traceback
def pcolor(string, color):
colorstr = '\003%s' % colors.get(color, 'black')
print colorstr + str(string) + '\003'
def print_debug(string):
if __debugging__:
pcolor(string, 'orange')
def print_error(string): pcolor(string, 'red')
def print_help(string): pcolor(string, 'blue')
def print_success(string): pcolor(string, 'dkgreen')
def echo(string): pcolor(string, 'ltblue')
print_info = print_success
cmd_help = {}
gen_help = [
"\037[help, ?]\037 <cmd> - prints this help or detailed help for 'cmd'\n",
"\037[ls, list]\037 - lists files in queue\n",
"\037get\037 [bot] [#, #-#] - adds 'send' cmds to queue\n",
"\037rm\037 [bot] <#, #-#> - removes 'send' cmds from queue\n",
"\037[cancel,stop]\037 - cancels current transfer\n",
"\037multiq\037 - toggles multi-queue mode (1 queue per bot)",
]
cmd_help['msg'] = " " + " ".join(gen_help)
cmd_help['?'] = """\n\002/xdccq\002 \037[help, ?]\037 <cmd>\n \00303By itself, help/? prints out the available commands along withshort descriptions of each. If you supply a command \002cmd\002, a longer help description of that command and its usage is given.\003"""
cmd_help['ls'] = """\n\002/xdccq\002 \037[ls, list]\037\n \00303Lists the file gets currently in the queue.\00303"""
cmd_help['get'] = """\n\002/xdccq\002 \037get\037 [bot] [#, #-#, #-#%#]\n \00303Adds commands to the local queue "/ctcp \037bot\037 xdcc send \037#\037". A number ["7"], a number range ["1-5"], a range with step ["0-10%2"] (1, 3, 5, ...), or a comma separated list ["7, 8-11, 15, 16-25%3"] may be given.\003"""
cmd_help['rm'] = """\n\003/xdccq\002 \037rm\037 [bot] <#, #-#>\n \00303Removes commands from local queue. If only \037bot\037 is supplied, it removes all commands dealing with \037bot\037. If it is supplied with a number or number range, any commands in that range are removed. If you had numbers "8, 9, 11, 12, 14" queued, and removed "8-14", it would remove all of those package numbers for that bot without resulting in an error for the missing "10" and "13".\003\n \00303New in 0.3: rm will now also call 'cancel' if the active transfer is from [bot] and the optional range argument was not given.\003"""
cmd_help['cancel'] = """\n\003/xdccq\002 \037cancel\037\n \00303If a DCC handled by xdccq is currently active, it cancels that transfer and starts the next one on the queue. If the 'active' transfer is remotely queued, xdccq attempts to unqueue it by issuing a '/msg <bot> xdcc remove' command. If this command fails, this transfer will continue to remotely pend; take notice! In multiq mode, this cancels \037all\037 transfering packs.\003"""
cmd_help['multiq'] = """\n\003/xdccq\002 \037multiq\037\n \00303The default mode for xdccq is to operate with one global queue and dish out 1 pack request at a time. This toggles 'Multi-queue' mode, which keeps track of 1 transfer per bot in the queue.\003"""
def numToList(string):
"""Converts a string like '3,5,7-9,14' into a list."""
ret = []
numsplit = string.split(",")
# the following code makes nums into a list of all integers
for n in numsplit:
nr = n.split('-')
# handle the case of a single number
if len(nr) == 1:
try: ret.append(int(n))
except: raise ValueError("number")
# handle the case of a range
elif len(nr) == 2:
try:
low = int(nr[0])
nx = nr[1].split("%", 1)
if len(nx) == 1:
high = int(nr[1]) + 1
step = 1
else:
high = int(nx[0]) + 1
step = int(nx[1])
if low > high: raise ValueError("number")
ret += range(low, high, step)
except ValueError: raise ValueError("number")
else: raise ValueError("range")
return ret
class Command:
def __init__(self, bot, num):
self.bot = str(bot)
self.num = int(num)
self.channel = hexchat.get_info("channel")
self.network = hexchat.get_info("network")
self.context = hexchat.get_context()
self.file = ""
self.retries = 0
self.queue_position = 0
self.queued = False
self.transfering = False
self.dead = False
self.s = "ctcp %s xdcc send #%d" % (str(bot), int(num))
def __str__(self):
return "#%d on %s (%s)" % (self.num, self.bot, self.channel)
def execute(self, retry=False):
if retry: self.retries += 1
self.context.command(self.s)
def retry(self):
if self.retries < 3:
self.execute(True)
return True
return False
def dccclose(self):
if self.transfering:
s = "dcc close get %s %s" % (self.bot, self.file)
print_debug("/%s" % (s))
self.context.command(s)
def dequeue(self):
if self.queued:
s = "msg %s xdcc remove" % (self.bot)
print_debug("/%s" % (s))
self.context.command(s)
class Queue:
def __init__(self):
self.data = []
def put(self, item):
self.data.append(item)
def get(self):
tmp = self.data[0]
del self.data[0]
return tmp
def __getitem__(self, key):
return self.data[key]
def __delitem__(self, key):
del self.data[key]
def __len__(self):
return len(self.data)
def __iter__(self):
return self.data.__iter__()
def remove(self, item):
self.data.remove(item)
def append(self, item):
self.data.append(item)
class cmdQueue(Queue):
"""Utility functions for dealing with a queue of Commands"""
def getBotSet(self):
"""Returns a list of bots with packages in the queue."""
return list(set([cmd.bot.lower() for cmd in self.data]))
def getBotPackSet(self, botname, r=[]):
"""Gets the pack set in the Queue for a bot."""
packs = [cmd for cmd in self.data if cmd.bot.lower() == botname.lower()]
# if supplied a range, cut list to those in the range
if r: packs = [cmd for cmd in packs if cmd.num in r]
return packs
def removeBot(self, botname, r=[]):
"""Remove the packs from bot `botname` with numbers in `r`."""
packs = self.getBotPackSet(botname, r)
for cmd in packs:
self.data.remove(cmd)
# this might be better as the actual list someday?
return len(packs)
def transfering(self):
"""Return the actively transfering commands."""
return [cmd for cmd in self.data if cmd.transfering]
def queued(self):
"""Return the queued commands."""
return [cmd for cmd in self.data if cmd.queued]
def cancel(self, bot, r=[]):
items = self.getBotPackSet(bot, r)
for item in items:
item.dccclose()
item.dequeue()
self.remove(item)
return items
class Irc(object):
"""Separation of logic from hexchat internals."""
@staticmethod
def getCurrentBotCPS(bot):
dcclist = hexchat.get_list('dcc')
cps = 'Unknown CPS'
for item in dcclist:
if str(item.nick).lower() == bot.lower():
cps = '%s CPS' % (str(item.cps))
break
return cps
CommandQueue = cmdQueue()
# NOTE: In an old version, `Active` was the active command or `False`. Now,
# it's a list to allow for multiq mode. If multiq mode is OFF, then this will
# only ever have one command
Active = cmdQueue()
Watchdog = False
# set to True to enable multiq mode by default
MULTIQUEUE = False
# help, ? -- print out explanations of commands
def help(a):
if not len(a) == 3:
usage(True)
return
msg = cmd_help.get(a[2], None)
if msg:
print_help(msg)
# ls, list -- print out a list of files in the queue
def ls(a):
global CommandQueue, Active
s = []
for cmd in Active.transfering():
cps = Irc.getCurrentBotCPS(cmd.bot)
s.append("Pack #%d from %s on %s@%s being transfered at %s." % (cmd.num, cmd.bot, cmd.channel, cmd.network, cps))
for cmd in Active.queued():
s.append("Pack #%d from %s on %s@%s queued remotely [position %d]." % (cmd.num, cmd.bot, cmd.channel, cmd.network, cmd.queue_position))
if len(CommandQueue) == 0:
print_info("No files in the queue. " + s)
else:
if not s:
print_info("No files being transfered.")
else:
for line in s: print_info(line)
for f in CommandQueue:
print_info(f)
# get -- add commands to the queue
def get(a):
if len(a) != 4:
print_error("Error: invalid arguments for get. /xdccq get [bot] [#, #-#]")
return
bot = str(a[2])
try: nums = numToList(a[3])
except ValueError, exc:
# exc.args[0] is in the set ['number', 'range']
print_error("Error: %s contains invalid %s." % (a[3], exc.args[0]))
return
print_info("adding packs for bot [%s]: %s" % (bot, str(nums)))
for num in nums:
CommandQueue.put(Command(bot, num))
# if we actually added something, try to start this party
if len(nums):
run()
# rm -- remove commands from the queue
def rm(a):
global CommandQueue, Active
if len(a) not in (3, 4):
print_error("Error: invalid arguments for 'rm'. /xdccq rm [bot] <#, #-#>")
return
items_to_delete = []
if len(a) == 4:
try: items_to_delete = numToList(a[3])
except ValueError, exc:
# exc.args[0] is in the set ['number', 'range']
print_error("Error: %s contains invalid %s." % (a[3], exc.args[0]))
return
# if we removed all from bot, and the bot is currently transfering...
removed = CommandQueue.removeBot(a[2], items_to_delete)
active_removed = Active.cancel(a[2], items_to_delete)
info = 'deleted %d commands from the wait queue.' % removed
if active_removed:
info = info[:-1] + ', and %d from the active.' % len(active_removed)
print_info(info)
# we won't be needing a, and it'l clean the call elsewhere
def cancel(a=[]):
global Active
if not Active:
print_error("No currently transfering packages.")
return
for item in Active.transfering():
item.dccclose()
Active.remove(item)
for item in Active.queued():
item.dequeue()
Active.remove(item)
run()
def multiq(a=[]):
global MULTIQUEUE
MULTIQUEUE = not MULTIQUEUE
if MULTIQUEUE: s = 'on'
else: s = 'off'
print_info("Multiq mode %s" % s)
USAGE_STR = "Usage: \002/xdccq\002 [cmd] [args], \002/xdccq\002 \037help\037 for commands."
def usage(verbose=False):
"""Prints a usage message, and extra help if requested."""
print USAGE_STR
if verbose: print cmd_help['msg']
def dispatch(argv, arg_to_eol, c):
print_debug(argv)
echo("/" + str(arg_to_eol[0]))
try:
{
"help" : help,
"?" : help,
"ls" : ls,
"list" : ls,
"get" : get,
"rm" : rm,
"cancel" : cancel,
"stop" : cancel,
"multiq" : multiq,
}[argv[1]](argv)
except:
if __debugging__: traceback.print_exc(sys.stdout)
usage()
return hexchat.EAT_HEXCHAT
# watchdog callback
def transferCheck(data):
"""This watchdog function runs as long as we are transferring something. It
goes through the active queue looking for packs that are neither transferring
nor queued. If it finds one as such, it marks it as 'dead'. If it stays 'dead'
until the next run, it will retry the request. If the retry threshold (3) is
met, the command will be removed."""
global Active
# if active has already finished, stop the timer
if not Active:
return False
for cmd in Active:
if cmd.transfering or cmd.queued:
continue
if not cmd.dead:
cmd.dead = True
elif cmd.retry():
print_error("Previous attempt to get file (#%d from %s) failed. Repeating (%d of 3 retries)" % (cmd.num, cmd.bot, cmd.retries))
else:
print_info("At this point we'd want to remove (#%d from %s, %d)." % (cmd.num, cmd.bot, cmd.retries))
# run()
return True
def run():
"""This is the logic on what packs actually get added to the queue. It's
run just about any time there is an interaction with the queue (get, delete,
dcc events, etc)."""
global CommandQueue, Active, Watchdog
if not MULTIQUEUE:
# If there's an active transfer, we return
if Active: return
if not CommandQueue: return
# If not, we start one and start a watchdog timer
cmd = CommandQueue.get()
Active.append(cmd)
cmd.execute()
if not Watchdog:
Watchdog = hexchat.hook_timer(45000, transferCheck)
return
# We are in MULTIQUEUE mode ...
aps = sorted(Active.getBotSet())
cps = sorted(CommandQueue.getBotSet())
missing = [bot for bot in cps if bot not in aps]
print_debug('multiq: a: %s, q: %s, missing: %s' % (aps, cps, missing))
# if we have the same bots in each, we are already transfering at full..
if not missing:
return
for bot in missing:
cmd = CommandQueue.getBotPackSet(bot)
if not cmd: return
cmd = cmd[0]
Active.append(cmd)
CommandQueue.remove(cmd)
print_debug("/%s on %s@%s" % (cmd.s, cmd.channel, cmd.network))
cmd.execute()
# set up a watchdog every 45 seconds
if Active and not Watchdog:
Watchdog = hexchat.hook_timer(45000, transferCheck)
""" some other messages are possible:
** Closing Connection: Unable to transfer data (Broken pipe)
** Closing Connection: DCC Timeout (180 Sec Timeout)
"""
def notice(split, full, data):
global CommandQueue, Active
# ['jonas|srvr', 'Total Offered: 0.3 MB Total Transferred: 0.30 MB']
botname = split[0]
message = split[1]
for cmd in Active:
re_pos = re.compile(r"position [0-9]+")
if 'queue' in message.lower():
cmd.queued = True
print_info("xdccq detected the active file being placed on a remote queue")
res = re_pos.search(message.lower())
if res:
try:
postr = message[res.start() : res.end()]
pos = int(postr.split()[1])
cmd.queue_position = pos
except:
cmd.queue_position = 0
print_error("an error occured parsing the remote queue position")
def dccComplete(split, full, data):
# ['just4u.txt', '/home/jmoiron/.hexchat2/downloads/just4u.txt.1', 'just|4|u', '38373']
if not Active: return
#print_debug(str(split))
for cmd in Active.transfering():
if cmd.bot == str(split[2]):
# we assume it was our file
print_info("Received file \"%s\" from %s on %s@%s." % (cmd.file, cmd.bot, cmd.channel, cmd.network))
Active.remove(cmd)
run()
def dccConnect(split, full, data):
global CommandQueue, Active
if not Active: return
#print_debug(str(split))
botname = str(split[0])
for cmd in (c for c in Active if c.bot == botname):
print_info("Requested file \"%s\" is being sent." % (split[2]))
cmd.file = split[2]
cmd.queued = False
cmd.transfering = True
def dccStall(split, full, data):
global CommandQueue, Active
botname = str(split[2])
for cmd in (c for c in Active if c.bot == botname):
print_error("Requested file \"%s\" has stalled during transport." % (split[1]))
ret = cmd.retry()
if ret:
print_info("Re-requesting file \"%s\" (%d of 3 retries)" % (split[1], Active.retries))
else:
print_error("Retry limit reached for file \"%s\". Stopping the queue." % (split[1]))
__unhook__ = hexchat.hook_command("xdccq", dispatch, help=USAGE_STR)
print_info("XdccQ-TNG loaded successfully")
usage()
noticeHook = hexchat.hook_print("Notice", notice, "data")
dccRecvCompleteHook = hexchat.hook_print("DCC RECV Complete", dccComplete, "data")
dccRecvConnectHook = hexchat.hook_print("DCC RECV Connect", dccConnect, "data")
dccRecvStallHook = hexchat.hook_print("DCC Stall", dccStall, "data")
if __name__ == "__main__":
def tests():
"""
>>> numToList("1")
[1]
>>> numToList("1,2,4")
[1, 2, 4]
>>> numToList("1-4")
[1, 2, 3, 4]
>>> numToList("1-4,8,12")
[1, 2, 3, 4, 8, 12]
>>> numToList("1-4,8,12-48%4,50")
[1, 2, 3, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 50]
"""
pass
import doctest
doctest.testmod()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment