class UpdateSettingsDialog(FormClass, BaseClass): updater_branch = Settings.persisted_property( 'updater/branch', type=str, default_value=UpdateBranch.Prerelease.name) updater_downgrade = Settings.persisted_property('updater/downgrade', type=bool, default_value=False) def __init__(self, *args, **kwargs): BaseClass.__init__(self, *args, **kwargs) self.setModal(True) def setup(self): self.setupUi(self) self.cbChannel.setCurrentIndex(UpdateBranch[self.updater_branch].value) self.cbDowngrade.setChecked(self.updater_downgrade) self.buttonBox.accepted.connect(self.accepted) self.buttonBox.rejected.connect(lambda: self.close()) def accepted(self): branch = UpdateBranch(self.cbChannel.currentIndex()) self.updater_branch = branch.name self.updater_downgrade = self.cbDowngrade.isChecked() self.close()
class UpdateSettings: updater_branch = Settings.persisted_property( 'updater/branch', type=str, default_value=UpdateBranch.Prerelease.name) def should_notify(self, releases, force=True): self._logger.debug(releases) if force: return True server_releases = [ release for release in releases if release['branch'] == 'server' ] stable_releases = [ release for release in releases if release['branch'] == 'stable' ] pre_releases = [ release for release in releases if release['branch'] == 'pre' ] beta_releases = [ release for release in releases if release['branch'] == 'beta' ] have_server = len(server_releases) > 0 have_stable = len(stable_releases) > 0 have_pre = len(pre_releases) > 0 have_beta = len(beta_releases) > 0 current_version = Version(config.VERSION) # null out build because we don't care about it current_version.build = () notify_stable = have_stable and self.updater_branch == UpdateBranch.Stable.name \ and Version(stable_releases[0]['new_version']) > current_version notify_pre = have_pre and self.updater_branch == UpdateBranch.Prerelease.name \ and Version(pre_releases[0]['new_version']) > current_version notify_beta = have_beta and self.updater_branch == UpdateBranch.Unstable.name \ and Version(beta_releases[0]['new_version']) > current_version return have_server or notify_stable or notify_pre or notify_beta
class ChatWidget(FormClass, BaseClass, SimpleIRCClient): use_chat = Settings.persisted_property('chat/enabled', type=bool, default_value=True) ''' This is the chat lobby module for the FAF client. It manages a list of channels and dispatches IRC events (lobby inherits from irclib's client class) ''' def __init__(self, client, *args, **kwargs): if not self.use_chat: logger.info("Disabling chat") return logger.debug("Lobby instantiating.") BaseClass.__init__(self, *args, **kwargs) SimpleIRCClient.__init__(self) self.setupUi(self) # CAVEAT: These will fail if loaded before theming is loaded import json chat.OPERATOR_COLORS = json.loads( util.readfile("chat/formatters/operator_colors.json")) self.client = client self.channels = {} #avatar downloader self.nam = QNetworkAccessManager() self.nam.finished.connect(self.finishDownloadAvatar) #nickserv stuff self.identified = False #IRC parameters self.ircServer = IRC_SERVER self.ircPort = IRC_PORT self.crucialChannels = ["#aeolus"] self.optionalChannels = [] #We can't send command until the welcom message is received self.welcomed = False # Load colors and styles from theme self.a_style = util.readfile("chat/formatters/a_style.qss") #load UI perform some tweaks self.tabBar().setTabButton(0, 1, None) #add self to client's window self.client.chatTab.layout().addWidget(self) self.tabCloseRequested.connect(self.closeChannel) #Hook with client's connection and autojoin mechanisms self.client.authorized.connect(self.connect) self.client.autoJoin.connect(self.autoJoin) self.channelsAvailable = [] self.timer = QtCore.QTimer(self) self.timer.timeout.connect(self.poll) # disconnection checks self.canDisconnect = False @QtCore.pyqtSlot() def poll(self): self.timer.stop() self.once() self.timer.start(POLLING_INTERVAL) def disconnect(self): self.canDisconnect = True self.irc_disconnect() self.timer.stop() @QtCore.pyqtSlot(object) def connect(self, player): try: self.irc_connect(self.ircServer, self.ircPort, player.login, ssl=True, ircname=player.login, username=player.id) self.timer.start() except: logger.debug("Unable to connect to IRC server.") self.serverLogArea.appendPlainText( "Unable to connect to the chat server, but you should still be able to host and join games." ) logger.error("IRC Exception", exc_info=sys.exc_info()) def finishDownloadAvatar(self, reply): ''' this take care of updating the avatars of players once they are downloaded ''' img = QtGui.QImage() img.loadFromData(reply.readAll()) pix = util.respix(reply.url().toString()) if pix: pix = QtGui.QIcon(QtGui.QPixmap(img)) else: util.addrespix(reply.url().toString(), QtGui.QPixmap(img)) for player in util.curDownloadAvatar(reply.url().toString()): for channel in self.channels: if player in self.channels[channel].chatters: self.channels[channel].chatters[player].avatarItem.setIcon( QtGui.QIcon(util.respix(reply.url().toString()))) self.channels[channel].chatters[ player].avatarItem.setToolTip( self.channels[channel].chatters[player].avatarTip) def closeChannel(self, index): ''' Closes a channel tab. ''' channel = self.widget(index) for name in self.channels: if self.channels[name] is channel: if not self.channels[ name].private and self.connection.is_connected( ): # Channels must be parted (if still connected) self.connection.part([name], "tab closed") else: # Queries and disconnected channel windows can just be closed self.removeTab(index) del self.channels[name] break @QtCore.pyqtSlot(str) def announce(self, broadcast): ''' Notifies all crucial channels about the status of the client. ''' logger.debug("BROADCAST:" + broadcast) for channel in self.crucialChannels: self.sendMsg(channel, broadcast) def setTopic(self, chan, topic): self.connection.topic(chan, topic) def sendMsg(self, target, text): if self.connection.is_connected(): self.connection.privmsg(target, text) return True else: logger.error("IRC connection lost.") for channel in self.crucialChannels: if channel in self.channels: self.channels[channel].printRaw("Server", "IRC is disconnected") return False def sendAction(self, target, text): if self.connection.is_connected(): self.connection.action(target, text) return True else: logger.error("IRC connection lost.") for channel in self.crucialChannels: if channel in self.channels: self.channels[channel].printAction("IRC", "was disconnected.") return False def openQuery(self, name, activate=False): # Ignore ourselves. if name == self.client.login: return False if name not in self.channels: self.channels[name] = Channel(self, name, True) self.addTab(self.channels[name], name) # Add participants to private channel self.channels[name].addChatter(name) self.channels[name].addChatter(self.client.login) if activate: self.setCurrentWidget(self.channels[name]) self.channels[name].resizing() return True @QtCore.pyqtSlot(list) def autoJoin(self, channels): for channel in channels: if channel in self.channels: continue if (self.connection.is_connected()) and self.welcomed: #directly join self.connection.join(channel) else: #Note down channels for later. self.optionalChannels.append(channel) def join(self, channel): if channel not in self.channels: self.connection.join(channel) def log_event(self, e): self.serverLogArea.appendPlainText( "[%s: %s->%s]" % (e.eventtype(), e.source(), e.target()) + "\n".join(e.arguments())) #SimpleIRCClient Class Dispatcher Attributes follow here. def on_welcome(self, c, e): self.log_event(e) self.welcomed = True def nickservIdentify(self): if not self.identified: self.serverLogArea.appendPlainText("[Identify as : %s]" % (self.client.login)) self.connection.privmsg( 'NickServ', 'identify %s %s' % (self.client.login, util.md5text(self.client.password))) def on_identified(self): if self.connection.get_nickname() != self.client.login: self.serverLogArea.appendPlainText( "[Retrieving our nickname : %s]" % (self.client.login)) self.connection.privmsg( 'NickServ', 'recover %s %s' % (self.client.login, util.md5text(self.client.password))) #Perform any pending autojoins (client may have emitted autoJoin signals before we talked to the IRC server) self.autoJoin(self.optionalChannels) self.autoJoin(self.crucialChannels) def nickservRegister(self): if hasattr(self, '_nickserv_registered'): return self.connection.privmsg( 'NickServ', 'register %s %s' % (util.md5text(self.client.password), '{}@users.faforever.com'.format(self.client.me.login))) self._nickserv_registered = True self.autoJoin(self.optionalChannels) self.autoJoin(self.crucialChannels) def on_version(self, c, e): self.connection.privmsg( e.source(), "Forged Alliance Forever " + util.VERSION_STRING) def on_motd(self, c, e): self.log_event(e) self.nickservIdentify() def on_endofmotd(self, c, e): self.log_event(e) def on_namreply(self, c, e): self.log_event(e) channel = e.arguments()[1] listing = e.arguments()[2].split() for user in listing: name = user.strip(chat.IRC_ELEVATION) id = -1 if name in self.client.players: id = self.client.players[name].id self.channels[channel].addChatter( name, id, user[0] if user[0] in chat.IRC_ELEVATION else None, '') logger.debug("Added " + str(len(listing)) + " Chatters") def on_whoisuser(self, c, e): self.log_event(e) def on_join(self, c, e): channel = e.target() # If we're joining, we need to open the channel for us first. if channel not in self.channels: self.channels[channel] = Channel(self, channel) if (channel.lower() in self.crucialChannels): self.insertTab( 1, self.channels[channel], channel) #CAVEAT: This is assumes a server tab exists. self.client.localBroadcast.connect( self.channels[channel].printRaw) self.channels[channel].printAnnouncement( "Welcome to Forged Alliance Forever!", "red", "+3") self.channels[channel].printAnnouncement( "Check out the wiki: http://wiki.faforever.com for help with common issues.", "white", "+1") self.channels[channel].printAnnouncement("", "black", "+1") self.channels[channel].printAnnouncement("", "black", "+1") else: self.addTab(self.channels[channel], channel) if channel.lower( ) in self.crucialChannels: #Make the crucial channels not closeable, and make the last one the active one self.setCurrentWidget(self.channels[channel]) self.tabBar().setTabButton(self.currentIndex(), QtGui.QTabBar.RightSide, None) name, id, elevation, hostname = parse_irc_source(e.source()) self.channels[channel].addChatter(name, id, elevation, hostname, True) if channel.lower( ) in self.crucialChannels and name != self.client.login: # TODO: search better solution, that html in nick & channel no rendered self.client.notificationSystem.on_event( ns.Notifications.USER_ONLINE, { 'user': id, 'channel': channel }) self.channels[channel].resizing() def on_part(self, c, e): channel = e.target() name = user2name(e.source()) if name == self.client.login: #We left ourselves. self.removeTab(self.indexOf(self.channels[channel])) del self.channels[channel] else: #Someone else left self.channels[channel].removeChatter(name, "left.") def on_quit(self, c, e): name = user2name(e.source()) for channel in self.channels: if (not self.channels[channel].private) or ( self.channels[channel].name == user2name(name)): self.channels[channel].removeChatter(name, "quit.") def on_nick(self, c, e): self.log_event(e) def on_mode(self, c, e): if len(e.arguments()) < 2: return name = user2name(e.arguments()[1]) if e.target() in self.channels: self.channels[e.target()].elevateChatter(name, e.arguments()[0]) def on_umode(self, c, e): self.log_event(e) def on_notice(self, c, e): self.log_event(e) def on_topic(self, c, e): channel = e.target() if channel in self.channels: self.channels[channel].setAnnounceText(" ".join(e.arguments())) def on_currenttopic(self, c, e): channel = e.arguments()[0] if channel in self.channels: self.channels[channel].setAnnounceText(" ".join(e.arguments()[1:])) def on_topicinfo(self, c, e): self.log_event(e) def on_list(self, c, e): self.log_event(e) def on_bannedfromchan(self, c, e): self.log_event(e) def on_pubmsg(self, c, e): name, id, elevation, hostname = parse_irc_source(e.source()) target = e.target() if target in self.channels and not self.client.players.isFoe(id): self.channels[target].printMsg(name, "\n".join(e.arguments())) def on_privnotice(self, c, e): source = user2name(e.source()) notice = e.arguments()[0] prefix = notice.split(" ")[0] target = prefix.strip("[]") if source and source.lower() == 'nickserv': if notice.find("registered under your account") >= 0 or \ notice.find("Password accepted") >= 0: if not self.identified: self.identified = True self.on_identified() elif notice.find("isn't registered") >= 0: self.nickservRegister() elif notice.find("RELEASE") >= 0: self.connection.privmsg( 'nickserv', 'release %s %s' % (self.client.login, util.md5text(self.client.password))) elif notice.find("hold on") >= 0: self.connection.nick(self.client.login) message = "\n".join(e.arguments()).lstrip(prefix) if target in self.channels: self.channels[target].printMsg(source, message) elif source == "Global": for channel in self.channels: self.channels[channel].printAnnouncement( message, "yellow", "+2") elif source == "AeonCommander": for channel in self.channels: self.channels[channel].printMsg(source, message) else: self.serverLogArea.appendPlainText("%s: %s" % (source, notice)) def on_disconnect(self, c, e): if not self.canDisconnect: logger.warn("IRC disconnected - reconnecting.") self.identified = False self.timer.stop() self.connect(self.client.me) def on_privmsg(self, c, e): name, id, elevation, hostname = parse_irc_source(e.source()) if self.client.players.isFoe(id): return # Create a Query if it's not open yet, and post to it if it exists. if self.openQuery(name): self.channels[name].printMsg(name, "\n".join(e.arguments())) def on_action(self, c, e): name, id, elevation, hostname = parse_irc_source( e.source()) #user2name(e.source()) target = e.target() if self.client.players.isFoe(id): return # Create a Query if it's not an action intended for a channel if target not in self.channels: self.openQuery(name) self.channels[name].printAction(name, "\n".join(e.arguments())) else: self.channels[target].printAction(name, "\n".join(e.arguments())) def on_default(self, c, e): self.serverLogArea.appendPlainText( "[%s: %s->%s]" % (e.eventtype(), e.source(), e.target()) + "\n".join(e.arguments())) if "Nickname is already in use." in "\n".join(e.arguments()): self.connection.nick(self.client.login + "_") def on_kick(self, c, e): pass
class UpdateDialog(FormClass, BaseClass): changelog_url = Settings.persisted_property( 'updater/changelog_url', type=str, default_value='https://github.com/FAForever/client/releases/tag') def __init__(self, *args, **kwargs): BaseClass.__init__(self, *args, **kwargs) self._logger.debug("UpdateDialog instantiating") self.setModal(True) def setup(self, releases): self.setupUi(self) self.btnStart.clicked.connect(self.startUpdate) self.btnAbort.clicked.connect(self.abort) self.btnSettings.clicked.connect(self.showSettings) self.cbReleases.currentIndexChanged.connect(self.indexChanged) self.layout().setSizeConstraint(QLayout.SetFixedSize) self.releases = releases self.reset_controls() def reset_controls(self): self.pbDownload.hide() self.btnCancel.hide() self.btnAbort.setEnabled(True) current_version = Version(config.VERSION) if any([release['branch'] == 'server' for release in self.releases]): self.lblUpdatesFound.setText( 'Your client version is outdated - you must update to play.') else: self.lblUpdatesFound.setText('Client releases were found.') if len(self.releases) > 0: currIdx = 0 preferIdx = None labels = dict([('server', 'Server Version'), ('stable', 'Stable Version'), ('pre', 'Stable Prerelease'), ('beta', 'Unstable')]) self.cbReleases.blockSignals(True) self.cbReleases.clear() for currIdx, release in enumerate(self.releases): self._logger.debug(release) key = release['branch'] label = labels[key] branch_to_key = dict(Stable='stable', Prerelease='pre', Unstable='beta') prefer = key == branch_to_key[UpdateSettings().updater_branch] is_update = ' [New!]' if Version( release['new_version']) > current_version else '' self.cbReleases.insertItem( 99, '{} {}{}'.format(label, release['new_version'], is_update), release) if prefer and preferIdx is None: preferIdx = currIdx if preferIdx is None: preferIdx = 0 self.cbReleases.setCurrentIndex(preferIdx) self.indexChanged(preferIdx) self.cbReleases.blockSignals(False) self.btnStart.setEnabled(True) @QtCore.pyqtSlot(int) def indexChanged(self, index): def _format_changelog(version): if version is not None: return "<a href=\"{}/{}\">Release Info</a>".format( self.changelog_url, version) else: return 'Not available' release = self.cbReleases.itemData(index) self.lblInfo.setText(_format_changelog(release['new_version'])) def startUpdate(self): sender = self.sender() release = self.cbReleases.itemData(self.cbReleases.currentIndex()) url = release['update'] self.btnStart.setEnabled(False) self.btnAbort.setEnabled(False) client_updater = ClientUpdater(parent=self, progress_bar=self.pbDownload, cancel_btn=self.btnCancel) client_updater.finished.connect(self.finishUpdate) client_updater.exec_(url) def finishUpdate(self): self.reset_controls() def abort(self): self.close() def showSettings(self): dialog = UpdateSettingsDialog(self) dialog.setup() dialog.show()
class UpdateChecker(QObject): gh_releases_url = Settings.persisted_property( 'updater/gh_release_url', type=str, default_value='https://api.github.com/repos/' 'FAForever/client/releases?per_page=20') updater_downgrade = Settings.persisted_property('updater/downgrade', type=bool, default_value=False) finished = QtCore.pyqtSignal(list) def __init__(self, parent, respect_notify=True): QObject.__init__(self, parent) self._network_manager = client.NetworkManager self.respect_notify = respect_notify self._releases = None def start(self, reset_server=True): gh_url = QUrl(self.gh_releases_url) self._rep = self._network_manager.get(QNetworkRequest(gh_url)) self._rep.finished.connect(self._req_done) if reset_server: self._server_info = None def server_update(self, message): self._server_info = message self._check_updates_complete() def server_session(self): self._server_info = {} self._check_updates_complete() def _parse_releases(self, release_info): def _parse_release(release_dict): client_version = Version(config.VERSION) for asset in release_dict['assets']: if '.msi' in asset['browser_download_url']: download_url = asset['browser_download_url'] tag = release_dict['tag_name'] release_version = Version(tag) # We never want to return the current version itself, # but if `updater_downgrade` is set, we do return # older releases. # strange comparison logic is because of semantic_version # so that build info is ignored if self.updater_downgrade: if not (release_version < client_version or client_version < release_version): return None else: if not (release_version > client_version): return None branch = None if release_version.minor % 2 == 1: branch = 'beta' elif release_version.minor % 2 == 0: if release_version.prerelease == (): branch = 'stable' else: branch = 'pre' return dict(branch=branch, update=download_url, new_version=tag) try: releases = json.loads(release_info.decode('utf-8')) if not isinstance(releases, list): releases = [releases] self._logger.debug('Loaded {} github releases'.format( len(releases))) return [ release for release in [_parse_release(release) for release in releases] if release is not None ] except: self._logger.exception("Error parsing network reply: {}".format( repr(release_info))) return [] def _req_done(self): if self._rep.error() == QNetworkReply.NoError: self._releases = self._parse_releases(bytes(self._rep.readAll())) else: self._releases = [] self._check_updates_complete() def _check_updates_complete(self): if self._server_info is not None and self._releases is not None: releases = self._releases if self._server_info != {}: releases.append( dict(branch='server', update=self._server_info['update'], new_version=self._server_info['new_version'])) if UpdateSettings().should_notify(releases, force=not self.respect_notify): self.finished.emit(releases)
class ReplayVaultWidgetHandler(object): HOST = "lobby.faforever.com" PORT = 11002 # connect to save/restore persistence settings for checkboxes & search parameters automatic = Settings.persisted_property("replay/automatic", default_value=False, type=bool) spoiler_free = Settings.persisted_property("replay/spoilerFree", default_value=True, type=bool) def __init__(self, widget, dispatcher, client, gameset, playerset): self._w = widget self._dispatcher = dispatcher self.client = client self._gameset = gameset self._playerset = playerset self.onlineReplays = {} self.selectedReplay = None self.vault_connection = ReplaysConnection(self._dispatcher, self.HOST, self.PORT) self.client.lobby_info.replayVault.connect(self.replayVault) self.replayDownload = QNetworkAccessManager() self.replayDownload.finished.connect(self.finishRequest) self.searching = False self.searchInfo = "<font color='gold'><b>Searching...</b></font>" _w = self._w _w.onlineTree.setItemDelegate(ReplayItemDelegate(_w)) _w.onlineTree.itemDoubleClicked.connect(self.onlineTreeDoubleClicked) _w.onlineTree.itemPressed.connect(self.onlineTreeClicked) _w.searchButton.pressed.connect(self.searchVault) _w.playerName.returnPressed.connect(self.searchVault) _w.mapName.returnPressed.connect(self.searchVault) _w.automaticCheckbox.stateChanged.connect(self.automaticCheckboxchange) _w.spoilerCheckbox.stateChanged.connect(self.spoilerCheckboxchange) _w.RefreshResetButton.pressed.connect(self.resetRefreshPressed) # restore persistent checkbox settings _w.automaticCheckbox.setChecked(self.automatic) _w.spoilerCheckbox.setChecked(self.spoiler_free) def searchVault(self, minRating=None, mapName=None, playerName=None, modListIndex=None): w = self._w if minRating is not None: w.minRating.setValue(minRating) if mapName is not None: w.mapName.setText(mapName) if playerName is not None: w.playerName.setText(playerName) if modListIndex is not None: w.modList.setCurrentIndex(modListIndex) # Map Search helper - the secondary server has a problem with blanks (fix until change to api) map_name = w.mapName.text().replace(" ", "*") """ search for some replays """ self._w.searchInfoLabel.setText(self.searchInfo) self.searching = True self.vault_connection.connect() self.vault_connection.send( dict(command="search", rating=w.minRating.value(), map=map_name, player=w.playerName.text(), mod=w.modList.currentText())) self._w.onlineTree.clear() def reloadView(self): if not self.searching: # something else is already in the pipe from SearchVault if self.automatic or self.onlineReplays == {}: # refresh on Tab change or only the first time self._w.searchInfoLabel.setText(self.searchInfo) self.vault_connection.connect() self.vault_connection.send(dict(command="list")) def onlineTreeClicked(self, item): if QtWidgets.QApplication.mouseButtons() == QtCore.Qt.RightButton: if type(item.parent) == ReplaysWidget: # FIXME - hack item.pressed(item) else: self.selectedReplay = item if hasattr(item, "moreInfo"): if item.moreInfo is False: self.vault_connection.connect() self.vault_connection.send( dict(command="info_replay", uid=item.uid)) elif item.spoiled != self._w.spoilerCheckbox.isChecked(): self._w.replayInfos.clear() self._w.replayInfos.setHtml(item.replayInfo) item.resize() else: self._w.replayInfos.clear() item.generateInfoPlayersHtml() def onlineTreeDoubleClicked(self, item): if hasattr(item, "duration"): # it's a game not a date separator if "playing" in item.duration: # live game will not be in vault # search result isn't updated automatically - so game status might have changed if item.uid in self._gameset.games: # game still running game = self._gameset.games[item.uid] if not game.launched_at: # we frown upon those return if game.has_live_replay: # live game over 5min for name in game.players: # find a player ... if name in self._playerset: # still logged in self._startReplay(name) break else: wait_str = time.strftime( '%M Min %S Sec', time.gmtime(game.LIVE_REPLAY_DELAY_SECS - (time.time() - game.launched_at))) QtWidgets.QMessageBox.information( client.instance, "5 Minute Live Game Delay", "It is too early to join the Game.\n" "You have to wait " + wait_str + " to join.") else: # game ended - ask to start replay if QtWidgets.QMessageBox.question( client.instance, "Live Game ended", "Would you like to watch the replay from the vault?", QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No ) == QtWidgets.QMessageBox.Yes: self.replayDownload.get( QNetworkRequest(QtCore.QUrl(item.url))) else: # start replay if hasattr(item, "url"): self.replayDownload.get( QNetworkRequest(QtCore.QUrl(item.url))) def _startReplay(self, name): if name is None or name not in self._playerset: return player = self._playerset[name] if not player.currentGame: return replay(player.currentGame.url(player.id)) def automaticCheckboxchange(self, state): self.automatic = state def spoilerCheckboxchange(self, state): self.spoiler_free = state if self.selectedReplay: # if something is selected in the tree to the left if type(self.selectedReplay) == ReplayItem: # and if it is a game self.selectedReplay.generateInfoPlayersHtml( ) # then we redo it def resetRefreshPressed( self): # reset search parameter and reload recent Replays List self._w.searchInfoLabel.setText(self.searchInfo) self.vault_connection.connect() self.vault_connection.send(dict(command="list")) self._w.minRating.setValue(0) self._w.mapName.setText("") self._w.playerName.setText("") self._w.modList.setCurrentIndex(0) # "All" def finishRequest(self, reply): if reply.error() != QNetworkReply.NoError: QtWidgets.QMessageBox.warning(self._w, "Network Error", reply.errorString()) else: faf_replay = QtCore.QFile( os.path.join(util.CACHE_DIR, "temp.fafreplay")) faf_replay.open(QtCore.QIODevice.WriteOnly | QtCore.QIODevice.Truncate) faf_replay.write(reply.readAll()) faf_replay.flush() faf_replay.close() replay(os.path.join(util.CACHE_DIR, "temp.fafreplay")) def replayVault(self, message): action = message["action"] self._w.searchInfoLabel.clear() if action == "list_recents": self.onlineReplays = {} replays = message["replays"] for replay in replays: uid = replay["id"] if uid not in self.onlineReplays: self.onlineReplays[uid] = ReplayItem(uid, self._w) self.onlineReplays[uid].update(replay, self.client) else: self.onlineReplays[uid].update(replay, self.client) self.updateOnlineTree() self._w.replayInfos.clear() self._w.RefreshResetButton.setText("Refresh Recent List") elif action == "info_replay": uid = message["uid"] if uid in self.onlineReplays: self.onlineReplays[uid].infoPlayers(message["players"]) elif action == "search_result": self.searching = False self.onlineReplays = {} replays = message["replays"] for replay in replays: uid = replay["id"] if uid not in self.onlineReplays: self.onlineReplays[uid] = ReplayItem(uid, self._w) self.onlineReplays[uid].update(replay, self.client) else: self.onlineReplays[uid].update(replay, self.client) self.updateOnlineTree() self._w.replayInfos.clear() self._w.RefreshResetButton.setText("Reset Search to Recent") def updateOnlineTree(self): self.selectedReplay = None # clear because won't be part of the new tree self._w.replayInfos.clear() self._w.onlineTree.clear() buckets = {} for uid in self.onlineReplays: bucket = buckets.setdefault(self.onlineReplays[uid].startDate, []) bucket.append(self.onlineReplays[uid]) for bucket in buckets.keys(): bucket_item = QtWidgets.QTreeWidgetItem() self._w.onlineTree.addTopLevelItem(bucket_item) bucket_item.setIcon(0, util.THEME.icon("replays/bucket.png")) bucket_item.setText(0, "<font color='white'>" + bucket + "</font>") bucket_item.setText( 1, "<font color='white'>" + str(len(buckets[bucket])) + " replays</font>") for replay in buckets[bucket]: bucket_item.addChild(replay) replay.setFirstColumnSpanned(True) replay.setIcon(0, replay.icon) bucket_item.setExpanded(True)
class ChatWidget(FormClass, BaseClass, SimpleIRCClient): use_chat = Settings.persisted_property('chat/enabled', type=bool, default_value=True) irc_port = Settings.persisted_property('chat/port', type=int, default_value=6667) irc_host = Settings.persisted_property('chat/host', type=str, default_value='irc.' + defaults['host']) irc_tls = Settings.persisted_property('chat/tls', type=bool, default_value=False) auto_join_channels = Settings.persisted_property('chat/auto_join_channels', default_value=[]) """ This is the chat lobby module for the FAF client. It manages a list of channels and dispatches IRC events (lobby inherits from irclib's client class) """ def __init__(self, client, playerset, me, *args, **kwargs): if not self.use_chat: logger.info("Disabling chat") return logger.debug("Lobby instantiating.") BaseClass.__init__(self, *args, **kwargs) SimpleIRCClient.__init__(self) self.setupUi(self) self.client = client self._me = me self._chatters = IrcUserset(playerset) self.channels = {} # avatar downloader self.nam = QNetworkAccessManager() self.nam.finished.connect(self.finish_download_avatar) # nickserv stuff self.identified = False # IRC parameters self.crucialChannels = ["#aeolus"] self.optionalChannels = [] # We can't send command until the welcome message is received self.welcomed = False # Load colors and styles from theme self.a_style = util.THEME.readfile("chat/formatters/a_style.qss") # load UI perform some tweaks self.tabBar().setTabButton(0, 1, None) self.tabCloseRequested.connect(self.close_channel) # Hook with client's connection and autojoin mechanisms self.client.authorized.connect(self.connect) self.client.autoJoin.connect(self.auto_join) self.channelsAvailable = [] self._notifier = None self._timer = QTimer() self._timer.timeout.connect(self.once) # disconnection checks self.canDisconnect = False def disconnect(self): self.canDisconnect = True try: self.irc_disconnect() except ServerConnectionError: pass if self._notifier: self._notifier.activated.disconnect(self.once) self._notifier = None @QtCore.pyqtSlot(object) def connect(self, me): try: logger.info("Connecting to IRC at: {}:{}. TLS: {}".format( self.irc_host, self.irc_port, self.irc_tls)) self.irc_connect(self.irc_host, self.irc_port, me.login, ssl=self.irc_tls, ircname=me.login, username=me.id) self._notifier = QSocketNotifier( self.ircobj.connections[0]._get_socket().fileno(), QSocketNotifier.Read, self) self._notifier.activated.connect(self.once) self._timer.start(PONG_INTERVAL) except: logger.debug("Unable to connect to IRC server.") self.serverLogArea.appendPlainText( "Unable to connect to the chat server, but you should still be able to host and join games." ) logger.error("IRC Exception", exc_info=sys.exc_info()) def finish_download_avatar(self, reply): """ this take care of updating the avatars of players once they are downloaded """ img = QtGui.QImage() img.loadFromData(reply.readAll()) url = reply.url().toString() if not util.respix(url): util.addrespix(url, QtGui.QPixmap(img)) for chatter in util.curDownloadAvatar(url): # FIXME - hack to prevent touching chatter if it was removed channel = chatter.channel ircuser = chatter.user if ircuser in channel.chatters: chatter.update_avatar() util.delDownloadAvatar(url) def add_channel(self, name, channel, index=None): self.channels[name] = channel if index is None: self.addTab(self.channels[name], name) else: self.insertTab(index, self.channels[name], name) def sort_channels(self): for channel in self.channels.values(): channel.sort_chatters() def update_channels(self): for channel in self.channels.values(): channel.update_chatters() def close_channel(self, index): """ Closes a channel tab. """ channel = self.widget(index) for name in self.channels: if self.channels[name] is channel: if not self.channels[ name].private and self.connection.is_connected( ): # Channels must be parted (if still connected) self.connection.part([name], "tab closed") else: # Queries and disconnected channel windows can just be closed self.removeTab(index) del self.channels[name] break @QtCore.pyqtSlot(str) def announce(self, broadcast): """ Notifies all crucial channels about the status of the client. """ logger.debug("BROADCAST:" + broadcast) for channel in self.crucialChannels: self.send_msg(channel, broadcast) def set_topic(self, chan, topic): self.connection.topic(chan, topic) def send_msg(self, target, text): if self.connection.is_connected(): self.connection.privmsg(target, text) return True else: logger.error("IRC connection lost.") for channel in self.crucialChannels: if channel in self.channels: self.channels[channel].print_raw("Server", "IRC is disconnected") return False def send_action(self, target, text): if self.connection.is_connected(): self.connection.action(target, text) return True else: logger.error("IRC connection lost.") for channel in self.crucialChannels: if channel in self.channels: self.channels[channel].print_action( "IRC", "was disconnected.") return False def open_query(self, chatter, activate=False): # Ignore ourselves. if chatter.name == self.client.login: return False if chatter.name not in self.channels: priv_chan = Channel(self, chatter.name, self._chatters, self._me, True) self.add_channel(chatter.name, priv_chan) # Add participants to private channel priv_chan.add_chatter(chatter) if self.client.me.login is not None: my_login = self.client.me.login if my_login in self._chatters: priv_chan.add_chatter(self._chatters[my_login]) if activate: self.setCurrentWidget(priv_chan) return True @QtCore.pyqtSlot(list) def auto_join(self, channels): for channel in channels: if channel in self.channels: continue if (self.connection.is_connected()) and self.welcomed: # directly join self.connection.join(channel) else: # Note down channels for later. self.optionalChannels.append(channel) def join(self, channel): if channel not in self.channels: self.connection.join(channel) def log_event(self, e): self.serverLogArea.appendPlainText( "[%s: %s->%s]" % (e.eventtype(), e.source(), e.target()) + "\n".join(e.arguments())) def should_ignore(self, chatter): # Don't ignore mods from any crucial channels if any(chatter.is_mod(c) for c in self.crucialChannels): return False if chatter.player is None: return self.client.me.isFoe(name=chatter.name) else: return self.client.me.isFoe(id_=chatter.player.id) # SimpleIRCClient Class Dispatcher Attributes follow here. def on_welcome(self, c, e): self.log_event(e) self.welcomed = True def nickserv_identify(self): if not self.identified: self.serverLogArea.appendPlainText("[Identify as : %s]" % self.client.login) self.connection.privmsg( 'NickServ', 'identify %s %s' % (self.client.login, util.md5text(self.client.password))) def on_identified(self): if self.connection.get_nickname() != self.client.login: self.serverLogArea.appendPlainText( "[Retrieving our nickname : %s]" % (self.client.login)) self.connection.privmsg( 'NickServ', 'recover %s %s' % (self.client.login, util.md5text(self.client.password))) # Perform any pending autojoins (client may have emitted autoJoin signals before we talked to the IRC server) self.auto_join(self.optionalChannels) self.auto_join(self.crucialChannels) self.auto_join(self.auto_join_channels) self._schedule_actions_at_player_available() def _schedule_actions_at_player_available(self): self._me.playerAvailable.connect(self._at_player_available) if self._me.player is not None: self._at_player_available() def _at_player_available(self): self._me.playerAvailable.disconnect(self._at_player_available) self._autojoin_newbie_channel() def _autojoin_newbie_channel(self): if not self.client.useNewbiesChannel: return game_number_threshold = 50 if self.client.me.player.number_of_games <= game_number_threshold: self.auto_join(["#newbie"]) def nickserv_register(self): if hasattr(self, '_nickserv_registered'): return self.connection.privmsg( 'NickServ', 'register %s %s' % (util.md5text(self.client.password), '{}@users.faforever.com'.format(self.client.me.login))) self._nickserv_registered = True self.auto_join(self.optionalChannels) self.auto_join(self.crucialChannels) def on_version(self, c, e): self.connection.privmsg( e.source(), "Forged Alliance Forever " + util.VERSION_STRING) def on_motd(self, c, e): self.log_event(e) self.nickserv_identify() def on_endofmotd(self, c, e): self.log_event(e) def on_namreply(self, c, e): self.log_event(e) channel = e.arguments()[1] listing = e.arguments()[2].split() for user in listing: name = user.strip(chat.IRC_ELEVATION) elevation = user[0] if user[0] in chat.IRC_ELEVATION else None hostname = '' self._add_chatter(name, hostname) self._add_chatter_channel(self._chatters[name], elevation, channel, False) logger.debug("Added " + str(len(listing)) + " Chatters") def _add_chatter(self, name, hostname): if name not in self._chatters: self._chatters[name] = IrcUser(name, hostname) else: self._chatters[name].update(hostname=hostname) def _remove_chatter(self, name): if name not in self._chatters: return del self._chatters[name] # Channels listen to 'chatter removed' signal on their own def _add_chatter_channel(self, chatter, elevation, channel, join): chatter.set_elevation(channel, elevation) self.channels[channel].add_chatter(chatter, join) def _remove_chatter_channel(self, chatter, channel, msg): chatter.set_elevation(channel, None) self.channels[channel].remove_chatter(msg) def on_whoisuser(self, c, e): self.log_event(e) def on_join(self, c, e): channel = e.target() # If we're joining, we need to open the channel for us first. if channel not in self.channels: newch = Channel(self, channel, self._chatters, self._me) if channel.lower() in self.crucialChannels: self.add_channel( channel, newch, 1) # CAVEAT: This is assumes a server tab exists. self.client.localBroadcast.connect(newch.print_raw) newch.print_announcement("Welcome to Forged Alliance Forever!", "red", "+3") wiki_link = Settings.get("WIKI_URL") wiki_msg = "Check out the wiki: {} for help with common issues.".format( wiki_link) newch.print_announcement(wiki_msg, "white", "+1") newch.print_announcement("", "black", "+1") newch.print_announcement("", "black", "+1") else: self.add_channel(channel, newch) if channel.lower( ) in self.crucialChannels: # Make the crucial channels not closeable, and make the last one the active one self.setCurrentWidget(self.channels[channel]) self.tabBar().setTabButton(self.currentIndex(), QtWidgets.QTabBar.RightSide, None) name, _id, elevation, hostname = parse_irc_source(e.source()) self._add_chatter(name, hostname) self._add_chatter_channel(self._chatters[name], elevation, channel, True) def on_part(self, c, e): channel = e.target() name = user2name(e.source()) if name not in self._chatters: return chatter = self._chatters[name] if name == self.client.login: # We left ourselves. self.removeTab(self.indexOf(self.channels[channel])) del self.channels[channel] else: # Someone else left self._remove_chatter_channel(chatter, channel, "left.") def on_quit(self, c, e): name = user2name(e.source()) self._remove_chatter(name) def on_nick(self, c, e): oldnick = user2name(e.source()) newnick = e.target() if oldnick not in self._chatters: return self._chatters[oldnick].update(name=newnick) self.log_event(e) def on_mode(self, c, e): if e.target() not in self.channels: return if len(e.arguments()) < 2: return name = user2name(e.arguments()[1]) if name not in self._chatters: return chatter = self._chatters[name] self.elevate_chatter(chatter, e.target(), e.arguments()[0]) def elevate_chatter(self, chatter, channel, modes): add = re.compile(".*\+([a-z]+)") remove = re.compile(".*\-([a-z]+)") addmatch = re.search(add, modes) if addmatch: modes = addmatch.group(1) mode = None if "v" in modes: mode = "+" if "o" in modes: mode = "@" if "q" in modes: mode = "~" if mode is not None: chatter.set_elevation(channel, mode) removematch = re.search(remove, modes) if removematch: modes = removematch.group(1) el = chatter.elevation[channel] chatter_mode = {"@": "o", "~": "q", "+": "v"}[el] if chatter_mode in modes: chatter.set_elevation(channel, None) def on_umode(self, c, e): self.log_event(e) def on_notice(self, c, e): self.log_event(e) def on_topic(self, c, e): channel = e.target() if channel in self.channels: self.channels[channel].set_announce_text(" ".join(e.arguments())) def on_currenttopic(self, c, e): channel = e.arguments()[0] if channel in self.channels: self.channels[channel].set_announce_text(" ".join( e.arguments()[1:])) def on_topicinfo(self, c, e): self.log_event(e) def on_list(self, c, e): self.log_event(e) def on_bannedfromchan(self, c, e): self.log_event(e) def on_pubmsg(self, c, e): name, id, elevation, hostname = parse_irc_source(e.source()) target = e.target() if name not in self._chatters or target not in self.channels: return if not self.should_ignore(self._chatters[name]): self.channels[target].print_msg(name, "\n".join(e.arguments())) def on_privnotice(self, c, e): source = user2name(e.source()) notice = e.arguments()[0] prefix = notice.split(" ")[0] target = prefix.strip("[]") if source and source.lower() == 'nickserv': if notice.find("registered under your account") >= 0 or \ notice.find("Password accepted") >= 0: if not self.identified: self.identified = True self.on_identified() elif notice.find("isn't registered") >= 0: self.nickserv_register() elif notice.find("RELEASE") >= 0: self.connection.privmsg( 'nickserv', 'release %s %s' % (self.client.login, util.md5text(self.client.password))) elif notice.find("hold on") >= 0: self.connection.nick(self.client.login) message = "\n".join(e.arguments()).lstrip(prefix) if target in self.channels: self.channels[target].print_msg(source, message) elif source == "Global": for channel in self.channels: if not channel in self.crucialChannels: continue self.channels[channel].print_announcement( message, "yellow", "+2") elif source == "AeonCommander": for channel in self.channels: if not channel in self.crucialChannels: continue self.channels[channel].print_msg(source, message) else: self.serverLogArea.appendPlainText("%s: %s" % (source, notice)) def on_disconnect(self, c, e): if not self.canDisconnect: logger.warning("IRC disconnected - reconnecting.") self.serverLogArea.appendPlainText( "IRC disconnected - reconnecting.") self.identified = False self._timer.stop() self.connect(self.client.me) def on_privmsg(self, c, e): name, id, elevation, hostname = parse_irc_source(e.source()) if name not in self._chatters: return chatter = self._chatters[name] if self.should_ignore(chatter): return # Create a Query if it's not open yet, and post to it if it exists. if self.open_query(chatter): self.channels[name].print_msg(name, "\n".join(e.arguments())) def on_action(self, c, e): name, id, elevation, hostname = parse_irc_source(e.source()) if name not in self._chatters: return chatter = self._chatters[name] target = e.target() if self.should_ignore(chatter): return # Create a Query if it's not an action intended for a channel if target not in self.channels: self.open_query(chatter) self.channels[name].print_action(name, "\n".join(e.arguments())) else: self.channels[target].print_action(name, "\n".join(e.arguments())) def on_nosuchnick(self, c, e): self.nickserv_register() def on_default(self, c, e): self.serverLogArea.appendPlainText( "[%s: %s->%s]" % (e.eventtype(), e.source(), e.target()) + "\n".join(e.arguments())) if "Nickname is already in use." in "\n".join(e.arguments()): self.connection.nick(self.client.login + "_") def on_kick(self, c, e): pass