Mercurial > libervia-backend
view sat_frontends/quick_frontend/quick_blog.py @ 4061:fddd76dedc97
tests (units): tests for plugin XEP-0293:
fix 437
author | Goffi <goffi@goffi.org> |
---|---|
date | Mon, 29 May 2023 17:58:05 +0200 |
parents | 524856bd7b19 |
children | 4b842c1fb686 |
line wrap: on
line source
#!/usr/bin/env python3 # helper class for making a SAT frontend # Copyright (C) 2011-2021 Jérôme Poisson <goffi@goffi.org> # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # from sat.core.i18n import _, D_ from sat.core.log import getLogger log = getLogger(__name__) from sat_frontends.quick_frontend.constants import Const as C from sat_frontends.quick_frontend import quick_widgets from sat_frontends.tools import jid from sat.tools.common import data_format try: # FIXME: to be removed when an acceptable solution is here str("") # XXX: unicode doesn't exist in pyjamas except ( TypeError, AttributeError, ): # Error raised is not the same depending on pyjsbuild options str = str ENTRY_CLS = None COMMENTS_CLS = None class Item(object): """Manage all (meta)data of an item""" def __init__(self, data): """ @param data(dict): microblog data as return by bridge methods if data values are not defined, set default values """ self.id = data["id"] self.title = data.get("title") self.title_rich = None self.title_xhtml = data.get("title_xhtml") self.tags = data.get('tags', []) self.content = data.get("content") self.content_rich = None self.content_xhtml = data.get("content_xhtml") self.author = data["author"] try: author_jid = data["author_jid"] self.author_jid = jid.JID(author_jid) if author_jid else None except KeyError: self.author_jid = None self.author_verified = data.get("author_jid_verified", False) try: self.updated = float( data["updated"] ) # XXX: int doesn't work here (pyjamas bug) except KeyError: self.updated = None try: self.published = float( data["published"] ) # XXX: int doesn't work here (pyjamas bug) except KeyError: self.published = None self.comments = data.get("comments") try: self.comments_service = jid.JID(data["comments_service"]) except KeyError: self.comments_service = None self.comments_node = data.get("comments_node") # def loadComments(self): # """Load all the comments""" # index = str(main_entry.comments_count - main_entry.hidden_count) # rsm = {'max': str(main_entry.hidden_count), 'index': index} # self.host.bridge.call('getMblogComments', self.mblogsInsert, main_entry.comments_service, main_entry.comments_node, rsm) class EntriesManager(object): """Class which manages list of (micro)blog entries""" def __init__(self, manager): """ @param manager (EntriesManager, None): parent EntriesManager must be None for QuickBlog (and only for QuickBlog) """ self.manager = manager if manager is None: self.blog = self else: self.blog = manager.blog self.entries = [] self.edit_entry = None @property def level(self): """indicate how deep is this entry in the tree if level == -1, we have a QuickBlog if level == 0, we have a main item else we have a comment """ level = -1 manager = self.manager while manager is not None: level += 1 manager = manager.manager return level def _add_mb_items(self, items_tuple, service=None, node=None): """Add Microblog items to this panel update is NOT called after addition @param items_tuple(tuple): (items_data,items_metadata) tuple as returned by mb_get """ items, metadata = items_tuple for item in items: self.add_entry(item, service=service, node=node, with_update=False) def _add_mb_items_with_comments(self, items_tuple, service=None, node=None): """Add Microblog items to this panel update is NOT called after addition @param items_tuple(tuple): (items_data,items_metadata) tuple as returned by mb_get """ items, metadata = items_tuple for item, comments in items: self.add_entry(item, comments, service=service, node=node, with_update=False) def add_entry(self, item=None, comments=None, service=None, node=None, with_update=True, editable=False, edit_entry=False): """Add a microblog entry @param editable (bool): True if the entry can be modified @param item (dict, None): blog item data, or None for an empty entry @param comments (list, None): list of comments data if available @param service (jid.JID, None): service where the entry is coming from @param service (unicode, None): node hosting the entry @param with_update (bool): if True, udpate is called with the new entry @param edit_entry(bool): if True, will be in self.edit_entry instead of self.entries, so it can be managed separately (e.g. first or last entry regardless of sorting) """ new_entry = ENTRY_CLS(self, item, comments, service=service, node=node) new_entry.set_editable(editable) if edit_entry: self.edit_entry = new_entry else: self.entries.append(new_entry) if with_update: self.update() return new_entry def update(self, entry=None): """Update the display with entries @param entry (Entry, None): if not None, must be the new entry. If None, all the items will be checked to update the display """ # update is separated from add_entry to allow adding # several entries at once, and updating at the end raise NotImplementedError class Entry(EntriesManager): """Graphical representation of an Item This class must be overriden by frontends""" def __init__( self, manager, item_data=None, comments_data=None, service=None, node=None ): """ @param blog(QuickBlog): the parent QuickBlog @param manager(EntriesManager): the parent EntriesManager @param item_data(dict, None): dict containing the blog item data, or None for an empty entry @param comments_data(list, None): list of comments data """ assert manager is not None EntriesManager.__init__(self, manager) self.service = service self.node = node self.editable = False self.reset(item_data) self.blog.id2entries[self.item.id] = self if self.item.comments: node_tuple = (self.item.comments_service, self.item.comments_node) self.blog.node2entries.setdefault(node_tuple, []).append(self) def reset(self, item_data): """Reset the entry with given data used during init (it's a set and not a reset then) or later (e.g. message sent, or cancellation of an edition @param idem_data(dict, None): data as in __init__ """ if item_data is None: self.new = True item_data = { "id": None, # TODO: find a best author value "author": self.blog.host.whoami.node, } else: self.new = False self.item = Item(item_data) self.author_jid = self.blog.host.whoami.bare if self.new else self.item.author_jid if self.author_jid is None and self.service and self.service.node: self.author_jid = self.service self.mode = ( C.ENTRY_MODE_TEXT if self.item.content_xhtml is None else C.ENTRY_MODE_XHTML ) def refresh(self): """Refresh the display when data have been modified""" pass def set_editable(self, editable=True): """tell if the entry can be edited or not @param editable(bool): True if the entry can be edited """ # XXX: we don't use @property as property setter doesn't play well with pyjamas raise NotImplementedError def add_comments(self, comments_data): """Add comments to this entry by calling add_entry repeatidly @param comments_data(tuple): data as returned by mb_get_from_many*RTResults """ # TODO: manage seperator between comments of coming from different services/nodes for data in comments_data: service, node, failure, comments, metadata = data for comment in comments: if not failure: self.add_entry(comment, service=jid.JID(service), node=node) else: log.warning("getting comment failed: {}".format(failure)) self.update() def send(self): """Send entry according to parent QuickBlog configuration and current level""" # keys to keep other than content*, title* and tag* # FIXME: see how to avoid comments node hijacking (someone could bind his post to another post's comments node) keys_to_keep = ("id", "comments", "author", "author_jid", "published") mb_data = {} for key in keys_to_keep: value = getattr(self.item, key) if value is not None: mb_data[key] = str(value) for prefix in ("content", "title"): for suffix in ("", "_rich", "_xhtml"): name = "{}{}".format(prefix, suffix) value = getattr(self.item, name) if value is not None: mb_data[name] = value mb_data['tags'] = self.item.tags if self.blog.new_message_target not in (C.PUBLIC, C.GROUP): raise NotImplementedError if self.level == 0: mb_data["allow_comments"] = True if self.blog.new_message_target == C.GROUP: mb_data['groups'] = list(self.blog.targets) self.blog.host.bridge.mb_send( str(self.service or ""), self.node or "", data_format.serialise(mb_data), profile=self.blog.profile, ) def delete(self): """Remove this Entry from parent manager This doesn't delete any entry in PubSub, just locally all children entries will be recursively removed too """ # XXX: named delete and not remove to avoid conflict with pyjamas log.debug("deleting entry {}".format("EDIT ENTRY" if self.new else self.item.id)) for child in self.entries: child.delete() try: self.manager.entries.remove(self) except ValueError: if self != self.manager.edit_entry: log.error("Internal Error: entry not found in manager") else: self.manager.edit_entry = None if not self.new: # we must remove references to self # in QuickBlog's dictionary del self.blog.id2entries[self.item.id] if self.item.comments: comments_tuple = (self.item.comments_service, self.item.comments_node) other_entries = self.blog.node2entries[comments_tuple].remove(self) if not other_entries: del self.blog.node2entries[comments_tuple] def retract(self): """Retract this item from microblog node if there is a comments node, it will be purged too """ # TODO: manage several comments nodes case. if self.item.comments: self.blog.host.bridge.ps_node_delete( str(self.item.comments_service) or "", self.item.comments_node, profile=self.blog.profile, ) self.blog.host.bridge.mb_retract( str(self.service or ""), self.node or "", self.item.id, profile=self.blog.profile, ) class QuickBlog(EntriesManager, quick_widgets.QuickWidget): def __init__(self, host, targets, profiles=None): """Panel used to show microblog @param targets (tuple(unicode)): contact groups displayed in this panel. If empty, show all microblogs from all contacts. targets is also used to know where to send new messages. """ EntriesManager.__init__(self, None) self.id2entries = {} # used to find an entry with it's item id # must be kept up-to-date by Entry self.node2entries = {} # same as above, values are lists in case of # two entries link to the same comments node if not targets: targets = () # XXX: we use empty tuple instead of None to workaround a pyjamas bug quick_widgets.QuickWidget.__init__(self, host, targets, C.PROF_KEY_NONE) self._targets_type = C.ALL else: assert isinstance(targets[0], str) quick_widgets.QuickWidget.__init__(self, host, targets[0], C.PROF_KEY_NONE) for target in targets[1:]: assert isinstance(target, str) self.add_target(target) self._targets_type = C.GROUP @property def new_message_target(self): if self._targets_type == C.ALL: return C.PUBLIC elif self._targets_type == C.GROUP: return C.GROUP else: raise ValueError("Unkown targets type") def __str__(self): return "Blog Widget [target: {}, profile: {}]".format( ", ".join(self.targets), self.profile ) def _get_results_cb(self, data, rt_session): remaining, results = data log.debug( "Got {got_len} results, {rem_len} remaining".format( got_len=len(results), rem_len=remaining ) ) for result in results: service, node, failure, items_data, metadata = result for item_data in items_data: item_data[0] = data_format.deserialise(item_data[0]) for item_metadata in item_data[1]: item_metadata[3] = [data_format.deserialise(i) for i in item_metadata[3]] if not failure: self._add_mb_items_with_comments((items_data, metadata), service=jid.JID(service)) self.update() if remaining: self._get_results(rt_session) def _get_results_eb(self, failure): log.warning("microblog get_from_many error: {}".format(failure)) def _get_results(self, rt_session): """Manage results from mb_get_from_many RT Session @param rt_session(str): session id as returned by mb_get_from_many """ self.host.bridge.mb_get_from_many_with_comments_rt_result( rt_session, profile=self.profile, callback=lambda data: self._get_results_cb(data, rt_session), errback=self._get_results_eb, ) def get_all(self): """Get all (micro)blogs from self.targets""" def got_session(rt_session): self._get_results(rt_session) if self._targets_type in (C.ALL, C.GROUP): targets = tuple(self.targets) if self._targets_type is C.GROUP else () self.host.bridge.mb_get_from_many_with_comments( self._targets_type, targets, 10, 10, {}, {"subscribe": C.BOOL_TRUE}, profile=self.profile, callback=got_session, ) own_pep = self.host.whoami.bare self.host.bridge.mb_get_from_many_with_comments( C.JID, (str(own_pep),), 10, 10, {}, {}, profile=self.profile, callback=got_session, ) else: raise NotImplementedError( "{} target type is not managed".format(self._targets_type) ) def is_jid_accepted(self, jid_): """Tell if a jid is actepted and must be shown in this panel @param jid_(jid.JID): jid to check @return: True if the jid is accepted """ if self._targets_type == C.ALL: return True assert self._targets_type is C.GROUP # we don't manage other types for now for group in self.targets: if self.host.contact_lists[self.profile].is_entity_in_group(jid_, group): return True return False def add_entry_if_accepted(self, service, node, mb_data, groups, profile): """add entry to this panel if it's acceptable This method check if the entry is new or an update, if it below to a know node, or if it acceptable anyway @param service(jid.JID): jid of the emitting pubsub service @param node(unicode): node identifier @param mb_data: microblog data @param groups(list[unicode], None): groups which can receive this entry None to accept everything @param profile: %(doc_profile)s """ try: entry = self.id2entries[mb_data["id"]] except KeyError: # The entry is new try: parent_entries = self.node2entries[(service, node)] except: # The node is unknown, # we need to check that we can accept the entry if ( self.is_jid_accepted(service) or ( groups is None and service == self.host.profiles[self.profile].whoami.bare ) or (groups and groups.intersection(self.targets)) ): self.add_entry(mb_data, service=service, node=node) else: # the entry is a comment in a known node for parent_entry in parent_entries: parent_entry.add_entry(mb_data, service=service, node=node) else: # The entry exist, it's an update entry.reset(mb_data) entry.refresh() def delete_entry_if_present(self, service, node, item_id, profile): """Delete and entry if present in this QuickBlog @param sender(jid.JID): jid of the entry sender @param mb_data: microblog data @param service(jid.JID): sending service @param node(unicode): hosting node """ try: entry = self.id2entries[item_id] except KeyError: pass else: entry.delete() def register_class(type_, cls): global ENTRY_CLS, COMMENTS_CLS if type_ == "ENTRY": ENTRY_CLS = cls elif type == "COMMENT": COMMENTS_CLS = cls else: raise ValueError("type_ should be ENTRY or COMMENT") if COMMENTS_CLS is None: COMMENTS_CLS = ENTRY_CLS