source: python/implugin.py @ e9face7

Last change on this file since e9face7 was e9face7, checked in by Wilmer van der Gaast <wilmer@…>, at 2015-05-07T09:56:01Z

Example plugin is now a very simple NewsBlur client.

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