[f3af614] | 1 | #!/usr/bin/python |
---|
| 2 | |
---|
| 3 | import sys |
---|
| 4 | import bjsonrpc |
---|
| 5 | from bjsonrpc.handlers import BaseHandler |
---|
| 6 | |
---|
[e9face7] | 7 | import operator |
---|
[f3af614] | 8 | import random |
---|
| 9 | import re |
---|
| 10 | import socket |
---|
| 11 | import time |
---|
| 12 | |
---|
[e9face7] | 13 | import requests |
---|
[2533d10] | 14 | |
---|
[f3af614] | 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. |
---|
[f8feb8a] | 18 | # (Currently login/-out, buddy_msg.) |
---|
[f3af614] | 19 | SUPPORTED_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 | |
---|
[b20014b] | 27 | def 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 | |
---|
[f3af614] | 34 | class 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 | |
---|
| 47 | class BitlBeeIMPlugin(BaseHandler): |
---|
[c5a7b8d] | 48 | # Protocol name to be used in the BitlBee CLI, etc. |
---|
[f3af614] | 49 | NAME = "rpc-test" |
---|
[c5a7b8d] | 50 | |
---|
| 51 | # See account.h (TODO: Add constants.) |
---|
[e9face7] | 52 | ACCOUNT_FLAGS = 0 |
---|
[c5a7b8d] | 53 | |
---|
| 54 | # Supported away states. If your protocol supports a specific set of |
---|
| 55 | # away states, put them in a list in this variable. |
---|
[2533d10] | 56 | AWAY_STATES = None |
---|
[f3af614] | 57 | |
---|
| 58 | # Filled in during initialisation: |
---|
[b20014b] | 59 | # Version number as a three-tuple, so 3.2 becomes (3, 2, 0). |
---|
[f3af614] | 60 | bitlbee_version = None |
---|
| 61 | # Full version string |
---|
| 62 | bitlbee_version_str = None |
---|
[c5a7b8d] | 63 | # Will become an RpcForwarder object to call into BitlBee |
---|
| 64 | bee = None |
---|
[f3af614] | 65 | |
---|
[e9face7] | 66 | BASE_URL = "https://newsblur.com" |
---|
| 67 | |
---|
[f3af614] | 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 | |
---|
[e9face7] | 78 | def url(self, path): |
---|
| 79 | return (self.BASE_URL + path) |
---|
| 80 | |
---|
[f3af614] | 81 | def init(self, bee): |
---|
| 82 | self.bee = RpcForwarder(bee["method_list"], self._conn.call) |
---|
[b20014b] | 83 | self.bitlbee_version = make_version_tuple(bee["version"]) |
---|
[f3af614] | 84 | self.bitlbee_version_str = bee["version_str"] |
---|
[2533d10] | 85 | |
---|
[f3af614] | 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)), |
---|
[c5a7b8d] | 90 | "account_flags": self.ACCOUNT_FLAGS, |
---|
| 91 | "away_state_list": self.AWAY_STATES, |
---|
[f3af614] | 92 | "settings": { |
---|
| 93 | "oauth": { |
---|
| 94 | "default": "off", |
---|
[f15553d] | 95 | "type": "bool", |
---|
[f3af614] | 96 | }, |
---|
| 97 | "test": { |
---|
| 98 | "default": "123", |
---|
[f15553d] | 99 | "type": "int", |
---|
[f3af614] | 100 | }, |
---|
[f15553d] | 101 | "stringetje": { |
---|
| 102 | "default": "testje", |
---|
| 103 | "flags": 0x04, |
---|
| 104 | } |
---|
[f3af614] | 105 | }, |
---|
| 106 | } |
---|
| 107 | |
---|
| 108 | def login(self, account): |
---|
[e9face7] | 109 | self.ua = requests.Session() |
---|
| 110 | creds = {"username": account["user"], "password": account["pass"]} |
---|
| 111 | r = self.ua.post(self.url("/api/login"), creds) |
---|
[b20014b] | 112 | self.bee.log("You're running BitlBee %d.%d.%d" % self.bitlbee_version) |
---|
[e9face7] | 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() |
---|
[f3af614] | 124 | |
---|
| 125 | def logout(self): |
---|
| 126 | self.bee.error("Ok bye!") |
---|
| 127 | |
---|
[2533d10] | 128 | def buddy_msg(self, handle, msg, flags): |
---|
| 129 | feed = self.feeds[handle] |
---|
| 130 | cmd = re.split(r"\s+", msg) |
---|
[f15553d] | 131 | |
---|
| 132 | def set_set(self, setting, value): |
---|
| 133 | print "Setting %s changed to %r" % (setting, value) |
---|
[e9face7] | 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. |
---|
[17aa9a2] | 146 | feed_hashes = r.json()["unread_feed_story_hashes"] |
---|
[e9face7] | 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 | |
---|
[17aa9a2] | 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"]} |
---|
[e9face7] | 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 | |
---|
[f3af614] | 176 | |
---|
| 177 | def 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 | |
---|
| 188 | RunPlugin(BitlBeeIMPlugin) |
---|