source: python/wa.py @ 2446e4c

Last change on this file since 2446e4c was 2446e4c, checked in by Wilmer van der Gaast <wilmer@…>, at 2015-05-24T18:31:13Z

Minor WA bugfixes.

  • Property mode set to 100755
File size: 12.0 KB
RevLine 
[63b017e]1#!/usr/bin/python
2
3import logging
4import threading
[2b4402f]5import time
[63b017e]6
7import yowsup
8
9from yowsup.layers.auth                        import YowAuthenticationProtocolLayer
[5f8ad281]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
[63b017e]16from yowsup.layers.protocol_messages           import YowMessagesProtocolLayer
[5f8ad281]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
[63b017e]21from yowsup.layers.protocol_receipts           import YowReceiptProtocolLayer
22from yowsup.layers.network                     import YowNetworkLayer
23from yowsup.layers.coder                       import YowCoderLayer
[2b4402f]24from yowsup.stacks import YowStack, YowStackBuilder
[63b017e]25from yowsup.common import YowConstants
26from yowsup.layers import YowLayerEvent
27from yowsup.stacks import YowStack, YOWSUP_CORE_LAYERS
28from yowsup import env
29
[5f8ad281]30from yowsup.layers.interface                             import YowInterfaceLayer, ProtocolEntityCallback
[63b017e]31from yowsup.layers.protocol_acks.protocolentities        import *
[5f8ad281]32from yowsup.layers.protocol_chatstate.protocolentities   import *
33from yowsup.layers.protocol_contacts.protocolentities    import *
34from yowsup.layers.protocol_groups.protocolentities      import *
[63b017e]35from yowsup.layers.protocol_ib.protocolentities          import *
36from yowsup.layers.protocol_iq.protocolentities          import *
37from yowsup.layers.protocol_media.mediauploader import MediaUploader
[5f8ad281]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 *
[63b017e]43from yowsup.layers.protocol_profiles.protocolentities    import *
[5f8ad281]44from yowsup.layers.protocol_receipts.protocolentities    import *
[63b017e]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):
[2b4402f]63                print "Received: %r" % entity
[b09ce17]64                #print entity
[63b017e]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()
[b09ce17]70                #print entity
[63b017e]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                self.cb.connected()
[cb1b973]81                self.toLower(ListGroupsIqProtocolEntity())
[b09ce17]82                try:
83                        self.toLower(PresenceProtocolEntity(name=self.b.setting("name")))
84                except KeyError:
85                        pass
86                # Should send the contact list now, but BitlBee hasn't given
87                # it yet. See set_away() and send_initial_contacts() below.
[63b017e]88       
89        @ProtocolEntityCallback("failure")
90        def onFailure(self, entity):
91                self.b = self.getStack().getProp("org.bitlbee.Bijtje")
92                self.cb = self.b.bee
93                self.cb.error(entity.getReason())
94                self.cb.logout(False)
[5f8ad281]95
96        def onEvent(self, event):
[b09ce17]97                print "Received event: %s name %s" % (event, event.getName())
[5f8ad281]98                if event.getName() == "disconnect":
99                        self.getStack().execDetached(self.daemon.StopDaemon)
100       
101        @ProtocolEntityCallback("presence")
102        def onPresence(self, pres):
[2b4402f]103                status = 8 # MOBILE
[b09ce17]104                if pres.getType() != "unavailable":
105                        status |= 1 # ONLINE
106                self.cb.buddy_status(pres.getFrom(), status, None, None)
[cb1b973]107                try:
108                        # Last online time becomes idle time which I guess is
109                        # sane enough?
110                        self.cb.buddy_times(pres.getFrom(), 0, int(pres.getLast()))
[2446e4c]111                except (ValueError, TypeError):
112                        # Could be "error" or, more likely, "deny", or None.
[cb1b973]113                        pass
[63b017e]114       
115        @ProtocolEntityCallback("message")
116        def onMessage(self, msg):
117                receipt = OutgoingReceiptProtocolEntity(msg.getId(), msg.getFrom())
118                self.toLower(receipt)
119
[b09ce17]120                if msg.getParticipant():
121                        group = self.b.groups.get(msg.getFrom(), None)
[2446e4c]122                        if not group or "id" not in group:
[b09ce17]123                                self.cb.log("Warning: Activity in room %s" % msg.getFrom())
[433c90b]124                                self.b.groups.setdefault(msg.getFrom(), {}).setdefault("queue", []).append(msg)
[b09ce17]125                                return
126                        self.cb.chat_msg(group["id"], msg.getParticipant(), msg.getBody(), 0, msg.getTimestamp())
127                else:
128                        self.cb.buddy_msg(msg.getFrom(), msg.getBody(), 0, msg.getTimestamp())
129
[63b017e]130        @ProtocolEntityCallback("receipt")
131        def onReceipt(self, entity):
[5f8ad281]132                ack = OutgoingAckProtocolEntity(entity.getId(), entity.getTag(),
133                                                entity.getType(), entity.getFrom())
[63b017e]134                self.toLower(ack)
135
[2b4402f]136        @ProtocolEntityCallback("iq")
137        def onIq(self, entity):
138                if isinstance(entity, ResultSyncIqProtocolEntity):
[433c90b]139                        print "XXX SYNC RESULT RECEIVED!"
[2b4402f]140                        return self.onSyncResult(entity)
[433c90b]141                elif isinstance(entity, ListParticipantsResultIqProtocolEntity):
142                        return self.b.chat_join_participants(entity)
[cb1b973]143                elif isinstance(entity, ListGroupsResultIqProtocolEntity):
144                        return self.onListGroupsResult(entity)
[2b4402f]145       
146        def onSyncResult(self, entity):
[433c90b]147                # TODO HERE AND ELSEWHERE: Thread idiocy happens when going
[2b4402f]148                # from here to the IMPlugin. Check how bjsonrpc lets me solve that.
[cb1b973]149                # ALSO TODO: See why this one doesn't seem to be called for adds later.
[433c90b]150                ok = set(jid.lower() for jid in entity.inNumbers.values())
[2b4402f]151                for handle in self.b.contacts:
[433c90b]152                        if handle.lower() in ok:
[2b4402f]153                                self.toLower(SubscribePresenceProtocolEntity(handle))
154                                self.cb.add_buddy(handle, "")
155                if entity.outNumbers:
[433c90b]156                        self.cb.error("Not on WhatsApp: %s" %
157                                      ", ".join(entity.outNumbers.keys()))
[2b4402f]158                if entity.invalidNumbers:
[433c90b]159                        self.cb.error("Invalid numbers: %s" %
160                                      ", ".join(entity.invalidNumbers.keys()))
[2b4402f]161
[cb1b973]162        def onListGroupsResult(self, groups):
163                """Save group info for later if the user decides to join."""
164                for g in groups.getGroups():
165                        jid = g.getId()
166                        if "@" not in jid:
167                                jid += "@g.us"
168                        group = self.b.groups.setdefault(jid, {})
169                        group["info"] = g
170
[2b4402f]171        @ProtocolEntityCallback("notification")
172        def onNotification(self, ent):
173                if isinstance(ent, StatusNotificationProtocolEntity):
174                        return self.onStatusNotification(ent)
[cb1b973]175
[2b4402f]176        def onStatusNotification(self, status):
177                print "New status for %s: %s" % (status.getFrom(), status.status)
[cb1b973]178                self.bee.buddy_status_msg(status.getFrom(), status.status)
179
180        @ProtocolEntityCallback("media")
181        def onMedia(self, med):
182                """Your PC better be MPC3 compliant!"""
183                print "YAY MEDIA! %r" % med
184                print med
[2b4402f]185
[b09ce17]186        #@ProtocolEntityCallback("chatstate")
187        #def onChatstate(self, entity):
188        #       print(entity)
[5f8ad281]189
190
[63b017e]191class YowsupDaemon(threading.Thread):
192        daemon = True
193        stack = None
194
195        class Terminate(Exception):
196                pass
197
198        def run(self):
199                try:
200                        self.stack.loop(timeout=0.2, discrete=0.2, count=1)
201                except YowsupDaemon.Terminate:
202                        print "Exiting loop!"
203                        pass
204       
205        def StopDaemon(self):
206                # Ugly, but yowsup offers no "run single iteration" version
207                # of their event loop :-(
208                raise YowsupDaemon.Terminate
209
[2b4402f]210
[63b017e]211class YowsupIMPlugin(implugin.BitlBeeIMPlugin):
212        NAME = "wa"
213        SETTINGS = {
214                "cc": {
215                        "type": "int",
216                },
217                "name": {
218                        "flags": 0x100, # NULL_OK
219                },
220        }
[b09ce17]221        AWAY_STATES = ["Away"]
[5f8ad281]222        ACCOUNT_FLAGS = 14 # HANDLE_DOMAINS + STATUS_MESSAGE + LOCAL_CONTACTS
[63b017e]223        # TODO: LOCAL LIST CAUSES CRASH!
224        # TODO: HANDLE_DOMAIN in right place (add ... ... nick bug)
[5f8ad281]225        # TODO? Allow set_away (for status msg) even if AWAY_STATES not set?
226        #   and/or, see why with the current value set_away state is None.
227
[63b017e]228        def login(self, account):
229                self.stack = self.build_stack(account)
230                self.daemon = YowsupDaemon(name="yowsup")
231                self.daemon.stack = self.stack
232                self.daemon.start()
233                self.bee.log("Started yowsup thread")
[5f8ad281]234               
[2b4402f]235                self.logging_in = True
[5f8ad281]236                self.contacts = set()
[b09ce17]237                self.groups = {}
238                self.groups_by_id = {}
[63b017e]239
240        def keepalive(self):
[b09ce17]241                # Too noisy while debugging
242                pass
243                #self.yow.Ship(PingIqProtocolEntity(to="s.whatsapp.net"))
[63b017e]244
245        def logout(self):
246                self.stack.broadcastEvent(YowLayerEvent(YowNetworkLayer.EVENT_STATE_DISCONNECT))
247                self.stack.execDetached(self.daemon.StopDaemon)
248
249        def buddy_msg(self, to, text, flags):
250                msg = TextMessageProtocolEntity(text, to=to)
251                self.yow.Ship(msg)
252
253        def add_buddy(self, handle, _group):
[2b4402f]254                if self.logging_in:
255                        # Need to batch up the initial adds. This is a "little" ugly.
256                        self.contacts.add(handle)
257                else:
258                        self.yow.Ship(GetSyncIqProtocolEntity(
259                            ["+" + handle.split("@")[0]], mode=GetSyncIqProtocolEntity.MODE_DELTA))
260                        self.yow.Ship(SubscribePresenceProtocolEntity(handle))
[63b017e]261
262        def remove_buddy(self, handle, _group):
263                self.yow.Ship(UnsubscribePresenceProtocolEntity(handle))
264
[b09ce17]265        def set_away(self, state, status):
[2b4402f]266                # When our first status is set, we've finalised login.
267                # Which means sync the full contact list now.
268                if self.logging_in:
269                        self.logging_in = False
270                        self.send_initial_contacts()
271               
[b09ce17]272                print "Trying to set status to %r, %r" % (state, status)
273                if state:
274                        # Only one option offered so None = available, not None = away.
275                        self.yow.Ship(AvailablePresenceProtocolEntity())
276                else:
277                        self.yow.Ship(UnavailablePresenceProtocolEntity())
278                if status:
279                        self.yow.Ship(SetStatusIqProtocolEntity(status))
[63b017e]280
[2b4402f]281        def send_initial_contacts(self):
282                if not self.contacts:
283                        return
284                numbers = [("+" + x.split("@")[0]) for x in self.contacts]
285                self.yow.Ship(GetSyncIqProtocolEntity(numbers))
286
[63b017e]287        def set_set_name(self, _key, value):
[2b4402f]288                self.yow.Ship(PresenceProtocolEntity(name=value))
[63b017e]289
[b09ce17]290        def chat_join(self, id, name, _nick, _password, settings):
291                print "New chat created with id: %d" % id
[433c90b]292                self.groups.setdefault(name, {}).update({"id": id, "name": name})
[cb1b973]293                group = self.groups[name]
294                self.groups_by_id[id] = group
295               
296                gi = group.get("info", None)
297                if gi:
298                        self.bee.chat_topic(id, gi.getSubjectOwner(),
299                                            gi.getSubject(), gi.getSubjectTime())
300               
301                # WA doesn't really have a concept of joined or not, just
302                # long-term membership. Let's just get a list of members and
303                # pretend we've "joined" then.
[433c90b]304                self.yow.Ship(ParticipantsGroupsIqProtocolEntity(name))
305
306        def chat_join_participants(self, entity):
307                group = self.groups[entity.getFrom()]
308                id = group["id"]
309                for p in entity.getParticipants():
310                        if p != self.account["user"]:
311                                self.bee.chat_add_buddy(id, p)
[cb1b973]312
[433c90b]313                # Add the user themselves last to avoid a visible join flood.
[b09ce17]314                self.bee.chat_add_buddy(id, self.account["user"])
[cb1b973]315                for msg in group.setdefault("queue", []):
[2446e4c]316                        self.bee.chat_msg(group["id"], msg.getParticipant(), msg.getBody(), 0, msg.getTimestamp())
[cb1b973]317                del group["queue"]
[b09ce17]318       
319        def chat_msg(self, id, text, flags):
320                msg = TextMessageProtocolEntity(text, to=self.groups_by_id[id]["name"])
321                self.yow.Ship(msg)
322
[cb1b973]323        def chat_leave(self, id):
324                # WA never really let us leave, so just disconnect id and jid.
325                group = self.groups_by_id[id]
326                del self.groups_by_id[id]
327                del group["id"]
328
[63b017e]329        def build_stack(self, account):
[b09ce17]330                self.account = account
[63b017e]331                creds = (account["user"].split("@")[0], account["pass"])
332
[2b4402f]333                stack = (YowStackBuilder()
334                         .pushDefaultLayers(False)
335                         .push(BitlBeeLayer)
336                         .build())
[63b017e]337                stack.setProp(YowAuthenticationProtocolLayer.PROP_CREDENTIALS, creds)
338                stack.setProp(YowNetworkLayer.PROP_ENDPOINT, YowConstants.ENDPOINTS[0])
339                stack.setProp(YowCoderLayer.PROP_DOMAIN, YowConstants.DOMAIN)
340                stack.setProp(YowCoderLayer.PROP_RESOURCE, env.CURRENT_ENV.getResource())
341                stack.setProp("org.bitlbee.Bijtje", self)
342
343                stack.broadcastEvent(YowLayerEvent(YowNetworkLayer.EVENT_STATE_CONNECT))
344
345                return stack
346
347implugin.RunPlugin(YowsupIMPlugin, debug=True)
Note: See TracBrowser for help on using the repository browser.