diff --git a/plugins/FServe.py b/plugins/FServe.py new file mode 100644 index 000000000..9f554867c --- /dev/null +++ b/plugins/FServe.py @@ -0,0 +1,596 @@ +#!/usr/bin/env python + +import plugins + +import socket +import textwrap +import threading +import struct +import time +import os +import os.path +import glob + +import dcc +import conf +import utils +import world +import ircmsgs +import ircutils +import privmsgs +import callbacks +import registry + +conf.registerPlugin('FServe') +conf.registerGlobalValue(conf.supybot.plugins.FServe, 'packetSize', + registry.Integer(1024, 'Size of packets to send/receive with DCC')) +conf.registerGroup(conf.supybot.plugins.FServe, 'queues') +conf.registerChannelValue(conf.supybot.plugins.FServe, 'queueNames', + registry.SpaceSeparatedListOfStrings([], + 'List of queues for this channel')) + +# --- Exceptions --- + +class QueueException(Exception): + msg = "Could not queue file" + def __init__(self, m): + self.msg = m + + +# --- Handlers --- + +class SendHandler(dcc.SendHandler): + caller = None + + def clientConnected(self): + self.sock.settimeout(self.caller.timeout) + + def packetSent(self): + self.currentSpeed = (self.bytesSent / (time.time() - self.startTime)) + if (self.currentSpeed > self.caller.maxSpeed): + time.sleep(0.9) + if (self.currentSpeed < self.caller.minSpeed): + #Allow a little leeway + slow += 1 + if (slow > 5): + # Too Slow + self.log.info('\'%s\': Send too slow' % (self.filename)) + raise SendException('Too slow') + + def clientClosed(self): + # alert queue that we're done + self.caller.sendFinished(self) + + def registerPort(self): + pass + + + +class ChatHandler(dcc.ChatHandler): + + def __init__(self, irc, nick, mask, queue=None): + self.caller = queue + dcc.ChatHandler.__init__(self, irc, nick, mask) + if (queue): + self.currentDir = os.path.join(conf.supybot.directories.data(), + queue.sendDir) + else: + self.currentDir = os.path.join(conf.supybot.directories.data()) + self.baseDir = self.currentDir + + def cmd_ls(self, args): + """ Show files in current directory, using globs """ + if args: + files = [] + for a in args: + for i in glob.glob(os.path.join(self.currentDir, a)): + if (i[:2] == "./"): + i = i[2:] + if (i[:len(self.currentDir)] == self.currentDir): + i = i[len(self.currentDir)+1:] + if (not i in files): + files.append(i) + else: + files = os.listdir(self.currentDir) + if (not files): + self.sock.send("There are no matching files.\n"); + files.sort() + flist = [] + dirlist = [] + for f in files: + full = os.path.join(self.currentDir, f) + if (os.path.isdir(full)): + dirlist.append(ircutils.mircColor(' %s/' % f, 'blue')) + else: + size = os.path.getsize(full) + if (size > 1024000): + size = '%sMb' % (size / 1024000) + elif (size > 1024): + size = '%sKb' % (size / 1024) + flist.append('%s [%s]' % (f, size)) + self.sock.send('\n'.join(dirlist) + '\n' + '\n'.join(flist) + "\n") + + def cmd_dir(self, args): + return self.cmd_ls(args) + def cmd_list(self, args): + return self.cmd_ls(args) + + def cmd_cd(self, args): + """ Change current directory """ + d = args[0] + full = os.path.join(self.currentDir, d) + full = os.path.abspath(full) + if (len(full) < len(self.baseDir)): + self.sock.send("Already in root directory\n"); + return + if (os.path.exists(full)): + if (os.path.isdir(full)): + self.currentDir = full + self.sock.send('Current directory: %s\n' % + self.currentDir[len(self.baseDir)+1:]) + else: + self.sock.send('%s is not a directory\n' % d) + else: + self.sock.send('%s does not exist\n' % d) + + def cmd_get(self, args): + fn = ' '.join(args) + full = os.path.join(self.currentDir, fn) + + try: + size = os.path.getsize(full) + except OSError, e: + f = glob.glob(full) + if (len(f) == 1): + full = f[0] + size = os.path.getsize(full) + else: + self.sock.send('That matches more than one file\n') + return + min = self.caller.instantSendSize + if (size < min): + # Instant send + self.caller.spawnSend(self.irc, self.nick, self.hostmask, full) + else: + # Check queues + try: + posn = self.caller.addFile(self.irc, self.nick, self.hostmask, full) + self.sock.send('File queued at position %s\n' % + (posn)) + self.caller.maybeSend() + except QueueException, e: + self.sock.send(e.msg + "\n") + + def cmd_close(self, line): + self.sock.send('Bye!\n') + self.sock.close() + def cmd_quit(self, line): + self.cmd_close([]) + + def cmd_queues(self, args): + queues = self.caller.queuedFiles + lines = [] + for q in range(len(queues)): + if (not args or queues[q][0] == args[0]): + fn = os.path.split(queues[q][1])[1] + when = time.ctime(queues[q][3]) + lines.append('%s: %s (by %s@%s)' % (q, fn, queues[q][0], when[4:-5])) + if (lines): + self.sock.send('\n'.join(lines) + '\n') + else: + self.sock.send('There is currently nothing queued.\n') + + def cmd_myqueues(self, args): + self.cmd_queues([self.msg.nick]) + + def cmd_sends(self, line): + sends = self.caller.sends + lines = [] + for i in (range(len(sends))): + s = sends[i] + sender = s[4] + fn = os.path.split(s[1])[1] + secs = (sender.filesize - sender.currentPosition) / sender.currentSpeed + if (secs > 3600): + t = '%sh' % int(secs / 3600) + elif (secs > 60): + t = '%sm' % int(secs / 60) + else: + t = '%ss' % int(secs) + lines.append('%s: %s\n (to %s @ %sCPS, %s remaining)' % (i, fn, s[0], int(sender.currentSpeed), t)) + + if (lines): + self.sock.send('\n'.join(lines) + '\n') + else: + self.sock.send('There is currently nothing sending.\n') + + def cmd_remove(self, args): + # remove a queue by number + idx = args[0] + if (not idx.isdigit()): + self.sock.send('Usage: remove \n') + else: + posn = int(idx) + queues = self.caller.queuedFiles + if (len(queues) < posn or posn < 0): + self.sock.send('There is no such queue position\n') + else: + # XXX infintesimal race condition + del self.caller.queuedFiles[posn] + self.sock.send('Removed queue.\n') + + def cmd_help(self, line): + pass + + def chatConnected(self): + self.sock.send("Welcome.\n") + self.cmd_ls([]) + + def handleLine(self, line): + args = line.split() + if (not args): + return + if (not hasattr(self, 'cmd_%s' % args[0])): + self.sock.send('Unknown command: %s\n' % args[0]) + else: + fn = getattr(self, 'cmd_%s' % args[0]) + fn(args[1:]) + + +class AdminHandler(ChatHandler): + + def cmd_config(self, args): + pass + + + + +# --- Queue Object --- + +class Queue: + name = "" + plugin = None + channel = "" + timeout = 0 + maxSpeed = 0 + minSpeed = 0 + maxQueues = 0 + instantSendSize = 0 + maxSends = 0 + receiveDir = "" + sendDir = "" + allowedModes = [] + allowedMasks = [] + allowedNicks = [] + bannedNicks = [] + bannedMasks = [] + chatHandler = "" + sendHandler = "" + + sends = [] + queuedFiles = [] + filesSent = 0 + bytesSent = 0 + leeches = {} + + def __init__(self, plug, channel, name): + self.name = name + self.plugin = plug + self.channel = channel + + try: + self.timeout = plug.registryValue("queues.%s.timeout" % name, channel) + except: + conf.registerGroup(conf.supybot.plugins.FServe.queues, name) + group = conf.supybot.plugins.FServe.queues.get(name) + conf.registerChannelValue(group, 'timeout', registry.Integer(90, '')) + conf.registerChannelValue(group, 'maxSpeed', registry.Integer(0, '')) + conf.registerChannelValue(group, 'minSpeed', registry.Integer(0, '')) + conf.registerChannelValue(group, 'maxQueues', registry.Integer(5, '')) + conf.registerChannelValue(group, 'instantSendSize', + registry.Integer(50000, '')) + conf.registerChannelValue(group, 'maxSends', registry.Integer(2, '')) + conf.registerChannelValue(group, 'receiveDir', + registry.String('incoming', '')) + conf.registerChannelValue(group, 'sendDir', + registry.String('files', '')) + conf.registerChannelValue(group, 'allowedModes', + registry.SpaceSeparatedListOfStrings([], '')) + conf.registerChannelValue(group, 'allowedMasks', + registry.SpaceSeparatedListOfStrings([], '')) + conf.registerChannelValue(group, 'allowedNicks', + registry.SpaceSeparatedListOfStrings([], '')) + conf.registerChannelValue(group, 'bannedMasks', + registry.SpaceSeparatedListOfStrings([], '')) + conf.registerChannelValue(group, 'bannedNicks', + registry.SpaceSeparatedListOfStrings([], '')) + conf.registerChannelValue(group, 'chatHandler', + registry.String('ChatHandler', '')) + conf.registerChannelValue(group, 'sendHandler', + registry.String('SendHandler', '')) + + + self.timeout = plug.registryValue("queues.%s.timeout" % name, channel) + self.maxSpeed = plug.registryValue("queues.%s.maxSpeed" % name, + channel) + self.minSpeed = plug.registryValue("queues.%s.minSpeed" % name, + channel) + self.maxQueues = plug.registryValue("queues.%s.maxQueues" % name, + channel) + self.instantSendSize = plug.registryValue("queues.%s.instantSendSize" + % name, channel) + self.maxSends = plug.registryValue("queues.%s.maxSends" % name, + channel) + self.receiveDir = plug.registryValue("queues.%s.receiveDir" % name, + channel) + self.sendDir = plug.registryValue("queues.%s.sendDir" % name, channel) + self.allowedModes = plug.registryValue("queues.%s.allowedModes" + % name, channel) + self.allowedMasks = plug.registryValue("queues.%s.allowedMasks" + % name, channel) + self.allowedNicks = plug.registryValue("queues.%s.allowedNicks" + % name, channel) + self.bannedNicks = plug.registryValue("queues.%s.bannedNicks" % name, + channel) + self.bannedMasks = plug.registryValue("queues.%s.bannedMasks" % name, + channel) + self.chatHandler = plug.registryValue("queues.%s.chatHandler" % name, + channel) + self.sendHandler = plug.registryValue("queues.%s.sendHandler" % name, + channel) + + # End of Configurables + self.sends = [] + self.queuedFiles = [] + self.filesSent = 0 + self.bytesSent = 0 + self.leeches = {} + + def spawnChat(self, irc, nick): + # first check permitted in queue + hostmask = irc.state.nickToHostmask(nick) + if (hostmask in self.bannedMasks or + (self.allowedMasks and not hostmask in self.allowedMasks)): + irc.reply("You are not allowed to use this queue."); + return + if (nick in self.bannedNicks or + (self.allowedNicks and not nick in self.allowedNicks)): + irc.reply("You are not allowed to use this queue."); + return + if (self.allowedModes): + ops = irc.state.channels[self.channel].ops + hops = irc.state.channels[self.channel].halfops + voices = irc.state.channels[self.channel].voices + users = irc.state.channels[self.channel].users + okay = 0 + if (nick in ops and 'op' in self.allowedModes): + okay = 1 + elif (nick in hops and 'halfop' in self.allowedModes): + okay = 1 + elif (nick in voices and 'voice' in self.allowedModes): + okay = 1 + elif (nick in users and 'user' in self.allowedModes): + okay = 1 + if (not okay): + irc.reply("You are not allowed to use this queue."); + return + + # We're okay to talk to. Spawn DCC Chat + parent = self.plugin._getHandlerClass(self.chatHandler) + new = parent(irc, nick, hostmask, queue=self) + new.start() + + def spawnSend(self, irc, nick, hostmask, file): + parent = self.plugin._getHandlerClass(self.sendHandler) + cxn = parent(irc, nick, hostmask, file) + cxn.caller = self + self.sends.append([nick, hostmask, file, irc, cxn]) + cxn.start() + + def getConnectionByPort(self, port): + for s in self.sends: + if (s[-1].port == port): + return s[-1] + return None + + def addFile(self, irc, nick, mask, file, posn=-1): + for q in self.queuedFiles: + if (q[0] == nick): + if (q[1] == file): + raise(QueueException( + 'You have already queued that file')) + nqs += 1 + if (nqs >= self.maxQueues): + raise(QueueException( + 'You have already queued the maximum number of files')) + + if (posn < 0 or posn > len(self.queuedFiles)): + # add to the end + self.queuedFiles.append([nick, mask, file, irc, time.time()]) + return len(self.queuedFiles) + else: + self.queuedFiles = self.queuedFiles[:posn] + \ + [[nick, mask, file, irc, time.time()]] + self.queuedFiles[posn:] + return posn + + def sendFinished(self, handler): + # One send finished, so delete and check to see if we should start another + for s in range(len(self.sends)): + if (self.sends[s][-1] == handler): + del self.sends[s] + break + self.maybeSend() + + def maybeSend(self): + # maybe send another queued file + if (self.queuedFiles and len(self.sends) < self.maxSends): + # find a send to someone we're not sending to already + sendingTo = [] + for s in self.sends: + sendingTo.append(s[0]) + for q in range(len(self.queuedFiles)): + if (not self.queuedFiles[q][0] in sendingTo): + #found a file to send + qo = self.queuedFiles[q] + del self.queuedFiles[q] + self.spawnSend(qo[3], qo[0], qo[1], qo[2]) + break + + def updateNick(self, old, new, mask): + for q in range(len(self.queues)): + if self.queues[q][0] == old and self.queues[q][1] == mask: + self.queues[q][0] = new + +class ResumeReqHandler(dcc.ResumeReqHandler): + plugin = None + + def _getSendHandler(self): + return self.plugin.getConnectionByPort(self.port) + + +class AcceptHandler(dcc.DCCReqHandler): + pass + + + +# --- Plugin --- + +class FServe(callbacks.Privmsg): + queues = {} + handlers = {'ChatHandler' : ChatHandler, + 'SendHandler' : SendHandler} + requestHandlers = {'RESUME' : ResumeReqHandler, + 'ACCEPT' : AcceptHandler, + 'SEND' : dcc.SendReqHandler} + + def _getHandlerClass(self, type): + return self.handlers.get(type, None) + + def getConnectionByPort(self, port): + for chan in self.queues: + for q in self.queues[chan]: + qo = self.queues[chan][q] + s = qo.getConnectionByPort(port) + if s: + return s + + + def list(self, irc, msg, args): + """ Show list of queues for this channel """ + channel = privmsgs.getChannel(msg, args) + qs = self.queues[channel].keys() + irc.reply('Known queues: %s' % ' '.join(qs)) + + + def chat(self, irc, msg, args): + """ Start a DCC chat interface (FServe) """ + channel = privmsgs.getChannel(msg, args) + if (len(args) != 1): + irc.reply("Unknown queue.") + return + name = args[0] + if (not self.queues.has_key(channel)): + # Silently ignore as we have no Queues here + return + queue = self.queues[channel].get(name, None) + if (queue): + queue.spawnChat(irc, msg.nick) + + def add(self, irc, msg, args): + """ Add a queue """ + channel = privmsgs.getChannel(msg, args) + name = args[0] + # First add to per channel config + current = self.registryValue('queueNames', channel) + if (name in current): + irc.reply("That queue already exists on this channel.") + else: + current.append(name) + self.setRegistryValue('queueNames', current, channel) + try: + self.registryValue('queues.%s' % name) + except registry.NonExistentRegistryEntry, e: + # Register channel name group + conf.registerGroup(conf.supybot.plugins.FServe.queues, name) + + # And register configs + group = conf.supybot.plugins.FServe.queues.get(name) + conf.registerChannelValue(group, 'timeout', registry.Integer(90, '')) + conf.registerChannelValue(group, 'maxSpeed', registry.Integer(0, '')) + conf.registerChannelValue(group, 'minSpeed', registry.Integer(0, '')) + conf.registerChannelValue(group, 'maxQueues', registry.Integer(5, '')) + conf.registerChannelValue(group, 'instantSendSize', + registry.Integer(50000, '')) + conf.registerChannelValue(group, 'maxSends', registry.Integer(2, '')) + conf.registerChannelValue(group, 'receiveDir', + registry.String('incoming', '')) + conf.registerChannelValue(group, 'sendDir', + registry.String('files', '')) + conf.registerChannelValue(group, 'allowedModes', + registry.SpaceSeparatedListOfStrings([], '')) + conf.registerChannelValue(group, 'allowedMasks', + registry.SpaceSeparatedListOfStrings([], '')) + conf.registerChannelValue(group, 'allowedNicks', + registry.SpaceSeparatedListOfStrings([], '')) + conf.registerChannelValue(group, 'bannedMasks', + registry.SpaceSeparatedListOfStrings([], '')) + conf.registerChannelValue(group, 'bannedNicks', + registry.SpaceSeparatedListOfStrings([], '')) + conf.registerChannelValue(group, 'chatHandler', + registry.String('ChatHandler', '')) + conf.registerChannelValue(group, 'sendHandler', + registry.String('SendHandler', '')) + + self.queues[channel][name] = Queue(self, channel, name) + irc.reply('Created queue named %r' % name) + + add = privmsgs.checkCapability(add, 'owner') + + def remove(self, irc, msg, args): + """ Remove a queue """ + channel = privmsgs.getChannel(msg, args) + name = args[0] + # Remove config only from list of Queues to build + current = self.registryValue('queueNames', channel) + if (name in current): + current.remove(name) + irc.reply('Removed queue named %r' % name) + else: + irc.reply('There is no such queue') + + remove = privmsgs.checkCapability(remove, 'owner') + + def doJoin(self, irc, msg): + """ Maybe build some internal representations of our config """ + channel = msg.args[0] + if (ircutils.nickEqual(msg.nick, irc.nick)): + if (not self.queues.has_key(channel)): + queues = self.registryValue('queueNames', channel) + self.queues[channel] = {} + for q in queues: + self.queues[channel][q] = Queue(self, channel, q) + + def doPrivmsg(self, irc, msg): + """ Maybe respond to DCC request """ + if (ircutils.isCtcp(msg) and dcc.isDCC(msg)): + ctcpArgs = msg.args[1][1:-1].split() + dccType = ctcpArgs[1] + dccArgs = ctcpArgs[2:] + + parent = self.requestHandlers[dccType] + handler = parent(irc, msg, dccArgs) + handler.plugin = self + handler.start() + + + def doNick(self, irc, msg): + """ Let queues know that nick has changed """ + newNick = msg.args[0] + oldNick = msg.nick + hostmask = irc.state.nickToHostmask(msg.nick) + for chan in self.queues: + for queue in self.queues[chan]: + self.queues[chan][queue].updateNick(oldNick, newNick, hostmask) + +Class = FServe +# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78: