source: python/wa.py @ 433c90b

Last change on this file since 433c90b was 433c90b, checked in by Wilmer van der Gaast <wilmer@…>, at 2015-05-21T11:43:42Z

Minor updates on group support.

Also getting to the point where this needs some cleanup instead of just
piling on stuff that I think of.

  • Property mode set to 100755
File size: 10.6 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                self.cb.connected()
81                try:
82                        self.toLower(PresenceProtocolEntity(name=self.b.setting("name")))
83                except KeyError:
84                        pass
85                # Should send the contact list now, but BitlBee hasn't given
86                # it yet. See set_away() and send_initial_contacts() below.
87       
88        @ProtocolEntityCallback("failure")
89        def onFailure(self, entity):
90                self.b = self.getStack().getProp("org.bitlbee.Bijtje")
91                self.cb = self.b.bee
92                self.cb.error(entity.getReason())
93                self.cb.logout(False)
94
95        def onEvent(self, event):
96                print "Received event: %s name %s" % (event, event.getName())
97                if event.getName() == "disconnect":
98                        self.getStack().execDetached(self.daemon.StopDaemon)
99       
100        @ProtocolEntityCallback("presence")
101        def onPresence(self, pres):
102                status = 8 # MOBILE
103                if pres.getType() != "unavailable":
104                        status |= 1 # ONLINE
105                self.cb.buddy_status(pres.getFrom(), status, None, None)
106       
107        @ProtocolEntityCallback("message")
108        def onMessage(self, msg):
109                receipt = OutgoingReceiptProtocolEntity(msg.getId(), msg.getFrom())
110                self.toLower(receipt)
111
112                if msg.getParticipant():
113                        group = self.b.groups.get(msg.getFrom(), None)
114                        if not group:
115                                self.cb.log("Warning: Activity in room %s" % msg.getFrom())
116                                self.b.groups.setdefault(msg.getFrom(), {}).setdefault("queue", []).append(msg)
117                                return
118                        self.cb.chat_msg(group["id"], msg.getParticipant(), msg.getBody(), 0, msg.getTimestamp())
119                else:
120                        self.cb.buddy_msg(msg.getFrom(), msg.getBody(), 0, msg.getTimestamp())
121
122        @ProtocolEntityCallback("receipt")
123        def onReceipt(self, entity):
124                ack = OutgoingAckProtocolEntity(entity.getId(), entity.getTag(),
125                                                entity.getType(), entity.getFrom())
126                self.toLower(ack)
127
128        @ProtocolEntityCallback("iq")
129        def onIq(self, entity):
130                if isinstance(entity, ResultSyncIqProtocolEntity):
131                        print "XXX SYNC RESULT RECEIVED!"
132                        return self.onSyncResult(entity)
133                elif isinstance(entity, ListParticipantsResultIqProtocolEntity):
134                        return self.b.chat_join_participants(entity)
135       
136        def onSyncResult(self, entity):
137                # TODO HERE AND ELSEWHERE: Thread idiocy happens when going
138                # from here to the IMPlugin. Check how bjsonrpc lets me solve that.
139                ok = set(jid.lower() for jid in entity.inNumbers.values())
140                for handle in self.b.contacts:
141                        if handle.lower() in ok:
142                                self.toLower(SubscribePresenceProtocolEntity(handle))
143                                self.cb.add_buddy(handle, "")
144                if entity.outNumbers:
145                        self.cb.error("Not on WhatsApp: %s" %
146                                      ", ".join(entity.outNumbers.keys()))
147                if entity.invalidNumbers:
148                        self.cb.error("Invalid numbers: %s" %
149                                      ", ".join(entity.invalidNumbers.keys()))
150
151        @ProtocolEntityCallback("notification")
152        def onNotification(self, ent):
153                if isinstance(ent, StatusNotificationProtocolEntity):
154                        return self.onStatusNotification(ent)
155       
156        def onStatusNotification(self, status):
157                print "New status for %s: %s" % (status.getFrom(), status.status)
158
159        #@ProtocolEntityCallback("chatstate")
160        #def onChatstate(self, entity):
161        #       print(entity)
162
163
164class YowsupDaemon(threading.Thread):
165        daemon = True
166        stack = None
167
168        class Terminate(Exception):
169                pass
170
171        def run(self):
172                try:
173                        self.stack.loop(timeout=0.2, discrete=0.2, count=1)
174                except YowsupDaemon.Terminate:
175                        print "Exiting loop!"
176                        pass
177       
178        def StopDaemon(self):
179                # Ugly, but yowsup offers no "run single iteration" version
180                # of their event loop :-(
181                raise YowsupDaemon.Terminate
182
183
184class YowsupIMPlugin(implugin.BitlBeeIMPlugin):
185        NAME = "wa"
186        SETTINGS = {
187                "cc": {
188                        "type": "int",
189                },
190                "name": {
191                        "flags": 0x100, # NULL_OK
192                },
193        }
194        AWAY_STATES = ["Away"]
195        ACCOUNT_FLAGS = 14 # HANDLE_DOMAINS + STATUS_MESSAGE + LOCAL_CONTACTS
196        # TODO: LOCAL LIST CAUSES CRASH!
197        # TODO: HANDLE_DOMAIN in right place (add ... ... nick bug)
198        # TODO? Allow set_away (for status msg) even if AWAY_STATES not set?
199        #   and/or, see why with the current value set_away state is None.
200
201        def login(self, account):
202                self.stack = self.build_stack(account)
203                self.daemon = YowsupDaemon(name="yowsup")
204                self.daemon.stack = self.stack
205                self.daemon.start()
206                self.bee.log("Started yowsup thread")
207               
208                self.logging_in = True
209                self.contacts = set()
210                self.groups = {}
211                self.groups_by_id = {}
212
213        def keepalive(self):
214                # Too noisy while debugging
215                pass
216                #self.yow.Ship(PingIqProtocolEntity(to="s.whatsapp.net"))
217
218        def logout(self):
219                self.stack.broadcastEvent(YowLayerEvent(YowNetworkLayer.EVENT_STATE_DISCONNECT))
220                self.stack.execDetached(self.daemon.StopDaemon)
221
222        def buddy_msg(self, to, text, flags):
223                msg = TextMessageProtocolEntity(text, to=to)
224                self.yow.Ship(msg)
225
226        def add_buddy(self, handle, _group):
227                if self.logging_in:
228                        # Need to batch up the initial adds. This is a "little" ugly.
229                        self.contacts.add(handle)
230                else:
231                        self.yow.Ship(GetSyncIqProtocolEntity(
232                            ["+" + handle.split("@")[0]], mode=GetSyncIqProtocolEntity.MODE_DELTA))
233                        self.yow.Ship(SubscribePresenceProtocolEntity(handle))
234
235        def remove_buddy(self, handle, _group):
236                self.yow.Ship(UnsubscribePresenceProtocolEntity(handle))
237
238        def set_away(self, state, status):
239                # When our first status is set, we've finalised login.
240                # Which means sync the full contact list now.
241                if self.logging_in:
242                        self.logging_in = False
243                        self.send_initial_contacts()
244               
245                print "Trying to set status to %r, %r" % (state, status)
246                if state:
247                        # Only one option offered so None = available, not None = away.
248                        self.yow.Ship(AvailablePresenceProtocolEntity())
249                else:
250                        self.yow.Ship(UnavailablePresenceProtocolEntity())
251                if status:
252                        self.yow.Ship(SetStatusIqProtocolEntity(status))
253
254        def send_initial_contacts(self):
255                if not self.contacts:
256                        return
257                numbers = [("+" + x.split("@")[0]) for x in self.contacts]
258                self.yow.Ship(GetSyncIqProtocolEntity(numbers))
259
260        def set_set_name(self, _key, value):
261                self.yow.Ship(PresenceProtocolEntity(name=value))
262
263        def chat_join(self, id, name, _nick, _password, settings):
264                print "New chat created with id: %d" % id
265                self.groups.setdefault(name, {}).update({"id": id, "name": name})
266                self.groups_by_id[id] = self.groups[name]
267                self.yow.Ship(ParticipantsGroupsIqProtocolEntity(name))
268
269                for msg in self.groups[name].get("queue", []):
270                        self.cb.chat_msg(group["id"], msg.getParticipant(), msg.getBody(), 0, msg.getTimestamp())
271
272        def chat_join_participants(self, entity):
273                group = self.groups[entity.getFrom()]
274                id = group["id"]
275                for p in entity.getParticipants():
276                        if p != self.account["user"]:
277                                self.bee.chat_add_buddy(id, p)
278                # Add the user themselves last to avoid a visible join flood.
279                self.bee.chat_add_buddy(id, self.account["user"])
280       
281        def chat_msg(self, id, text, flags):
282                msg = TextMessageProtocolEntity(text, to=self.groups_by_id[id]["name"])
283                self.yow.Ship(msg)
284
285        def build_stack(self, account):
286                self.account = account
287                creds = (account["user"].split("@")[0], account["pass"])
288
289                stack = (YowStackBuilder()
290                         .pushDefaultLayers(False)
291                         .push(BitlBeeLayer)
292                         .build())
293                stack.setProp(YowAuthenticationProtocolLayer.PROP_CREDENTIALS, creds)
294                stack.setProp(YowNetworkLayer.PROP_ENDPOINT, YowConstants.ENDPOINTS[0])
295                stack.setProp(YowCoderLayer.PROP_DOMAIN, YowConstants.DOMAIN)
296                stack.setProp(YowCoderLayer.PROP_RESOURCE, env.CURRENT_ENV.getResource())
297                stack.setProp("org.bitlbee.Bijtje", self)
298
299                stack.broadcastEvent(YowLayerEvent(YowNetworkLayer.EVENT_STATE_CONNECT))
300
301                return stack
302
303implugin.RunPlugin(YowsupIMPlugin, debug=True)
Note: See TracBrowser for help on using the repository browser.