# HG changeset patch # User souliane # Date 1395883460 -3600 # Node ID fc7e0828b18e413e3a41688799c59b5636f1abc3 # Parent 255e6953b2c35fec70421b7892b196cc678ab813 plugin account, groupblog: user can erase all their microblogs at once diff -r 255e6953b2c3 -r fc7e0828b18e src/memory/params.py --- 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): diff -r 255e6953b2c3 -r fc7e0828b18e src/plugins/plugin_misc_account.py --- 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 + diff -r 255e6953b2c3 -r fc7e0828b18e src/plugins/plugin_misc_groupblog.py --- 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)