summaryrefslogtreecommitdiffstats
path: root/module/interaction
diff options
context:
space:
mode:
Diffstat (limited to 'module/interaction')
-rw-r--r--module/interaction/CaptchaManager.py158
-rw-r--r--module/interaction/EventManager.py128
-rw-r--r--module/interaction/InteractionManager.py85
-rw-r--r--module/interaction/InteractionTask.py129
-rw-r--r--module/interaction/__init__.py2
5 files changed, 502 insertions, 0 deletions
diff --git a/module/interaction/CaptchaManager.py b/module/interaction/CaptchaManager.py
new file mode 100644
index 000000000..02cd10a11
--- /dev/null
+++ b/module/interaction/CaptchaManager.py
@@ -0,0 +1,158 @@
+# -*- coding: utf-8 -*-
+
+"""
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License,
+ or (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, see <http://www.gnu.org/licenses/>.
+
+ @author: mkaay, RaNaN
+"""
+
+from time import time
+from traceback import print_exc
+from threading import Lock
+
+class CaptchaManager():
+ def __init__(self, core):
+ self.lock = Lock()
+ self.core = core
+ self.tasks = [] #task store, for outgoing tasks only
+
+ self.ids = 0 #only for internal purpose
+
+ def newTask(self, img, format, file, result_type):
+ task = CaptchaTask(self.ids, img, format, file, result_type)
+ self.ids += 1
+ return task
+
+ def removeTask(self, task):
+ self.lock.acquire()
+ if task in self.tasks:
+ self.tasks.remove(task)
+ self.lock.release()
+
+ def getTask(self):
+ self.lock.acquire()
+ for task in self.tasks:
+ if task.status in ("waiting", "shared-user"):
+ self.lock.release()
+ return task
+ self.lock.release()
+ return None
+
+ def getTaskByID(self, tid):
+ self.lock.acquire()
+ for task in self.tasks:
+ if task.id == str(tid): #task ids are strings
+ self.lock.release()
+ return task
+ self.lock.release()
+ return None
+
+ def handleCaptcha(self, task):
+ cli = self.core.isClientConnected()
+
+ if cli: #client connected -> should solve the captcha
+ task.setWaiting(50) #wait 50 sec for response
+
+ for plugin in self.core.hookManager.activePlugins():
+ try:
+ plugin.newCaptchaTask(task)
+ except:
+ if self.core.debug:
+ print_exc()
+
+ if task.handler or cli: #the captcha was handled
+ self.tasks.append(task)
+ return True
+
+ task.error = _("No Client connected for captcha decrypting")
+
+ return False
+
+
+class CaptchaTask():
+ def __init__(self, id, img, format, file, result_type='textual'):
+ self.id = str(id)
+ self.captchaImg = img
+ self.captchaFormat = format
+ self.captchaFile = file
+ self.captchaResultType = result_type
+ self.handler = [] #the hook plugins that will take care of the solution
+ self.result = None
+ self.waitUntil = None
+ self.error = None #error message
+
+ self.status = "init"
+ self.data = {} #handler can store data here
+
+ def getCaptcha(self):
+ return self.captchaImg, self.captchaFormat, self.captchaResultType
+
+ def setResult(self, text):
+ if self.isTextual():
+ self.result = text
+ if self.isPositional():
+ try:
+ parts = text.split(',')
+ self.result = (int(parts[0]), int(parts[1]))
+ except:
+ self.result = None
+
+ def getResult(self):
+ try:
+ res = self.result.encode("utf8", "replace")
+ except:
+ res = self.result
+
+ return res
+
+ def getStatus(self):
+ return self.status
+
+ def setWaiting(self, sec):
+ """ let the captcha wait secs for the solution """
+ self.waitUntil = max(time() + sec, self.waitUntil)
+ self.status = "waiting"
+
+ def isWaiting(self):
+ if self.result or self.error or time() > self.waitUntil:
+ return False
+
+ return True
+
+ def isTextual(self):
+ """ returns if text is written on the captcha """
+ return self.captchaResultType == 'textual'
+
+ def isPositional(self):
+ """ returns if user have to click a specific region on the captcha """
+ return self.captchaResultType == 'positional'
+
+ def setWatingForUser(self, exclusive):
+ if exclusive:
+ self.status = "user"
+ else:
+ self.status = "shared-user"
+
+ def timedOut(self):
+ return time() > self.waitUntil
+
+ def invalid(self):
+ """ indicates the captcha was not correct """
+ [x.captchaInvalid(self) for x in self.handler]
+
+ def correct(self):
+ [x.captchaCorrect(self) for x in self.handler]
+
+ def __str__(self):
+ return "<CaptchaTask '%s'>" % self.id
diff --git a/module/interaction/EventManager.py b/module/interaction/EventManager.py
new file mode 100644
index 000000000..38faa3c46
--- /dev/null
+++ b/module/interaction/EventManager.py
@@ -0,0 +1,128 @@
+# -*- coding: utf-8 -*-
+
+from traceback import print_exc
+from time import time
+
+class EventManager:
+ """
+ Handles all Event related task, also stores an Event queue for clients, so they can retrieve them later.
+
+ **Known Events:**
+ Most hook methods exists as events. These are some additional known events.
+
+ ===================== ================ ===========================================================
+ Name Arguments Description
+ ===================== ================ ===========================================================
+ metaEvent eventName, *args Called for every event, with eventName and orginal args
+ downloadPreparing fid A download was just queued and will be prepared now.
+ downloadStarts fid A plugin will immediately starts the download afterwards.
+ linksAdded links, pid Someone just added links, you are able to modify the links.
+ allDownloadsProcessed Every link was handled, pyload would idle afterwards.
+ allDownloadsFinished Every download in queue is finished.
+ unrarFinished folder, fname An Unrar job finished
+ configChanged sec, opt, value The config was changed.
+ ===================== ================ ===========================================================
+
+ | Notes:
+ | allDownloadsProcessed is *always* called before allDownloadsFinished.
+ | configChanged is *always* called before pluginConfigChanged.
+ """
+
+ CLIENT_EVENTS = ("packageUpdated", "packageInserted", "linkUpdated", "packageDeleted")
+
+ def __init__(self, core):
+ self.core = core
+ self.log = core.log
+
+ # uuid : list of events
+ self.clients = {}
+ self.events = {"metaEvent": []}
+
+ def getEvents(self, uuid):
+ """ Get accumulated events for uuid since last call, this also registeres new client """
+ if uuid not in self.clients:
+ self.clients[uuid] = Client()
+ return self.clients[uuid].get()
+
+ def addEvent(self, event, func):
+ """Adds an event listener for event name"""
+ if event in self.events:
+ if func in self.events[event]:
+ self.log.debug("Function already registered %s" % func)
+ else:
+ self.events[event].append(func)
+ else:
+ self.events[event] = [func]
+
+ def removeEvent(self, event, func):
+ """removes previously added event listener"""
+ if event in self.events:
+ self.events[event].remove(func)
+
+ def dispatchEvent(self, event, *args):
+ """dispatches event with args"""
+ for f in self.events["metaEvent"]:
+ try:
+ f(event, *args)
+ except Exception, e:
+ self.log.warning("Error calling event handler %s: %s, %s, %s"
+ % ("metaEvent", f, args, str(e)))
+ if self.core.debug:
+ print_exc()
+
+ if event in self.events:
+ for f in self.events[event]:
+ try:
+ f(*args)
+ except Exception, e:
+ self.log.warning("Error calling event handler %s: %s, %s, %s"
+ % (event, f, args, str(e)))
+ if self.core.debug:
+ print_exc()
+
+ # append to client event queue
+ if event in self.CLIENT_EVENTS:
+ for uuid, client in self.clients.items():
+ if client.delete():
+ del self.clients[uuid]
+ else:
+ client.append(event, args)
+
+
+ def removeFromEvents(self, func):
+ """ Removes func from all known events """
+ for name, events in self.events.iteritems():
+ if func in events:
+ events.remove(func)
+
+
+
+class Client:
+
+ # delete clients after this time
+ TIMEOUT = 60 * 60
+ # max events, if this value is reached you should assume that older events were dropped
+ MAX = 30
+
+ def __init__(self):
+ self.lastActive = time()
+ self.events = []
+
+ def delete(self):
+ return self.lastActive + self.TIMEOUT < time()
+
+ def append(self, event, args):
+ ev = (event, args)
+ if ev not in self.events:
+ self.events.insert(0, ev)
+
+ del self.events[self.MAX:]
+
+
+ def get(self):
+ self.lastActive = time()
+
+ events = self.events
+ self.events = []
+
+ return [(ev, [str(x) for x in args]) for ev, args in events] \ No newline at end of file
diff --git a/module/interaction/InteractionManager.py b/module/interaction/InteractionManager.py
new file mode 100644
index 000000000..5ebcd1fcd
--- /dev/null
+++ b/module/interaction/InteractionManager.py
@@ -0,0 +1,85 @@
+# -*- coding: utf-8 -*-
+"""
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License,
+ or (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, see <http://www.gnu.org/licenses/>.
+
+ @author: RaNaN
+"""
+from utils import lock
+from traceback import print_exc
+from threading import Lock
+
+class InteractionManager:
+ """
+ Class that gives ability to interact with the user.
+ Arbitary task with predefined output and input type can be set off.
+ Asyncronous callbacks and default values keeps the ability to fallback if no user is present.
+ """
+ def __init__(self, core):
+ self.lock = Lock()
+ self.core = core
+ self.tasks = [] #task store, for outgoing tasks only
+
+ self.ids = 0 #only for internal purpose
+
+ def work(self):
+ """Mainloop that gets the work done"""
+
+ def newTask(self, img, format, file, result_type):
+ task = CaptchaTask(self.ids, img, format, file, result_type)
+ self.ids += 1
+ return task
+
+ @lock
+ def removeTask(self, task):
+ if task in self.tasks:
+ self.tasks.remove(task)
+
+ @lock
+ def getTask(self):
+ for task in self.tasks:
+ if task.status in ("waiting", "shared-user"):
+ return task
+
+ @lock
+ def getTaskByID(self, tid):
+ for task in self.tasks:
+ if task.id == str(tid): #task ids are strings
+ self.lock.release()
+ return task
+
+ def handleCaptcha(self, task):
+ cli = self.core.isClientConnected()
+
+ if cli: #client connected -> should solve the captcha
+ task.setWaiting(50) #wait 50 sec for response
+
+ for plugin in self.core.hookManager.activePlugins():
+ try:
+ plugin.newCaptchaTask(task)
+ except:
+ if self.core.debug:
+ print_exc()
+
+ if task.handler or cli: #the captcha was handled
+ self.tasks.append(task)
+ return True
+
+ task.error = _("No Client connected for captcha decrypting")
+
+ return False
+
+
+if __name__ == "__main__":
+
+ it = InteractionTask() \ No newline at end of file
diff --git a/module/interaction/InteractionTask.py b/module/interaction/InteractionTask.py
new file mode 100644
index 000000000..97cb16794
--- /dev/null
+++ b/module/interaction/InteractionTask.py
@@ -0,0 +1,129 @@
+# -*- coding: utf-8 -*-
+"""
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License,
+ or (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, see <http://www.gnu.org/licenses/>.
+
+ @author: RaNaN
+"""
+
+from module.Api import InteractionTask as BaseInteractionTask
+from module.Api import Input, Output
+
+#noinspection PyUnresolvedReferences
+class InteractionTask(BaseInteractionTask):
+ """
+ General Interaction Task extends ITask defined by thrift with additional fields and methods.
+ """
+ #: Plugins can put needed data here
+ storage = None
+ #: Timestamp when task expires
+ waitUntil = 0
+ #: Default data to be used, or True if preset should be used
+ default = None
+ #: The received result as string representation
+ result = None
+ #: List of registered handles
+ handler = None
+ #: Callback functions
+ callbacks = None
+ #: Error Message
+ error = None
+ #: Status string
+ status = None
+
+ def __init__(self, *args, **kwargs):
+ BaseInteractionTask.__init__(self, *args, **kwargs)
+
+ # additional internal attributes
+ self.storage = {}
+ self.default = []
+ self.handler = []
+ self.callbacks = []
+
+
+class CaptchaTask:
+ def __init__(self, id, img, format, file, result_type='textual'):
+ self.id = str(id)
+ self.captchaImg = img
+ self.captchaFormat = format
+ self.captchaFile = file
+ self.captchaResultType = result_type
+ self.handler = [] #the hook plugins that will take care of the solution
+ self.result = None
+ self.waitUntil = None
+ self.error = None #error message
+
+ self.status = "init"
+ self.data = {} #handler can store data here
+
+ def getCaptcha(self):
+ return self.captchaImg, self.captchaFormat, self.captchaResultType
+
+ def setResult(self, text):
+ if self.isTextual():
+ self.result = text
+ if self.isPositional():
+ try:
+ parts = text.split(',')
+ self.result = (int(parts[0]), int(parts[1]))
+ except:
+ self.result = None
+
+ def getResult(self):
+ try:
+ res = self.result.encode("utf8", "replace")
+ except:
+ res = self.result
+
+ return res
+
+ def getStatus(self):
+ return self.status
+
+ def setWaiting(self, sec):
+ """ let the captcha wait secs for the solution """
+ self.waitUntil = max(time() + sec, self.waitUntil)
+ self.status = "waiting"
+
+ def isWaiting(self):
+ if self.result or self.error or time() > self.waitUntil:
+ return False
+
+ return True
+
+ def isTextual(self):
+ """ returns if text is written on the captcha """
+ return self.captchaResultType == 'textual'
+
+ def isPositional(self):
+ """ returns if user have to click a specific region on the captcha """
+ return self.captchaResultType == 'positional'
+
+ def setWatingForUser(self, exclusive):
+ if exclusive:
+ self.status = "user"
+ else:
+ self.status = "shared-user"
+
+ def timedOut(self):
+ return time() > self.waitUntil
+
+ def invalid(self):
+ """ indicates the captcha was not correct """
+ [x.captchaInvalid(self) for x in self.handler]
+
+ def correct(self):
+ [x.captchaCorrect(self) for x in self.handler]
+
+ def __str__(self):
+ return "<CaptchaTask '%s'>" % self.id
diff --git a/module/interaction/__init__.py b/module/interaction/__init__.py
new file mode 100644
index 000000000..de6d13128
--- /dev/null
+++ b/module/interaction/__init__.py
@@ -0,0 +1,2 @@
+__author__ = 'christian'
+ \ No newline at end of file