source: python/wa.py @ a852b2b

Last change on this file since a852b2b was a852b2b, checked in by Wilmer van der Gaast <wilmer@…>, at 2015-05-25T05:30:26Z

WA: Request contact list during login, only call the connected callback when
we're *really* done.

  • Property mode set to 100755
File size: 11.9 KB
Line 
1#!/usr/bin/python
2
3import logging
4import threading
5import time
6
7import yowsup
8
9from yowsup.layers.auth                        import YowAuthenticationProtocolLayer
10from yowsup.layers.protocol_acks               import YowAckProtocolLayer
11from yowsup.layers.protocol_chatstate          import YowChatstateProtocolLayer
12from yowsup.layers.protocol_contacts           import YowContactsIqProtocolLayer
13from yowsup.layers.protocol_groups             import YowGroupsProtocolLayer
14from yowsup.layers.protocol_ib                 import YowIbProtocolLayer
15from yowsup.layers.protocol_iq                 import YowIqProtocolLayer
16from yowsup.layers.protocol_messages           import YowMessagesProtocolLayer
17from yowsup.layers.protocol_notifications      import YowNotificationsProtocolLayer
18from yowsup.layers.protocol_presence           import YowPresenceProtocolLayer
19from yowsup.layers.protocol_privacy            import YowPrivacyProtocolLayer
20from yowsup.layers.protocol_profiles           import YowProfilesProtocolLayer
21from yowsup.layers.protocol_receipts           import YowReceiptProtocolLayer
22from yowsup.layers.network                     import YowNetworkLayer
23from yowsup.layers.coder                       import YowCoderLayer
24from yowsup.stacks import YowStack, YowStackBuilder
25from yowsup.common import YowConstants
26from yowsup.layers import YowLayerEvent
27from yowsup.stacks import YowStack, YOWSUP_CORE_LAYERS
28from yowsup import env
29
30from yowsup.layers.interface                             import YowInterfaceLayer, ProtocolEntityCallback
31from yowsup.layers.protocol_acks.protocolentities        import *
32from yowsup.layers.protocol_chatstate.protocolentities   import *
33from yowsup.layers.protocol_contacts.protocolentities    import *
34from yowsup.layers.protocol_groups.protocolentities      import *
35from yowsup.layers.protocol_ib.protocolentities          import *
36from yowsup.layers.protocol_iq.protocolentities          import *
37from yowsup.layers.protocol_media.mediauploader import MediaUploader
38from yowsup.layers.protocol_media.protocolentities       import *
39from yowsup.layers.protocol_messages.protocolentities    import *
40from yowsup.layers.protocol_notifications.protocolentities import *
41from yowsup.layers.protocol_presence.protocolentities    import *
42from yowsup.layers.protocol_privacy.protocolentities     import *
43from yowsup.layers.protocol_profiles.protocolentities    import *
44from yowsup.layers.protocol_receipts.protocolentities    import *
45from yowsup.layers.axolotl.protocolentities.iq_key_get import GetKeysIqProtocolEntity
46from yowsup.layers.axolotl import YowAxolotlLayer
47from yowsup.common.tools import ModuleTools
48
49import implugin
50
51logger = logging.getLogger("yowsup.layers.network.layer")
52logger.setLevel(logging.DEBUG)
53ch = logging.StreamHandler()
54ch.setLevel(logging.DEBUG)
55logger.addHandler(ch)
56
57class BitlBeeLayer(YowInterfaceLayer):
58
59        def __init__(self, *a, **kwa):
60                super(BitlBeeLayer, self).__init__(*a, **kwa)
61
62        def receive(self, entity):
63                print "Received: %r" % entity
64                #print entity
65                super(BitlBeeLayer, self).receive(entity)
66
67        def Ship(self, entity):
68                """Send an entity into Yowsup, but through the correct thread."""
69                print "Queueing: %s" % entity.getTag()
70                #print entity
71                def doit():
72                        self.toLower(entity)
73                self.getStack().execDetached(doit)
74
75        @ProtocolEntityCallback("success")
76        def onSuccess(self, entity):
77                self.b = self.getStack().getProp("org.bitlbee.Bijtje")
78                self.cb = self.b.bee
79                self.b.yow = self
80               
81                self.cb.log("Authenticated, syncing contact list")
82               
83                # We're done once this set is empty.
84                self.todo = set(["contacts", "groups"])
85               
86                # Supposedly WA can also do national-style phone numbers without
87                # a + prefix BTW (relative to I guess the user's country?). I
88                # don't want to support this at least for now.
89                numbers = [("+" + x.split("@")[0]) for x in self.cb.get_local_contacts()]
90                self.toLower(GetSyncIqProtocolEntity(numbers))
91                self.toLower(ListGroupsIqProtocolEntity())
92               
93                try:
94                        self.toLower(PresenceProtocolEntity(name=self.b.setting("name")))
95                except KeyError:
96                        pass
97
98        def check_connected(self, done):
99                if self.todo is None:
100                        return
101                self.todo.remove(done)
102                if not self.todo:
103                        self.todo = None
104                        self.cb.connected()
105       
106        @ProtocolEntityCallback("failure")
107        def onFailure(self, entity):
108                self.b = self.getStack().getProp("org.bitlbee.Bijtje")
109                self.cb = self.b.bee
110                self.cb.error(entity.getReason())
111                self.cb.logout(False)
112
113        def onEvent(self, event):
114                print "Received event: %s name %s" % (event, event.getName())
115                if event.getName() == "disconnect":
116                        self.getStack().execDetached(self.daemon.StopDaemon)
117       
118        @ProtocolEntityCallback("presence")
119        def onPresence(self, pres):
120                if pres.getFrom() == self.b.account["user"]:
121                        # WA returns our own presence. Meh.
122                        return
123               
124                status = 8 # MOBILE
125                if pres.getType() != "unavailable":
126                        status |= 1 # ONLINE
127                self.cb.buddy_status(pres.getFrom(), status, None, None)
128               
129                try:
130                        # Last online time becomes idle time which I guess is
131                        # sane enough?
132                        self.cb.buddy_times(pres.getFrom(), 0, int(pres.getLast()))
133                except (ValueError, TypeError):
134                        # Could be "error" or, more likely, "deny", or None.
135                        pass
136       
137        @ProtocolEntityCallback("message")
138        def onMessage(self, msg):
139                receipt = OutgoingReceiptProtocolEntity(msg.getId(), msg.getFrom())
140                self.toLower(receipt)
141
142                if msg.getParticipant():
143                        group = self.b.groups.get(msg.getFrom(), None)
144                        if not group or "id" not in group:
145                                self.cb.log("Warning: Activity in room %s" % msg.getFrom())
146                                self.b.groups.setdefault(msg.getFrom(), {}).setdefault("queue", []).append(msg)
147                                return
148                        self.cb.chat_msg(group["id"], msg.getParticipant(), msg.getBody(), 0, msg.getTimestamp())
149                else:
150                        self.cb.buddy_msg(msg.getFrom(), msg.getBody(), 0, msg.getTimestamp())
151
152        @ProtocolEntityCallback("receipt")
153        def onReceipt(self, entity):
154                ack = OutgoingAckProtocolEntity(entity.getId(), entity.getTag(),
155                                                entity.getType(), entity.getFrom())
156                self.toLower(ack)
157
158        @ProtocolEntityCallback("iq")
159        def onIq(self, entity):
160                if isinstance(entity, ResultSyncIqProtocolEntity):
161                        return self.onSyncResult(entity)
162                elif isinstance(entity, ListParticipantsResultIqProtocolEntity):
163                        return self.b.chat_join_participants(entity)
164                elif isinstance(entity, ListGroupsResultIqProtocolEntity):
165                        return self.onListGroupsResult(entity)
166       
167        def onSyncResult(self, entity):
168                # TODO HERE AND ELSEWHERE: Thread idiocy happens when going
169                # from here to the IMPlugin. Check how bjsonrpc lets me solve that.
170                for num, jid in entity.inNumbers.iteritems():
171                        self.toLower(SubscribePresenceProtocolEntity(jid))
172                        self.cb.add_buddy(jid, "")
173                if entity.outNumbers:
174                        self.cb.error("Not on WhatsApp: %s" %
175                                      ", ".join(entity.outNumbers.keys()))
176                if entity.invalidNumbers:
177                        self.cb.error("Invalid numbers: %s" %
178                                      ", ".join(entity.invalidNumbers.keys()))
179
180                self.check_connected("contacts")
181
182        def onListGroupsResult(self, groups):
183                """Save group info for later if the user decides to join."""
184                for g in groups.getGroups():
185                        jid = g.getId()
186                        if "@" not in jid:
187                                jid += "@g.us"
188                        group = self.b.groups.setdefault(jid, {})
189                        group["info"] = g
190
191                self.check_connected("groups")
192
193        @ProtocolEntityCallback("notification")
194        def onNotification(self, ent):
195                if isinstance(ent, StatusNotificationProtocolEntity):
196                        return self.onStatusNotification(ent)
197
198        def onStatusNotification(self, status):
199                print "New status for %s: %s" % (status.getFrom(), status.status)
200                self.bee.buddy_status_msg(status.getFrom(), status.status)
201
202        @ProtocolEntityCallback("media")
203        def onMedia(self, med):
204                """Your PC better be MPC3 compliant!"""
205                print "YAY MEDIA! %r" % med
206                print med
207
208        #@ProtocolEntityCallback("chatstate")
209        #def onChatstate(self, entity):
210        #       print(entity)
211
212
213class YowsupDaemon(threading.Thread):
214        daemon = True
215        stack = None
216
217        class Terminate(Exception):
218                pass
219
220        def run(self):
221                try:
222                        self.stack.loop(timeout=0.2, discrete=0.2, count=1)
223                except YowsupDaemon.Terminate:
224                        print "Exiting loop!"
225                        pass
226       
227        def StopDaemon(self):
228                # Ugly, but yowsup offers no "run single iteration" version
229                # of their event loop :-(
230                raise YowsupDaemon.Terminate
231
232
233class YowsupIMPlugin(implugin.BitlBeeIMPlugin):
234        NAME = "wa"
235        SETTINGS = {
236                "cc": {
237                        # Country code. Seems to be required for registration only.
238                        "type": "int",
239                },
240                "name": {
241                        "flags": 0x100, # NULL_OK
242                },
243        }
244        AWAY_STATES = ["Away"]
245        ACCOUNT_FLAGS = 14 # HANDLE_DOMAINS + STATUS_MESSAGE + LOCAL_CONTACTS
246        # TODO: LOCAL LIST CAUSES CRASH!
247        # TODO: HANDLE_DOMAIN in right place (add ... ... nick bug)
248        # TODO? Allow set_away (for status msg) even if AWAY_STATES not set?
249        #   and/or, see why with the current value set_away state is None.
250
251        def login(self, account):
252                self.stack = self.build_stack(account)
253                self.daemon = YowsupDaemon(name="yowsup")
254                self.daemon.stack = self.stack
255                self.daemon.start()
256                self.bee.log("Started yowsup thread")
257               
258                self.groups = {}
259                self.groups_by_id = {}
260
261        def keepalive(self):
262                # Too noisy while debugging
263                pass
264                #self.yow.Ship(PingIqProtocolEntity(to="s.whatsapp.net"))
265
266        def logout(self):
267                self.stack.broadcastEvent(YowLayerEvent(YowNetworkLayer.EVENT_STATE_DISCONNECT))
268                self.stack.execDetached(self.daemon.StopDaemon)
269
270        def buddy_msg(self, to, text, flags):
271                msg = TextMessageProtocolEntity(text, to=to)
272                self.yow.Ship(msg)
273
274        def add_buddy(self, handle, _group):
275                self.yow.Ship(GetSyncIqProtocolEntity(
276                    ["+" + handle.split("@")[0]], mode=GetSyncIqProtocolEntity.MODE_DELTA))
277
278        def remove_buddy(self, handle, _group):
279                self.yow.Ship(UnsubscribePresenceProtocolEntity(handle))
280
281        def set_away(self, state, status):
282                print "Trying to set status to %r, %r" % (state, status)
283                if state:
284                        # Only one option offered so None = available, not None = away.
285                        self.yow.Ship(AvailablePresenceProtocolEntity())
286                else:
287                        self.yow.Ship(UnavailablePresenceProtocolEntity())
288                if status:
289                        self.yow.Ship(SetStatusIqProtocolEntity(status))
290
291        def set_set_name(self, _key, value):
292                self.yow.Ship(PresenceProtocolEntity(name=value))
293
294        def chat_join(self, id, name, _nick, _password, settings):
295                print "New chat created with id: %d" % id
296                self.groups.setdefault(name, {}).update({"id": id, "name": name})
297                group = self.groups[name]
298                self.groups_by_id[id] = group
299               
300                gi = group.get("info", None)
301                if gi:
302                        self.bee.chat_topic(id, gi.getSubjectOwner(),
303                                            gi.getSubject(), gi.getSubjectTime())
304               
305                # WA doesn't really have a concept of joined or not, just
306                # long-term membership. Let's just get a list of members and
307                # pretend we've "joined" then.
308                self.yow.Ship(ParticipantsGroupsIqProtocolEntity(name))
309
310        def chat_join_participants(self, entity):
311                group = self.groups[entity.getFrom()]
312                id = group["id"]
313                for p in entity.getParticipants():
314                        if p != self.account["user"]:
315                                self.bee.chat_add_buddy(id, p)
316
317                # Add the user themselves last to avoid a visible join flood.
318                self.bee.chat_add_buddy(id, self.account["user"])
319                for msg in group.setdefault("queue", []):
320                        self.bee.chat_msg(group["id"], msg.getParticipant(), msg.getBody(), 0, msg.getTimestamp())
321                del group["queue"]
322       
323        def chat_msg(self, id, text, flags):
324                msg = TextMessageProtocolEntity(text, to=self.groups_by_id[id]["name"])
325                self.yow.Ship(msg)
326
327        def chat_leave(self, id):
328                # WA never really let us leave, so just disconnect id and jid.
329                group = self.groups_by_id[id]
330                del self.groups_by_id[id]
331                del group["id"]
332
333        def build_stack(self, account):
334                self.account = account
335                creds = (account["user"].split("@")[0], account["pass"])
336
337                stack = (YowStackBuilder()
338                         .pushDefaultLayers(False)
339                         .push(BitlBeeLayer)
340                         .build())
341                stack.setProp(YowAuthenticationProtocolLayer.PROP_CREDENTIALS, creds)
342                stack.setProp(YowNetworkLayer.PROP_ENDPOINT, YowConstants.ENDPOINTS[0])
343                stack.setProp(YowCoderLayer.PROP_DOMAIN, YowConstants.DOMAIN)
344                stack.setProp(YowCoderLayer.PROP_RESOURCE, env.CURRENT_ENV.getResource())
345                stack.setProp("org.bitlbee.Bijtje", self)
346
347                stack.broadcastEvent(YowLayerEvent(YowNetworkLayer.EVENT_STATE_CONNECT))
348
349                return stack
350
351implugin.RunPlugin(YowsupIMPlugin, debug=True)
Note: See TracBrowser for help on using the repository browser.