source: python/implugin.py @ 2d88cac4

Last change on this file since 2d88cac4 was 17aa9a2, checked in by Wilmer van der Gaast <wilmer@…>, at 2015-05-14T14:29:33Z

Minor cleanup before I try splitting implugin and the test NewsBlur client.

Still no clue why r.json()[xxx] was throwing exceptoins last week...

  • Property mode set to 100755
File size: 5.5 KB
Line 
1#!/usr/bin/python
2
3import sys
4import bjsonrpc
5from bjsonrpc.handlers import BaseHandler
6
7import operator
8import random
9import re
10import socket
11import time
12
13import requests
14
15# List of functions an IM plugin can export. This library will indicate to
16# BitlBee which functions are actually implemented so omitted features
17# will be disabled, but note that some/many functions are simply mandatory.
18# (Currently login/-out, buddy_msg.)
19SUPPORTED_FUNCTIONS = [
20        'login', 'keepalive', 'logout', 'buddy_msg', 'set_away',
21        'send_typing', 'add_buddy', 'remove_buddy', 'add_permit',
22        'add_deny', 'rem_permit', 'rem_deny', 'get_info', 'chat_invite',
23        'chat_kick', 'chat_leave', 'chat_msg', 'chat_with', 'chat_join',
24        'chat_topic'
25]
26
27def make_version_tuple(hex):
28        """Convert the BitlBee binary-encoded version number into something
29        more "Pythonic". Could use distutils.version instead but its main
30        benefit appears to be string parsing which here is not that useful."""
31
32        return (hex >> 16, (hex >> 8) & 0xff, hex & 0xff)
33
34class RpcForwarder(object):
35        """Tiny object that forwards RPCs from local Python code to BitlBee
36        with a marginally nicer syntax. This layer could eventually be
37        used to add basic parameter checking though I don't think that should
38        be done here."""
39       
40        def __init__(self, methods, target):
41                for m in methods:
42                        # imc(b)_ prefix is not useful here, chop it.
43                        # (Maybe do this in BitlBee already as well.)
44                        newname = re.sub("^imcb?_", "", m)
45                        self.__setattr__(newname, target.__getattr__(m))
46
47class BitlBeeIMPlugin(BaseHandler):
48        # Protocol name to be used in the BitlBee CLI, etc.
49        NAME = "rpc-test"
50
51        # See account.h (TODO: Add constants.)
52        ACCOUNT_FLAGS = 0
53       
54        # Supported away states. If your protocol supports a specific set of
55        # away states, put them in a list in this variable.
56        AWAY_STATES = None
57       
58        # Filled in during initialisation:
59        # Version number as a three-tuple, so 3.2 becomes (3, 2, 0).
60        bitlbee_version = None
61        # Full version string
62        bitlbee_version_str = None
63        # Will become an RpcForwarder object to call into BitlBee
64        bee = None
65       
66        BASE_URL = "https://newsblur.com"
67       
68        @classmethod
69        def _factory(cls, *args, **kwargs):
70                def handler_factory(connection):
71                        handler = cls(connection, *args, **kwargs)
72                        return handler
73                return handler_factory
74       
75        #def __init__(self, connection, *args, **kwargs):
76        #       BaseHandler.__init__(self,connection)
77
78        def url(self, path):
79                return (self.BASE_URL + path)
80
81        def init(self, bee):
82                self.bee = RpcForwarder(bee["method_list"], self._conn.call)
83                self.bitlbee_version = make_version_tuple(bee["version"])
84                self.bitlbee_version_str = bee["version_str"]
85
86                # TODO: See how to call into the module here.
87                return {
88                        "name": self.NAME,
89                        "method_list": list(set(dir(self)) & set(SUPPORTED_FUNCTIONS)),
90                        "account_flags": self.ACCOUNT_FLAGS,
91                        "away_state_list": self.AWAY_STATES,
92                        "settings": {
93                                "oauth": {
94                                        "default": "off",
95                                        "type": "bool",
96                                },
97                                "test": {
98                                        "default": "123",
99                                        "type": "int",
100                                },
101                                "stringetje": {
102                                        "default": "testje",
103                                        "flags": 0x04,
104                                }
105                        },
106                }
107       
108        def login(self, account):
109                self.ua = requests.Session()
110                creds = {"username": account["user"], "password": account["pass"]}
111                r = self.ua.post(self.url("/api/login"), creds)
112                self.bee.log("You're running BitlBee %d.%d.%d" % self.bitlbee_version)
113                if r.status_code != 200:
114                        self.bee.error("HTTP error %d" % r.status_code)
115                        self.bee.logout(True)
116                elif r.json()["errors"]:
117                        self.bee.error("Authentication error")
118                        self.bee.logout(False)
119                else:
120                        self.bee.add_buddy("rss", None)
121                        self.bee.connected()
122                        self.seen_hashes = set()
123                        self.keepalive()
124
125        def logout(self):
126                self.bee.error("Ok bye!")
127
128        def buddy_msg(self, handle, msg, flags):
129                feed = self.feeds[handle]
130                cmd = re.split(r"\s+", msg)
131       
132        def set_set(self, setting, value):
133                print "Setting %s changed to %r" % (setting, value)
134       
135        # BitlBee will call us here every minute which is actually a neat way
136        # to get periodic work (like RSS polling) scheduled. :-D
137        def keepalive(self):
138                r = self.ua.post(
139                        self.url("/reader/unread_story_hashes"),
140                        {"include_timestamps": True})
141                if r.status_code != 200:
142                        self.bee.error("HTTP error %d" % r.status_code)
143                        return
144
145                # Throw all unread-post hashes in a long list and sort it by posting time.
146                feed_hashes = r.json()["unread_feed_story_hashes"]
147                all_hashes = []
148                for feed, hashes in feed_hashes.iteritems():
149                        all_hashes += [tuple(h) for h in hashes]
150                all_hashes.sort(key=operator.itemgetter(1))
151               
152                # Look at the most recent 20, grab the ones we haven't shown yet.
153                req_hashes = []
154                for hash, _ in all_hashes[-20:]:
155                        if hash not in self.seen_hashes:
156                                req_hashes.append(hash)
157               
158                if not req_hashes:
159                        return
160               
161                # Grab post details.
162                r = self.ua.post(self.url("/reader/river_stories"), {"h": req_hashes})
163                if r.status_code != 200:
164                        self.bee.error("HTTP error %d" % r.status_code)
165                        return
166               
167                # Response is not in the order we requested. :-( Make it a hash
168                # and reconstruct order from our request.
169                stories = {s["story_hash"]: s for s in r.json()["stories"]}
170                for s in (stories[hash] for hash in req_hashes):
171                        line = "%(story_title)s <%(story_permalink)s>" % s
172                        ts = int(s.get("story_timestamp", "0"))
173                        self.bee.buddy_msg("rss", line, 0, ts)
174                        self.seen_hashes.add(s["story_hash"])
175
176
177def RunPlugin(plugin, debug=True):
178        sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
179        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
180        sock.bind("/tmp/rpcplugins/test2")
181        sock.listen(3)
182       
183        srv = bjsonrpc.server.Server(sock, plugin._factory())
184       
185        srv.debug_socket(debug)
186        srv.serve()
187
188RunPlugin(BitlBeeIMPlugin)
Note: See TracBrowser for help on using the repository browser.