def __init__(self, client, jid): log.debug("Initializing channel %s", jid) # Avoid invalid channel names user, domain = jid.split("@", 1) if len(user) == 0 or len(domain) == 0: raise InvalidChannelName(jid) self.client = client self.jid = jid # All the channels items, newest first (as returned by the server) self.atoms = UpdatableAtomsList() self.atoms_lock = threading.RLock() self.load_event = threading.Event() self.loading = False self.oldest_id = None # Callbacks self.callback_config = None self.callback_post = None self.callback_retract = None self.callback_status = None
class Channel: """A buddycloud channel, tied to a client""" # {{{ Channel init CONFIG_MAP = ( ("title", "pubsub#title"), ("description", "pubsub#description"), ("creation", "pubsub#creation_date"), ("type", "buddycloud#channel_type") ) def __init__(self, client, jid): log.debug("Initializing channel %s", jid) # Avoid invalid channel names user, domain = jid.split("@", 1) if len(user) == 0 or len(domain) == 0: raise InvalidChannelName(jid) self.client = client self.jid = jid # All the channels items, newest first (as returned by the server) self.atoms = UpdatableAtomsList() self.atoms_lock = threading.RLock() self.load_event = threading.Event() self.loading = False self.oldest_id = None # Callbacks self.callback_config = None self.callback_post = None self.callback_retract = None self.callback_status = None def __iter__(self): return iter(self.atoms) def __repr__(self): return "<bccc.client.Channel {}>".format(self.jid) def set_callbacks(self, cb_config=None, cb_post=None, cb_retract=None, cb_status=None): if cb_config is not None: self.callback_config = cb_config if cb_post is not None: self.callback_post = cb_post if cb_retract is not None: self.callback_retract = cb_retract if cb_status is not None: self.callback_status = cb_status # }}} # {{{ Subscriptions/affiliations def get_subscriptions(self): channels = [] subnode = "/user/" + self.jid + "/subscriptions" items = self.client.ps.get_items(self.client.inbox_jid, subnode, block=True) for item in items["pubsub"]["items"]: try: chan = self.client.get_channel(item["id"]) channels.append(chan) except ChannelError: pass return channels # }}} # {{{ PubSub event handlers def handle_post_event(self, entries): # Incoming entries: add them and trigger the callback if len(entries) == 0: return atoms = [] for elt in entries: a = self.atoms.add(elt) if a is not None: atoms.append(a) if len(atoms) > 0 and self.callback_post is not None: self.callback_post(atoms) def handle_retract_event(self, entries): if len(entries) == 0: return # Remove retracted items from self.atoms with self.atoms_lock: for id_ in entries: self.atoms.remove(id_) if self.callback_retract is not None: self.callback_retract(entries) def handle_status_event(self, entries): if len(entries) == 0: return elt = entries[0] if elt is not None: a = Atom(elt) if self.callback_status is not None: self.callback_status(a) def handle_config_event(self, config_events): for conf in config_events: # Convert conf to a dict val = conf["form"]["values"] config = {} for (dk, ik) in self.CONFIG_MAP: if ik in val: config[dk] = val[ik].strip() if "creation" in config: config["creation"] = dateutil.parser.parse(config["creation"]) if self.callback_config is not None: self.callback_config(config) # }}} # {{{ Internal helpers def _items_to_atoms(self, items, callback=None): atoms = [] with self.atoms_lock: for item in items["pubsub"]["items"]: elt = item.get_payload() a = self.atoms.add(elt) if a is not None: atoms.append(a) if len(atoms) > 0 and callback is not None: callback(atoms) return atoms # }}} # {{{ PubSub requests def pubsub_get_items(self, node, callback, max=None, before=None, after=None): """ Request the contents of a node's items. This is based on sleekxmpp.plugins.xep_0060.pubsub.xep_0060.get_items(), but uses XEP-0059 instead of the "max_items" attribute (cf. XEP-0060:6.5.7). """ iq = self.client.ps.xmpp.Iq(sto=self.client.inbox_jid, stype="get") iq["pubsub"]["items"]["node"] = node if max is not None: iq["pubsub"]["rsm"]["max"] = str(max) if before is not None: iq["pubsub"]["rsm"]["before"] = before if after is not None: iq["pubsub"]["rsm"]["after"] = after iq.send(callback=callback) def pubsub_get_post(self, item_id): node = "/user/{}/posts".format(self.jid) cb = lambda items: self._items_to_atoms(items, self.callback_post) self.client.ps.get_item(self.client.inbox_jid, node, item_id, block=False, callback=cb) def pubsub_get_posts(self, max=None, before=None, after=None): node = "/user/{}/posts".format(self.jid) cb = lambda items: self._items_to_atoms(items, self.callback_post) return self.pubsub_get_items(node, cb, max, before, after) def pubsub_get_status(self): def _status_cb(items): entries = [item.get_payload() for item in items["pubsub"]["items"]] while None in entries: entries.remove(None) if len(entries) > 0: self.handle_status_event(entries) node = "/user/{}/status".format(self.jid) self.pubsub_get_items(node, callback=_status_cb, max=1) def pubsub_get_config(self): def _config_cb(iq): conf = iq["pubsub_owner"]["configure"] self.handle_config_event([conf]) node = "/user/{}/posts".format(self.jid) self.client.ps.get_node_config(self.client.inbox_jid, node, callback=_config_cb) # }}} # {{{ Thread loading def get_partial_thread(self, first_id, last_id): # Hard to read. Sorry. node = "/user/{}/posts".format(self.jid) # Callback for items after first_id. Requests more items until last_id is found. def _other_posts_cb(atoms): if self.callback_post is not None: self.callback_post(atoms) ids = [a.id for a in atoms] if last_id not in ids: # Request next self.pubsub_get_items(node, cb2, max=20, before=ids[0]) # Callback for first item. If found, will request the next ones. def _first_post_cb(atoms): if self.callback_post is not None: self.callback_post(atoms) ids = [a.id for a in atoms] if first_id in ids: self.pubsub_get_items(node, cb2, max=20, before=first_id) cb1 = lambda items: self._items_to_atoms(items, _first_post_cb) cb2 = lambda items: self._items_to_atoms(items, _other_posts_cb) # Request first item self.client.ps.get_item(self.client.inbox_jid, node, first_id, block=False, callback=cb1) # }}} # {{{ Items publishing def _make_atom(self, text, author_name=None, id_=None, in_reply_to=None, update_time=None): # Build something that looks like an Atom and return it entry = ET.Element("entry", xmlns=ATOM_NS) if author_name is None: author_name = self.client.boundjid.bare if update_time is None: update_time = datetime.datetime.utcnow().isoformat() content = ET.SubElement(entry, "content") author = ET.SubElement(entry, "author") name = ET.SubElement(author, "name") updated = ET.SubElement(entry, "updated") content.text = text name.text = author_name updated.text = update_time if id_ is not None: # Probably not necessary: added by the server. id_el = ET.SubElement(entry, "id") id_el.text = id_ if in_reply_to is not None: irt = ET.SubElement(entry, "{{{}}}in-reply-to".format(ATOM_THR_NS), ref=in_reply_to) return entry def publish(self, text, author_name=None, id_=None, in_reply_to=None): log.debug("Publishing to channel %s...", self.jid) entry = self._make_atom(text, author_name=author_name, id_=id_, in_reply_to=in_reply_to) node = "/user/{}/posts".format(self.jid) res = self.client.ps.publish(self.client.inbox_jid, node, payload=entry, id=id_) new_id = res["pubsub"]["publish"]["item"]["id"] log.info("Published to channel %s with id %s", self.jid, new_id) return new_id def retract(self, id_): log.debug("Retracting %s from channel %s", id_, self.jid) node = "/user/{}/posts".format(self.jid) self.client.ps.retract(self.client.inbox_jid, node, id_, notify=True) def set_status(self, text, author_name=None): log.debug("Setting status for channel %s...", self.jid) entry = self._make_atom(text, author_name=author_name) node = "/user/{}/status".format(self.jid) res = self.client.ps.publish(self.client.inbox_jid, node, payload=entry) id_ = res["pubsub"]["publish"]["item"]["id"] log.info("Status set for channel %s with id %s", self.jid, id_) return id_ def update_config(self, **kwds): # Create config form form = self.client.data_forms.make_form(ftype="submit") form.add_field(var="FORM_TYPE", ftype="hidden", value="http://jabber.org/protocol/pubsub#node_config") for (dk, ik) in self.CONFIG_MAP: if dk in kwds: form.add_field(var=ik, value=kwds[dk]) log.info("Updating config for channel %s", self.jid) node = "/user/{}/posts".format(self.jid) self.client.ps.set_node_config(self.client.inbox_jid, node, form)