source: python/wa.py @ eb89823

Last change on this file since eb89823 was 2700925, checked in by Wilmer van der Gaast <wilmer@…>, at 2015-05-25T16:07:49Z

Dead code that I might revisit: Status fetching. Yowsup is not feeding back
the result so for now this is not useful. :-(

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