Example #1
0
    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
Example #2
0
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)