source: python/wa.py @ 3f44e43

Last change on this file since 3f44e43 was ba52ac5, checked in by Wilmer van der Gaast <github@…>, at 2016-10-07T12:19:18Z

Update WhatsApp plugin for yowsup 2.5 (#87)

The removed imports appear to be unnecessary -- nothing else uses those
names in this file -- and they have been removed in yowsup. The other
change is due to a change in API in the YowsupEnv module.

I'm not certain whether this commit is sufficient or even correct, but
it has made the plugin work for me.

  • Property mode set to 100755
File size: 18.4 KB
RevLine 
[63b017e]1#!/usr/bin/python
2
[b73409f]3import collections
[63b017e]4import logging
5import threading
[2b4402f]6import time
[63b017e]7
8import yowsup
9
10from yowsup.layers.auth                        import YowAuthenticationProtocolLayer
[5f8ad281]11from yowsup.layers.protocol_acks               import YowAckProtocolLayer
12from yowsup.layers.protocol_chatstate          import YowChatstateProtocolLayer
13from yowsup.layers.protocol_contacts           import YowContactsIqProtocolLayer
14from yowsup.layers.protocol_groups             import YowGroupsProtocolLayer
15from yowsup.layers.protocol_ib                 import YowIbProtocolLayer
16from yowsup.layers.protocol_iq                 import YowIqProtocolLayer
[63b017e]17from yowsup.layers.protocol_messages           import YowMessagesProtocolLayer
[5f8ad281]18from yowsup.layers.protocol_notifications      import YowNotificationsProtocolLayer
19from yowsup.layers.protocol_presence           import YowPresenceProtocolLayer
20from yowsup.layers.protocol_privacy            import YowPrivacyProtocolLayer
21from yowsup.layers.protocol_profiles           import YowProfilesProtocolLayer
[63b017e]22from yowsup.layers.protocol_receipts           import YowReceiptProtocolLayer
23from yowsup.layers.network                     import YowNetworkLayer
24from yowsup.layers.coder                       import YowCoderLayer
[2b4402f]25from yowsup.stacks import YowStack, YowStackBuilder
[63b017e]26from yowsup.common import YowConstants
27from yowsup.layers import YowLayerEvent
28from yowsup.stacks import YowStack, YOWSUP_CORE_LAYERS
29from yowsup import env
30
[5f8ad281]31from yowsup.layers.interface                             import YowInterfaceLayer, ProtocolEntityCallback
[63b017e]32from yowsup.layers.protocol_acks.protocolentities        import *
[5f8ad281]33from yowsup.layers.protocol_chatstate.protocolentities   import *
34from yowsup.layers.protocol_contacts.protocolentities    import *
35from yowsup.layers.protocol_groups.protocolentities      import *
[63b017e]36from yowsup.layers.protocol_ib.protocolentities          import *
37from yowsup.layers.protocol_iq.protocolentities          import *
38from yowsup.layers.protocol_media.mediauploader import MediaUploader
[5f8ad281]39from yowsup.layers.protocol_media.protocolentities       import *
40from yowsup.layers.protocol_messages.protocolentities    import *
41from yowsup.layers.protocol_notifications.protocolentities import *
42from yowsup.layers.protocol_presence.protocolentities    import *
43from yowsup.layers.protocol_privacy.protocolentities     import *
[63b017e]44from yowsup.layers.protocol_profiles.protocolentities    import *
[5f8ad281]45from yowsup.layers.protocol_receipts.protocolentities    import *
[63b017e]46from yowsup.layers.axolotl.protocolentities.iq_key_get import GetKeysIqProtocolEntity
47
48import implugin
49
[15c3a6b]50logger = logging.getLogger("yowsup.layers.logger.layer")
[63b017e]51logger.setLevel(logging.DEBUG)
52ch = logging.StreamHandler()
53ch.setLevel(logging.DEBUG)
54logger.addHandler(ch)
55
[114154c]56"""
57TODO/Things I'm unhappy about:
58
[15c3a6b]59About the fact that WhatsApp is a rubbish protocol that happily rejects
60every second stanza you send it if you're trying to implement a client that
61doesn't keep local state. See how to cope with that.. It'd help if Yowsup
62came with docs on what a normal login sequence looks like instead of just
63throwing some stanzas over a wall but hey.
64
[114154c]65The randomness of where which bits/state live, in the implugin and the
66yowsup layer. Can't really merge this but at least state should live in
67one place.
68
69Mix of silly CamelCase and proper_style. \o/
70
71Most important: This is NOT thread-clean. implugin can call into yowsup
72cleanly by throwing closures into a queue, but there's no mechanism in
73the opposite direction, I'll need to cook up some hack to make this
74possible through bjsonrpc's tiny event loop. I think I know how...
75
76And more. But let's first get this into a state where it even works..
77"""
[2700925]78
[63b017e]79class BitlBeeLayer(YowInterfaceLayer):
80
81        def __init__(self, *a, **kwa):
82                super(BitlBeeLayer, self).__init__(*a, **kwa)
[114154c]83                # Offline messages are sent while we're still logging in.
84                self.msg_queue = []
[63b017e]85
86        def receive(self, entity):
[2b4402f]87                print "Received: %r" % entity
[b09ce17]88                #print entity
[63b017e]89                super(BitlBeeLayer, self).receive(entity)
90
91        def Ship(self, entity):
92                """Send an entity into Yowsup, but through the correct thread."""
93                print "Queueing: %s" % entity.getTag()
[b09ce17]94                #print entity
[63b017e]95                def doit():
96                        self.toLower(entity)
97                self.getStack().execDetached(doit)
98
99        @ProtocolEntityCallback("success")
100        def onSuccess(self, entity):
101                self.b = self.getStack().getProp("org.bitlbee.Bijtje")
102                self.cb = self.b.bee
103                self.b.yow = self
[a852b2b]104               
105                self.cb.log("Authenticated, syncing contact list")
106               
107                # We're done once this set is empty.
[d832164]108                self.todo = set(["contacts", "groups", "ping"])
[a852b2b]109               
110                # Supposedly WA can also do national-style phone numbers without
111                # a + prefix BTW (relative to I guess the user's country?). I
112                # don't want to support this at least for now.
113                numbers = [("+" + x.split("@")[0]) for x in self.cb.get_local_contacts()]
114                self.toLower(GetSyncIqProtocolEntity(numbers))
[cb1b973]115                self.toLower(ListGroupsIqProtocolEntity())
[d832164]116                self.b.keepalive()
[a852b2b]117               
[b09ce17]118                try:
119                        self.toLower(PresenceProtocolEntity(name=self.b.setting("name")))
120                except KeyError:
121                        pass
[a852b2b]122
123        def check_connected(self, done):
[114154c]124                if not self.todo:
[a852b2b]125                        return
126                self.todo.remove(done)
127                if not self.todo:
128                        self.cb.connected()
[114154c]129                        self.flush_msg_queue()
130       
131        def flush_msg_queue(self):
132                for msg in self.msg_queue:
133                        self.onMessage(msg)
134                self.msg_queue = None
[63b017e]135       
136        @ProtocolEntityCallback("failure")
137        def onFailure(self, entity):
138                self.b = self.getStack().getProp("org.bitlbee.Bijtje")
139                self.cb = self.b.bee
140                self.cb.error(entity.getReason())
141                self.cb.logout(False)
[5f8ad281]142
143        def onEvent(self, event):
[b73409f]144                # TODO: Make this work without, hmm, over-recursing. (This handler
145                # getting called when we initiated the disconnect, which upsets yowsup.)
146                if event.getName() == "orgopenwhatsapp.yowsup.event.network.disconnected":
147                        self.cb.error(event.getArg("reason"))
148                        self.cb.logout(True)
[5f8ad281]149                        self.getStack().execDetached(self.daemon.StopDaemon)
[b73409f]150                else:
151                        print "Received event: %s name %s" % (event, event.getName())
[5f8ad281]152       
153        @ProtocolEntityCallback("presence")
154        def onPresence(self, pres):
[a852b2b]155                if pres.getFrom() == self.b.account["user"]:
156                        # WA returns our own presence. Meh.
157                        return
158               
[c82a88d]159                # Online/offline is not really how WA works. Let's show everyone
160                # as online but unavailable folks as away. This also solves the
161                # problem of offline->IRC /quit causing the persons to leave chat
162                # channels as well (and not reappearing there when they return).
163                status = 8 | 1  # MOBILE | ONLINE
164                if pres.getType() == "unavailable":
165                        status |= 4  # AWAY
[b09ce17]166                self.cb.buddy_status(pres.getFrom(), status, None, None)
[a852b2b]167               
[cb1b973]168                try:
169                        # Last online time becomes idle time which I guess is
170                        # sane enough?
171                        self.cb.buddy_times(pres.getFrom(), 0, int(pres.getLast()))
[2446e4c]172                except (ValueError, TypeError):
173                        # Could be "error" or, more likely, "deny", or None.
[cb1b973]174                        pass
[63b017e]175       
176        @ProtocolEntityCallback("message")
177        def onMessage(self, msg):
[114154c]178                if self.todo:
179                        # We're still logging in, so wait.
180                        self.msg_queue.append(msg)
181                        return
[63b017e]182
[114154c]183                self.b.show_message(msg)
[b73409f]184
185                # ACK is required! So only use return above in case of errors.
186                # (So that we will/might get a retry after restarting.)
187                self.toLower(OutgoingReceiptProtocolEntity(msg.getId(), msg.getFrom()))
[b09ce17]188
[63b017e]189        @ProtocolEntityCallback("receipt")
190        def onReceipt(self, entity):
[5f8ad281]191                ack = OutgoingAckProtocolEntity(entity.getId(), entity.getTag(),
192                                                entity.getType(), entity.getFrom())
[63b017e]193                self.toLower(ack)
194
[2b4402f]195        @ProtocolEntityCallback("iq")
196        def onIq(self, entity):
197                if isinstance(entity, ResultSyncIqProtocolEntity):
198                        return self.onSyncResult(entity)
[cb1b973]199                elif isinstance(entity, ListGroupsResultIqProtocolEntity):
200                        return self.onListGroupsResult(entity)
[31e2b09]201                elif type(entity) == IqProtocolEntity:  # Pong has no type, sigh.
202                        self.b.last_pong = time.time()
203                        if self.todo:
204                                return self.onLoginPong()
[2b4402f]205       
206        def onSyncResult(self, entity):
[433c90b]207                # TODO HERE AND ELSEWHERE: Thread idiocy happens when going
[2b4402f]208                # from here to the IMPlugin. Check how bjsonrpc lets me solve that.
[a852b2b]209                for num, jid in entity.inNumbers.iteritems():
210                        self.toLower(SubscribePresenceProtocolEntity(jid))
211                        self.cb.add_buddy(jid, "")
[2b4402f]212                if entity.outNumbers:
[433c90b]213                        self.cb.error("Not on WhatsApp: %s" %
214                                      ", ".join(entity.outNumbers.keys()))
[2b4402f]215                if entity.invalidNumbers:
[433c90b]216                        self.cb.error("Invalid numbers: %s" %
[c82a88d]217                                      ", ".join(entity.invalidNumbers))
[2b4402f]218
[d832164]219                #self.getStatuses(entity.inNumbers.values())
220                self.check_connected("contacts")
221
222        def onSyncResultFail(self):
223                # Whatsapp rate-limits sync stanzas, so in case of failure
224                # just assume all contacts are valid.
225                for jid in self.cb.get_local_contacts():
226                        self.toLower(SubscribePresenceProtocolEntity(jid))
227                        self.cb.add_buddy(jid, "")
228                #self.getStatuses?
[a852b2b]229                self.check_connected("contacts")
230
[cb1b973]231        def onListGroupsResult(self, groups):
232                """Save group info for later if the user decides to join."""
233                for g in groups.getGroups():
234                        jid = g.getId()
235                        if "@" not in jid:
236                                jid += "@g.us"
[b73409f]237                        group = self.b.groups[jid]
[31e2b09]238                        try:
239                                group["participants"] = g.getParticipants().keys()
240                        except AttributeError:
241                                # Depends on a change I made to yowsup that may
242                                # or may not get merged..
243                                group["participants"] = []
[b73409f]244                       
245                        # Save it. We're going to mix ListGroups elements and
246                        # Group-Subject notifications there, which don't have
247                        # consistent fieldnames for the same bits of info \o/
248                        g.getSubjectTimestamp = g.getSubjectTime
249                        group["topic"] = g
[cb1b973]250
[a852b2b]251                self.check_connected("groups")
252
[31e2b09]253        def onLoginPong(self):
254                if "contacts" in self.todo:
255                        # Shitty Whatsapp rejected the sync request, and
256                        # annoying Yowsup doesn't inform on error responses.
257                        # So instead, if we received no response to it but
258                        # did get our ping back, declare failure.
259                        self.onSyncResultFail()
260                if "groups" in self.todo:
261                        # Well fuck this. Just reject ALL the things!
262                        # Maybe I don't need this one then.
263                        self.check_connected("groups")
264                self.check_connected("ping")
265
[d832164]266        def getStatuses(self, contacts):
267                return # Disabled since yowsup won't give us the result...
268                self.toLower(GetStatusIqProtocolEntity(contacts))
269                self.todo.add("statuses")
270
[2b4402f]271        @ProtocolEntityCallback("notification")
272        def onNotification(self, ent):
273                if isinstance(ent, StatusNotificationProtocolEntity):
274                        return self.onStatusNotification(ent)
[b73409f]275                elif isinstance(ent, SubjectGroupsNotificationProtocolEntity):
276                        return self.onGroupSubjectNotification(ent)
[cb1b973]277
[2b4402f]278        def onStatusNotification(self, status):
279                print "New status for %s: %s" % (status.getFrom(), status.status)
[365b25a]280                self.cb.buddy_status_msg(status.getFrom(), status.status)
[b73409f]281       
282        def onGroupSubjectNotification(self, sub):
283                print "New /topic for %s: %s" % (sub.getFrom(), sub.getSubject())
284                group = self.b.groups[sub.getFrom()]
285                group["topic"] = sub
286                id = group.get("id", None)
287                if id is not None:
288                        self.cb.chat_topic(id, sub.getSubjectOwner(),
289                                           sub.getSubject(), sub.getSubjectTimestamp())
[cb1b973]290
291        @ProtocolEntityCallback("media")
292        def onMedia(self, med):
293                """Your PC better be MPC3 compliant!"""
294                print "YAY MEDIA! %r" % med
295                print med
[2b4402f]296
[b09ce17]297        #@ProtocolEntityCallback("chatstate")
298        #def onChatstate(self, entity):
299        #       print(entity)
[5f8ad281]300
301
[63b017e]302class YowsupDaemon(threading.Thread):
303        daemon = True
304        stack = None
305
306        class Terminate(Exception):
307                pass
308
309        def run(self):
310                try:
311                        self.stack.loop(timeout=0.2, discrete=0.2, count=1)
312                except YowsupDaemon.Terminate:
313                        print "Exiting loop!"
314                        pass
315       
316        def StopDaemon(self):
317                # Ugly, but yowsup offers no "run single iteration" version
318                # of their event loop :-(
319                raise YowsupDaemon.Terminate
320
[2b4402f]321
[63b017e]322class YowsupIMPlugin(implugin.BitlBeeIMPlugin):
323        NAME = "wa"
324        SETTINGS = {
325                "cc": {
[a852b2b]326                        # Country code. Seems to be required for registration only.
[63b017e]327                        "type": "int",
328                },
[7051acb]329                "reg_mode": {
330                        "default": "sms",
331                },
[63b017e]332                "name": {
333                        "flags": 0x100, # NULL_OK
334                },
[7051acb]335                # EW! Need to include this setting to trick BitlBee into
336                # doing registration instead of refusing to login w/o pwd.
337                # TODO: Make this a flag instead of faking oauth.
338                "oauth": {
339                        "default": True,
340                },
[63b017e]341        }
[b09ce17]342        AWAY_STATES = ["Away"]
[5f8ad281]343        ACCOUNT_FLAGS = 14 # HANDLE_DOMAINS + STATUS_MESSAGE + LOCAL_CONTACTS
[63b017e]344        # TODO: HANDLE_DOMAIN in right place (add ... ... nick bug)
[d832164]345        PING_INTERVAL = 299 # seconds
[31e2b09]346        PING_TIMEOUT = 360 # seconds
[5f8ad281]347
[63b017e]348        def login(self, account):
[7051acb]349                super(YowsupIMPlugin, self).login(account)
350                self.account = account
351                self.number = self.account["user"].split("@")[0]
352                self.registering = False
353                if not self.account["pass"]:
354                        return self._register()
355               
356                self.stack = self._build_stack()
[63b017e]357                self.daemon = YowsupDaemon(name="yowsup")
358                self.daemon.stack = self.stack
359                self.daemon.start()
360                self.bee.log("Started yowsup thread")
[5f8ad281]361               
[b73409f]362                self.groups = collections.defaultdict(dict)
[b09ce17]363                self.groups_by_id = {}
[63b017e]364
[d832164]365                self.next_ping = None
[31e2b09]366                self.last_pong = time.time()
[d832164]367
[63b017e]368        def keepalive(self):
[31e2b09]369                if (time.time() - self.last_pong) > self.PING_TIMEOUT:
[e1352e15]370                        self.bee.error("Ping timeout")
371                        self.bee.logout(True)
[31e2b09]372                        return
[d832164]373                if self.next_ping and (time.time() < self.next_ping):
374                        return
375                self.yow.Ship(PingIqProtocolEntity(to="s.whatsapp.net"))
376                self.next_ping = time.time() + self.PING_INTERVAL
[63b017e]377
378        def logout(self):
379                self.stack.broadcastEvent(YowLayerEvent(YowNetworkLayer.EVENT_STATE_DISCONNECT))
380                self.stack.execDetached(self.daemon.StopDaemon)
381
[7051acb]382        def _register(self):
383                self.registering = True
384                self.bee.log("New account, starting registration")
385                from yowsup.registration import WACodeRequest
386                cr = WACodeRequest(str(self.setting("cc")), self.number,
387                                   "000", "000", "000", "000",
388                                   self.setting("reg_mode"))
389                res = cr.send()
390                res = {k: v for k, v in res.iteritems() if v is not None}
391                if res.get("status", "") != "sent":
392                        self.bee.error("Failed to start registration: %r" % res)
393                        self.bee.logout(False)
394                        return
395               
396                text = ("Registration request sent. You will receive a SMS or "
397                        "call with a confirmation code. Please respond to this "
398                        "message with that code.")
399                sender = "wa_%s" % self.number
400                self.bee.add_buddy(sender, "")
401                self.bee.buddy_msg(sender, text, 0, 0)
402
403        def _register_confirm(self, code):
404                from yowsup.registration import WARegRequest
405                code = code.strip().replace("-", "")
406                rr = WARegRequest(str(self.setting("cc")), self.number, code)
407                res = rr.send()
408                res = {k: v for k, v in res.iteritems() if v is not None}
409                if (res.get("status", "") != "ok") or (not self.get("pw", "")):
410                        self.bee.error("Failed to finish registration: %r" % res)
411                        self.bee.logout(False)
412                        return
413                self.bee.log("Registration finished, attempting login")
414                self.bee.set_setstr("password", res["pw"])
415                self.account["pass"] = res["pw"]
416                self.login(self.account)
417
[63b017e]418        def buddy_msg(self, to, text, flags):
[7051acb]419                if self.registering:
420                        return self._register_confirm(text)
[63b017e]421                msg = TextMessageProtocolEntity(text, to=to)
422                self.yow.Ship(msg)
423
424        def add_buddy(self, handle, _group):
[a852b2b]425                self.yow.Ship(GetSyncIqProtocolEntity(
426                    ["+" + handle.split("@")[0]], mode=GetSyncIqProtocolEntity.MODE_DELTA))
[63b017e]427
428        def remove_buddy(self, handle, _group):
429                self.yow.Ship(UnsubscribePresenceProtocolEntity(handle))
430
[b09ce17]431        def set_away(self, state, status):
432                print "Trying to set status to %r, %r" % (state, status)
433                if state:
434                        # Only one option offered so None = available, not None = away.
435                        self.yow.Ship(AvailablePresenceProtocolEntity())
436                else:
437                        self.yow.Ship(UnavailablePresenceProtocolEntity())
438                if status:
439                        self.yow.Ship(SetStatusIqProtocolEntity(status))
[63b017e]440
441        def set_set_name(self, _key, value):
[7051acb]442                #self.yow.Ship(PresenceProtocolEntity(name=value))
443                pass
[63b017e]444
[b09ce17]445        def chat_join(self, id, name, _nick, _password, settings):
446                print "New chat created with id: %d" % id
[cb1b973]447                group = self.groups[name]
[b73409f]448                group.update({"id": id, "name": name})
[cb1b973]449                self.groups_by_id[id] = group
450               
[b73409f]451                gi = group.get("topic", None)
[cb1b973]452                if gi:
453                        self.bee.chat_topic(id, gi.getSubjectOwner(),
[b73409f]454                                            gi.getSubject(), gi.getSubjectTimestamp())
[cb1b973]455               
456                # WA doesn't really have a concept of joined or not, just
[b73409f]457                # long-term membership. Let's just sync state (we have
458                # basic info but not yet a member list) and ACK the join
459                # once that's done.
[15c3a6b]460                # Well except that WA/YS killed this one. \o/
461                #self.yow.Ship(ParticipantsGroupsIqProtocolEntity(name))
462               
463                # So for now do without a participant list..
[31e2b09]464                self.chat_join_participants(group)
[15c3a6b]465                self.chat_send_backlog(group)
[433c90b]466
[31e2b09]467        def chat_join_participants(self, group):
468                for p in group.get("participants", []):
[433c90b]469                        if p != self.account["user"]:
[31e2b09]470                                self.bee.chat_add_buddy(group["id"], p)
[cb1b973]471
[15c3a6b]472        def chat_send_backlog(self, group):
[433c90b]473                # Add the user themselves last to avoid a visible join flood.
[15c3a6b]474                self.bee.chat_add_buddy(group["id"], self.account["user"])
[cb1b973]475                for msg in group.setdefault("queue", []):
[10d089d]476                        self.show_message(msg)
[cb1b973]477                del group["queue"]
[b09ce17]478       
479        def chat_msg(self, id, text, flags):
480                msg = TextMessageProtocolEntity(text, to=self.groups_by_id[id]["name"])
481                self.yow.Ship(msg)
482
[cb1b973]483        def chat_leave(self, id):
484                # WA never really let us leave, so just disconnect id and jid.
485                group = self.groups_by_id[id]
486                del self.groups_by_id[id]
487                del group["id"]
488
[7051acb]489        def _build_stack(self):
490                creds = (self.number, self.account["pass"])
[63b017e]491
[2b4402f]492                stack = (YowStackBuilder()
[365b25a]493                         .pushDefaultLayers(True)
[2b4402f]494                         .push(BitlBeeLayer)
495                         .build())
[63b017e]496                stack.setProp(YowAuthenticationProtocolLayer.PROP_CREDENTIALS, creds)
497                stack.setProp(YowNetworkLayer.PROP_ENDPOINT, YowConstants.ENDPOINTS[0])
498                stack.setProp(YowCoderLayer.PROP_DOMAIN, YowConstants.DOMAIN)
[ba52ac5]499                stack.setProp(YowCoderLayer.PROP_RESOURCE, env.YowsupEnv.getCurrent().getResource())
[dcfa886]500                try:
501                        stack.setProp(YowIqProtocolLayer.PROP_PING_INTERVAL, 0)
502                except AttributeError:
503                        # Ping setting only exists since May 2015.
504                        from yowsup.layers.protocol_iq.layer import YowPingThread
505                        YowPingThread.start = lambda x: None
506
[63b017e]507                stack.setProp("org.bitlbee.Bijtje", self)
508
509                stack.broadcastEvent(YowLayerEvent(YowNetworkLayer.EVENT_STATE_CONNECT))
510
511                return stack
512
[b73409f]513
[114154c]514        # Not RPCs from here on.
515        def show_message(self, msg):
516                if hasattr(msg, "getBody"):
517                        text = msg.getBody()
518                elif hasattr(msg, "getCaption") and hasattr(msg, "getMediaUrl"):
519                        lines = []
520                        if msg.getMediaUrl():
521                                lines.append(msg.getMediaUrl())
522                        else:
523                                lines.append("<Broken link>")
524                        if msg.getCaption():
525                                lines.append(msg.getCaption())
526                        text = "\n".join(lines)
[365b25a]527                else:
528                        text = "Message of unknown type %r" % type(msg)
[114154c]529
530                if msg.getParticipant():
531                        group = self.groups[msg.getFrom()]
532                        if "id" in group:
[15c3a6b]533                                self.bee.chat_add_buddy(group["id"], msg.getParticipant())
[114154c]534                                self.bee.chat_msg(group["id"], msg.getParticipant(), text, 0, msg.getTimestamp())
535                        else:
536                                self.bee.log("Warning: Activity in room %s" % msg.getFrom())
537                                self.groups[msg.getFrom()].setdefault("queue", []).append(msg)
538                else:
539                        self.bee.buddy_msg(msg.getFrom(), text, 0, msg.getTimestamp())
540
541
[63b017e]542implugin.RunPlugin(YowsupIMPlugin, debug=True)
Note: See TracBrowser for help on using the repository browser.