Commit e7cb124f authored by Guillaume BINET's avatar Guillaume BINET

preliminary web-socket-io support

parent 01fb9885
......@@ -130,9 +130,11 @@ class Backend(object):
if private:
response.setTo(mess.getFrom())
response.setType('chat')
response.setFrom(self.jid)
else:
response.setTo(mess.getFrom().getStripped())
response.setType(mess.getType())
response.setFrom(self.jid)
return response
def callback_message(self, conn, mess):
......
import logging
import sys
from errbot.utils import mess_2_embeddablehtml
try:
from PySide import QtCore, QtGui, QtWebKit
from PySide.QtGui import QCompleter
......@@ -64,14 +66,8 @@ class ConnectionMock(Connection, QtCore.QObject):
self.send(mess)
def send(self, mess):
if hasattr(mess, 'getBody') and mess.getBody() and not mess.getBody().isspace():
html_content = mess.getHTML()
if html_content:
body = html_content.getTag('body')
answer = ''.join([unicode(kid) for kid in body.kids]) + body.getData()
else:
answer = mess.getBody()
self.newAnswer.emit(answer, bool(html_content))
content, is_html = mess_2_embeddablehtml(mess)
self.newAnswer.emit(content, is_html)
import re
urlfinder = re.compile(r'http([^\.\s]+\.[^\.\s]*)+[^\.\s]{2,}')
......@@ -117,12 +113,12 @@ class GraphicBackend(ErrBot):
def build_message(self, text):
txt, node = self.build_text_html_message_pair(text)
if node :
return Message(txt, html = node) # rebuild a pure html snippet to include directly in the console html
return Message(txt)
msg = Message(txt, html = node) if node else Message(txt)
msg.setFrom(self.jid)
return msg # rebuild a pure html snippet to include directly in the console html
def serve_forever(self):
self.jid = Identifier('blah') # whatever
self.jid = Identifier(node='Err')
self.connect() # be sure we are "connected" before the first command
self.connect_callback() # notify that the connection occured
......
......@@ -145,11 +145,17 @@ class BotPlugin(BotPluginBase):
def callback_message(self, conn, mess):
"""
Override to get a notified on *ANY* XMPP message.
Override to get a notified on *ANY* message.
If you are interested only by chatting message you can filter for example mess.getType() in ('groupchat', 'chat')
"""
pass
def callback_botmessage(self, mess):
"""
Override to get a notified on messages from the bot itself (emitted from your plugin sisters and brothers for example).
"""
pass
# Proxyfy some useful tools from the motherbot
# this is basically the contract between the plugins and the main bot
......
<!DOCTYPE html>
<html>
<head>
<link href="stylesheets/style.css" rel="stylesheet">
<script type="text/javascript" src="http://code.jquery.com/jquery-1.6.1.min.js">
</script><script type="text/javascript" src="socket.io.js"></script>
<script>
WEB_SOCKET_SWF_LOCATION = "/WebSocketMain.swf";
WEB_SOCKET_DEBUG = true;
// socket.io specific code
var socket = io.connect();
socket.on('connect', function () {
$('#chat').addClass('connected');
});
socket.on('announcement', announce);
socket.on('nicknames', function (nicknames) {
$('#nicknames').empty().append($('<span>Online: </span>'));
for (var i in nicknames) {
$().append($('<b>').text(nicknames[i]));
}
});
socket.on('msg_to_room', message);
socket.on('reconnect', function () {
announce('Reconnected to the server');
});
socket.on('reconnecting', function () {
announce('Attempting to re-connect to the server');
});
socket.on('error', function (e) {
announce(e ? e : 'A unknown error occurred');
});
function announce (msg) {
$('#lines').append($('<p>').append($('<em>').text(msg)));
}
function message (from, msg) {
$('#lines').append($('<p id="msg">').append($('<b>').text(from), '<br/>' + msg));
}
// dom manipulation
$(function () {
$('#set-nickname').submit(function (ev) {
socket.emit('nickname', $('#nick').val(), function (set) {
if (!set) {
clear();
return $('#chat').addClass('nickname-set');
}
$('#nickname-err').css('visibility', 'visible');
});
return false;
});
$('#send-message').submit(function () {
message('me', $('#message').val());
socket.emit('user message', $('#message').val());
clear();
$('#lines').get(0).scrollTop = 10000000;
return false;
});
function clear () {
$('#message').val('').focus();
};
});
</script>
</head>
<body>
<div id="chat">
<div id="nickname">
<form id="set-nickname" class="wrap">
<p>Please type in your nickname and press enter.</p>
<input id="nick">
<p id="nickname-err">Nickname already in use</p>
</form>
</div>
<div id="connecting">
<div class="wrap">Connecting to socket.io server</div>
</div>
<div id="messages">
<div id="nicknames"></div>
<div id="lines"></div>
</div>
<form id="send-message">
<input id="message">
<button>Send</button>
</form>
</div>
</body>
</html>
This source diff could not be displayed because it is too large. You can view the blob instead.
#chat,
#nickname,
#messages {
width: 600px;
}
#chat {
position: relative;
border: 1px solid #ccc;
}
#nickname,
#connecting {
position: absolute;
height: 410px;
z-index: 100;
left: 0;
top: 0;
background: #fff;
text-align: center;
width: 600px;
font: 15px Georgia;
color: #666;
display: block;
}
#nickname .wrap,
#connecting .wrap {
padding-top: 150px;
}
#nickname input {
border: 1px solid #ccc;
padding: 10px;
}
#nickname input:focus {
border-color: #999;
outline: 0;
}
#nickname #nickname-err {
color: #8b0000;
font-size: 12px;
visibility: hidden;
}
.connected #connecting {
display: none;
}
.nickname-set #nickname {
display: none;
}
#messages {
height: 380px;
background: #eee;
}
#messages em {
text-shadow: 0 1px 0 #fff;
color: #999;
}
#messages p {
padding: 0;
margin: 0;
font: 12px Helvetica, Arial;
padding: 5px 10px;
}
#messages p b {
display: inline-block;
padding-right: 10px;
}
#messages #msg:nth-child(even) {
background: #fafafa;
}
#messages #nicknames {
background: #ccc;
padding: 2px 4px 4px;
font: 11px Helvetica;
}
#messages #nicknames span {
color: #000;
}
#messages #nicknames b {
display: inline-block;
color: #fff;
background: #999;
padding: 3px 6px;
margin-right: 5px;
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
border-radius: 10px;
text-shadow: 0 1px 0 #666;
}
#messages #lines {
height: 355px;
overflow: auto;
overflow-x: hidden;
overflow-y: auto;
}
#messages #lines::-webkit-scrollbar {
width: 6px;
height: 6px;
}
#messages #lines::-webkit-scrollbar-button:start:decrement,
#messages #lines ::-webkit-scrollbar-button:end:increment {
display: block;
height: 10px;
}
#messages #lines::-webkit-scrollbar-button:vertical:increment {
background-color: #fff;
}
#messages #lines::-webkit-scrollbar-track-piece {
background-color: #fff;
-webkit-border-radius: 3px;
}
#messages #lines::-webkit-scrollbar-thumb:vertical {
height: 50px;
background-color: #ccc;
-webkit-border-radius: 3px;
}
#messages #lines::-webkit-scrollbar-thumb:horizontal {
width: 50px;
background-color: #fff;
-webkit-border-radius: 3px;
}
#send-message {
background: #fff;
position: relative;
}
#send-message input {
border: none;
height: 30px;
padding: 0 10px;
line-height: 30px;
vertical-align: middle;
width: 580px;
}
#send-message input:focus {
outline: 0;
}
#send-message button {
position: absolute;
top: 5px;
right: 5px;
}
button {
margin: 0;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
display: inline-block;
text-decoration: none;
background: #43a1f7;
background: -webkit-gradient(linear, left top, left bottom, color-stop(0, #43a1f7), color-stop(1, #377ad0));
background: -webkit-linear-gradient(top, #43a1f7 0%, #377ad0 100%);
background: -moz-linear-gradient(top, #43a1f7 0%, #377ad0 100%);
background: linear-gradient(top, #43a1f7 0%, #377ad0 100%);
border: 1px solid #2e70c4;
-webkit-border-radius: 16px;
-moz-border-radius: 16px;
border-radius: 16px;
color: #fff;
font-family: "lucida grande", sans-serif;
font-size: 11px;
font-weight: normal;
line-height: 1;
padding: 3px 10px 5px 10px;
text-align: center;
text-shadow: 0 -1px 1px #2d6dc0;
}
button:hover,
button.hover {
background: darker;
background: -webkit-gradient(linear, left top, left bottom, color-stop(0, #43a1f7), color-stop(1, #2e70c4));
background: -webkit-linear-gradient(top, #43a1f7 0%, #2e70c4 100%);
background: -moz-linear-gradient(top, #43a1f7 0%, #2e70c4 100%);
background: linear-gradient(top, #43a1f7 0%, #2e70c4 100%);
border: 1px solid #2e70c4;
cursor: pointer;
text-shadow: 0 -1px 1px #2c6bbb;
}
button:active,
button.active {
background: #2e70c4;
border: 1px solid #2e70c4;
border-bottom: 1px solid #2861aa;
text-shadow: 0 -1px 1px #2b67b5;
}
button:focus,
button.focus {
outline: none;
-webkit-box-shadow: 0 1px 0 0 rgba(255,255,255,0.4), 0 0 4px 0 #377ad0;
-moz-box-shadow: 0 1px 0 0 rgba(255,255,255,0.4), 0 0 4px 0 #377ad0;
box-shadow: 0 1px 0 0 rgba(255,255,255,0.4), 0 0 4px 0 #377ad0;
}
from inspect import getmembers, ismethod
import logging
import os
from threading import Thread
import urllib2
from errbot import holder
import simplejson
from simplejson.decoder import JSONDecodeError
from werkzeug.serving import ThreadedWSGIServer
from werkzeug.wsgi import SharedDataMiddleware
from flask.views import View
from flask import Flask, request, send_file, redirect, Response
from errbot import holder
from errbot import botcmd
from errbot import BotPlugin
from errbot.utils import mess_2_embeddablehtml
from errbot.version import VERSION
from errbot.plugin_manager import get_all_active_plugin_objects
from errbot.bundled.exrex import generate
from errbot.backends.base import Identifier
from flask.views import View
from flask import request
from flask import Response
OK = Response()
......@@ -48,11 +53,13 @@ class WebView(View):
raise Exception('Problem finding back the correct Handlerfor func %s', self.func)
def webhook(*args, **kwargs):
"""
Simple shortcut for the plugins to be notified on webhooks
"""
def decorate(method, uri_rule, methods=('POST',), form_param = None):
def decorate(method, uri_rule, methods=('POST',), form_param=None):
logging.info("webhooks: Bind %s to %s" % (uri_rule, method.__name__))
for rule in holder.flask_app.url_map._rules:
......@@ -60,7 +67,7 @@ def webhook(*args, **kwargs):
holder.flask_app.view_functions[rule.endpoint] = WebView.as_view(method.__name__, method, form_param) # in case of reload just update the view fonction reference
return method
holder.flask_app.add_url_rule(uri_rule, view_func=WebView.as_view(method.__name__, method, form_param), methods = methods)
holder.flask_app.add_url_rule(uri_rule, view_func=WebView.as_view(method.__name__, method, form_param), methods=methods)
return method
if isinstance(args[0], basestring):
......@@ -68,27 +75,70 @@ def webhook(*args, **kwargs):
return decorate(args[0], '/' + args[0].__name__ + '/', **kwargs)
class Webserver(BotPlugin):
min_err_version = VERSION # don't copy paste that for your plugin, it is just because it is a bundled plugin !
max_err_version = VERSION
webserver_thread = None
server = None
webchat_mode = False
def run_webserver(self):
try:
host = self.config['HOST']
port = self.config['PORT']
logging.info('Starting the webserver on %s:%i' % (host, port))
self.server = ThreadedWSGIServer(host, port, holder.flask_app)
if self.webchat_mode:
# EVERYTHING NEEDS TO BE IN THE SAME THREAD OTHERWISE Socket.IO barfs
from socketio import socketio_manage
from socketio.namespace import BaseNamespace
from socketio.mixins import RoomsMixin, BroadcastMixin
class ChatNamespace(BaseNamespace, RoomsMixin, BroadcastMixin):
def on_nickname(self, nickname):
self.environ.setdefault('nicknames', []).append(nickname)
self.socket.session['nickname'] = nickname
self.broadcast_event('announcement', '%s has connected' % nickname)
self.broadcast_event('nicknames', self.environ['nicknames'])
# Just have them join a default-named room
self.join('main_room')
def on_user_message(self, msg):
self.emit_to_room('main_room', 'msg_to_room', self.socket.session['nickname'], msg)
message = holder.bot.build_message(msg)
message.setType('groupchat') # really important for security reasons
message.setFrom(Identifier(node=self.socket.session['nickname'], domain=host))
holder.bot.callback_message(holder.bot.conn, message)
def recv_message(self, message):
print "PING!!!", message
@holder.flask_app.route('/')
def index():
return redirect('/chat.html')
@holder.flask_app.route("/socket.io/<path:path>")
def run_socketio(path):
socketio_manage(request.environ, {'': ChatNamespace})
holder.flask_app = SharedDataMiddleware(holder.flask_app, {
'/': os.path.join(os.path.dirname(__file__), 'web-static')
})
from socketio.server import SocketIOServer
self.server = SocketIOServer((host, port), holder.flask_app, namespace="socket.io", policy_server=False)
else:
self.server = ThreadedWSGIServer(host, port, holder.flask_app)
self.server.serve_forever()
logging.debug('Webserver stopped')
except Exception as e:
logging.exception('The webserver exploded.')
def get_configuration_template(self):
return {'HOST': '0.0.0.0', 'PORT': 3141, 'EXTRA_FLASK_CONFIG': None}
return {'HOST': '0.0.0.0', 'PORT': 3141, 'EXTRA_FLASK_CONFIG': None, 'WEBCHAT': False}
def activate(self):
if not self.config:
......@@ -96,9 +146,12 @@ class Webserver(BotPlugin):
return
if self.config['EXTRA_FLASK_CONFIG']:
holder.flask_app.config.update(self.config['EXTRA_FLASK_CONFIG'])
self.webchat_mode = self.config['WEBCHAT']
if self.webserver_thread:
raise Exception('Invalid state, you should not have a webserver already running.')
self.webserver_thread = Thread(target=self.run_webserver, name = 'Webserver Thread')
self.webserver_thread = Thread(target=self.run_webserver, name='Webserver Thread')
self.webserver_thread.start()
super(Webserver, self).activate()
......@@ -133,7 +186,7 @@ class Webserver(BotPlugin):
"""
endpoint = args[0]
content = ' '.join(args[1:])
for rule in holder.flask_app.url_map.iter_rules():
for rule in holder.flask_app.url_map.iter_rules():
if endpoint == rule.endpoint:
with holder.flask_app.test_client() as client:
logging.debug('Found the matching rule : %s' % rule.rule)
......@@ -157,14 +210,32 @@ class Webserver(BotPlugin):
logging.debug('Detected your post as : %s' % contenttype)
response = client.post(generated_url, data=content, content_type = contenttype)
return TEST_REPORT %(rule.rule, generated_url, contenttype, response.status_code)
response = client.post(generated_url, data=content, content_type=contenttype)
return TEST_REPORT % (rule.rule, generated_url, contenttype, response.status_code)
return 'Could not find endpoint %s. Check with !webstatus which endpoints are deployed' % endpoint
#@webhook(r'/zourby/')
#def zourby(self, incoming_request):
# logging.debug(type(incoming_request))
# logging.debug(str(incoming_request))
# return str(holder.bot.status(None, None))
def emit_mess_to_webroom(self, mess):
if hasattr(mess, 'getBody') and mess.getBody() and not mess.getBody().isspace():
content, is_html = mess_2_embeddablehtml(mess)
if not is_html:
content = '<pre>' + content + '</pre>'
else:
content = '<div>' + content + '</div>'
pkt = dict(type="event",
name='msg_to_room',
args=(mess.getFrom().getNode(), content),
endpoint='')
room_name = '_main_room'
for sessid, socket in self.server.sockets.iteritems():
if 'rooms' not in socket.session:
continue
if room_name in socket.session['rooms']:
socket.send_packet(pkt)
def callback_message(self, conn, mess):
if mess.getFrom().getDomain() != self.config['HOST']: # TODO FIXME this is too ugly
self.emit_mess_to_webroom(mess)
def callback_botmessage(self, mess):
if self.webchat_mode:
self.emit_mess_to_webroom(mess)
\ No newline at end of file
......@@ -107,16 +107,23 @@ class ErrBot(Backend, StoreMixin):
self.all_candidates = all_candidates
return errors
def send_message(self, mess):
super(ErrBot, self).send_message(mess)
# Act only in the backend tells us that this message is OK to broadcast
for bot in get_all_active_plugin_objects():
try:
bot.callback_botmessage(mess)
except Exception:
logging.exception("Crash in a callback_botmessage handler")
def callback_message(self, conn, mess):
if super(ErrBot, self).callback_message(conn, mess):
# Act only in the backend tells us that this message is OK to broadcast
for bot in get_all_active_plugin_objects():
if hasattr(bot, 'callback_message'):
try:
bot.callback_message(conn, mess)
except Exception:
logging.exception("Probably a type error")
try:
bot.callback_message(conn, mess)
except Exception:
logging.exception("Crash in a callback_message handler")
def activate_non_started_plugins(self):
logging.info('Activating all the plugins...')
......
......@@ -197,3 +197,11 @@ def unicode_filter(key):
return key.encode('utf-8')
return key
def mess_2_embeddablehtml(mess):
html_content = mess.getHTML()
if html_content:
body = html_content.getTag('body')
return ''.join([unicode(kid) for kid in body.kids]) + body.getData(), True
else:
return mess.getBody(), False
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment