summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar RaNaN <Mast3rRaNaN@hotmail.de> 2013-03-23 21:56:42 +0100
committerGravatar RaNaN <Mast3rRaNaN@hotmail.de> 2013-03-23 21:56:42 +0100
commit6e8a0f79f5ad7182a0bc35308ae06c63222667ed (patch)
tree0026179d34f19b64bc689c63af85b949ce57fb83
parentshow button when files are selected (diff)
downloadpyload-6e8a0f79f5ad7182a0bc35308ae06c63222667ed.tar.xz
implemented interactions for multi user, show waiting queries on webui
-rw-r--r--module/api/CoreApi.py5
-rw-r--r--module/api/UserInteractionApi.py32
-rw-r--r--module/config/default.py1
-rw-r--r--module/interaction/InteractionManager.py143
-rw-r--r--module/interaction/InteractionTask.py37
-rw-r--r--module/plugins/Base.py5
-rw-r--r--module/plugins/hoster/BasePlugin.py6
-rw-r--r--module/remote/apitypes.py38
-rw-r--r--module/remote/apitypes_debug.py16
-rw-r--r--module/remote/pyload.thrift34
-rw-r--r--module/remote/wsbackend/AsyncHandler.py20
-rw-r--r--module/web/static/css/default/style.less10
-rw-r--r--module/web/static/js/collections/InteractionList.js47
-rw-r--r--module/web/static/js/models/InteractionTask.js27
-rw-r--r--module/web/static/js/utils/apitypes.js4
-rw-r--r--module/web/static/js/views/headerView.js18
-rw-r--r--module/web/static/js/views/notificationView.js65
-rw-r--r--module/web/templates/default/base.html21
-rw-r--r--tests/manager/test_interactionManager.py71
19 files changed, 415 insertions, 185 deletions
diff --git a/module/api/CoreApi.py b/module/api/CoreApi.py
index d75fe6ad6..e5c5e8b41 100644
--- a/module/api/CoreApi.py
+++ b/module/api/CoreApi.py
@@ -1,13 +1,12 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
-from module.Api import Api, RequirePerm, Permission, ServerStatus
+from module.Api import Api, RequirePerm, Permission, ServerStatus, Interaction
from module.utils.fs import join, free_space
from module.utils import compare_time
from ApiComponent import ApiComponent
-
class CoreApi(ApiComponent):
""" This module provides methods for general interaction with the core, like status or progress retrieval """
@@ -34,7 +33,7 @@ class CoreApi(ApiComponent):
serverStatus = ServerStatus(0,
total[0], queue[0],
total[1], queue[1],
- 0,
+ self.isInteractionWaiting(Interaction.All),
not self.core.threadManager.pause and self.isTimeDownload(),
self.core.threadManager.pause,
self.core.config['reconnect']['activated'] and self.isTimeReconnect())
diff --git a/module/api/UserInteractionApi.py b/module/api/UserInteractionApi.py
index ded305c30..b95b7c468 100644
--- a/module/api/UserInteractionApi.py
+++ b/module/api/UserInteractionApi.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
-from module.Api import Api, RequirePerm, Permission, InteractionTask
+from module.Api import Api, RequirePerm, Permission, Interaction
from ApiComponent import ApiComponent
@@ -15,40 +15,36 @@ class UserInteractionApi(ApiComponent):
:param mode: binary or'ed output type
:return: boolean
"""
- return self.core.interactionManager.isTaskWaiting(mode)
+ return self.core.interactionManager.isTaskWaiting(self.primaryUID, mode)
@RequirePerm(Permission.Interaction)
- def getInteractionTask(self, mode):
+ def getInteractionTasks(self, mode):
"""Retrieve task for specific mode.
- :param mode: binary or'ed output type
- :return: :class:`InteractionTask`
+ :param mode: binary or'ed interaction types which should be retrieved
+ :rtype list of :class:`InteractionTask`
"""
- task = self.core.interactionManager.getTask(mode)
- return InteractionTask(-1) if not task else task
+ tasks = self.core.interactionManager.getTasks(self.primaryUID, mode)
+ # retrieved tasks count as seen
+ for t in tasks:
+ t.seen = True
+ if t.type == Interaction.Notification:
+ t.setWaiting(self.core.interactionManager.CLIENT_THRESHOLD)
+ return tasks
@RequirePerm(Permission.Interaction)
def setInteractionResult(self, iid, result):
"""Set Result for a interaction task. It will be immediately removed from task queue afterwards
:param iid: interaction id
- :param result: result as string
+ :param result: result as json string
"""
task = self.core.interactionManager.getTaskByID(iid)
- if task:
+ if task and self.primaryUID == task.owner:
task.setResult(result)
@RequirePerm(Permission.Interaction)
- def getNotifications(self):
- """List of all available notifcations. They stay in queue for some time, client should\
- save which notifications it already has seen.
-
- :return: list of :class:`InteractionTask`
- """
- return self.core.interactionManager.getNotifications()
-
- @RequirePerm(Permission.Interaction)
def getAddonHandler(self):
pass
diff --git a/module/config/default.py b/module/config/default.py
index 8515a8f33..dfa967284 100644
--- a/module/config/default.py
+++ b/module/config/default.py
@@ -5,6 +5,7 @@ Configuration layout for default base config
"""
#TODO: write tooltips and descriptions
+#TODO: use apis config related classes
def make_config(config):
# Check if gettext is installed
diff --git a/module/interaction/InteractionManager.py b/module/interaction/InteractionManager.py
index 1d26b1665..e4ae05501 100644
--- a/module/interaction/InteractionManager.py
+++ b/module/interaction/InteractionManager.py
@@ -17,11 +17,13 @@
"""
from threading import Lock
from time import time
+from base64 import standard_b64encode
from new_collections import OrderedDict
-from module.utils import lock, bits_set, to_list
-from module.Api import Input, Output
+from module.utils import lock, bits_set
+from module.Api import Interaction as IA
+from module.Api import InputType, Input
from InteractionTask import InteractionTask
@@ -29,51 +31,38 @@ class InteractionManager:
"""
Class that gives ability to interact with the user.
Arbitrary tasks with predefined output and input types can be set off.
- Asynchronous callbacks and default values keep the ability to fallback if no user is present.
"""
# number of seconds a client is classified as active
CLIENT_THRESHOLD = 60
+ NOTIFICATION_TIMEOUT = 60 * 60 * 30
+ MAX_NOTIFICATIONS = 50
def __init__(self, core):
self.lock = Lock()
self.core = core
- self.tasks = OrderedDict() #task store, for outgoing tasks only
- self.notifications = [] #list of notifications
+ self.tasks = OrderedDict() #task store, for all outgoing tasks
- self.last_clients = {
- Output.Notification : 0,
- Output.Captcha : 0,
- Output.Query : 0,
- }
+ self.last_clients = {}
+ self.ids = 0 #uniue interaction ids
- self.ids = 0 #only for internal purpose
-
-
- def isClientConnected(self, mode=Output.All):
- if mode == Output.All:
- return max(self.last_clients.values()) + self.CLIENT_THRESHOLD <= time()
- else:
- self.last_clients.get(mode, 0) + self.CLIENT_THRESHOLD <= time()
-
- def updateClient(self, mode):
- t = time()
- for output in self.last_clients:
- if bits_set(output, mode):
- self.last_clients[output] = t
+ def isClientConnected(self, user):
+ return self.last_clients.get(user, 0) + self.CLIENT_THRESHOLD > time()
@lock
def work(self):
# old notifications will be removed
- for n in [x for x in self.notifications if x.timedOut()]:
- self.notifications.remove(n)
-
- # store at most 100 notifications
- del self.notifications[50:]
+ for n in [k for k, v in self.tasks.iteritems() if v.timedOut()]:
+ del self.tasks[n]
+ # keep notifications count limited
+ n = [k for k,v in self.tasks.iteritems() if v.type == IA.Notification]
+ n.reverse()
+ for v in n[:self.MAX_NOTIFICATIONS]:
+ del self.tasks[v]
@lock
- def createNotification(self, title, content, desc="", plugin=""):
+ def createNotification(self, title, content, desc="", plugin="", owner=None):
""" Creates and queues a new Notification
:param title: short title
@@ -82,67 +71,86 @@ class InteractionManager:
:param plugin: plugin name
:return: :class:`InteractionTask`
"""
- task = InteractionTask(self.ids, Input.Text, [content], Output.Notification, "", title, desc, plugin)
+ task = InteractionTask(self.ids, IA.Notification, Input(InputType.Text, content), "", title, desc, plugin,
+ owner=owner)
self.ids += 1
- self.notifications.insert(0, task)
- self.handleTask(task)
+ self.queueTask(task)
return task
@lock
- def newQueryTask(self, input, data, desc, default="", plugin=""):
- task = InteractionTask(self.ids, input, to_list(data), Output.Query, default, _("Query"), desc, plugin)
+ def createQueryTask(self, input, desc, default="", plugin="", owner=None):
+ # input type was given, create a input widget
+ if type(input) == int:
+ input = Input(input)
+ if not isinstance(input, Input):
+ raise TypeError("'Input' class expected not '%s'" % type(input))
+
+ task = InteractionTask(self.ids, IA.Query, input, default, _("Query"), desc, plugin, owner=owner)
self.ids += 1
+ self.queueTask(task)
return task
@lock
- def newCaptchaTask(self, img, format, filename, plugin="", input=Input.Text):
+ def createCaptchaTask(self, img, format, filename, plugin="", type=InputType.Text, owner=None):
+ """ Createss a new captcha task.
+
+ :param img: image content (not base encoded)
+ :param format: img format
+ :param type: :class:`InputType`
+ :return:
+ """
+ if type == 'textual':
+ type = InputType.Text
+ elif type == 'positional':
+ type = InputType.Click
+
+ input = Input(type, [standard_b64encode(img), format, filename])
+
#todo: title desc plugin
- task = InteractionTask(self.ids, input, [img, format, filename],Output.Captcha,
- "", _("Captcha request"), _("Please solve the captcha."), plugin)
+ task = InteractionTask(self.ids, IA.Captcha, input,
+ None, _("Captcha request"), _("Please solve the captcha."), plugin, owner=owner)
+
self.ids += 1
+ self.queueTask(task)
return task
@lock
def removeTask(self, task):
if task.iid in self.tasks:
del self.tasks[task.iid]
+ self.core.evm.dispatchEvent("interaction:deleted", task.iid)
@lock
- def getTask(self, mode=Output.All):
- self.updateClient(mode)
-
- for task in self.tasks.itervalues():
- if mode == Output.All or bits_set(task.output, mode):
- return task
+ def getTaskByID(self, iid):
+ return self.tasks.get(iid, None)
@lock
- def getNotifications(self):
- """retrieves notifications, old ones are only deleted after a while\
- client has to make sure itself to dont display it twice"""
- for n in self.notifications:
- n.setWaiting(self.CLIENT_THRESHOLD * 5, True)
- #store notification for shorter period, lock the timeout
+ def getTasks(self, user, mode=IA.All):
+ # update last active clients
+ self.last_clients[user] = time()
- return self.notifications
+ # filter current mode
+ tasks = [t for t in self.tasks.itervalues() if mode == IA.All or bits_set(t.type, mode)]
+ # filter correct user / or shared
+ tasks = [t for t in tasks if user is None or user == t.owner or t.shared]
- def isTaskWaiting(self, mode=Output.All):
- return self.getTask(mode) is not None
+ return tasks
- @lock
- def getTaskByID(self, iid):
- if iid in self.tasks:
- task = self.tasks[iid]
- del self.tasks[iid]
- return task
+ def isTaskWaiting(self, user, mode=IA.All):
+ tasks = [t for t in self.getTasks(user, mode) if not t.type == IA.Notification or not t.seen]
+ return len(tasks) > 0
- def handleTask(self, task):
- cli = self.isClientConnected(task.output)
+ def queueTask(self, task):
+ cli = self.isClientConnected(task.owner)
- if cli: #client connected -> should handle the task
- task.setWaiting(self.CLIENT_THRESHOLD) # wait for response
+ # set waiting times based on threshold
+ if cli:
+ task.setWaiting(self.CLIENT_THRESHOLD)
+ else: # TODO: higher threshold after client connects?
+ task.setWaiting(self.CLIENT_THRESHOLD / 3)
- if task.output == Output.Notification:
- task.setWaiting(60 * 60 * 30) # notifications are valid for 30h
+ if task.type == IA.Notification:
+ task.setWaiting(self.NOTIFICATION_TIMEOUT) # notifications are valid for 30h
for plugin in self.core.addonManager.activePlugins():
try:
@@ -150,10 +158,9 @@ class InteractionManager:
except:
self.core.print_exc()
- if task.output != Output.Notification:
- self.tasks[task.iid] = task
+ self.tasks[task.iid] = task
+ self.core.evm.dispatchEvent("interaction:added", task)
if __name__ == "__main__":
-
it = InteractionTask() \ No newline at end of file
diff --git a/module/interaction/InteractionTask.py b/module/interaction/InteractionTask.py
index b372321b0..d2877b2b0 100644
--- a/module/interaction/InteractionTask.py
+++ b/module/interaction/InteractionTask.py
@@ -19,12 +19,12 @@
from time import time
from module.Api import InteractionTask as BaseInteractionTask
-from module.Api import Input, Output
+from module.Api import Interaction, InputType, Input
#noinspection PyUnresolvedReferences
class InteractionTask(BaseInteractionTask):
"""
- General Interaction Task extends ITask defined by thrift with additional fields and methods.
+ General Interaction Task extends ITask defined by api with additional fields and methods.
"""
#: Plugins can put needed data here
storage = None
@@ -38,8 +38,21 @@ class InteractionTask(BaseInteractionTask):
error = None
#: Timeout locked
locked = False
+ #: A task that was retrieved counts as seen
+ seen = False
+ #: A task that is relevant to every user
+ shared = False
+ #: primary uid of the owner
+ owner = None
def __init__(self, *args, **kwargs):
+ if 'owner' in kwargs:
+ self.owner = kwargs['owner']
+ del kwargs['owner']
+ if 'shared' in kwargs:
+ self.shared = kwargs['shared']
+ del kwargs['shared']
+
BaseInteractionTask.__init__(self, *args, **kwargs)
# additional internal attributes
@@ -54,28 +67,34 @@ class InteractionTask(BaseInteractionTask):
def getResult(self):
return self.result
+ def setShared(self):
+ """ enable shared mode, should not be reversed"""
+ self.shared = True
+
def setResult(self, value):
self.result = self.convertResult(value)
def setWaiting(self, sec, lock=False):
+ """ sets waiting in seconds from now, < 0 can be used as infinitive """
if not self.locked:
- self.wait_until = max(time() + sec, self.wait_until)
+ if sec < 0:
+ self.wait_until = -1
+ else:
+ self.wait_until = max(time() + sec, self.wait_until)
+
if lock: self.locked = True
def isWaiting(self):
- if self.result or self.error or time() > self.waitUntil:
+ if self.result or self.error or self.timedOut():
return False
return True
def timedOut(self):
- return time() > self.wait_until > 0
+ return time() > self.wait_until > -1
def correct(self):
[x.taskCorrect(self) for x in self.handler]
def invalid(self):
- [x.taskInvalid(self) for x in self.handler]
-
- def __str__(self):
- return "<InteractionTask '%s'>" % self.id \ No newline at end of file
+ [x.taskInvalid(self) for x in self.handler] \ No newline at end of file
diff --git a/module/plugins/Base.py b/module/plugins/Base.py
index 6ae2da249..70805b7f3 100644
--- a/module/plugins/Base.py
+++ b/module/plugins/Base.py
@@ -304,9 +304,8 @@ class Base(object):
ocr = OCR()
result = ocr.get_captcha(temp_file.name)
else:
- task = self.im.newCaptchaTask(img, imgtype, temp_file.name, result_type)
+ task = self.im.createCaptchaTask(img, imgtype, temp_file.name, self.__name__, result_type)
self.task = task
- self.im.handleTask(task)
while task.isWaiting():
if self.abort():
@@ -322,7 +321,7 @@ class Base(object):
elif task.error:
self.fail(task.error)
elif not task.result:
- self.fail(_("No captcha result obtained in appropriate time by any of the plugins."))
+ self.fail(_("No captcha result obtained in appropriate time."))
result = task.result
self.log.debug("Received captcha result: %s" % str(result))
diff --git a/module/plugins/hoster/BasePlugin.py b/module/plugins/hoster/BasePlugin.py
index 293049a1a..c07164161 100644
--- a/module/plugins/hoster/BasePlugin.py
+++ b/module/plugins/hoster/BasePlugin.py
@@ -31,10 +31,8 @@ class BasePlugin(Hoster):
#TODO: remove debug
if pyfile.url.lower().startswith("debug"):
- self.setWait(30)
- self.wait()
- self.decryptCaptcha("http://pyload.org/pie.png")
- self.download("http://pyload.org/random100.bin")
+ self.decryptCaptcha("http://download.pyload.org/pie.png")
+ self.download("http://download.pyload.org/random100.bin")
return
#
# if pyfile.url == "79":
diff --git a/module/remote/apitypes.py b/module/remote/apitypes.py
index 83368c6de..83eb19450 100644
--- a/module/remote/apitypes.py
+++ b/module/remote/apitypes.py
@@ -43,7 +43,7 @@ class FileStatus:
Missing = 1
Remote = 2
-class Input:
+class InputType:
NA = 0
Text = 1
Int = 2
@@ -58,6 +58,12 @@ class Input:
List = 11
Table = 12
+class Interaction:
+ All = 0
+ Notification = 1
+ Captcha = 2
+ Query = 4
+
class MediaType:
All = 0
Other = 1
@@ -67,12 +73,6 @@ class MediaType:
Document = 16
Archive = 32
-class Output:
- All = 0
- Notification = 1
- Captcha = 2
- Query = 4
-
class PackageStatus:
Ok = 0
Paused = 1
@@ -150,13 +150,13 @@ class ConfigInfo(BaseObject):
self.activated = activated
class ConfigItem(BaseObject):
- __slots__ = ['name', 'label', 'description', 'type', 'default_value', 'value']
+ __slots__ = ['name', 'label', 'description', 'input', 'default_value', 'value']
- def __init__(self, name=None, label=None, description=None, type=None, default_value=None, value=None):
+ def __init__(self, name=None, label=None, description=None, input=None, default_value=None, value=None):
self.name = name
self.label = label
self.description = description
- self.type = type
+ self.input = input
self.default_value = default_value
self.value = value
@@ -211,14 +211,20 @@ class FileInfo(BaseObject):
class Forbidden(ExceptionObject):
pass
+class Input(BaseObject):
+ __slots__ = ['type', 'data']
+
+ def __init__(self, type=None, data=None):
+ self.type = type
+ self.data = data
+
class InteractionTask(BaseObject):
- __slots__ = ['iid', 'input', 'data', 'output', 'default_value', 'title', 'description', 'plugin']
+ __slots__ = ['iid', 'type', 'input', 'default_value', 'title', 'description', 'plugin']
- def __init__(self, iid=None, input=None, data=None, output=None, default_value=None, title=None, description=None, plugin=None):
+ def __init__(self, iid=None, type=None, input=None, default_value=None, title=None, description=None, plugin=None):
self.iid = iid
+ self.type = type
self.input = input
- self.data = data
- self.output = output
self.default_value = default_value
self.title = title
self.description = description
@@ -438,12 +444,10 @@ class Iface(object):
pass
def getFilteredFiles(self, state):
pass
- def getInteractionTask(self, mode):
+ def getInteractionTasks(self, mode):
pass
def getLog(self, offset):
pass
- def getNotifications(self):
- pass
def getPackageContent(self, pid):
pass
def getPackageInfo(self, pid):
diff --git a/module/remote/apitypes_debug.py b/module/remote/apitypes_debug.py
index 4fab11f96..6909464d4 100644
--- a/module/remote/apitypes_debug.py
+++ b/module/remote/apitypes_debug.py
@@ -9,9 +9,9 @@ enums = [
"DownloadState",
"DownloadStatus",
"FileStatus",
- "Input",
+ "InputType",
+ "Interaction",
"MediaType",
- "Output",
"PackageStatus",
"Permission",
"Role",
@@ -23,21 +23,22 @@ classes = {
'AddonService' : [basestring, basestring, (list, basestring), (None, int)],
'ConfigHolder' : [basestring, basestring, basestring, basestring, (list, ConfigItem), (None, (list, AddonInfo)), (None, (list, InteractionTask))],
'ConfigInfo' : [basestring, basestring, basestring, basestring, bool, (None, bool)],
- 'ConfigItem' : [basestring, basestring, basestring, basestring, (None, basestring), basestring],
+ 'ConfigItem' : [basestring, basestring, basestring, Input, basestring, basestring],
'DownloadInfo' : [basestring, basestring, basestring, int, basestring, basestring],
'DownloadProgress' : [int, int, int, int],
'EventInfo' : [basestring, (list, basestring)],
'FileDoesNotExists' : [int],
'FileInfo' : [int, basestring, int, int, int, int, int, int, int, (None, DownloadInfo)],
- 'InteractionTask' : [int, int, (list, basestring), int, (None, basestring), basestring, basestring, basestring],
+ 'Input' : [int, (None, basestring)],
+ 'InteractionTask' : [int, int, Input, (None, basestring), basestring, basestring, basestring],
'InvalidConfigSection' : [basestring],
'LinkStatus' : [basestring, basestring, basestring, int, int, basestring],
- 'OnlineCheck' : [int, (dict, basestring, LinkStatus)],
+ 'OnlineCheck' : [int, (None, (dict, basestring, LinkStatus))],
'PackageDoesNotExists' : [int],
'PackageInfo' : [int, basestring, basestring, int, int, basestring, basestring, basestring, int, (list, basestring), int, bool, int, PackageStats, (list, int), (list, int)],
'PackageStats' : [int, int, int, int],
'ProgressInfo' : [basestring, basestring, basestring, int, int, int, (None, DownloadProgress)],
- 'ServerStatus' : [int, int, int, int, int, int, bool, bool, bool],
+ 'ServerStatus' : [int, int, int, int, int, bool, bool, bool, bool],
'ServiceDoesNotExists' : [basestring, basestring],
'ServiceException' : [basestring],
'TreeCollection' : [PackageInfo, (dict, int, FileInfo), (dict, int, PackageInfo)],
@@ -86,9 +87,8 @@ methods = {
'getFileTree': TreeCollection,
'getFilteredFileTree': TreeCollection,
'getFilteredFiles': TreeCollection,
- 'getInteractionTask': InteractionTask,
+ 'getInteractionTasks': (list, InteractionTask),
'getLog': (list, basestring),
- 'getNotifications': (list, InteractionTask),
'getPackageContent': TreeCollection,
'getPackageInfo': PackageInfo,
'getPluginConfig': (list, ConfigInfo),
diff --git a/module/remote/pyload.thrift b/module/remote/pyload.thrift
index 06add4208..76e755de0 100644
--- a/module/remote/pyload.thrift
+++ b/module/remote/pyload.thrift
@@ -69,7 +69,7 @@ enum PackageStatus {
// some may only be place holder currently not supported
// also all input - output combination are not reasonable, see InteractionManager for further info
// Todo: how about: time, ip, s.o.
-enum Input {
+enum InputType {
NA,
Text,
Int,
@@ -88,7 +88,7 @@ enum Input {
// this describes the type of the outgoing interaction
// ensure they can be logcial or'ed
-enum Output {
+enum Interaction {
All = 0,
Notification = 1,
Captcha = 2,
@@ -111,6 +111,11 @@ enum Role {
User = 1
}
+struct Input {
+ 1: InputType type,
+ 2: optional JSONString data,
+}
+
struct DownloadProgress {
1: FileID fid,
2: PackageID pid,
@@ -200,7 +205,7 @@ struct ServerStatus {
3: i16 linksqueue,
4: ByteCount sizetotal,
5: ByteCount sizequeue,
- 6: i16 notifications,
+ 6: bool notifications,
7: bool paused,
8: bool download,
9: bool reconnect,
@@ -208,13 +213,12 @@ struct ServerStatus {
struct InteractionTask {
1: InteractionID iid,
- 2: Input input,
- 3: list<string> data,
- 4: Output output,
- 5: optional JSONString default_value,
- 6: string title,
- 7: string description,
- 8: PluginName plugin,
+ 2: Interaction type,
+ 3: Input input,
+ 4: optional JSONString default_value,
+ 5: string title,
+ 6: string description,
+ 7: PluginName plugin,
}
struct AddonService {
@@ -234,7 +238,7 @@ struct ConfigItem {
1: string name,
2: string label,
3: string description,
- 4: string type,
+ 4: Input input,
5: JSONString default_value,
6: JSONString value,
}
@@ -360,7 +364,7 @@ service Pyload {
map<string, ConfigHolder> getConfig(),
string getConfigValue(1: string section, 2: string option),
- // two methods with ambigous classification, could be configuration or addon related
+ // two methods with ambigous classification, could be configuration or addon/plugin related
list<ConfigInfo> getCoreConfig(),
list<ConfigInfo> getPluginConfig(),
list<ConfigInfo> getAvailablePlugins(),
@@ -473,16 +477,14 @@ service Pyload {
// User Interaction
///////////////////////
- // mode = Output types binary ORed
+ // mode = interaction types binary ORed
bool isInteractionWaiting(1: i16 mode),
- InteractionTask getInteractionTask(1: i16 mode),
+ list<InteractionTask> getInteractionTasks(1: i16 mode),
void setInteractionResult(1: InteractionID iid, 2: JSONString result),
// generate a download link, everybody can download the file until timeout reached
string generateDownloadLink(1: FileID fid, 2: i16 timeout),
- list<InteractionTask> getNotifications(),
-
///////////////////////
// Account Methods
///////////////////////
diff --git a/module/remote/wsbackend/AsyncHandler.py b/module/remote/wsbackend/AsyncHandler.py
index 99ffe9894..b40f0ea4e 100644
--- a/module/remote/wsbackend/AsyncHandler.py
+++ b/module/remote/wsbackend/AsyncHandler.py
@@ -23,7 +23,7 @@ from time import time
from mod_pywebsocket.msgutil import receive_message
-from module.Api import EventInfo
+from module.Api import EventInfo, Interaction
from module.utils import lock
from AbstractHandler import AbstractHandler
@@ -44,7 +44,8 @@ class AsyncHandler(AbstractHandler):
COMMAND = "start"
PROGRESS_INTERVAL = 2
- EVENT_PATTERN = re.compile(r"^(package|file)", re.I)
+ EVENT_PATTERN = re.compile(r"^(package|file|interaction)", re.I)
+ INTERACTION = Interaction.All
def __init__(self, api):
AbstractHandler.__init__(self, api)
@@ -58,6 +59,7 @@ class AsyncHandler(AbstractHandler):
req.queue = Queue()
req.interval = self.PROGRESS_INTERVAL
req.events = self.EVENT_PATTERN
+ req.interaction = self.INTERACTION
req.mode = Mode.STANDBY
req.t = time() # time when update should be pushed
self.clients.append(req)
@@ -76,6 +78,18 @@ class AsyncHandler(AbstractHandler):
event = EventInfo(event, [x.toInfoData() if hasattr(x, 'toInfoData') else x for x in args])
for req in self.clients:
+ # filter events that these user is no owner of
+ # TODO: events are security critical, this should be revised later
+ if not req.api.user.isAdmin():
+ skip = False
+ for arg in args:
+ if hasattr(arg, 'owner') and arg.owner != req.api.primaryUID:
+ skip = True
+ break
+
+ # user should not get this event
+ if skip: break
+
if req.events.search(event.eventname):
self.log.debug("Pushing event %s" % event)
req.queue.put(event)
@@ -115,6 +129,8 @@ class AsyncHandler(AbstractHandler):
req.interval = args[0]
elif func == "setEvents":
req.events = re.compile(args[0], re.I)
+ elif func == "setInteraction":
+ req.interaction = args[0]
elif func == self.COMMAND:
req.mode = Mode.RUNNING
diff --git a/module/web/static/css/default/style.less b/module/web/static/css/default/style.less
index b3020d30f..f48aff9fd 100644
--- a/module/web/static/css/default/style.less
+++ b/module/web/static/css/default/style.less
@@ -326,6 +326,7 @@ header .logo {
.header-area {
display: none; // hidden by default
position: absolute;
+ bottom: -28px;
line-height: 18px;
top: @header-height;
padding: 4px 10px 6px 10px;
@@ -339,12 +340,19 @@ header .logo {
#notification-area {
.header-area;
left: 140px;
+
+ .badge {
+ vertical-align: top;
+ }
+
+ .btn-query, .btn-notification {
+ cursor: pointer;
+ }
}
#selection-area {
.header-area;
left: 50%;
- bottom: -28px;
min-width: 15%;
i {
diff --git a/module/web/static/js/collections/InteractionList.js b/module/web/static/js/collections/InteractionList.js
new file mode 100644
index 000000000..88651970e
--- /dev/null
+++ b/module/web/static/js/collections/InteractionList.js
@@ -0,0 +1,47 @@
+define(['jquery', 'backbone', 'underscore', 'app', 'models/InteractionTask'],
+ function($, Backbone, _, App, InteractionTask) {
+
+ return Backbone.Collection.extend({
+
+ model: InteractionTask,
+
+ comparator: function(task) {
+ return task.get('iid');
+ },
+
+ fetch: function(options) {
+ options = App.apiRequest('getInteractionTasks/0');
+
+ return Backbone.Collection.prototype.fetch.apply(this, options);
+ },
+
+ toJSON: function() {
+ var data = {queries: 0, notifications: 0, empty: false};
+
+ this.map(function(task) {
+ if (task.isNotification())
+ data.notifications++;
+ else
+ data.queries++;
+ });
+
+ if (!data.queries && !data.notifications)
+ data.empty = true;
+
+ return data;
+ },
+
+ // a task is waiting for attention (no notification)
+ hasTaskWaiting: function() {
+ var tasks = 0;
+ this.map(function(task) {
+ if (!task.isNotification())
+ tasks++;
+ });
+
+ return tasks > 0;
+ }
+
+ });
+
+ }); \ No newline at end of file
diff --git a/module/web/static/js/models/InteractionTask.js b/module/web/static/js/models/InteractionTask.js
new file mode 100644
index 000000000..4ba88a539
--- /dev/null
+++ b/module/web/static/js/models/InteractionTask.js
@@ -0,0 +1,27 @@
+define(['jquery', 'backbone', 'underscore', 'utils/apitypes'],
+ function($, Backbone, _, Api) {
+
+ return Backbone.Model.extend({
+
+ idAttribute: 'iid',
+
+ defaults: {
+ iid: -1,
+ type: null,
+ input: null,
+ default_value: null,
+ title: "",
+ description: "",
+ plugin: ""
+ },
+
+ // Model Constructor
+ initialize: function() {
+
+ },
+
+ isNotification: function() {
+ return this.get('type') === Api.Interaction.Notification;
+ }
+ });
+ }); \ No newline at end of file
diff --git a/module/web/static/js/utils/apitypes.js b/module/web/static/js/utils/apitypes.js
index c9fca48d6..28620250e 100644
--- a/module/web/static/js/utils/apitypes.js
+++ b/module/web/static/js/utils/apitypes.js
@@ -4,9 +4,9 @@ define([], function() {
DownloadState: {'Failed': 3, 'All': 0, 'Unmanaged': 4, 'Finished': 1, 'Unfinished': 2},
DownloadStatus: {'Downloading': 10, 'NA': 0, 'Processing': 14, 'Waiting': 9, 'Decrypting': 13, 'Paused': 4, 'Failed': 7, 'Finished': 5, 'Skipped': 6, 'Unknown': 16, 'Aborted': 12, 'Online': 2, 'TempOffline': 11, 'Offline': 1, 'Custom': 15, 'Starting': 8, 'Queued': 3},
FileStatus: {'Remote': 2, 'Ok': 0, 'Missing': 1},
- Input: {'Multiple': 10, 'Int': 2, 'NA': 0, 'List': 11, 'Bool': 7, 'File': 3, 'Text': 1, 'Table': 12, 'Folder': 4, 'Password': 6, 'Click': 8, 'Select': 9, 'Textbox': 5},
+ InputType: {'Multiple': 10, 'Int': 2, 'NA': 0, 'List': 11, 'Bool': 7, 'File': 3, 'Text': 1, 'Table': 12, 'Folder': 4, 'Password': 6, 'Click': 8, 'Select': 9, 'Textbox': 5},
+ Interaction: {'Captcha': 2, 'All': 0, 'Query': 4, 'Notification': 1},
MediaType: {'All': 0, 'Audio': 2, 'Image': 4, 'Other': 1, 'Video': 8, 'Document': 16, 'Archive': 32},
- Output: {'Captcha': 2, 'All': 0, 'Query': 4, 'Notification': 1},
PackageStatus: {'Paused': 1, 'Remote': 3, 'Folder': 2, 'Ok': 0},
Permission: {'All': 0, 'Interaction': 32, 'Modify': 4, 'Add': 1, 'Accounts': 16, 'Plugins': 64, 'Download': 8, 'Delete': 2},
Role: {'Admin': 0, 'User': 1},
diff --git a/module/web/static/js/views/headerView.js b/module/web/static/js/views/headerView.js
index 9e18734d4..b5b4a9d24 100644
--- a/module/web/static/js/views/headerView.js
+++ b/module/web/static/js/views/headerView.js
@@ -1,6 +1,6 @@
define(['jquery', 'underscore', 'backbone', 'app', 'models/ServerStatus', 'collections/ProgressList',
- 'views/progressView', 'helpers/formatSize', 'flot'],
- function($, _, Backbone, App, ServerStatus, ProgressList, ProgressView, formatSize) {
+ 'views/progressView', 'views/notificationView', 'helpers/formatSize', 'flot'],
+ function($, _, Backbone, App, ServerStatus, ProgressList, ProgressView, notificationView, formatSize) {
// Renders the header with all information
return Backbone.View.extend({
@@ -18,7 +18,6 @@ define(['jquery', 'underscore', 'backbone', 'app', 'models/ServerStatus', 'colle
// html elements
grabber: null,
- notifications: null,
header: null,
progress: null,
speedgraph: null,
@@ -29,12 +28,15 @@ define(['jquery', 'underscore', 'backbone', 'app', 'models/ServerStatus', 'colle
progressList: null,
speeds: null,
+ // sub view
+ notificationView: null,
+
// save if last progress was empty
wasEmpty: false,
initialize: function() {
var self = this;
- this.notifications = this.$('#notification-area').calculateHeight().height(0);
+ this.notificationView = new notificationView();
this.status = new ServerStatus();
this.listenTo(this.status, 'change', this.render);
@@ -98,7 +100,9 @@ define(['jquery', 'underscore', 'backbone', 'app', 'models/ServerStatus', 'colle
// queue/processing size?
var status = this.status.toJSON();
- status.maxspeed = _.max(this.speeds, function(speed) {return speed[1];})[1] * 1024;
+ status.maxspeed = _.max(this.speeds, function(speed) {
+ return speed[1];
+ })[1] * 1024;
this.$('.status-block').html(
this.templateStatus(status)
);
@@ -152,8 +156,8 @@ define(['jquery', 'underscore', 'backbone', 'app', 'models/ServerStatus', 'colle
if (data === null) return;
if (data['@class'] === "ServerStatus") {
+ // TODO: load interaction when none available
this.status.set(data);
-
this.speeds = this.speeds.slice(1);
this.speeds.push([this.speeds[this.speeds.length - 1][0] + 1, Math.floor(data.speed / 1024)]);
@@ -169,7 +173,7 @@ define(['jquery', 'underscore', 'backbone', 'app', 'models/ServerStatus', 'colle
else if (data['@class'] === 'EventInfo')
this.onEvent(data.eventname, data.event_args);
else
- console.log('Unknown Async input');
+ console.log('Unknown Async input', data);
},
diff --git a/module/web/static/js/views/notificationView.js b/module/web/static/js/views/notificationView.js
new file mode 100644
index 000000000..22c727304
--- /dev/null
+++ b/module/web/static/js/views/notificationView.js
@@ -0,0 +1,65 @@
+define(['jquery', 'backbone', 'underscore', 'app', 'collections/InteractionList'],
+ function($, Backbone, _, App, InteractionList) {
+
+ // Renders context actions for selection packages and files
+ return Backbone.View.extend({
+ el: '#notification-area',
+ template: _.compile($("#template-notification").html()),
+
+ events: {
+ 'click .btn-query': 'openQuery',
+ 'click .btn-notification': 'openNotifications'
+ },
+
+ tasks: null,
+ // current open task
+ current: null,
+ // area is slided out
+ visible: false,
+
+ initialize: function() {
+ this.tasks = new InteractionList();
+
+ this.$el.calculateHeight().height(0);
+
+ App.vent.on('interaction:added', _.bind(this.onAdd, this));
+ App.vent.on('interaction:deleted', _.bind(this.onDelete, this));
+
+ var render = _.bind(this.render, this);
+ this.listenTo(this.tasks, 'add', render);
+ this.listenTo(this.tasks, 'remove', render);
+
+ },
+
+ onAdd: function(task) {
+ this.tasks.add(task);
+ },
+
+ onDelete: function(task) {
+ this.tasks.remove(task);
+ },
+
+ render: function() {
+ this.$el.html(this.template(this.tasks.toJSON()));
+
+ if (this.tasks.length > 0 && !this.visible) {
+ this.$el.slideOut();
+ this.visible = true;
+ }
+ else if (this.tasks.length === 0 && this.visible) {
+ this.$el.slideIn();
+ this.visible = false;
+ }
+
+ return this;
+ },
+
+ openQuery: function() {
+
+ },
+
+ openNotifications: function() {
+
+ }
+ });
+ }); \ No newline at end of file
diff --git a/module/web/templates/default/base.html b/module/web/templates/default/base.html
index 6c0e7b999..dfcfb9e3a 100644
--- a/module/web/templates/default/base.html
+++ b/module/web/templates/default/base.html
@@ -92,6 +92,23 @@
</span>
</script>
+ <script type="text/template" id="template-notification">
+ <%= if queries %>
+ <span class="btn-query">
+ Queries <span class="badge badge-info"><% queries %></span>
+ </span>
+ <%/if%>
+ <%= if notifications %>
+ <span class="btn-notification">
+ Notifications <span class="badge badge-success"><% notifications %></span>
+ </span>
+ <%/if%>
+ <%= if empty %>
+ Nothing to show
+ <%/if%>
+ </%if%>
+ </script>
+
{% block head %}
{% endblock %}
</head>
@@ -157,9 +174,7 @@
</div>
{% endif %}
</div>
- <div id="notification-area" style="">
- Notifications
- <span class="badge badge-info">88</span>
+ <div id="notification-area">
</div>
<div id="selection-area">
</div>
diff --git a/tests/manager/test_interactionManager.py b/tests/manager/test_interactionManager.py
index db233bb25..0ab7af80d 100644
--- a/tests/manager/test_interactionManager.py
+++ b/tests/manager/test_interactionManager.py
@@ -1,12 +1,19 @@
# -*- coding: utf-8 -*-
from unittest import TestCase
+
from tests.helper.Stubs import Core
-from module.Api import Input, Output
+from module.Api import InputType, Interaction
from module.interaction.InteractionManager import InteractionManager
+
class TestInteractionManager(TestCase):
+ ADMIN = None
+ USER = 1
+
+ def assertEmpty(self, list1):
+ return self.assertListEqual(list1, [])
@classmethod
def setUpClass(cls):
@@ -15,44 +22,60 @@ class TestInteractionManager(TestCase):
def setUp(self):
self.im = InteractionManager(self.core)
+ self.assertFalse(self.im.isClientConnected(self.ADMIN))
+ self.assertFalse(self.im.isTaskWaiting(self.ADMIN))
+ self.assertEmpty(self.im.getTasks(self.ADMIN))
def test_notifications(self):
-
n = self.im.createNotification("test", "notify")
- assert self.im.getNotifications() == [n]
+
+ self.assertTrue(self.im.isTaskWaiting(self.ADMIN))
+ self.assertListEqual(self.im.getTasks(self.ADMIN), [n])
+
+ n.seen = True
+ self.assertFalse(self.im.isTaskWaiting(self.ADMIN))
for i in range(10):
self.im.createNotification("title", "test")
- assert len(self.im.getNotifications()) == 11
+ self.assertEqual(len(self.im.getTasks(self.ADMIN)), 11)
+ self.assertFalse(self.im.getTasks(self.USER))
+ self.assertFalse(self.im.getTasks(self.ADMIN, Interaction.Query))
def test_captcha(self):
- assert self.im.getTask() is None
-
- t = self.im.newCaptchaTask("1", "", "")
- assert t.output == Output.Captcha
- self.im.handleTask(t)
- assert t is self.im.getTask()
+ t = self.im.createCaptchaTask("1", "png", "", owner=self.ADMIN)
- t2 = self.im.newCaptchaTask("2", "", "")
- self.im.handleTask(t2)
-
- assert self.im.getTask(Output.Query) is None
- assert self.im.getTask() is t
+ self.assertEqual(t.type, Interaction.Captcha)
+ self.assertListEqual(self.im.getTasks(self.ADMIN), [t])
+ self.assertEmpty(self.im.getTasks(self.USER))
+ t.setShared()
+ self.assertListEqual(self.im.getTasks(self.USER), [t])
+ t2 = self.im.createCaptchaTask("2", "png", "", owner=self.USER)
+ self.assertTrue(self.im.isTaskWaiting(self.USER))
+ self.assertEmpty(self.im.getTasks(self.USER, Interaction.Query))
self.im.removeTask(t)
- assert self.im.getTask() is t2
- self.im.getTaskByID(t2.iid)
- assert self.im.getTask() is None
+ self.assertListEqual(self.im.getTasks(self.ADMIN), [t2])
+ self.assertIs(self.im.getTaskByID(t2.iid), t2)
def test_query(self):
- assert self.im.getTask() is None
- t = self.im.newQueryTask(Input.Text, None, "text")
- assert t.description == "text"
- self.im.handleTask(t)
+ t = self.im.createQueryTask(InputType.Text, "text", owner=self.ADMIN)
+
+ self.assertEqual(t.description, "text")
+ self.assertListEqual(self.im.getTasks(self.ADMIN, Interaction.Query), [t])
+ self.assertEmpty(self.im.getTasks(Interaction.Captcha))
+
+
+ def test_clients(self):
+ self.im.getTasks(self.ADMIN, Interaction.Captcha)
+
+ self.assertTrue(self.im.isClientConnected(self.ADMIN))
+ self.assertFalse(self.im.isClientConnected(self.USER))
+
- assert self.im.getTask(Output.Query) is t
- assert self.im.getTask(Output.Captcha) is None \ No newline at end of file
+ def test_users(self):
+ t = self.im.createCaptchaTask("1", "png", "", owner=self.USER)
+ self.assertListEqual(self.im.getTasks(self.ADMIN), [t])