280 lines
9.6 KiB
Python
Executable File
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_())
|