From 0b79f6b728951bedf9fca8f8027e231249ff28a5 Mon Sep 17 00:00:00 2001 From: "louiz@4325f9fc-e183-4c21-96ce-0ab188b42d13" Date: Sun, 10 Jan 2010 20:14:17 +0000 Subject: first commit --- src/client.py | 56 +++++++++++++++ src/config.py | 56 +++++++++++++++ src/connection.py | 129 +++++++++++++++++++++++++++++++++++ src/gui.py | 79 ++++++++++++++++++++++ src/handler.py | 134 ++++++++++++++++++++++++++++++++++++ src/logging.py | 45 +++++++++++++ src/multiuserchat.py | 187 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/singleton.py | 25 +++++++ 8 files changed, 711 insertions(+) create mode 100644 src/client.py create mode 100644 src/config.py create mode 100644 src/connection.py create mode 100644 src/gui.py create mode 100644 src/handler.py create mode 100644 src/logging.py create mode 100644 src/multiuserchat.py create mode 100644 src/singleton.py (limited to 'src') diff --git a/src/client.py b/src/client.py new file mode 100644 index 00000000..e51c278e --- /dev/null +++ b/src/client.py @@ -0,0 +1,56 @@ +#!/usr/bin/python +# -*- coding:utf-8 -*- +# +# Copyright 2010 Le Coz Florent +# +# This file is part of Poezio. +# +# Poezio is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, version 3 of the License. +# +# Poezio is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Poezio. If not, see . + +from connection import Connection +from multiuserchat import MultiUserChat +from config import config +from handler import Handler +from gui import Gui + +class Client(object): + """ + Main class + Do what should be done automatically by the Client: + join the rooms at startup, for example + """ + def __init__(self): + self.handler = Handler() + + self.resource = config.get('resource') + self.server = config.get('server') + self.connection = Connection(self.server, self.resource) + self.connection.start() + + self.muc = MultiUserChat(self.connection.client) + self.gui = Gui() + self.rooms = config.get('rooms').split(':') + + import time + time.sleep(1) # remove + for room in self.rooms: + self.handler.emit('join-room', room = room.split('/')[0], nick=room.split('/')[1]) + while 1: + self.connection.process() + + +def main(): + client = Client() + +if __name__ == '__main__': + main() diff --git a/src/config.py b/src/config.py new file mode 100644 index 00000000..ed50f622 --- /dev/null +++ b/src/config.py @@ -0,0 +1,56 @@ +#!/usr/bin/python +# -*- coding:utf-8 -*- +# +# Copyright 2009 chickenzilla +# Copyright 2010 Le Coz Florent +# +# This file is part of Poezio. +# +# Poezio is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, version 3 of the License. +# +# Poezio is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Poezio. If not, see . + +from ConfigParser import RawConfigParser + +class Config(RawConfigParser): + """ + load/save the config to a file + """ + def __init__(self, file_name): + self.defsection = "Poezio" + self.file_name = file_name + RawConfigParser.__init__(self, None) + RawConfigParser.read(self, file_name) + + def get(self, option): + return RawConfigParser.get(self, self.defsection, option) + + def getint(self, option): + return int(self.get(option)) + + def getfloat(self, option): + return float(self.get(option)) + + def getboolean(self, option): + return RawConfigParser.getboolean(self, self.defsection, option) + + def set(self, option, value): + RawConfigParser.set(self, self.defsection, option, value) + + def save(self): + with copen(self.filename, "w", "utf-8", "ignore") as f: + RawConfigParser.write(self, f) + + def setAndSave(self, option, value): + self.set(option, value) + self.save() + +config = Config('poezio.cfg') diff --git a/src/connection.py b/src/connection.py new file mode 100644 index 00000000..4e34384b --- /dev/null +++ b/src/connection.py @@ -0,0 +1,129 @@ +#!/usr/bin/python +# -*- coding:utf-8 -*- +# +# Copyright 2010 Le Coz Florent +# +# This file is part of Poezio. +# +# Poezio is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, version 3 of the License. +# +# Poezio is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Poezio. If not, see . + +import sys + +import xmpp +from config import config +from logging import log +from threading import Thread +from multiuserchat import MultiUserChat +from handler import Handler + +class Connection(Thread): + """ + Handles all network transactions + """ + def __init__(self, server, resource): + Thread.__init__(self) + self.handler = Handler() + + self.server = server + self.resource = resource + self.online = 0 # 1:connected, 2:auth confirmed + self.jid = '' # we don't know our jid yet (anon account) + if not self.server: + log.error('You should set a server in the configuration file') + self.port = int(config.get('port')) + if not self.port: + log.warning('No port set in configuration file, defaulting to 5222') + self.port = 5222 + + def run(self): + """ + connect to server + """ + self.client = xmpp.Client(self.server) + if not self.connect_to_server(self.server, self.port): + log.error('Could not connect to server') + sys.exit(-1) + if not self.authenticate(): + log.error('Could not authenticate to server') + sys.exit(-1) + self.client.sendInitPresence() + self.online = 1 # 2 when confirmation of auth is received + self.register_handlers() + + def connect_to_server(self, server, port): + # TODO proxy stuff + return self.client.connect((server, port)) + + def authenticate(self, anon=True): + if anon: + return self.client.auth(None, None, self.resource) + else: + log.error('Non-anonymous connections not handled currently') + return None + + def register_handlers(self): + self.client.RegisterHandler('message', self.handler_message) + self.client.RegisterHandler('presence', self.handler_presence) + self.client.RegisterHandler('iq', self.handler_iq) + + def handler_message(self, connection, message): + # body = message.getTag('body').getData() + # fro = str(message.getAttr('from')) + # room, nick = fro.split('/') + # print 'Message from %s in %s :' % (nick, room), body + self.handler.emit('xmpp-message-handler', message=message) + + def handler_presence(self, connection, presence): + affil, role = u'', u'' + fro = presence.getFrom()#presence.getAttr('from') + room_from, nick_from = fro.getStripped().encode('utf-8'), fro.getResource().encode('utf-8') + to = presence.getAttr('to') + room_to, nick_to = to.getStripped().encode('utf-8'), to.getResource().encode('utf-8') + if fro == to: # own presence + self.online = 2 + self.jid = to + print 'Authentification confirmation received!' + return + for x in presence.getTags('x'): + if x.getTag('item'): + affil = x.getTagAttr('item', 'affiliation').encode('utf-8') + role = x.getTagAttr('item', 'role').encode('utf-8') + break +# print '[%s] in room {%s}. (%s - %s)'% (nick_from, room_from, affil, role) + self.handler.emit('xmpp-presence-handler', presence=presence) + + def handler_iq(self, connection, iq): + pass + + def process(self, timeout=10): + if self.online: + self.client.Process(timeout) + else: + log.warning('disconnecting...') + +if __name__ == '__main__': + resource = config.get('resource') + server = config.get('server') + connection = Connection(server, resource) + connection.start() + rooms = config.get('rooms').split(':') + from time import sleep + print connection.online + sleep(2) + print connection.online + for room in rooms: + connection.send_join_room(room.split('/')[0], room.split('/')[1]) + i = 17 + while i: + connection.process() + i -= 1 diff --git a/src/gui.py b/src/gui.py new file mode 100644 index 00000000..44b35f73 --- /dev/null +++ b/src/gui.py @@ -0,0 +1,79 @@ +#!/usr/bin/python +# -*- coding:utf-8 -*- +# +# Copyright 2010 Le Coz Florent +# +# This file is part of Poezio. +# +# Poezio is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, version 3 of the License. +# +# Poezio is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Poezio. If not, see . + +from handler import Handler +import curses +from curses import textpad + +class Gui(object): + """ + Graphical user interface using ncurses + """ + def __init__(self): + self.handler = Handler() + + self.handler.connect('on-muc-message-received', self.on_message) + self.handler.connect('join-room', self.on_join_room) + self.handler.connect('on-muc-presence-changed', self.on_presence) + + self.init_curses() + + def __del__(self): + curses.nocbreak(); + self.stdscr.keypad(0); + curses.echo() + curses.endwin() + + def init_curses(self): + curses.initscr() + self.stdscr = curses.newwin(1, 1000, 0, 0) + curses.noecho() + curses.cbreak() + curses.meta(True) + self.stdscr.keypad(1) + self.input = textpad.Textbox(self.stdscr) + + def on_message(self, jid, msg, subject, typ, stanza): + print "on_message", jid, msg, subject, typ + + def on_join_room(self, room, nick): + print "on_join_room", room, nick + + def on_presence(self, jid, priority, show, status, stanza): + print "on presence", jid, priority, show, status + + def get_input(self): + return self.stdscr.getch() +def sigwinch_handler(n, frame): + fd = open('fion', 'a') + fd.write(str(n)+ '\n') + fd.close() + +if __name__ == '__main__': + gui = Gui() + import signal + signal.signal(signal.SIGWINCH, sigwinch_handler) + while 1: + key = gui.stdscr.getch() + if key == curses.KEY_RESIZE: + print "FION" + import sys + sys.exit() + gui.input.do_command(key) + diff --git a/src/handler.py b/src/handler.py new file mode 100644 index 00000000..b08280a0 --- /dev/null +++ b/src/handler.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- + +# Copyright 2009, 2010 Erwan Briand +# Copyright 2010, Florent Le Coz + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation version 3 of the License. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from singleton import Singleton + +class Handler(Singleton): + """This class is the global handler for the software's signals.""" + __is_first_instance = True + + def __init__(self): + if Handler.__is_first_instance: + Handler.__is_first_instance = False + + self.__signals__ = { + # - XMPP's core handlers (don't use outside of src/jabber/*.py) + + 'xmpp-presence-handler': list(), + # A presence is received + # Args: the stanza object + + 'xmpp-iq-handler': list(), + # An iq is received + # Args: the stanza object + + 'xmpp-message-handler': list(), + # A message is received + # Args: the stanza object + + # - GUI event + + 'on-quit': list(), + # When the user wants to quit. + + # - Roster and presence + + 'on-connected': list(), + # At the end of a successful connection process. + + 'on-disconnected': list(), + # When the user is disconnected from the server. + + 'on-message-received': list(), + # When a message is received. + # Args: jid, msg, subject, typ + + 'send-message': list(), + # Send a message to someone. + # Args: jid, msg, subj, typ + + # - vCard (XEP-0054) + + 'vcard-request': list(), + # Request a vcard. + # Args: jid + + 'on-vcard-received': list(), + # When a vcard is received. + # Args: jid, vcard + + # - Multi-User Chat (XEP-0045) + + 'gui-join-room': list(), + # Join a room inside the GUI (call `join-room`). + # Args: room, nickname + + 'join-room': list(), + # Join a room. + # Args: room, nick + + 'quit-room': list(), + # Quit a room. + # Args: room, nick + + 'on-muc-message-received': list(), + # When a message is received. + # Args: jid, msg, subject, typ, stanza + + 'on-muc-presence-changed': list(), + # When someone in the roster changes his presence. + # Args: jid, priority, show, status, stanza + + 'on-muc-error': list(), + # When the MUC composant sends an error + # Args: room, code, msg + + 'eject-user': list(), + # When the user try to eject another one. + # Args: room, action, nick, reason + + 'change-user-role': list(), + # When the user try to change the role of someone. + # Args: room, nick, role + + 'change-user-affiliation': list(), + # When the user try to change the affiliation of someone. + # Args: room, jid, aff + + 'change-subject': list(), + # When the user try to change the topic. + # Args: room, subject + + 'change-nick': list() + # When the user try to change his nick. + # Args: room, nick + } + + def connect(self, signal, func): + """Connect a function to a signal.""" + if func not in self.__signals__[signal]: + self.__signals__[signal].append(func) + else: + print "%s doesn't exist." % signal + + def emit(self, signal, **kwargs): + """Emit a signal.""" + if self.__signals__.has_key(signal): + for func in self.__signals__[signal]: + func(**kwargs) + else: + print "%s doesn't exist." % signal diff --git a/src/logging.py b/src/logging.py new file mode 100644 index 00000000..b0b8b1aa --- /dev/null +++ b/src/logging.py @@ -0,0 +1,45 @@ +#!/usr/bin/python +# -*- coding:utf-8 -*- +# +# Copyright 2010 Le Coz Florent +# +# This file is part of Poezio. +# +# Poezio is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, version 3 of the License. +# +# Poezio is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Poezio. If not, see . + +from config import config +import sys + +class Logger(object): + """ + Appends things to files. Error/information/warning logs + and also log the conversations to logfiles + """ + def __init__(self): + self.logfile = config.get('logfile') + + def warning(self, msg): + # change me + # add timestamp and stuff like that, it's cool + print 'Warning/error ' + msg + if self.logfile: + fd = open(self.logfile, 'a') + fd.write(msg+'\n') + fd.close() + + def error(self, msg): + # change me too + self.warning(msg) + sys.exit(-1) + +log = Logger() diff --git a/src/multiuserchat.py b/src/multiuserchat.py new file mode 100644 index 00000000..c3a382cd --- /dev/null +++ b/src/multiuserchat.py @@ -0,0 +1,187 @@ +# -*- coding: utf-8 -*- + +# Copyright 2009, 2010 Erwan Briand +# Copyright 2010, Florent Le Coz + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation version 3 of the License. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# Implementation of the XEP-0045: Multi-User Chat. + +from xmpp import NS_MUC_ADMIN +from xmpp.protocol import Presence, Iq, Message, JID + +from handler import Handler + +def get_stripped_jid(jid): + """Return the stripped JID (bare representation)""" + if isinstance(jid, basestring): + jid = JID(jid) + return jid.getStripped() + +def is_jid(jid): + """Return True if this is a valid JID""" + if JID(jid).getNode() != '': + return True + +class MultiUserChat(object): + def __init__(self, connection): + self.connection = connection + + self.rooms = [] + self.rn = {} + + self.handler = Handler() + self.handler.connect('join-room', self.join_room) + self.handler.connect('quit-room', self.quit_room) + self.handler.connect('on-disconnected', self.on_disconnect) + self.handler.connect('xmpp-iq-handler', self.on_iq) + self.handler.connect('xmpp-presence-handler', self.on_presence) + self.handler.connect('xmpp-message-handler', self.on_message) + self.handler.connect('eject-user', self.eject_user) + self.handler.connect('change-user-role', self.change_role) + self.handler.connect('change-user-affiliation', self.change_aff) + self.handler.connect('change-subject', self.change_subject) + self.handler.connect('change-nick', self.change_nick) + + def join_room(self, room, nick): + """Join a new room""" + self.rooms.append(room) + self.rn[room] = nick + + pres = Presence(to='%s/%s' % (room, nick)) + self.connection.send(pres) + + def quit_room(self, room, nick): + """Quit a room""" + if room is None and nick is None: + self.on_disconnect() + return + + pres = Presence(to='%s/%s' % (room, nick), typ='unavailable') + self.connection.send(pres) + + self.rooms.remove(unicode(room)) + del self.rn[room] + + def on_disconnect(self): + """Called at disconnection""" + for room in self.rooms: + pres = Presence(to='%s/%s' % (room, self.rn[room]), + typ='unavailable') + self.connection.send(pres) + + self.rooms = [] + self.rn = {} + + def on_iq(self, iq): + """Receive a MUC iq notification""" + from_ = iq.getFrom().__str__() + + if get_stripped_jid(from_) in self.rooms: + children = iq.getChildren() + for child in children: + if child.getName() == 'error': + code = int(child.getAttr('code')) + msg = None + + echildren = child.getChildren() + for echild in echildren: + if echild.getName() == 'text': + msg = echild.getData() + + self.handler.emit('on-muc-error', + room=from_, + code=code, + msg=msg) + + + + def on_presence(self, presence): + """Receive a MUC presence notification""" + from_ = presence.getFrom().__str__() + + if get_stripped_jid(from_) in self.rooms: + self.handler.emit('on-muc-presence-changed', + jid=from_.encode('utf-8'), + priority=presence.getPriority(), + show=presence.getShow(), + status=presence.getStatus(), + stanza=presence + ) + + def on_message(self, message): + """Receive a MUC message notification""" + from_ = message.getFrom().__str__().encode('utf-8') + + if get_stripped_jid(from_) in self.rooms: + body_ = message.getBody() + type_ = message.getType() + subj_ = message.getSubject() + self.handler.emit('on-muc-message-received', + jid=from_, msg=body_, subject=subj_, + typ=type_, stanza=message) + + def eject_user(self, room, action, nick, reason): + """Eject an user from a room""" + iq = Iq(typ='set', to=room) + query = iq.addChild('query', namespace=NS_MUC_ADMIN) + item = query.addChild('item') + + if action == 'kick': + item.setAttr('role', 'none') + if is_jid(nick): + item.setAttr('jid', nick) + else: + item.setAttr('nick', nick) + elif action == 'ban': + item.setAttr('affiliation', 'outcast') + item.setAttr('jid', nick) + + if reason is not None: + rson = item.addChild('reason') + rson.setData(reason) + + self.connection.send(iq) + + def change_role(self, room, nick, role): + """Change the role of an user""" + iq = Iq(typ='set', to=room) + query = iq.addChild('query', namespace=NS_MUC_ADMIN) + item = query.addChild('item') + item.setAttr('nick', nick) + item.setAttr('role', role) + + self.connection.send(iq) + + def change_aff(self, room, jid, aff): + """Change the affiliation of an user""" + iq = Iq(typ='set', to=room) + query = iq.addChild('query', namespace=NS_MUC_ADMIN) + item = query.addChild('item') + item.setAttr('jid', jid) + item.setAttr('affiliation', aff) + + self.connection.send(iq) + + def change_subject(self, room, subject): + """Change the subject of a room""" + message = Message(typ='groupchat', to=room) + subj = message.addChild('subject') + subj.setData(subject) + + self.connection.send(message) + + def change_nick(self, room, nick): + """Change the nickname""" + pres = Presence(to='%s/%s' % (room, nick)) + self.connection.send(pres) diff --git a/src/singleton.py b/src/singleton.py new file mode 100644 index 00000000..4390bc0e --- /dev/null +++ b/src/singleton.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- + +# Copyright 2009, 2010 Erwan Briand +# Copyright 2010, Florent Le Coz + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +class Singleton(object): + """ Canonic Singleton implementation. + """ + _instance = None + def __new__(cls, *args, **kwargs): + if cls._instance is None: + cls._instance = super(Singleton, cls).__new__(cls, *args, **kwargs) + return cls._instance -- cgit v1.2.3