2011-08-26 21:19:53 +00:00

280 lines
9.6 KiB
Python
Executable File

#!/usr/bin/env python
# -*- coding: utf8 -*-
###
# Copyright (c) 2011, Valentin Lorentz
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
# Standard library
from __future__ import print_function
import threading
import hashlib
import socket
import time
import sys
import re
# Third-party modules
from PyQt4 import QtCore, QtGui
# Local modules
import connection
import window
# FIXME: internationalize
_ = lambda x:x
refreshingTree = threading.Lock()
class ConfigurationTreeRefresh:
def __init__(self, eventsManager, window):
if not refreshingTree.acquire(False):
return
self._eventsManager = eventsManager
parentItem = QtGui.QStandardItemModel()
window.connect(parentItem, QtCore.SIGNAL('itemClicked()'),
window.configurationItemActivated)
window.configurationTree.setModel(parentItem)
self.items = {'supybot': parentItem}
hash_ = eventsManager.sendCommand('config search ""')
eventsManager.hook(hash_, self.slot)
def slot(self, reply):
"""Slot called when a childs list is got."""
childs = reply.split(', ')
for child in childs:
if '\x02' in child:
hash_ = self._eventsManager.sendCommand('more')
self._eventsManager.hook(hash_, self.slot)
break
elif ' ' in child:
refreshingTree.release()
break
splitted = child.split('.')
parent, name = '.'.join(splitted[0:-1]), splitted[-1]
item = QtGui.QStandardItem(name)
item.name = QtCore.QString(child)
self.items[parent].appendRow(item)
self.items[child] = item
class Connection(QtGui.QTabWidget, connection.Ui_connection):
"""Represents the connection dialog."""
def __init__(self, parent=None):
QtGui.QWidget.__init__(self, parent)
self.setupUi(self)
def accept(self):
"""Signal called when the button 'accept' is clicked."""
self.state.text = _('Connecting...')
if not self._connect():
self.state.text = _('Connection failed.')
return
self.state.text = _('Connected. Loading GUI...')
window = Window(self._eventsManager)
window.show()
window.commandEdit.setFocus()
self._eventsManager.callbackConnectionClosed = window.connectionClosed
self._eventsManager.defaultCallback = window.replyReceived
self.hide()
def _connect(self):
"""Connects to the server, using the filled fields in the GUI.
Return wheter or not the connection succeed. Note that a successful
connection with a failed authentication is interpreted as successful.
"""
server = str(self.editServer.text()).split(':')
username = str(self.editUsername.text())
password = str(self.editPassword.text())
assert len(server) == 2
assert re.match('[0-9]+', server[1])
assert ' ' not in username
assert ' ' not in password
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server[1] = int(server[1])
try:
sock.connect(tuple(server))
except socket.error:
return False
sock.settimeout(0.01)
self._eventsManager = EventsManager(sock)
self._eventsManager.sendCommand('identify %s %s' %
(username, password))
return True
def reject(self):
"""Signal called when the button 'close' is clicked."""
exit()
class Window(QtGui.QTabWidget, window.Ui_window):
"""Represents the main window."""
def __init__(self, eventsManager, parent=None):
QtGui.QWidget.__init__(self, parent)
self._eventsManager = eventsManager
self.setupUi(self)
self.connect(self.commandEdit, QtCore.SIGNAL('returnPressed()'),
self.commandSendHandler)
self.connect(self.commandSend, QtCore.SIGNAL('clicked()'),
self.commandSendHandler)
self.connect(self.refreshConfigurationTree, QtCore.SIGNAL('clicked()'),
self._refreshConfigurationTree)
def commandSendHandler(self):
"""Slot called when the user clicks 'Send' or presses 'Enter' in the
raw commands tab."""
command = self.commandEdit.text()
self.commandEdit.clear()
try:
# No hooking, because the callback would be the default callback
self._eventsManager.sendCommand(command)
s = _('<-- ') + command
except socket.error:
s = _('(not sent) <-- ') + command
self.commandsHistory.appendPlainText(s)
def replyReceived(self, reply):
"""Called by the events manager when a reply to a raw command is
received."""
self.commandsHistory.appendPlainText(_('--> ') + reply.decode('utf8'))
def connectionClosed(self):
"""Called by the events manager when a special message has to be
displayed."""
self.commandsHistory.appendPlainText(_('* connection closed *'))
self.commandEdit.readOnly = True
self._eventsManager.stop()
def _refreshConfigurationTree(self):
"""Slot called when the user clicks 'Refresh' under the configuration
tree."""
ConfigurationTreeRefresh(self._eventsManager, self)
def configurationItemActivated(self, item):
print(repr(item))
class EventsManager(QtCore.QObject):
"""This class handles all incoming messages, and call the associated
callback (using hook() method)"""
def __init__(self, sock):
self._sock = sock
self.defaultCallback = lambda x:x
self._currentLine = ''
self._hooks = {} # FIXME: should be cleared every minute
self._timerGetReplies = QtCore.QTimer()
self.connect(self._timerGetReplies, QtCore.SIGNAL('timeout()'),
self._getReplies);
self._timerGetReplies.start(100)
self._timerCleanHooks = QtCore.QTimer()
self.connect(self._timerCleanHooks, QtCore.SIGNAL('timeout()'),
self._cleanHooks);
self._timerCleanHooks.start(100)
def _getReplies(self):
"""Called by the QTimer; fetches the messages and calls the hooks."""
currentLine = self._currentLine
self.currentLine = ''
if not '\n' in currentLine:
try:
data = self._sock.recv(65536)
if not data: # Frontend closed connection
self.callbackConnectionClosed()
return
currentLine += data
except socket.timeout:
return
if '\n' in currentLine:
splitted = currentLine.split('\n')
nextLines = '\n'.join(splitted[1:-1])
splitted = splitted[0].split(': ')
hash_, reply = splitted[0], ': '.join(splitted[1:])
if hash_ in self._hooks:
self._hooks[hash_][0](reply)
else:
self.defaultCallback(reply)
else:
nextLines = currentLine
self._currentLine = nextLines
def hook(self, hash_, callback, lifeTime=60):
"""Attach a callback to a hash: everytime a reply with this hash is
received, the callback is called."""
self._hooks[hash_] = (callback, time.time() + lifeTime)
def unhook(self, hash_):
"""Undo hook()."""
return self._hooks.pop(hash_)
def _cleanHooks(self):
for hash_, data in self._hooks.items():
if data[1] < time.time():
self._hooks.pop(hash_)
def sendCommand(self, command):
"""Get a command, send it, and returns a unique hash, used to identify
replies to this command."""
hash_ = hashlib.sha1(str(time.time()) + command).hexdigest()
command = '%s: %s\n' % (hash_, unicode(command).encode('utf8', 'replace'))
self._sock.send(command)
return hash_
def stop(self):
"""Stops the loop."""
self._timer.stop()
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
connection = Connection()
connection.show()
sys.exit(app.exec_())