Skip to content

Commit

Permalink
Added xmpp sourcecode
Browse files Browse the repository at this point in the history
  • Loading branch information
FlorianWilhelm committed Jul 13, 2015
1 parent 4b3e4be commit 0630dac
Show file tree
Hide file tree
Showing 4 changed files with 275 additions and 3 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,5 @@ target/

# Custom
.ipynb_checkpoints/*
src/oauth.cfg
.idea/*
57 changes: 54 additions & 3 deletions medbot.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"# \"It's about time to take your medication!\"\n",
"## or how to write a friendly reminder bot ;-)\n",
"\n",
"* Florian Wilhelm\n",
"* Florian Wilhelm, Blue Yonder\n",
"* EuroPython 2015, Bilbao, Spain\n",
"* [https://github.com/blue-yonder/medbot/](https://github.com/blue-yonder/medbot/)"
]
Expand Down Expand Up @@ -41,7 +41,26 @@
}
},
"source": [
"# Motivation"
"# Motivation\n",
"\n",
"Why would anyone write a chat bot?"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"# Motivation\n",
"\n",
"Learn about...\n",
"* the basics of chat protocols like XMPP\n",
"* the concepts of event-driven programming\n",
"* how to write a small Google App\n",
"* help a friend with diabetes\n"
]
},
{
Expand All @@ -53,6 +72,8 @@
},
"source": [
"# XMPP\n",
"* Extensible Messaging and Presence Protocol based on XML\n",
"* developed by the Jabber open-source community in 1999 for near real-time instant messaging\n",
"* GTalk is based on XMPP but Hangout switched to a proprietary protocol\n",
"* Facebook provided an XMPP API until May, 2015\n",
"* AIM limited XMPP support"
Expand Down Expand Up @@ -81,7 +102,24 @@
}
},
"source": [
"#Event-driven Programming"
"# Google Hangouts protocol\n",
"* replaces Google Talk, Google+ Messenger and the Google+ video chat system\n",
"* proprietary protocol, cannot be integrated with multi-chat clients like Pidgin or Adium\n",
"* Some more features like photo messages and share location\n",
"* Python library: [hangups](https://github.com/tdryer/hangups)\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "slide"
}
},
"source": [
"#Event-driven Programming\n",
"\n",
"asynchronous I/O, or non-blocking I/O is a form of input/output processing that permits other processing to continue before the transmission has finished Wikipedia"
]
},
{
Expand Down Expand Up @@ -211,6 +249,19 @@
"![](gfx/dev_console_client_credentials.png)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"https://developers.google.com/oauthplayground/\n",
"Scope parameter is https://www.googleapis.com/auth/googletalk\n",
"Hangouts\n",
"https://www.googleapis.com/auth/plus.me (\"Google+ You\")\n",
"https://www.googleapis.com/auth/plus.login\n",
"https://www.googleapis.com/auth/hangout.av (\"Google+ Hangouts\")\n",
"https://www.googleapis.com/auth/hangout.participants (\"Google+ Hangouts\")"
]
},
{
"cell_type": "markdown",
"metadata": {
Expand Down
184 changes: 184 additions & 0 deletions src/medbot_xmpp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
A chat bot based on SleekXMPP
"""

from __future__ import absolute_import, division, print_function

import sys
import logging
import random
from datetime import datetime, timedelta
from time import sleep

from sleekxmpp import ClientXMPP
from sleekxmpp.exceptions import IqError, IqTimeout
from src.oauth import OAuth


_logger = logging.getLogger(__name__)

if sys.version_info < (3, 0):
from sleekxmpp.util.misc_ops import setdefaultencoding
setdefaultencoding('utf8')
else:
raw_input = input


class ChatClient(ClientXMPP):
def __init__(self, jid, oauth):
ClientXMPP.__init__(self, jid, password=None)
self.oauth = oauth
self.msg_callback = None
self.add_event_handler("session_start", self.session_started, threaded=True)
self.add_event_handler("message", self.message_received)

# Plugins
self.register_plugin('xep_0030') # Service Discovery
#self.register_plugin('xep_0199', {'keepalive': True, 'frequency': 60}) # XMPP Ping
#self.register_plugin('xep_0235')
#self.register_plugin('google')
#self.register_plugin('google_auth')

def add_msg_callback(self, func):
self.msg_callback = func

def get_recipient_id(self, recipient):
for k in self.client_roster.keys():
if self.client_roster[k]['name'] == recipient:
recipient_id = k
break
else:
recipient_id = None
return recipient_id

def connect(self, *args, **kwargs):
_logger.info("Connecting...")
self.credentials['access_token'] = self.oauth.access_token
return super(ChatClient, self).connect(*args, **kwargs)

def reconnect(self, *args, **kwargs):
_logger.info("Reconnecting")
self.credentials['access_token'] = self.oauth.access_token
return super(ChatClient, self).reconnect(*args, **kwargs)

def session_started(self, event):
self.send_presence()
try:
self.get_roster()
except IqError as err:
logging.error('There was an error getting the roster')
logging.error(err.iq['error']['condition'])
self.disconnect()
except IqTimeout:
logging.error('Server is taking too long to respond')
self.disconnect()

def send_msg(self, recipient, msg):
recipient_id = self.get_recipient_id(recipient)
self.send_message(mto=recipient_id, mbody=msg, mtype='chat')

def message_received(self, msg):
_logger.info("Got message from {}".format(msg['from']))
if self.msg_callback is None:
_logger.warn("No callback for message received registered")
else:
self.msg_callback(msg)


class State(object):
not_asked = 0
asked = 1


class MedBot(object):
alarm = ['Have you taken your long-acting insulin analogue?',
'Hey buddy, got your insulin?',
'Have you taken your daily dose of insulin?']
reminder = ['how about now?',
'and now?',
'... maybe now?']
praise = ['Great!', 'Good for you!', 'Well done']
give_up = ["Okay, I'am giving up!", "It can't be helped!"]

def __init__(self, chat_client, recipient, max_retries=5):
self.chat_client = chat_client
self.chat_client.add_msg_callback(self.handle_message)
self.recipient = recipient
self.positive_reply = False
self.curr_state = State.not_asked
self.max_retries = max_retries
self.retries = 0
self.retry_sleep = 1200

def send_alarm(self):
_logger.info("Alarm triggered")
self.positive_reply = False
self.curr_state = State.asked
self.retries = 0
self.chat_client.send_msg(self.recipient,
random.choice(self.alarm))
while not self.positive_reply:
sleep(self.retry_sleep)
if not self.ask_again():
break

def ask_again(self):
_logger.info("Asking again?")
if not self.positive_reply:
if self.retries < self.max_retries:
self.retries += 1
msg = random.choice(self.reminder)
answer = True
else:
msg = random.choice(self.give_up)
answer = False
self.chat_client.send_msg(self.recipient, msg)
return answer

def handle_message(self, msg):
recipient_id = self.chat_client.get_recipient_id(self.recipient)
from_recipient = msg['from'].full.startswith(recipient_id)
is_positive = msg['body'].lower().startswith('ja')
was_asked = self.curr_state == State.asked
if from_recipient and is_positive and was_asked:
_logger.info("Positive reply received")
self.positive_reply = True
self.curr_state = State.not_asked
self.chat_client.send_msg(self.recipient,
random.choice(self.praise))

def _get_secs_to(self, timestamp):
delta = timestamp - datetime.now()
return delta.total_seconds()

def _get_next_alarm(self):
today = datetime.now()
today_alarm = datetime(today.year, today.month, today.day, 18, 40, 0)
if (today_alarm - today - timedelta(seconds=15)).days >= 0:
return today_alarm
else:
return today_alarm + timedelta(days=1)

def run(self):
if self.chat_client.connect():
self.chat_client.process(block=False)
while True:
sleep(self._get_secs_to(self._get_next_alarm()))
self.send_alarm()
else:
raise RuntimeError("Unable to connect!")


if __name__ == '__main__':
logging.basicConfig(level=logging.INFO,
format='%(levelname)-8s %(message)s',
stream=sys.stdout)
oauth = OAuth()
oauth.read_cfg('oauth.cfg')
jid = '[email protected]'
chat_client = ChatClient(jid, oauth)
medbot = MedBot(chat_client, 'Buddy')
medbot.run()
35 changes: 35 additions & 0 deletions src/oauth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-

from __future__ import print_function, absolute_import, division

import requests
from ConfigParser import SafeConfigParser

__author__ = 'Florian Wilhelm'
__copyright__ = 'Blue Yonder'
__license__ = 'new BSD'


class OAuth(object):
def __init__(self, client_id=None, client_secret=None, refresh_token=None):
self.client_id = client_id
self.client_secret = client_secret
self.refresh_token = refresh_token
self.url = 'https://www.googleapis.com/oauth2/v3/token'

def from_cfg(self, filename):
parser = SafeConfigParser()
parser.readfp(filename=filename)
self.client_id = parser.get('credentials', 'client_id')
self.client_secret = parser.get('credentials', 'client_secret')
self.refresh_token = parser.get('credentials', 'refresh_token')

@property
def access_token(self):
params = dict(refresh_token=self.refresh_token,
client_id=self.client_id,
client_secret=self.client_secret,
grant_type='refresh_token')
resp = requests.post(self.url, data=params)
resp.raise_for_status()
return resp.json()["access_token"]

0 comments on commit 0630dac

Please sign in to comment.