Mercurial > libervia-backend
view frontends/src/quick_frontend/quick_app.py @ 1005:b4af31a8a4f2
core (logs): added formatting, name filter and outputs management:
- formatting is inspired from, and use when possible, standard logging. "message", "levelname", and "name" are the only format managed, depending on backend more can be managed (standard backend formats are specified in official python logging doc)
- name filter use regular expressions. It's possible to log only plugins with SAT_LOG_LOGGER="^sat.plugins". To log only XEPs 96 & 65, we can use SAT_LOG_LOGGER='(xep_0095|xep_0065)'
- output management use a particular syntax:
- output handler are name with "//", so far there are "//default" (most of time stderr), "//memory" and "//file"
- options can be specified in parenthesis, e.g. "//memory(50)" mean a 50 lines memory buffer (50 is the current default, so that's equivalent to "//memory")
- several handlers can be specified: "//file(/tmp/sat.log)//default" will use the default logging + a the /tmp/sat.log file
- if there is only one handler, it use the file handler: "/tmp/sat.log" is the same as "//file(/tmp/sat.log)"
- not finished, need more work for twisted and basic backends
author | Goffi <goffi@goffi.org> |
---|---|
date | Mon, 05 May 2014 18:58:34 +0200 |
parents | cad958239b5c |
children | d1084f7e56a5 |
line wrap: on
line source
#!/usr/bin/python # -*- coding: utf-8 -*- # helper class for making a SAT frontend # Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 Jérôme Poisson (goffi@goffi.org) # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # 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 Affero General Public License for more details. # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. from sat.core.i18n import _ import sys from logging import debug, info, warning, error from sat.tools.jid import JID from sat_frontends.bridge.DBus import DBusBridgeFrontend from sat.core.exceptions import BridgeExceptionNoService, BridgeInitError from sat_frontends.quick_frontend.quick_utils import escapePrivate, unescapePrivate from optparse import OptionParser from sat_frontends.quick_frontend.constants import Const class QuickApp(object): """This class contain the main methods needed for the frontend""" def __init__(self, single_profile=True): self.profiles = {} self.single_profile = single_profile self.check_options() ## bridge ## try: self.bridge = DBusBridgeFrontend() except BridgeExceptionNoService: print(_(u"Can't connect to SàT backend, are you sure it's launched ?")) sys.exit(1) except BridgeInitError: print(_(u"Can't init bridge")) sys.exit(1) self.bridge.register("connected", self.connected) self.bridge.register("disconnected", self.disconnected) self.bridge.register("connectionError", self.connectionError) self.bridge.register("newContact", self.newContact) self.bridge.register("newMessage", self._newMessage) self.bridge.register("newAlert", self.newAlert) self.bridge.register("presenceUpdate", self.presenceUpdate) self.bridge.register("subscribe", self.subscribe) self.bridge.register("paramUpdate", self.paramUpdate) self.bridge.register("contactDeleted", self.contactDeleted) self.bridge.register("entityDataUpdated", self.entityDataUpdated) self.bridge.register("askConfirmation", self.askConfirmation) self.bridge.register("actionResult", self.actionResult) self.bridge.register("actionResultExt", self.actionResult) self.bridge.register("roomJoined", self.roomJoined, "plugin") self.bridge.register("roomLeft", self.roomLeft, "plugin") self.bridge.register("roomUserJoined", self.roomUserJoined, "plugin") self.bridge.register("roomUserLeft", self.roomUserLeft, "plugin") self.bridge.register("roomUserChangedNick", self.roomUserChangedNick, "plugin") self.bridge.register("roomNewSubject", self.roomNewSubject, "plugin") self.bridge.register("tarotGameStarted", self.tarotGameStarted, "plugin") self.bridge.register("tarotGameNew", self.tarotGameNew, "plugin") self.bridge.register("tarotGameChooseContrat", self.tarotChooseContrat, "plugin") self.bridge.register("tarotGameShowCards", self.tarotShowCards, "plugin") self.bridge.register("tarotGameYourTurn", self.tarotMyTurn, "plugin") self.bridge.register("tarotGameScore", self.tarotScore, "plugin") self.bridge.register("tarotGameCardsPlayed", self.tarotCardsPlayed, "plugin") self.bridge.register("tarotGameInvalidCards", self.tarotInvalidCards, "plugin") self.bridge.register("quizGameStarted", self.quizGameStarted, "plugin") self.bridge.register("quizGameNew", self.quizGameNew, "plugin") self.bridge.register("quizGameQuestion", self.quizGameQuestion, "plugin") self.bridge.register("quizGamePlayerBuzzed", self.quizGamePlayerBuzzed, "plugin") self.bridge.register("quizGamePlayerSays", self.quizGamePlayerSays, "plugin") self.bridge.register("quizGameAnswerResult", self.quizGameAnswerResult, "plugin") self.bridge.register("quizGameTimerExpired", self.quizGameTimerExpired, "plugin") self.bridge.register("quizGameTimerRestarted", self.quizGameTimerRestarted, "plugin") self.bridge.register("chatStateReceived", self.chatStateReceived, "plugin") self.current_action_ids = set() self.current_action_ids_cb = {} self.media_dir = self.bridge.getConfig('', 'media_dir') def check_profile(self, profile): """Tell if the profile is currently followed by the application""" return profile in self.profiles.keys() def postInit(self): """Must be called after initialization is done, do all automatic task (auto plug profile)""" if self.options.profile: if not self.bridge.getProfileName(self.options.profile): error(_("Trying to plug an unknown profile (%s)" % self.options.profile)) else: self.plug_profile(self.options.profile) def check_options(self): """Check command line options""" usage = _(""" %prog [options] %prog --help for options list """) parser = OptionParser(usage=usage) parser.add_option("-p", "--profile", help=_("Select the profile to use")) (self.options, args) = parser.parse_args() if self.options.profile: self.options.profile = self.options.profile.decode('utf-8') return args def _getParamError(self, ignore): error(_("Can't get profile parameter")) def plug_profile(self, profile_key='@DEFAULT@'): """Tell application which profile must be used""" if self.single_profile and self.profiles: error(_('There is already one profile plugged (we are in single profile mode) !')) return profile = self.bridge.getProfileName(profile_key) if not profile: error(_("The profile asked doesn't exist")) return if profile in self.profiles: warning(_("The profile is already plugged")) return self.profiles[profile] = {} if self.single_profile: self.profile = profile ###now we get the essential params### self.bridge.asyncGetParamA("JabberID", "Connection", profile_key=profile, callback=lambda _jid: self.plug_profile_2(_jid, profile), errback=self._getParamError) def plug_profile_2(self, _jid, profile): self.profiles[profile]['whoami'] = JID(_jid) self.bridge.asyncGetParamA("autoconnect", "Connection", profile_key=profile, callback=lambda value: self.plug_profile_3(value == "true", profile), errback=self._getParamError) def plug_profile_3(self, autoconnect, profile): self.bridge.asyncGetParamA("Watched", "Misc", profile_key=profile, callback=lambda watched: self.plug_profile_4(watched, autoconnect, profile), errback=self._getParamError) def plug_profile_4(self, watched, autoconnect, profile): if autoconnect and not self.bridge.isConnected(profile): #Does the user want autoconnection ? self.bridge.asyncConnect(profile, callback=lambda: self.plug_profile_5(watched, autoconnect, profile), errback=lambda ignore: error(_('Error during autoconnection'))) else: self.plug_profile_5(watched, autoconnect, profile) def plug_profile_5(self, watched, autoconnect, profile): self.profiles[profile]['watched'] = watched.split() # TODO: put this in a plugin ## misc ## self.profiles[profile]['onlineContact'] = set() # FIXME: temporary #TODO: manage multi-profiles here if not self.bridge.isConnected(profile): self.setStatusOnline(False) else: self.setStatusOnline(True) ### now we fill the contact list ### for contact in self.bridge.getContacts(profile): self.newContact(*contact, profile=profile) presences = self.bridge.getPresenceStatuses(profile) for contact in presences: for res in presences[contact]: jabber_id = contact + ('/' + res if res else '') show = presences[contact][res][0] priority = presences[contact][res][1] statuses = presences[contact][res][2] self.presenceUpdate(jabber_id, show, priority, statuses, profile) data = self.bridge.getEntityData(contact, ['avatar', 'nick'], profile) for key in ('avatar', 'nick'): if key in data: self.entityDataUpdated(contact, key, data[key], profile) #The waiting subscription requests waitingSub = self.bridge.getWaitingSub(profile) for sub in waitingSub: self.subscribe(waitingSub[sub], sub, profile) #Now we open the MUC window where we already are: for room_args in self.bridge.getRoomsJoined(profile): self.roomJoined(*room_args, profile=profile) for subject_args in self.bridge.getRoomsSubjects(profile): self.roomNewSubject(*subject_args, profile=profile) #Finaly, we get the waiting confirmation requests for confirm_id, confirm_type, data in self.bridge.getWaitingConf(profile): self.askConfirmation(confirm_id, confirm_type, data, profile) def unplug_profile(self, profile): """Tell the application to not follow anymore the profile""" if not profile in self.profiles: warning(_("This profile is not plugged")) return self.profiles.remove(profile) def clear_profile(self): self.profiles.clear() def connected(self, profile): """called when the connection is made""" if not self.check_profile(profile): return debug(_("Connected")) self.setStatusOnline(True) def disconnected(self, profile): """called when the connection is closed""" if not self.check_profile(profile): return debug(_("Disconnected")) self.contact_list.clearContacts() self.setStatusOnline(False) def connectionError(self, error_type, profile): """called when something goes wrong with the connection""" if not self.check_profile(profile): return debug(_("Connection Error")) self.disconnected(profile) if error_type == "AUTH_ERROR": self.showDialog(_("Can't connect to account, please check your password"), _("Connection error"), "error") else: error(_('FIXME: error_type %s not implemented') % error_type) def newContact(self, JabberId, attributes, groups, profile): if not self.check_profile(profile): return entity = JID(JabberId) _groups = list(groups) self.contact_list.replace(entity, _groups, attributes) def _newMessage(self, from_jid_s, msg, _type, to_jid_s, extra, profile): """newMessage premanagement: a dirty hack to manage private messages if a private MUC message is detected, from_jid or to_jid is prefixed and resource is escaped""" if not self.check_profile(profile): return from_jid = JID(from_jid_s) to_jid = JID(to_jid_s) from_me = from_jid.bare == self.profiles[profile]['whoami'].bare win = to_jid if from_me else from_jid if _type != "groupchat" and self.contact_list.getSpecial(win) == "MUC": #we have a private message in a MUC room #XXX: normaly we use bare jid as key, here we need the full jid # so we cheat by replacing the "/" before the resource by # a "@", so the jid is invalid, new_jid = escapePrivate(win) if from_me: to_jid = new_jid else: from_jid = new_jid if new_jid not in self.contact_list: self.contact_list.add(new_jid) self.newMessage(from_jid, to_jid, msg, _type, extra, profile) def newMessage(self, from_jid, to_jid, msg, _type, extra, profile): from_me = from_jid.bare == self.profiles[profile]['whoami'].bare win = to_jid if from_me else from_jid self.current_action_ids = set() self.current_action_ids_cb = {} timestamp = extra.get('archive') self.chat_wins[win.bare].printMessage(from_jid, msg, profile, float(timestamp) if timestamp else '') def sendMessage(self, to_jid, message, subject='', mess_type="auto", extra={}, callback=None, errback=None, profile_key="@NONE@"): if to_jid.startswith(Const.PRIVATE_PREFIX): to_jid = unescapePrivate(to_jid) mess_type = "chat" if callback is None: callback = lambda: None if errback is None: errback = lambda failure: self.showDialog(unicode(failure), _(u"sendMessage Error"), "error") self.bridge.sendMessage(to_jid, message, subject, mess_type, extra, profile_key, callback=callback, errback=errback) def newAlert(self, msg, title, alert_type, profile): if not self.check_profile(profile): return assert alert_type in ['INFO', 'ERROR'] self.showDialog(unicode(msg), unicode(title), alert_type.lower()) def setStatusOnline(self, online=True, show="", statuses={}): raise NotImplementedError def presenceUpdate(self, jabber_id, show, priority, statuses, profile): if not self.check_profile(profile): return debug(_("presence update for %(jid)s (show=%(show)s, priority=%(priority)s, statuses=%(statuses)s) [profile:%(profile)s]") % {'jid': jabber_id, 'show': show, 'priority': priority, 'statuses': statuses, 'profile': profile}) from_jid = JID(jabber_id) if from_jid == self.profiles[profile]['whoami']: if show == "unavailable": self.setStatusOnline(False) else: self.setStatusOnline(True, show, statuses) return presences = self.profiles[profile].setdefault('presences', {}) if show != 'unavailable': #FIXME: must be moved in a plugin if from_jid.bare in self.profiles[profile].get('watched',[]) and not from_jid.bare in self.profiles[profile]['onlineContact']: self.showAlert(_("Watched jid [%s] is connected !") % from_jid.bare) presences[jabber_id] = {'show': show, 'priority': priority, 'statuses': statuses} self.profiles[profile].setdefault('onlineContact',set()).add(from_jid) # FIXME onlineContact is useless with CM, must be removed #TODO: vcard data (avatar) if show == "unavailable" and from_jid in self.profiles[profile].get('onlineContact',set()): try: del presences[jabber_id] except KeyError: pass self.profiles[profile]['onlineContact'].remove(from_jid) # check if the contact is connected with another resource, use the one with highest priority jids = [jid for jid in presences if JID(jid).bare == from_jid.bare] if jids: max_jid = max(jids, key=lambda jid: presences[jid]['priority']) data = presences[max_jid] max_priority = data['priority'] if show == "unavailable": # do not check the priority here, because 'unavailable' has a dummy one from_jid = JID(max_jid) show, priority, statuses = data['show'], data['priority'], data['statuses'] if not jids or priority >= max_priority: # case 1: not jids means all resources are disconnected, send the 'unavailable' presence # case 2: update (or confirm) with the values of the resource which takes precedence self.contact_list.updatePresence(from_jid, show, priority, statuses) def roomJoined(self, room_jid, room_nicks, user_nick, profile): """Called when a MUC room is joined""" if not self.check_profile(profile): return debug(_("Room [%(room_jid)s] joined by %(profile)s, users presents:%(users)s") % {'room_jid': room_jid, 'profile': profile, 'users': room_nicks}) self.chat_wins[room_jid].setUserNick(user_nick) self.chat_wins[room_jid].setType("group") self.chat_wins[room_jid].id = room_jid self.chat_wins[room_jid].setPresents(list(set([user_nick] + room_nicks))) self.contact_list.setSpecial(JID(room_jid), "MUC", show=True) def roomLeft(self, room_jid_s, profile): """Called when a MUC room is left""" if not self.check_profile(profile): return debug(_("Room [%(room_jid)s] left by %(profile)s") % {'room_jid': room_jid_s, 'profile': profile}) del self.chat_wins[room_jid_s] self.contact_list.remove(JID(room_jid_s)) def roomUserJoined(self, room_jid, user_nick, user_data, profile): """Called when an user joined a MUC room""" if not self.check_profile(profile): return if room_jid in self.chat_wins: self.chat_wins[room_jid].replaceUser(user_nick) debug(_("user [%(user_nick)s] joined room [%(room_jid)s]") % {'user_nick': user_nick, 'room_jid': room_jid}) def roomUserLeft(self, room_jid, user_nick, user_data, profile): """Called when an user joined a MUC room""" if not self.check_profile(profile): return if room_jid in self.chat_wins: self.chat_wins[room_jid].removeUser(user_nick) debug(_("user [%(user_nick)s] left room [%(room_jid)s]") % {'user_nick': user_nick, 'room_jid': room_jid}) def roomUserChangedNick(self, room_jid, old_nick, new_nick, profile): """Called when an user joined a MUC room""" if not self.check_profile(profile): return if room_jid in self.chat_wins: self.chat_wins[room_jid].changeUserNick(old_nick, new_nick) debug(_("user [%(old_nick)s] is now known as [%(new_nick)s] in room [%(room_jid)s]") % {'old_nick': old_nick, 'new_nick': new_nick, 'room_jid': room_jid}) def roomNewSubject(self, room_jid, subject, profile): """Called when subject of MUC room change""" if not self.check_profile(profile): return if room_jid in self.chat_wins: self.chat_wins[room_jid].setSubject(subject) debug(_("new subject for room [%(room_jid)s]: %(subject)s") % {'room_jid': room_jid, "subject": subject}) def tarotGameStarted(self, room_jid, referee, players, profile): if not self.check_profile(profile): return debug(_("Tarot Game Started \o/")) if room_jid in self.chat_wins: self.chat_wins[room_jid].startGame("Tarot", referee, players) debug(_("new Tarot game started by [%(referee)s] in room [%(room_jid)s] with %(players)s") % {'referee': referee, 'room_jid': room_jid, 'players': [str(player) for player in players]}) def tarotGameNew(self, room_jid, hand, profile): if not self.check_profile(profile): return debug(_("New Tarot Game")) if room_jid in self.chat_wins: self.chat_wins[room_jid].getGame("Tarot").newGame(hand) def tarotChooseContrat(self, room_jid, xml_data, profile): """Called when the player has to select his contrat""" if not self.check_profile(profile): return debug(_("Tarot: need to select a contrat")) if room_jid in self.chat_wins: self.chat_wins[room_jid].getGame("Tarot").chooseContrat(xml_data) def tarotShowCards(self, room_jid, game_stage, cards, data, profile): if not self.check_profile(profile): return debug(_("Show cards")) if room_jid in self.chat_wins: self.chat_wins[room_jid].getGame("Tarot").showCards(game_stage, cards, data) def tarotMyTurn(self, room_jid, profile): if not self.check_profile(profile): return debug(_("My turn to play")) if room_jid in self.chat_wins: self.chat_wins[room_jid].getGame("Tarot").myTurn() def tarotScore(self, room_jid, xml_data, winners, loosers, profile): """Called when the game is finished and the score are updated""" if not self.check_profile(profile): return debug(_("Tarot: score received")) if room_jid in self.chat_wins: self.chat_wins[room_jid].getGame("Tarot").showScores(xml_data, winners, loosers) def tarotCardsPlayed(self, room_jid, player, cards, profile): if not self.check_profile(profile): return debug(_("Card(s) played (%(player)s): %(cards)s") % {"player": player, "cards": cards}) if room_jid in self.chat_wins: self.chat_wins[room_jid].getGame("Tarot").cardsPlayed(player, cards) def tarotInvalidCards(self, room_jid, phase, played_cards, invalid_cards, profile): if not self.check_profile(profile): return debug(_("Cards played are not valid: %s") % invalid_cards) if room_jid in self.chat_wins: self.chat_wins[room_jid].getGame("Tarot").invalidCards(phase, played_cards, invalid_cards) def quizGameStarted(self, room_jid, referee, players, profile): if not self.check_profile(profile): return debug(_("Quiz Game Started \o/")) if room_jid in self.chat_wins: self.chat_wins[room_jid].startGame("Quiz", referee, players) debug(_("new Quiz game started by [%(referee)s] in room [%(room_jid)s] with %(players)s") % {'referee': referee, 'room_jid': room_jid, 'players': [str(player) for player in players]}) def quizGameNew(self, room_jid, data, profile): if not self.check_profile(profile): return debug(_("New Quiz Game")) if room_jid in self.chat_wins: self.chat_wins[room_jid].getGame("Quiz").quizGameNew(data) def quizGameQuestion(self, room_jid, question_id, question, timer, profile): """Called when a new question is asked""" if not self.check_profile(profile): return debug(_(u"Quiz: new question: %s") % question) if room_jid in self.chat_wins: self.chat_wins[room_jid].getGame("Quiz").quizGameQuestion(question_id, question, timer) def quizGamePlayerBuzzed(self, room_jid, player, pause, profile): """Called when a player pushed the buzzer""" if not self.check_profile(profile): return if room_jid in self.chat_wins: self.chat_wins[room_jid].getGame("Quiz").quizGamePlayerBuzzed(player, pause) def quizGamePlayerSays(self, room_jid, player, text, delay, profile): """Called when a player say something""" if not self.check_profile(profile): return if room_jid in self.chat_wins: self.chat_wins[room_jid].getGame("Quiz").quizGamePlayerSays(player, text, delay) def quizGameAnswerResult(self, room_jid, player, good_answer, score, profile): """Called when a player say something""" if not self.check_profile(profile): return if room_jid in self.chat_wins: self.chat_wins[room_jid].getGame("Quiz").quizGameAnswerResult(player, good_answer, score) def quizGameTimerExpired(self, room_jid, profile): """Called when nobody answered the question in time""" if not self.check_profile(profile): return if room_jid in self.chat_wins: self.chat_wins[room_jid].getGame("Quiz").quizGameTimerExpired() def quizGameTimerRestarted(self, room_jid, time_left, profile): """Called when the question is not answered, and we still have time""" if not self.check_profile(profile): return if room_jid in self.chat_wins: self.chat_wins[room_jid].getGame("Quiz").quizGameTimerRestarted(time_left) def chatStateReceived(self, from_jid_s, state, profile): """Callback when a new chat state is received. @param from_jid_s: JID of the contact who sent his state, or '@ALL@' @param state: new state (string) @profile: current profile """ if not self.check_profile(profile): return if from_jid_s == '@ALL@': target = '@ALL@' nick = Const.ALL_OCCUPANTS else: from_jid = JID(from_jid_s) target = from_jid.bare nick = from_jid.resource for bare in self.chat_wins.keys(): if target == '@ALL' or target == bare: chat_win = self.chat_wins[bare] if chat_win.type == 'one2one': chat_win.updateChatState(state) elif chat_win.type == 'group': chat_win.updateChatState(state, nick=nick) def _subscribe_cb(self, answer, data): entity, profile = data if answer: self.bridge.subscription("subscribed", entity.bare, profile_key=profile) else: self.bridge.subscription("unsubscribed", entity.bare, profile_key=profile) def subscribe(self, type, raw_jid, profile): """Called when a subsciption management signal is received""" if not self.check_profile(profile): return entity = JID(raw_jid) if type == "subscribed": # this is a subscription confirmation, we just have to inform user self.showDialog(_("The contact %s has accepted your subscription") % entity.bare, _('Subscription confirmation')) elif type == "unsubscribed": # this is a subscription refusal, we just have to inform user self.showDialog(_("The contact %s has refused your subscription") % entity.bare, _('Subscription refusal'), 'error') elif type == "subscribe": # this is a subscriptionn request, we have to ask for user confirmation self.showDialog(_("The contact %s wants to subscribe to your presence.\nDo you accept ?") % entity.bare, _('Subscription confirmation'), 'yes/no', answer_cb=self._subscribe_cb, answer_data=(entity, profile)) def showDialog(self, message, title, type="info", answer_cb=None): raise NotImplementedError def showAlert(self, message): pass #FIXME def paramUpdate(self, name, value, namespace, profile): if not self.check_profile(profile): return debug(_("param update: [%(namespace)s] %(name)s = %(value)s") % {'namespace': namespace, 'name': name, 'value': value}) if (namespace, name) == ("Connection", "JabberID"): debug(_("Changing JID to %s"), value) self.profiles[profile]['whoami'] = JID(value) elif (namespace, name) == ("Misc", "Watched"): self.profiles[profile]['watched'] = value.split() def contactDeleted(self, jid, profile): if not self.check_profile(profile): return target = JID(jid) self.contact_list.remove(target) try: self.profiles[profile]['onlineContact'].remove(target.bare) except KeyError: pass def entityDataUpdated(self, jid_str, key, value, profile): if not self.check_profile(profile): return jid = JID(jid_str) if key == "nick": if jid in self.contact_list: self.contact_list.setCache(jid, 'nick', value) self.contact_list.replace(jid) elif key == "avatar": if jid in self.contact_list: filename = self.bridge.getAvatarFile(value) self.contact_list.setCache(jid, 'avatar', filename) self.contact_list.replace(jid) def askConfirmation(self, confirm_id, confirm_type, data, profile): raise NotImplementedError def actionResult(self, type, id, data): raise NotImplementedError def onExit(self): """Must be called when the frontend is terminating""" #TODO: mange multi-profile here try: if self.bridge.isConnected(self.profile): if self.bridge.getParamA("autodisconnect", "Connection", profile_key=self.profile) == "true": #The user wants autodisconnection self.bridge.disconnect(self.profile) except: pass