| import logging |
| import sleekxmpp |
| import glob |
| import urllib |
| |
| # local imports |
| import hangupsthread |
| import rosterdb |
| |
| from sleekxmpp import ComponentXMPP |
| from sleekxmpp.xmlstream.matcher import MatchXPath |
| from sleekxmpp.xmlstream.handler import Callback |
| from hangups.auth import GoogleAuthError |
| |
| class XMPPThread(ComponentXMPP): |
| def __init__(self, name, host, secret='secret', port=5347): |
| ComponentXMPP.__init__(self, name, secret, host, port, |
| ## |
| # Note Here: this seems to be required for |
| # jabberd2. If the message event isn't |
| # firing change this to False |
| ## |
| use_jc_ns=True) |
| self.name = name |
| |
| #self.roster.set_backend(db=rosterdb.RosterDB('database.sqlite', 'roster')) |
| |
| self.registerPlugin('xep_0030') # service discovery |
| self.registerPlugin('xep_0004') # Data Forms |
| self.registerPlugin('xep_0054') # vcard |
| self.registerPlugin('xep_0060') # PubSub |
| self.registerPlugin('xep_0077') # registration |
| self.registerPlugin('xep_0085') # chat state notification |
| self.registerPlugin('xep_0172') # user nick |
| self.registerPlugin('xep_0199') # XMPP Ping |
| self.registerPlugin('xep_0280') # carbons |
| |
| self['xep_0030'].add_identity(category='gateway', itype='hangouts', name='Google Hangouts Transport') |
| |
| # god knows why the xep-0077 plugin doesn't do component registration |
| self['xep_0030'].add_feature('jabber:iq:register') |
| self.register_handler(Callback('Handle Registration', |
| MatchXPath('{%s}iq/{jabber:iq:register}query' % self.default_ns), |
| self._disco_register_handler)) |
| |
| # all handlers should be threaded |
| self.add_event_handler('message', self._message, threaded=True) |
| self.add_event_handler('chatstate', self._chatstate, threaded=True) |
| self.add_event_handler('presence_available', self._presence_available, |
| threaded=True) |
| self.add_event_handler('presence_unavailable', |
| self._presence_unavailable, |
| threaded=True) |
| self.add_event_handler('presence_probe', self._presence_probe, |
| threaded=True) |
| # session handler is unthreaded so we set up all the useful stuff first |
| self.add_event_handler("session_start", self._session_start, threaded=False) |
| self.add_event_handler("session_end", self._session_end, threaded=False) |
| |
| self.hangups_threads = {} |
| |
| def _session_start(self, event): |
| # Anyone can subscribe to the transport bot identity |
| self.roster[self.name].auto_authorize = True |
| self.roster[self.name].auto_subscribe = True |
| |
| # However you see it offline until you log in successfully |
| # to the google service |
| self.roster[self.name].send_presence(ptype='unavailable') |
| |
| for token in glob.glob('*-refresh_token.cache'): |
| name = token.replace('-refresh_token.cache', '') |
| logging.info('starting Hangups thread for %s', name) |
| try: |
| thread = hangupsthread.HangupsThread(self, name) |
| self.hangups_threads[name] = thread |
| thread.start() |
| self.add_subscription(self.name, name) |
| self.send_presence(pto=name, ptype='available') |
| except GoogleAuthError as e: |
| logging.error("FAILED TO RUN THREAD FOR %s", name, e) |
| |
| def _session_end(self, event): |
| for name in self.hangups_threads: |
| logging.info('Stopping Hangups thread for %s', name) |
| self.hangups_threads[name].stop() |
| |
| |
| def _disco_register_handler(self, iq): |
| if iq['type'] == 'set': |
| ifrom = iq['from'].bare |
| reg = iq['register'] |
| iq.reply() |
| try: |
| thread = hangupsthread.HangupsThread(self, ifrom, |
| gmail=reg['username'], |
| password=reg['password']) |
| self.hangups_threads[ifrom] = thread |
| thread.start() |
| |
| to=iq['to'] |
| # successfully registered |
| iq.send() |
| # now force the client to add the bot as a buddy |
| self.send_presence(pto=to, ptype='subscribe') |
| |
| except GoogleAuthError as e: |
| iq.error() |
| iq['error']['condition'] = 'not-allowed' |
| iq['error']['text'] = 'Login to Google Failed: {}'.format(e) |
| iq.send() |
| |
| |
| elif iq['type'] == 'get': |
| iq.reply(clear=True) |
| iq.enable('register') |
| register = iq['register'] |
| register['instructions'] = 'Please Enter your gmail login' |
| register.add_field('password') |
| register.add_field('username') |
| iq.send() |
| |
| def _message(self, msg): |
| mto = msg.get_to().bare |
| mfrom = msg.get_from().bare |
| if mto == self.name: |
| self.bot_message(msg) |
| elif mto in self.roster: |
| self.hangups_threads[mfrom].send_message(msg) |
| else: |
| msg.reply('got it').send() |
| |
| def bot_message(self, msg): |
| """handle messages directly to the service |
| A chatbot is far easier than a service implementation because |
| xmpp clients are very uneven in their service implementations |
| once you have the service subscribed, any client can send |
| messages to it |
| |
| current commands are: |
| |
| online - set the bot online |
| offline - set the bot offline""" |
| |
| cmd = msg['body'].split(' ') |
| |
| # strip resource so actions go to all clients |
| mfrom = msg.get_from().bare |
| |
| cmd[0] = cmd[0].lower() |
| |
| if cmd[0] == 'offline': |
| self.send_presence(ptype='unavailable', pto=mfrom) |
| elif cmd[0] == 'online': |
| self.send_presence(pto=mfrom) |
| elif cmd[0] == 'query': |
| if not mfrom in self.hangups_threads: |
| msg.reply("no jid found").send() |
| return |
| body=[] |
| for user in self.hangups_threads[mfrom].user_list.get_all(): |
| if user.is_self: |
| continue |
| body.append(user.full_name + '(' + user.id_.chat_id +')') |
| jid=sleekxmpp.JID(local=user.id_.gaia_id, domain=self.name) |
| self.roster.add(jid) |
| self.roster[jid][mfrom]['whitelisted'] = True |
| self.roster[jid][jid]['name'] = user.full_name |
| self.roster[jid].send_presence(ptype='subscribe',pto=mfrom,pnick=user.full_name) |
| body = "\n".join(body) |
| msg.reply(body).send() |
| else: |
| msg.reply('Command ' + msg['body'] + ' not understood').send() |
| |
| def add_subscription(self, jid, to): |
| self.roster[jid][to]['to'] = True |
| self.roster[jid][to]['from'] = True |
| self.roster[jid][to]['subscription'] = 'both' |
| |
| def incoming_typing(self, to, user, state): |
| jid = sleekxmpp.JID(local=user.id_.gaia_id, domain=self.name) |
| msg = self.Message(sfrom=jid, sto=to, stype='chat') |
| msg['chat_state'] = state |
| msg.send() |
| |
| def incoming_user(self, to, user): |
| jid=sleekxmpp.JID(local=user.id_.gaia_id, domain=self.name) |
| self.roster[jid][to]['whitelisted'] = True |
| self.add_subscription(jid, to) |
| vcard = self['xep_0054'].make_vcard() |
| vcard['FN'] = user.full_name |
| vcard['NICKNAME'] = user.full_name |
| if user.photo_url: |
| vcard['PHOTO']['TYPE'] = 'image/jpeg' |
| try: |
| vcard['PHOTO']['BINVAL'] = urllib.request.urlopen('https:' + user.photo_url).read() |
| except (urllib.error.HTTPError): |
| logging.info("Failed to load photo from %s", user.photo_url) |
| |
| self['xep_0054'].publish_vcard(jid=jid, vcard=vcard) |
| |
| def incoming_presence(self, to, user, reachable, available, mood_message): |
| jid=sleekxmpp.JID(local=user.id_.gaia_id, domain=self.name) |
| |
| if reachable: |
| if not available: |
| ptype = 'away' |
| else: |
| ptype = 'available' |
| else: |
| ptype = 'unavailable' |
| |
| self.roster[jid].send_presence(ptype=ptype, pto = to, |
| pnick = user.full_name, |
| pstatus = mood_message) |
| |
| def incoming_message(self, to, user, text): |
| mfrom = sleekxmpp.JID(local=user.id_.gaia_id, domain=self.name) |
| msg = self.send_message(to, text, mfrom=mfrom) |
| |
| def incoming_disconnect(self, to): |
| for jid in self.roster: |
| if jid == self.name: |
| continue |
| self.roster[jid][to].send_presence(ptype = 'unavailable') |
| |
| def _chatstate(self, msg): |
| mfrom = msg.get_from().bare |
| mto = msg.get_to().bare |
| if mto != self.name: |
| self.hangups_threads[mfrom].send_typing_notification(msg) |
| |
| def _presence_available(self, msg): |
| pass |
| |
| def _presence_probe(self, msg): |
| self._presence_available(msg) |
| |
| def _presence_unavailable(self, msg): |
| pass |