changeset 938:fc7e0828b18e

plugin account, groupblog: user can erase all their microblogs at once
author souliane <souliane@mailoo.org>
date Thu, 27 Mar 2014 02:24:20 +0100
parents 255e6953b2c3
children 01342bfe9f41
files src/memory/params.py src/plugins/plugin_misc_account.py src/plugins/plugin_misc_groupblog.py
diffstat 3 files changed, 216 insertions(+), 17 deletions(-) [+]
line wrap: on
line diff
--- a/src/memory/params.py	Tue Mar 25 17:26:31 2014 +0100
+++ b/src/memory/params.py	Thu Mar 27 02:24:20 2014 +0100
@@ -154,9 +154,12 @@
         if not self.storage.hasProfile(profile):
             info(_('Trying to delete an unknown profile'))
             return defer.fail(Failure(exceptions.ProfileUnknownError))
-        if not force and self.host.isConnected(profile):
-            info(_("Trying to delete a connected profile"))
-            return defer.fail(Failure(exceptions.ConnectedProfileError))
+        if self.host.isConnected(profile):
+            if force:
+                self.host.disconnect(profile)
+            else:
+                info(_("Trying to delete a connected profile"))
+                return defer.fail(Failure(exceptions.ConnectedProfileError))
         return self.storage.deleteProfile(profile)
 
     def getProfileName(self, profile_key, return_profile_keys = False):
--- a/src/plugins/plugin_misc_account.py	Tue Mar 25 17:26:31 2014 +0100
+++ b/src/plugins/plugin_misc_account.py	Thu Mar 27 02:24:20 2014 +0100
@@ -34,6 +34,7 @@
     "type": "MISC",
     "protocols": [],
     "dependencies": [],
+    "recommendations": ['GROUPBLOG'],
     "main": "MiscAccount",
     "handler": "no",
     "description": _(u"""SàT account creation""")
@@ -133,6 +134,13 @@
         self.__account_cb_id = host.registerCallback(self._accountDialogCb, with_data=True)
         self.__delete_account_id = host.registerCallback(self.__deleteAccountCb, with_data=True)
 
+        def deleteBlogCallback(posts, comments):
+            return lambda data, profile: self.__deleteBlogPostsCb(posts, comments, data, profile)
+
+        self.__delete_posts_comments_id = host.registerCallback(deleteBlogCallback(True, True), with_data=True)
+        self.__delete_posts_id = host.registerCallback(deleteBlogCallback(True, False), with_data=True)
+        self.__delete_comments_id = host.registerCallback(deleteBlogCallback(False, True), with_data=True)
+
     def getConfig(self, name):
         return self.host.memory.getConfig(CONFIG_SECTION, name) or default_conf[name]
 
