diff options
Diffstat (limited to 'module')
| -rw-r--r-- | module/api/CoreApi.py | 5 | ||||
| -rw-r--r-- | module/api/UserInteractionApi.py | 32 | ||||
| -rw-r--r-- | module/config/default.py | 1 | ||||
| -rw-r--r-- | module/interaction/InteractionManager.py | 143 | ||||
| -rw-r--r-- | module/interaction/InteractionTask.py | 37 | ||||
| -rw-r--r-- | module/plugins/Base.py | 5 | ||||
| -rw-r--r-- | module/plugins/hoster/BasePlugin.py | 6 | ||||
| -rw-r--r-- | module/remote/apitypes.py | 38 | ||||
| -rw-r--r-- | module/remote/apitypes_debug.py | 16 | ||||
| -rw-r--r-- | module/remote/pyload.thrift | 34 | ||||
| -rw-r--r-- | module/remote/wsbackend/AsyncHandler.py | 20 | ||||
| -rw-r--r-- | module/web/static/css/default/style.less | 10 | ||||
| -rw-r--r-- | module/web/static/js/collections/InteractionList.js | 47 | ||||
| -rw-r--r-- | module/web/static/js/models/InteractionTask.js | 27 | ||||
| -rw-r--r-- | module/web/static/js/utils/apitypes.js | 4 | ||||
| -rw-r--r-- | module/web/static/js/views/headerView.js | 18 | ||||
| -rw-r--r-- | module/web/static/js/views/notificationView.js | 65 | ||||
| -rw-r--r-- | module/web/templates/default/base.html | 21 | 
18 files changed, 368 insertions, 161 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>
  | 