@@ -239,6 +247,7 @@
         """
         form_ui = xml_tools.XMLUI("form", "tabs", title=D_("Manage your XMPP account"), submit_id=self.__account_cb_id)
         tab_container = form_ui.current_container
+
         tab_container.addTab("update", D_("Change your password"), container=xml_tools.PairsContainer)
         form_ui.addLabel(D_("Current password"))
         form_ui.addPassword("current_passwd", value="")
@@ -246,6 +255,16 @@
         form_ui.addPassword("new_passwd1", value="")
         form_ui.addLabel(D_("New password (again)"))
         form_ui.addPassword("new_passwd2", value="")
+
+        if 'GROUPBLOG' in self.host.plugins:
+            tab_container.addTab("delete_posts", D_("Delete your posts"), container=xml_tools.PairsContainer)
+            form_ui.addLabel(D_("Current password"))
+            form_ui.addPassword("delete_posts_passwd", value="")
+            form_ui.addLabel(D_("Delete all your posts and their comments"))
+            form_ui.addBool("delete_posts_checkbox", "false")
+            form_ui.addLabel(D_("Delete all your comments on other's posts"))
+            form_ui.addBool("delete_comments_checkbox", "false")
+
         tab_container.addTab("delete", D_("Delete your account"), container=xml_tools.PairsContainer)
         form_ui.addLabel(D_("Current password"))
         form_ui.addPassword("delete_passwd", value="")
@@ -261,7 +280,7 @@
         password = self.host.memory.getParamA("Password", "Connection", profile_key=profile)
 
         def error_ui():
-            error_ui = xml_tools.XMLUI("popup", title="Error")
+            error_ui = xml_tools.XMLUI("popup", title=D_("Error"))
             error_ui.addText(D_("Passwords don't match!"))
             return defer.succeed({'xmlui': error_ui.toXml()})
 
@@ -273,6 +292,18 @@
                 return self.__deleteAccount(profile)
             return error_ui()
 
+        # check for blog posts deletion
+        if 'GROUPBLOG' in self.host.plugins:
+            delete_posts_passwd = data[xml_tools.SAT_FORM_PREFIX + 'delete_posts_passwd']
+            delete_posts_checkbox = data[xml_tools.SAT_FORM_PREFIX + 'delete_posts_checkbox']
+            delete_comments_checkbox = data[xml_tools.SAT_FORM_PREFIX + 'delete_comments_checkbox']
+            posts = delete_posts_checkbox == 'true'
+            comments = delete_comments_checkbox == 'true'
+            if posts or comments:
+                if password == delete_posts_passwd:
+                    return self.__deleteBlogPosts(posts, comments, profile)
+                return error_ui()
+
         # check for password modification
         current_passwd = data[xml_tools.SAT_FORM_PREFIX + 'current_passwd']
         new_passwd1 = data[xml_tools.SAT_FORM_PREFIX + 'new_passwd1']
@@ -291,12 +322,12 @@
         """
         def passwordChanged(result):
             self.host.memory.setParam("Password", password, "Connection", profile_key=profile)
-            confirm_ui = xml_tools.XMLUI("popup", title="Confirmation")
+            confirm_ui = xml_tools.XMLUI("popup", title=D_("Confirmation"))
             confirm_ui.addText(D_("Your password has been changed."))
             return defer.succeed({'xmlui': confirm_ui.toXml()})
 
         def errback(failure):
-            error_ui = xml_tools.XMLUI("popup", title="Error")
+            error_ui = xml_tools.XMLUI("popup", title=D_("Error"))
             error_ui.addText(D_("Your password could not be changed: %s") % failure.getErrorMessage())
             return defer.succeed({'xmlui': error_ui.toXml()})
 
@@ -308,8 +339,10 @@
         """Ask for a confirmation before deleting the XMPP account and SàT profile
         @param profile
         """
-        form_ui = xml_tools.XMLUI("form", title=D_("Delete your account ?"), submit_id=self.__delete_account_id)
+        form_ui = xml_tools.XMLUI("form", title=D_("Delete your account?"), submit_id=self.__delete_account_id)
         form_ui.addText(D_("If you confirm this dialog, you will be disconnected and then your XMPP account AND your SàT profile will both be DELETED."))
+        target = D_('contact list, messages history, blog posts and comments' if 'GROUPBLOG' in self.host.plugins else D_('contact list and messages history'))
+        form_ui.addText(D_("All your data stored on %(server)s, including your %(target)s will be erased.") % {'server': self._getNewAccountDomain(), 'target': target})
         form_ui.addText(D_("There is no other confirmation dialog, this is the very last one! Are you sure?"))
         return {'xmlui': form_ui.toXml()}
 
@@ -327,15 +360,77 @@
             for jid_ in self.host.memory.getWaitingSub(profile):  # delete waiting subscriptions
                 self.host.memory.delWaitingSub(jid_)
 
-            self.host.disconnect(profile)
-            self.host.memory.asyncDeleteProfile(profile, force=True)
+            delete_profile = lambda: self.host.memory.asyncDeleteProfile(profile, force=True)
+            if 'GROUPBLOG' in self.host.plugins:
+                d = self.host.plugins['GROUPBLOG'].deleteAllGroupBlogsAndComments(profile_key=profile)
+                d.addCallback(lambda dummy: delete_profile())
+            else:
+                delete_profile()
+
             return defer.succeed({})
 
         def errback(failure):
-            error_ui = xml_tools.XMLUI("popup", title="Error")
+            error_ui = xml_tools.XMLUI("popup", title=D_("Error"))
             error_ui.addText(D_("Your XMPP account could not be deleted: %s") % failure.getErrorMessage())
             return defer.succeed({'xmlui': error_ui.toXml()})
 
         d = ProsodyRegisterProtocol.prosodyctl(self, 'deluser', profile=profile)
         d.addCallbacks(userDeleted, errback)
         return d
+
+    def __deleteBlogPosts(self, posts, comments, profile):
+        """Ask for a confirmation before deleting the blog posts
+        @param posts: delete all posts of the user (and their comments)
+        @param comments: delete all the comments of the user on other's posts
+        @param data
+        @param profile
+        """
+        if posts:
+            if comments:  # delete everything
+                form_ui = xml_tools.XMLUI("form", title=D_("Delete all your (micro-)blog posts and comments?"), submit_id=self.__delete_posts_comments_id)
+                form_ui.addText(D_("If you confirm this dialog, all the (micro-)blog data you submitted will be erased."))
+                form_ui.addText(D_("These are the public and private posts and comments you sent to any group."))
+                form_ui.addText(D_("There is no other confirmation dialog, this is the very last one! Are you sure?"))
+            else:  # delete only the posts
+                form_ui = xml_tools.XMLUI("form", title=D_("Delete all your (micro-)blog posts?"), submit_id=self.__delete_posts_id)
+                form_ui.addText(D_("If you confirm this dialog, all the public and private posts you sent to any group will be erased."))
+                form_ui.addText(D_("There is no other confirmation dialog, this is the very last one! Are you sure?"))
+        elif comments:  # delete only the comments
+            form_ui = xml_tools.XMLUI("form", title=D_("Delete all your (micro-)blog comments?"), submit_id=self.__delete_comments_id)
+            form_ui.addText(D_("If you confirm this dialog, all the public and private comments you made on other people's posts will be erased."))
+            form_ui.addText(D_("There is no other confirmation dialog, this is the very last one! Are you sure?"))
+
+        return {'xmlui': form_ui.toXml()}
+
+    def __deleteBlogPostsCb(self, posts, comments, data, profile):
+        """Actually delete the XMPP account and SàT profile
+        @param posts: delete all posts of the user (and their comments)
+        @param comments: delete all the comments of the user on other's posts
+        @param profile
+        """
+        if posts:
+            if comments:
+                target = D_('blog posts and comments')
+                d = self.host.plugins['GROUPBLOG'].deleteAllGroupBlogsAndComments(profile_key=profile)
+            else:
+                target = D_('blog posts')
+                d = self.host.plugins['GROUPBLOG'].deleteAllGroupBlogs(profile_key=profile)
+        elif comments:
+            target = D_('comments')
+            d = self.host.plugins['GROUPBLOG'].deleteAllGroupBlogsComments(profile_key=profile)
+
+        def deleted(result):
+            ui = xml_tools.XMLUI("popup", title=D_("Deletion confirmation"))
+            # TODO: change the message when delete/retract notifications are done with XEP-0060
+            ui.addText(D_("Your %(target)s have been deleted.") % {'target': target})
+            ui.addText(D_("Known issue of the demo version: you need to refresh the page to make the deleted posts actually disappear."))
+            return defer.succeed({'xmlui': ui.toXml()})
+
+        def errback(failure):
+            error_ui = xml_tools.XMLUI("popup", title=D_("Error"))
+            error_ui.addText(D_("Your %(target)s could not be deleted: %(message)s") % {'target': target, 'message': failure.getErrorMessage()})
+            return defer.succeed({'xmlui': error_ui.toXml()})
+
+        d.addCallbacks(deleted, errback)
+        return d
+
--- a/src/plugins/plugin_misc_groupblog.py	Tue Mar 25 17:26:31 2014 +0100
+++ b/src/plugins/plugin_misc_groupblog.py	Thu Mar 27 02:24:20 2014 +0100
@@ -22,7 +22,7 @@
 from logging import debug, info, warning, error
 from twisted.internet import defer
 from twisted.words.protocols.jabber import jid
-from twisted.words.xish.domish import Element
+from twisted.words.xish.domish import Element, generateElementsNamed
 from sat.core import exceptions
 from wokkel import disco, data_form, iwokkel
 from zope.interface import implements
@@ -53,7 +53,7 @@
 
 PLUGIN_INFO = {
     "name": "Group blogging throught collections",
-    "import_name": "groupblog",
+    "import_name": "GROUPBLOG",
     "type": "MISC",
     "protocols": [],
     "dependencies": ["XEP-0277"],
@@ -415,8 +415,8 @@
         def notify(d):
             # TODO: this works only on the same host, and notifications for item deletion should be
             # implemented according to http://xmpp.org/extensions/xep-0060.html#publisher-delete-success-notify
-            # instead. The notification mechanisms implemented in sat_pubsub and wokkel seem not compatible,
-            # see wokkel.pubsub.PubSubClient._onEvent_items and sat_pubsub.backend._doNotifyRetraction
+            # instead. The notification mechanism implemented in sat_pubsub and wokkel have apriori
+            # a problem with retrieving the subscriptions, or something else.
             service, node, item_id = pub_data
             publisher = self.host.getJidNStream(profile_key)[0]
             profile = self.host.memory.getProfileName(profile_key)
@@ -572,7 +572,6 @@
         d.addCallback(get_comments)
         return d
 
-
     def getLastGroupBlogs(self, pub_jid_s, max_items=10, profile_key=C.PROF_KEY_NONE):
         """Get the last published microblogs
         @param pub_jid_s: jid of the publisher
@@ -665,7 +664,7 @@
         """
 
         def sendResult(result):
-            """send result of DeferredList (list of microblogs to the calling method"""
+            """send result of DeferredList (dict of jid => microblogs) to the calling method"""
 
             ret = {}
 
@@ -693,7 +692,6 @@
 
             mblogs = []
 
-
             for jid_ in jids:
                 d = self.host.plugins["XEP-0060"].getItems(client.item_access_pubsub, self.getNodeName(jid_),
                                                            max_items=max_items, profile_key=profile_key)
@@ -773,6 +771,109 @@
         return self._initialise(profile_key).addCallback(initialised)
         #TODO: we need to use the server corresponding the the host of the jid
 
+    def deleteAllGroupBlogsAndComments(self, profile_key=C.PROF_KEY_NONE):
+        """Delete absolutely all the microblog data that the user has posted"""
+        calls = [self.deleteAllGroupBlogs(profile_key), self.deleteAllGroupBlogsComments(profile_key)]
+        return defer.DeferredList(calls)
+
+    def deleteAllGroupBlogs(self, profile_key=C.PROF_KEY_NONE):
+        """Delete all the main items and their comments that the user has posted
+        """
+        def initialised(result):
+            profile, client = result
+            service = client.item_access_pubsub
+            jid_ = client.jid
+
+            main_node = self.getNodeName(jid_)
+            d = self.host.plugins["XEP-0060"].deleteNode(service, main_node, profile_key=profile)
+            d.addCallback(lambda dummy: info(_("All microblog's main items from %s have been deleted!") % jid_.userhost()))
+            d.addErrback(lambda failure: error(_("Deletion of node %(node)s failed: %(message)s") %
+                                               {'node': main_node, 'message': failure.getErrorMessage()}))
+            return d
+
+        return self._initialise(profile_key).addCallback(initialised)
+
+    def deleteAllGroupBlogsComments(self, profile_key=C.PROF_KEY_NONE):
+        """Delete all the comments that the user posted on other's main items.
+        We avoid the conversions from item to microblog data as we only need
+        to retrieve some attributes, no need to convert text syntax...
+        """
+        def initialised(result):
+            """Get all the main items from our contact list
+            @return: a DeferredList
+            """
+            profile, client = result
+            service = client.item_access_pubsub
+            jids = [contact.jid.userhostJID() for contact in client.roster.getItems()]
+            blogs = []
+            for jid_ in jids:
+                main_node = self.getNodeName(jid_)
+                d = self.host.plugins["XEP-0060"].getItems(service, main_node, profile_key=profile)
+                d.addCallback(getComments, client)
+                d.addErrback(lambda failure, main_node: error(_("Retrieval of items for node %(node)s failed: %(message)s") %
+                                                              {'node': main_node, 'message': failure.getErrorMessage()}), main_node)
+                blogs.append(d)
+
+            return defer.DeferredList(blogs)
+
+        def getComments(items, client):
+            """Get all the comments for a list of items
+            @param items: a list of main items for one user
+            @param client: the client of the user
+            @return: a DeferredList
+            """
+            comments = []
+            for item in items:
+                try:
+                    entry = generateElementsNamed(item.elements(), 'entry').next()
+                    link = generateElementsNamed(entry.elements(), 'link').next()
+                except StopIteration:
+                    continue
+                href = link.getAttribute('href')
+                service, node = self.host.plugins['XEP-0277'].parseCommentUrl(href)
+                d = self.host.plugins["XEP-0060"].getItems(service, node, profile_key=profile_key)
+                d.addCallback(lambda items, service, node: (service, node, items), service, node)
+                d.addErrback(lambda failure, node: error(_("Retrieval of comments for node %(node)s failed: %(message)s") %
+                                                         {'node': node, 'message': failure.getErrorMessage()}), node)
+                comments.append(d)
+            dlist = defer.DeferredList(comments)
+            dlist.addCallback(deleteComments, client)
+            return dlist
+
+        def deleteComments(result, client):
+            """Delete all the comments of the user that are found in result
+            @param result: a list of couple (success, value) with success a
+            boolean and value a tuple (service as JID, node_id, comment_items)
+            @param client: the client of the user
+            @return: a DeferredList with the deletions result
+            """
+            user_jid_s = client.jid.userhost()
+            for (success, value) in result:
+                if not success:
+                    continue
+                service, node_id, comment_items = value
+                item_ids = []
+                for comment_item in comment_items:  # for all the comments on one post
+                    try:
+                        entry = generateElementsNamed(comment_item.elements(), 'entry').next()
+                        author = generateElementsNamed(entry.elements(), 'author').next()
+                        name = generateElementsNamed(author.elements(), 'name').next()
+                    except StopIteration:
+                        continue
+                    if name.children[0] == user_jid_s:
+                        item_ids.append(comment_item.getAttribute('id'))
+                deletions = []
+                if item_ids:  # remove the comments of the user on the given post
+                    d = self.host.plugins['XEP-0060'].retractItems(service, node_id, item_ids, profile_key=profile_key)
+                    d.addCallback(lambda dummy, node_id: debug(_('Comments of user %(user)s in node %(node)s have been retracted') %
+                                                      {'user': user_jid_s, 'node': node_id}), node_id)
+                    d.addErrback(lambda failure, node_id: error(_("Retraction of comments from %(user)s in node %(node)s failed: %(message)s") %
+                                                       {'user': user_jid_s, 'node': node_id, 'message': failure.getErrorMessage()}), node_id)
+                    deletions.append(d)
+            return defer.DeferredList(deletions)
+
+        return self._initialise(profile_key).addCallback(initialised)
+
 
 class GroupBlog_handler(XMPPHandler):
     implements(iwokkel.IDisco)