xmppchatwindow.py (66617B)
1 #Embedded file name: C:\BuildAgent\work\20d73f1b24eed06e\eve\release\V24.01\packages\xmppchatclient\xmppchatwindow.py 2 import logging 3 import random 4 import sys 5 from collections import deque 6 import blue 7 import carbonui.const as uiconst 8 import eveicon 9 import localization 10 import log 11 import telemetry 12 import threadutils 13 import uthread2 14 from caching.memoize import Memoize 15 from carbonui import AxisAlignment, TextColor 16 from carbonui.control.combo import Combo 17 from carbonui.control.contextMenu.menuData import MenuData 18 from carbonui.control.contextMenu.menuDataFactory import CreateMenuDataFromRawTuples 19 from carbonui.decorative.divider_line import DividerLine 20 from carbonui.primitives.containerAutoSize import ContainerAutoSize 21 from carbonui.primitives.fill import Fill 22 from clientsettings.setting import UserSettingBool, UserSettingEnum 23 from carbonui.text.settings import get_font_size_for_preset 24 from carbonui.uianimations import animations 25 from chatutil.filter import CleanText 26 from eve.client.script.ui.control.entries.util import GetFromClass 27 from menu import MenuLabel 28 from carbon.common.script.sys import service 29 from carbon.common.script.util.commonutils import StripTags 30 from carbon.common.script.util.format import FmtDate, GetTimeParts 31 from carbonui.primitives.sprite import Sprite 32 from carbonui.uicore import uicore 33 from carbonui.control.basicDynamicScroll import BasicDynamicScroll 34 from carbonui.control.scrollentries import ScrollEntryNode 35 from carbonui.primitives.container import Container 36 from carbonui.util.stringManip import SanitizeFilename 37 from chatutil import StripBreaks, IsChannelPrivate, ShouldHightlightBlink, CompleteAutoLinks, GetChannelCategory, MAX_TEXT_LEN, TruncateVeryLongText, GetChannelDifferentiator, InvalidSessionChatChannelNameError 38 from chatutil.spam import IsSpam 39 from eve.client.script.ui.control.allUserEntries import AllUserEntries 40 from eve.client.script.ui.control.divider import Divider 41 from eve.client.script.ui.control.eveEditPlainText import EditPlainText 42 from eve.client.script.ui.control.eveLabel import EveLabelSmall 43 from carbonui.fontconst import EVE_SMALL_FONTSIZE 44 from eve.client.script.ui.util.linkUtil import GetCharIDFromTextLink 45 from eve.common.script.sys.idCheckers import IsCharacter, IsEvePlayerCharacter 46 from eveprefs import prefs 47 from eveservices.xmppchat import GetChatService 48 from globalConfig.getFunctions import GetMaxChatChannelIdleTime, GetUpdateChannelCountIntervalSeconds, IsContentComplianceControlSystemActive 49 from player_messaging.client.ui_message import prompt_player_fsd_dialog 50 from safespace.client.exit_dialog_svc import get_safe_space_exit_dialog_service 51 from xmppchatclient.const import CATEGORY_PRIVATE, CATEGORY_PLAYER, CATEGORY_BOT, CATEGORY_SYSTEM, CATEGORY_FLEET, CATEGORY_CORP, CATEGORY_ALLIANCE, MESSAGEMODE_TEXTONLY, MESSAGEMODE_SMALLPORTRAIT, MESSAGEMODE_BIGPORTRAIT, SINGLE_WND_CATEGORIES, CATEGORY_WORMHOLE, CHANNELS_WITH_NO_PRESENCE_BROADCAST, CATEGORY_LOCAL, CATEGORY_LOCAL_SUPPRESSED, CATEGORY_NULLSEC, CATEGORY_TRIGLAVIAN 52 from xmppchatclient.xmppchannelsettings import ChannelSettingsDlg 53 from xmppchatclient.xmppchatchannels import XmppChatChannels 54 from xmppchatclient.xmppchatentry import XmppChatEntry 55 from eve.client.script.ui.shared.comtool.filterSettings import ChatFilterSettings 56 from bannedwords.client import bannedwords 57 from xmppchatclient.xmppchatuserentry2 import XmppChatSimpleUserEntry, XmppChatUserEntry 58 from chat.client.window import BaseChatWindow 59 from chat.client.const import AVAILABLE_FONT_SIZES, MAX_MESSAGES_IN_HISTORY, SLASH_EMOTE_STRING, SLASH_ME_STRING, CHATLOG_TEMPLATE 60 from chat.client.chat_settings import default_message_mode_setting, default_compact_member_entries_setting, default_show_member_list_setting, default_font_size_setting, global_font_size_setting, elevated_chat_highlighting_setting, show_message_timestamp_setting, highlight_my_messages_setting, ApplyFontSizeGloballySetting, EffectiveFontSizeSetting, auto_collapse_messages, auto_collapse_message_lines 61 from chat.client.util import should_hide_message, check_banned_message_in_new_player_channel 62 from chat.client.hide_messages import open_hide_message_settings, has_hide_message_setting 63 from chat.common.const import LOCAL_CHAT_CATEGORIES, ChatCategory 64 from chat.common.util import get_color_for_role 65 logger = logging.getLogger(__name__) 66 MESSAGEMODETEXTS = {MESSAGEMODE_TEXTONLY: 'UI/Chat/NoPortrait', 67 MESSAGEMODE_SMALLPORTRAIT: 'UI/Chat/SmallPortrait', 68 MESSAGEMODE_BIGPORTRAIT: 'UI/Chat/LargePortrait'} 69 ROLE_SLASH = service.ROLE_GML | service.ROLE_LEGIONEER 70 ACTION_ICON = 'res:/UI/Texture/classes/UtilMenu/BulletIcon.png' 71 MOTD_CHANNEL_TYPES = {CATEGORY_CORP, 72 CATEGORY_ALLIANCE, 73 CATEGORY_FLEET, 74 CATEGORY_PLAYER, 75 CATEGORY_SYSTEM} 76 77 class XmppChatWindow(BaseChatWindow): 78 __notifyevents__ = ['OnPortraitCreated'] 79 default_caption = 'Xmpp Chat' 80 default_windowID = 'XmppChat' 81 default_open = True 82 _has_left_channel = False 83 84 def OnUIRefresh(self): 85 pass 86 87 def ApplyAttributes(self, attributes): 88 self.throttleFunc = None 89 self._user_list_cont = None 90 self._user_list_underlay = None 91 BaseChatWindow.ApplyAttributes(self, attributes) 92 self.verbose = False 93 self.inputs = [''] 94 self.inputIndex = None 95 self.receiver = attributes.receiver 96 self.displayName = StripTags(attributes.caption) 97 self.channelName = attributes.channel 98 self.channelCategory = GetChannelCategory(self.receiver) 99 self.hasStartedUpdateApproximateCount = False 100 self.approximateMemberCount = 0 101 self.userlistNodesByName = {} 102 self.messages = [] 103 self.myPendingMessages = {} 104 self.motd = None 105 self.motd_set = False 106 self.access_lost = False 107 self.members = {} 108 self.muted_members = set() 109 self._message_style_setting = UserSettingEnum(settings_key='%s_mode' % self.name, default_value=lambda : default_message_mode_setting.get(), options=default_message_mode_setting.options) 110 self._font_size_setting = UserSettingEnum(settings_key='chatfontsize_%s' % self.name, default_value=lambda : default_font_size_setting.get(), options=AVAILABLE_FONT_SIZES) 111 self._apply_font_size_globally_setting = ApplyFontSizeGloballySetting(font_size_setting=self._font_size_setting) 112 self._effective_font_size_setting = EffectiveFontSizeSetting(font_size_setting=self._font_size_setting) 113 self._chat_notifications_enabled_setting = UserSettingBool(settings_key='chatWindowBlink_%s' % self.name, default_value=True) 114 self._show_member_list_setting = UserSettingBool(settings_key='%s_usermode' % self.name, default_value=lambda : default_show_member_list_setting.get()) 115 self._compact_member_entries_setting = UserSettingBool(settings_key=self._get_compact_member_entries_setting_key(), default_value=lambda : default_compact_member_entries_setting.get()) 116 self.removeIdleCharactersTasklet = None 117 self._ResetTrackActivityPerMember() 118 self.pendingPortraitReloads = set() 119 self.GetMainArea().clipChildren = True 120 input_cont = ContainerAutoSize(parent=self.GetMainArea(), align=uiconst.TOBOTTOM, alignMode=uiconst.TOBOTTOM, padTop=-3) 121 bottom_divider = Divider(name='bottom_divider', parent=input_cont, align=uiconst.TOTOP_NOPUSH, state=uiconst.UI_NORMAL, height=8) 122 self.input = EditPlainText(parent=input_cont, align=uiconst.TOBOTTOM, height=settings.user.ui.Get('chatinputsize_%s' % self.name, 64), innerPadding=(8, 8, 8, 8), padding=(-1, 3, -1, -1), pushContent=True, maxLength=MAX_TEXT_LEN) 123 self.input.ValidatePaste = self.ValidatePaste 124 self.input.OnReturn = self._HandleInputOnReturn 125 self.input.CtrlUp = self._HandleInputCtrlUp 126 self.input.CtrlDown = self._HandleInputCtrlDown 127 self.input.RegisterFocus = self._RegisterFocus 128 bottom_divider.Startup(self.input, 'height', 'y', 30, 96) 129 bottom_divider.OnSizeChanged = self.OnInputSizeChanged 130 self._user_list_cont = Container(parent=self.GetMainArea(), align=uiconst.TORIGHT, width=settings.user.ui.Get('%s_userlistwidth' % self.name, 128)) 131 DividerLine(parent=self._user_list_cont, align=uiconst.TOLEFT_NOPUSH) 132 self._user_list_underlay = Fill(bgParent=self._user_list_cont, align=uiconst.TOALL, padLeft=1, color=(1.0, 1.0, 1.0, 0.05)) 133 divider = Divider(name='user_list_divider', parent=self._user_list_cont, align=uiconst.TOLEFT_NOPUSH, state=uiconst.UI_NORMAL, width=8, cross_axis_alignment=AxisAlignment.START) 134 divider.Startup(victim=self._user_list_cont, attribute='width', xory='x', minValue=50, maxValue=200) 135 divider.OnSizeChanged = self.UserlistEndScale 136 self.numMembersCont = Container(name='numMembersCont', parent=self._user_list_cont, align=uiconst.TOTOP, height=16, padding=(8, 8, 8, 0)) 137 self.numMembersCont.display = False 138 if self.channelCategory in CHANNELS_WITH_NO_PRESENCE_BROADCAST: 139 memberHint = localization.GetByLabel('UI/Chat/CombinedCounterHint') 140 else: 141 memberHint = localization.GetByLabel('UI/Chat/CapsuleerCounterHint') 142 Sprite(name='numMembersSprite', parent=self.numMembersCont, align=uiconst.CENTERLEFT, width=16, height=16, texturePath=eveicon.person, color=TextColor.NORMAL, hint=memberHint) 143 self.numMembersLabel = EveLabelSmall(name='numMembersLabel', parent=self.numMembersCont, align=uiconst.CENTERLEFT, left=20, text='', hint=memberHint) 144 self.userlist = BasicDynamicScroll(name='userlist', parent=self._user_list_cont, align=uiconst.TOALL, padding=(8, 8, 8, 0), entry_spacing=self._get_user_list_entry_spacing(), pushContent=True) 145 self.userlist.isCondensed = self._compact_member_entries_setting.is_enabled() 146 self.userlist.GetContentContainer().OnDropData = self.OnDropCharacter 147 self.output = BasicDynamicScroll(parent=self.GetMainArea(), align=uiconst.TOALL, innerPadding=(8, 8, 8, 8), entry_spacing=4, pushContent=False, stickToBottom=True) 148 self.output.sr.content.GetMenu = self.GetOutputMenu 149 self._UpdateMinSize() 150 if elevated_chat_highlighting_setting.is_enabled(): 151 self.input.OnKeyUp = self.OnOutputOrInputKeyUp 152 self.output.OnKeyUp = self.OnOutputOrInputKeyUp 153 self.SetupUserlist() 154 self.output.Load(contentList=[], fixedEntryHeight=18) 155 if self.CanKillChannel(): 156 self.MakeKillable() 157 self.lastActivityTime = blue.os.GetWallclockTime() 158 self.checkConnectionStatusTasklet = uthread2.start_tasklet(self._CheckConnectionStatus) 159 self.logfile = None 160 if settings.user.ui.Get('logchat', 1): 161 uthread2.start_tasklet(self._SetupLogfile) 162 if self.channelCategory == CATEGORY_PRIVATE and GetChatService().IsPrivateChatPending(self.receiver): 163 self.AddPrivateConvoHint() 164 self._message_style_setting.on_change.connect(self._on_message_style_setting_changed) 165 self._font_size_setting.on_change.connect(self._on_font_size_setting_changed) 166 self._apply_font_size_globally_setting.on_change.connect(self._on_apply_font_size_globally_setting_changed) 167 global_font_size_setting.on_change.connect(self._on_global_font_size_setting_changed) 168 elevated_chat_highlighting_setting.on_change.connect(self._on_elevated_chat_highlighting_setting_changed) 169 self._show_member_list_setting.on_change.connect(self._on_show_member_list_setting_changed) 170 self._compact_member_entries_setting.on_change.connect(self._on_compact_member_entries_setting_changed) 171 172 @property 173 def _user_entry_type(self): 174 if self._compact_member_entries_setting.is_enabled(): 175 return XmppChatSimpleUserEntry 176 else: 177 return XmppChatUserEntry 178 179 def OnSetActive_(self, *args): 180 if self._user_list_underlay is not None: 181 animations.FadeTo(self._user_list_underlay, startVal=self._user_list_underlay.opacity, endVal=0.05, duration=0.1) 182 183 def OnSetInactive(self, *args): 184 super(XmppChatWindow, self).OnSetInactive() 185 if self._user_list_underlay is not None: 186 animations.FadeOut(self._user_list_underlay, duration=0.3) 187 188 def _ResetTrackActivityPerMember(self): 189 self.activityTimestampPerMember = {} 190 self.activityQueue = deque() 191 if self.channelCategory in CHANNELS_WITH_NO_PRESENCE_BROADCAST: 192 if not self.removeIdleCharactersTasklet: 193 self.removeIdleCharactersTasklet = uthread2.start_tasklet(self._RemoveIdleCharactersLoop) 194 elif self.removeIdleCharactersTasklet: 195 self.removeIdleCharactersTasklet.kill() 196 self.removeIdleCharactersTasklet = None 197 198 def _AddActivity(self, sender, timestamp): 199 self.activityTimestampPerMember[sender] = timestamp 200 self.activityQueue.append((timestamp, sender)) 201 202 def _RemoveIdleCharactersLoop(self): 203 while not self.destroyed: 204 now = blue.os.GetWallclockTime() 205 toRemove = [] 206 characterIdlePeriodSeconds = prefs.GetValue('chat_idle_character_removal_period', 900) 207 logger.debug('_RemoveIdleCharactersLoop, idle period is %s', characterIdlePeriodSeconds) 208 sleepDuration = characterIdlePeriodSeconds 209 while len(self.activityQueue) > 0: 210 timestamp, sender = self.activityQueue[0] 211 ageInSec = blue.os.TimeDiffInMs(timestamp, now) / 1000 212 if ageInSec > characterIdlePeriodSeconds: 213 self.activityQueue.popleft() 214 timestampForSender = self.activityTimestampPerMember[sender] 215 ageForSenderInSec = blue.os.TimeDiffInMs(timestampForSender, now) / 1000 216 if ageForSenderInSec > characterIdlePeriodSeconds: 217 toRemove.append(sender) 218 else: 219 sleepDuration = characterIdlePeriodSeconds - ageInSec + 1 220 break 221 222 for charid in toRemove: 223 logger.debug('Removing idle character %s', charid) 224 self.CharacterLeftChannel(charid) 225 226 uthread2.sleep(sleepDuration) 227 228 def UserlistEndScale(self, *args): 229 settings.user.ui.Set('%s_userlistwidth' % self.name, self._user_list_cont.width) 230 self._UpdateMinSize() 231 232 def _UpdateMinSize(self): 233 if self._user_list_cont is None: 234 return 235 default_min_width, default_min_height = self.default_minSize 236 user_list_width, _ = self.GetWindowSizeForContentSize(width=self._user_list_cont.width) 237 min_width = max(default_min_width, user_list_width) 238 self.SetMinSize((min_width, default_min_height)) 239 240 def OnInputSizeChanged(self): 241 settings.user.ui.Set('chatinputsize_%s' % self.name, self.input.height) 242 243 def _SetupLogfile(self): 244 if session.charid: 245 charName = cfg.eveowners.Get(session.charid).name 246 else: 247 logger.warning('_SetupLogfile %s: No charid in session. Listener will be unknown character in chatlog' % self.displayName) 248 charName = 'unknown_character' 249 chatlog = CHATLOG_TEMPLATE % (self.receiver, 250 self.displayName, 251 charName, 252 FmtDate(blue.os.GetWallclockTime())) 253 year, month, weekday, day, hour, minute, second, msec = blue.os.GetTimeParts(blue.os.GetWallclockTime()) 254 timeStamp = '%d%.2d%.2d_%.2d%.2d%.2d' % (year, 255 month, 256 day, 257 hour, 258 minute, 259 second) 260 filename = '%s_%s' % (self.displayName, timeStamp) 261 filename = SanitizeFilename(filename) 262 postFix = '_%s' % session.charid if session.charid else '' 263 filename = blue.sysinfo.GetUserDocumentsDirectory() + '/EVE/logs/Chatlogs/%s%s.txt' % (filename, postFix) 264 try: 265 self.logfile = blue.classes.CreateInstance('blue.ResFile') 266 if not self.logfile.Open(filename, 0): 267 self.logfile.Create(filename) 268 self.logfile.Write(chatlog.encode('utf-16')) 269 except Exception: 270 self.logfile = None 271 logger.exception('Failed to instantiate log file') 272 sys.exc_clear() 273 274 def ResetAfterReconnect(self): 275 self.myPendingMessages = {} 276 277 def Receive(self, sender, text, msg_id = None, stripFormatting = True): 278 try: 279 isMessage = False 280 senderId = int(sender) 281 except ValueError: 282 isMessage = GetChatService().IsSenderMessage(sender) 283 senderId = 0 284 285 strippedText = self._GetStrippedText(text, isMessage) 286 if self.logfile is not None and self.logfile.size > 0: 287 timestamp = blue.os.GetWallclockTime() 288 self._WriteToLogfile(sender, strippedText, timestamp) 289 if senderId == session.charid: 290 if not msg_id: 291 logger.warning('No message id in message from self') 292 try: 293 del self.myPendingMessages[msg_id] 294 if self.verbose: 295 logger.debug('Deleted pending message %s', msg_id) 296 except KeyError: 297 logger.warning('Unexpected message from self: %s', msg_id) 298 299 return 300 if stripFormatting: 301 text = strippedText 302 self._Output(sender, text) 303 304 def HandleErrorResponse(self, response): 305 msg_id = response.attributes.getValue('id') 306 if self.verbose: 307 logger.debug('HandleErrorResponse %s', msg_id) 308 try: 309 node, timestamp = self.myPendingMessages[msg_id] 310 del self.myPendingMessages[msg_id] 311 if self.verbose: 312 logger.debug('Deleted pending message %s', msg_id) 313 except KeyError: 314 logger.warning('Unexpected error response: %s', msg_id) 315 return 316 317 XmppChatEntry.MarkMessageUndelivered(node) 318 319 def _GetStrippedText(self, text, isMessage = False): 320 ignoredTags = ['b', 321 'i', 322 'u', 323 'url', 324 'br'] 325 if isMessage: 326 ignoredTags.append('color') 327 text = StripTags(text, ignoredTags=ignoredTags) 328 return text 329 330 def _Output(self, sender, text): 331 if self.verbose: 332 logger.debug('_Output: %s', text) 333 timestamp = blue.os.GetWallclockTime() 334 self.lastActivityTime = timestamp 335 if IsSpam(text): 336 return 337 if IsEvePlayerCharacter(sender): 338 self._AddActivity(sender, timestamp) 339 if sender not in self.members: 340 self.CharacterJoinedChannel(sender) 341 if self.channelCategory == CATEGORY_LOCAL: 342 logger.warning('Got a message in local from someone who is not a member') 343 if sender != session.charid and should_hide_message(self.receiver, self.channelCategory, text): 344 return 345 text = CompleteAutoLinks(text) 346 colorkey = self.GetColorKeyForSender(sender) 347 self.messages.append((sender, 348 text, 349 timestamp, 350 colorkey)) 351 if len(self.messages) >= MAX_MESSAGES_IN_HISTORY: 352 self.messages.pop(0) 353 if self.output.GetNodes(): 354 self.output.RemoveNodes([self.output.GetNodes()[0]]) 355 node = self._GetChatEntry(sender, text, timestamp, colorkey) 356 self.output.AddNodes(-1, [node]) 357 self._BlinkIfNeeded(sender, text) 358 return node 359 360 def _BlinkIfNeeded(self, sender, text): 361 if sender in (session.charid, const.ownerSystem): 362 return 363 blink_enabled = self._chat_notifications_enabled_setting.is_enabled() 364 if blink_enabled or ShouldHightlightBlink(text): 365 self.Blink() 366 if self.state == uiconst.UI_HIDDEN or self.IsMinimized(): 367 self.SetBlinking() 368 369 def SetBlinking(self): 370 super(XmppChatWindow, self).SetBlinking() 371 sm.ScatterEvent('OnChatWindowStartBlinking', self) 372 373 def SetNotBlinking(self): 374 super(XmppChatWindow, self).SetNotBlinking() 375 sm.ScatterEvent('OnChatWindowStopBlinking', self) 376 377 def _WriteToLogfile(self, sender, text, timestamp): 378 textWithoutTags = StripTags(text).replace('<', '<').replace('>', '>').replace('&', '&') 379 try: 380 if sender is None: 381 who = localization.GetByLabel('UI/Common/Message') 382 else: 383 who = cfg.eveowners.Get(int(sender)).name 384 except ValueError: 385 who = sender 386 387 line = '[%20s ] %s > %s\r\n' % (FmtDate(timestamp), who, textWithoutTags) 388 try: 389 self.logfile.Write(line.encode('utf-16')) 390 except IOError: 391 log.LogException(toAlertSvc=0) 392 sys.exc_clear() 393 394 def GetMessages(self): 395 messageText = 'Channel: %s\n' % self.displayName 396 for sender, text, timestamp, colorkey in self.messages: 397 messageText += '[%s] %s> %s\n' % (FmtDate(timestamp), sender, text) 398 399 return messageText 400 401 def SetMotd(self, sender, text): 402 if self.channelCategory == CATEGORY_FLEET: 403 text = sm.GetService('fleet').GetMotd() 404 if not text and self.motd is None: 405 return 406 if self.verbose: 407 logger.debug('SetMotd: sender=%s, text=%s', sender, text) 408 self.lastActivityTime = blue.os.GetWallclockTime() 409 if text == self.motd: 410 return 411 self.motd = text 412 if self.motd: 413 if sender and self.motd_set: 414 msg = localization.GetByLabel('UI/Chat/ChannelMotdChanged', newMotd=self.motd, admin=sender) 415 else: 416 msg = localization.GetByLabel('UI/Chat/ChannelMotd', motd=text) 417 elif sender in (None, 'admin'): 418 msg = localization.GetByLabel('UI/Chat/ChannelMotdClearedByAdmin') 419 else: 420 msg = localization.GetByLabel('UI/Chat/ChannelMotdCleared', admin=sender) 421 self.motd_set = True 422 self.Receive(const.ownerSystem, msg, stripFormatting=False) 423 424 def AddPrivateConvoHint(self): 425 msg = localization.GetByLabel('UI/Chat/BeingTalkingToOpenRequest') 426 self._Output(const.ownerSystem, unicode(msg)) 427 428 def GetMotd(self): 429 return self.motd 430 431 def GetMembers(self): 432 return self.members.keys() 433 434 def IsCharacterMember(self, char): 435 return char in self.members 436 437 def SetCharacterMuted(self, char, reason): 438 if char in self.muted_members: 439 return 440 if char == session.charid: 441 muteMsg = localization.GetByLabel('UI/Chat/MuteMessage', muted=char, admin=const.ownerSystem, reason=reason) 442 self._Output(const.ownerSystem, muteMsg) 443 self.muted_members.add(char) 444 445 def SetCharacterUnmuted(self, char): 446 if char in self.muted_members: 447 if char == session.charid: 448 muteMsg = localization.GetByLabel('UI/Chat/UnmuteMessage', unmuted=char, admin=const.ownerSystem) 449 self._Output(const.ownerSystem, muteMsg) 450 self.muted_members.remove(char) 451 452 def IsCharacterMuted(self, char): 453 return char in self.muted_members 454 455 def CharacterJoinedChannel(self, char): 456 if self.verbose: 457 logger.debug('%s joined channel %s', char, self.receiver) 458 self.lastActivityTime = blue.os.GetWallclockTime() 459 userdata = GetChatService().GetUserData(char) 460 self.members[char] = userdata 461 if self.verbose: 462 logger.debug('userdata for %s: %s', char, userdata) 463 if char not in self.userlistNodesByName: 464 if self.verbose: 465 logger.debug('%s added to channel %s', char, self.receiver) 466 role = userdata.get('role', 0) 467 if role & service.ROLE_CHTINVISIBLE: 468 if self.verbose: 469 logger.debug('Ignoring charID %s because role %s includes ROLE_CHTINVISIBLE', char, role) 470 return 471 ownerInfo = cfg.eveowners.Get(char) 472 if char in self.userlistNodesByName: 473 return 474 node = self._GetUserEntryNode(char, ownerInfo, userdata) 475 idx = self.GetIdxForUser_WithBinarySearch(node.charIndex) 476 self.userlistNodesByName[char] = node 477 updateScroll = False 478 self.userlist.AddNodes(idx, [node], updateScroll=updateScroll) 479 node.positionFromTop = idx * node.height 480 throttleFunc = self.GetThrottleFunc() 481 throttleFunc() 482 elif self.verbose: 483 logger.debug('%s is already in channel %s', char, self.receiver) 484 self.UpdateCaption() 485 486 def GetThrottleFunc(self): 487 if getattr(self, 'throttleFunc', None): 488 return self.throttleFunc 489 490 @threadutils.throttled(0.5) 491 def UpdateScrollAfterNodeAddition_throttled(): 492 clipperWidth, clipperHeight = self.userlist.sr.clipper.GetAbsoluteSize() 493 with self.userlist.KillUpdateThreadAndBlock(): 494 self.userlist.UpdateNodesWidthAndPosition(clipperWidth) 495 self.userlist.UpdatePosition(fromWhere='AddNodes') 496 497 self.throttleFunc = UpdateScrollAfterNodeAddition_throttled 498 return self.throttleFunc 499 500 @telemetry.ZONE_METHOD 501 def GetIdxForUser(self, ownerName): 502 idx = 0 503 for each in self.userlist.GetNodes(): 504 if each.charIndex > ownerName: 505 break 506 idx += 1 507 508 return idx 509 510 @telemetry.ZONE_METHOD 511 def GetIdxForUser_WithBinarySearch(self, ownerName): 512 a = self.userlist.GetNodes() 513 x = ownerName 514 lo = 0 515 hi = len(a) 516 while lo < hi: 517 mid = (lo + hi) // 2 518 if x < a[mid].charIndex: 519 hi = mid 520 else: 521 lo = mid + 1 522 523 return lo 524 525 def _GetUserEntryNode(self, charID, ownerInfo, userdata): 526 node = GetFromClass(self._user_entry_type, {'channelID': self.receiver, 527 'charID': charID, 528 'info': ownerInfo, 529 'corpID': userdata.get('corpid', None), 530 'allianceID': userdata.get('allianceid', None), 531 'warFactionID': userdata.get('warfactionid', None), 532 'color': get_color_for_role(userdata.get('role', None)), 533 'showChatBubble': not self._compact_member_entries_setting.is_enabled(), 534 'charIndex': ownerInfo.name.lower()}) 535 return node 536 537 def CharacterLeftChannel(self, char): 538 self.lastActivityTime = blue.os.GetWallclockTime() 539 if char in self.members: 540 if self.verbose: 541 logger.debug('%s left channel %s', char, self.receiver) 542 try: 543 userdata = self.members.pop(char, {}) 544 node = self.userlistNodesByName[char] 545 del self.userlistNodesByName[char] 546 self.userlist.RemoveNodes([node]) 547 except KeyError: 548 if userdata and userdata.get('role', 0) & service.ROLE_CHTINVISIBLE: 549 pass 550 else: 551 logger.exception('Error removing character from channel') 552 553 self.UpdateCaption() 554 elif self.verbose: 555 logger.debug('%s already left channel %s', char, self.receiver) 556 if char in self.userlistNodesByName: 557 try: 558 channelId = self.receiver 559 raise RuntimeError('Userlist node still exists for character after leaving channel') 560 except RuntimeError: 561 logger.exception('Userlist node still exists for character after leaving channel') 562 563 def CanInvite(self, charid): 564 channelName = self.receiver 565 if not IsChannelPrivate(channelName): 566 return False 567 if charid in self.members: 568 return False 569 return True 570 571 def CanKillChannel(self): 572 if self.channelCategory in [CATEGORY_PRIVATE, 573 CATEGORY_PLAYER, 574 CATEGORY_BOT, 575 CATEGORY_SYSTEM]: 576 return True 577 return False 578 579 def SetChannel(self, channel): 580 if channel == self.receiver: 581 return 582 logger.info('Switching %s from %s to %s', self.name, self.receiver, channel) 583 self.receiver = channel 584 self.userlist.Clear() 585 self.userlistNodesByName = {} 586 self.members = {} 587 self.SetCaptionAsDisplayName() 588 self.Receive(const.ownerSystem, localization.GetByLabel('UI/Chat/ChannelWindow/ChannelChangedToChannelName', channelName=GetChatService().GetDetailedDisplayName(channel))) 589 if GetChannelCategory(channel) != self.channelCategory: 590 logger.warning('Channel category changes from %s to %s', self.channelCategory, GetChannelCategory(channel)) 591 self.channelCategory = GetChannelCategory(channel) 592 if self.channelCategory in (CATEGORY_LOCAL, 593 CATEGORY_LOCAL_SUPPRESSED, 594 CATEGORY_WORMHOLE, 595 CATEGORY_NULLSEC, 596 CATEGORY_TRIGLAVIAN): 597 solarsystem = int(GetChannelDifferentiator(channel)) 598 if solarsystem != session.solarsystemid2: 599 logger.warning('Switching local channel to %s while current system is %s', solarsystem, session.solarsystemid2) 600 if not self.hasStartedUpdateApproximateCount: 601 if self.channelCategory in CHANNELS_WITH_NO_PRESENCE_BROADCAST: 602 self.hasStartedUpdateApproximateCount = True 603 uthread2.start_tasklet(self._UpdateApproximateCount) 604 self._ResetTrackActivityPerMember() 605 if self.channelCategory == CATEGORY_LOCAL_SUPPRESSED: 606 self.CharacterJoinedChannel(session.charid) 607 608 def SetCaptionAsDisplayName(self): 609 name = StripTags(GetChatService().GetDisplayName(self.receiver)) 610 if name is None: 611 if self.displayName is None: 612 logger.error('GetDisplayName returned None and display name is not currently set') 613 self.displayName = self.receiver 614 else: 615 self.displayName = name 616 self.SetCaption(self.displayName) 617 if self.verbose: 618 logger.debug('Caption set as %s', self.displayName) 619 uthread2.start_tasklet(GetChatService().StoreChannelList) 620 self.UpdateCaption() 621 622 def UpdateChannelCaptionCounter(self): 623 if not self.displayName: 624 self.displayName = StripTags(GetChatService().GetDisplayName(self.receiver)) 625 caption = self.displayName or '' 626 if self.channelCategory in [CATEGORY_WORMHOLE, CATEGORY_NULLSEC, CATEGORY_TRIGLAVIAN]: 627 count = 0 628 elif self.channelCategory in CHANNELS_WITH_NO_PRESENCE_BROADCAST: 629 count = self.approximateMemberCount 630 elif len(self.members) > 1: 631 count = len(self.members) 632 caption += ' [%d]' % count 633 else: 634 count = 0 635 self.SetCaption(caption) 636 self.SetMemberCountDisplay(count) 637 638 def _UpdateApproximateCount(self): 639 logger.debug('Starting update loop for approximate member count for %s', self.receiver) 640 while not self.destroyed: 641 interval = GetUpdateChannelCountIntervalSeconds(sm.GetService('machoNet')) 642 if interval > 0: 643 logger.debug('Getting member count for %s', self.receiver) 644 self.approximateMemberCount = GetChatService().GetNumChannelOccupants(self.receiver) 645 self.SetMemberCountDisplay(self.approximateMemberCount) 646 uthread2.sleep(interval) 647 else: 648 uthread2.sleep(300) 649 650 def UpdatePrivateChannelCaption(self): 651 caption = GetChatService().GetPrivateChannelDisplayName(self.receiver) 652 self.SetCaption(caption) 653 654 def SetMemberCountDisplay(self, count): 655 if self.channelCategory in (CATEGORY_PRIVATE, 656 CATEGORY_WORMHOLE, 657 CATEGORY_NULLSEC, 658 CATEGORY_TRIGLAVIAN) or not count: 659 self.numMembersCont.display = False 660 self.numMembersLabel.countDisplayed = count 661 return 662 self.numMembersCont.display = True 663 countDisplayed = getattr(self.numMembersLabel, 'memberCount', None) 664 if count != countDisplayed: 665 self.numMembersLabel.text = count 666 self.numMembersLabel.countDisplayed = count 667 668 def GetChannelId(self): 669 return self.receiver 670 671 def GetMenu(self, *args): 672 menu = MenuData() 673 menu.AddCaption(self.caption) 674 menu.AddSeparator() 675 self._AddChannelMenu(menu) 676 tab_menu = CreateMenuDataFromRawTuples(super(XmppChatWindow, self).GetMenu(*args)) 677 if tab_menu is not None and tab_menu.GetEntries(): 678 menu += tab_menu 679 menu.AddSeparator() 680 return menu 681 682 def _AddChannelMenu(self, menu): 683 gm_options = MenuData() 684 if session.role & service.ROLE_GML: 685 gm_options.AddEntry(text='channel id: ' + self.receiver, func=lambda : blue.pyos.SetClipboardData(self.receiver)) 686 gm_options.AddEntry(text='category id: ' + self.channelCategory, func=lambda : blue.pyos.SetClipboardData(self.channelCategory)) 687 gm_options.AddEntry(text='window id: ' + self.windowID, func=lambda : blue.pyos.SetClipboardData(self.windowID)) 688 pokeRoles = service.ROLE_LEGIONEER | service.ROLE_CENTURION | service.ROLE_GMH | service.ROLE_GMS | service.ROLE_GML | service.ROLE_WORLDMOD 689 if session.role & pokeRoles and GetChannelCategory(self.receiver) == CATEGORY_PRIVATE: 690 for memberID in self.GetMembers(): 691 if memberID != session.charid: 692 gm_options.AddEntry(text='Poke %s' % cfg.eveowners.Get(memberID).name, func=lambda _member_id = memberID: GetChatService().PokePlayerAboutChatMsgGm(_member_id, self.receiver)) 693 break 694 695 if gm_options.GetEntries(): 696 menu.AddEntry(text='GM / WM Extras', subMenuData=gm_options) 697 self._add_elevated_options(menu) 698 menu.AddLabel(localization.GetByLabel('UI/Chat/ChannelSettingsSection')) 699 if self._should_show_channel_settings_option(): 700 menu.AddEntry(text=localization.GetByLabel('UI/Chat/Configuration'), texturePath=eveicon.settings, func=self._OpenSettingsWindow) 701 member_list_options = MenuData() 702 member_list_options.AddCheckbox(text=localization.GetByLabel('UI/Chat/ShowMemberList'), setting=self._show_member_list_setting) 703 member_list_options.AddSeparator() 704 member_list_options.AddCheckbox(text=localization.GetByLabel('UI/Chat/ShowCompactMemberList'), setting=self._compact_member_entries_setting) 705 menu.AddEntry(text=localization.GetByLabel('UI/Chat/MemberList'), texturePath=eveicon.people, subMenuData=member_list_options) 706 menu.AddEntry(text=localization.GetByLabel('/Carbon/UI/Controls/EditRichText/FontSize'), texturePath=eveicon.font_size, subMenuData=self._get_font_size_menu) 707 menu.AddEntry(text=localization.GetByLabel('UI/Chat/MessagePortraits'), texturePath=eveicon.person, subMenuData=self._get_message_style_menu) 708 if has_hide_message_setting(self.channelCategory): 709 menu.AddEntry(text=localization.GetByLabel('UI/Chat/MessageFilterContextMenu'), texturePath=eveicon.filter, func=self._OpenHideMessageSettings) 710 menu.AddCheckbox(text=localization.GetByLabel('UI/Chat/BlinkOnNewActivity'), setting=self._chat_notifications_enabled_setting) 711 category = self.receiver.split('_')[0] 712 if category in MOTD_CHANNEL_TYPES: 713 menu.AddEntry(text=localization.GetByLabel('UI/Chat/ReloadChannelMOTD'), func=self.ShowMotdFromMenu) 714 menu.AddEntry(text=localization.GetByLabel('UI/Chat/ClearAllContent'), func=self.ClearContent) 715 menu.AddSeparator() 716 menu.AddLabel(localization.GetByLabel('UI/Chat/GlobalSettingsSection')) 717 autoCollapseMenu = MenuData() 718 autoCollapseMenu.AddCheckbox(localization.GetByLabel('UI/Chat/AutoCollapseMessagesEnabled'), hint=localization.GetByLabel('UI/Chat/AutoCollapseMessagesHint'), setting=auto_collapse_messages) 719 autoCollapseMenu.AddSlider(localization.GetByLabel('UI/Chat/AutoCollapseMessagesLines'), setting=auto_collapse_message_lines, isInteger=True) 720 menu.AddEntry(text=localization.GetByLabel('UI/Chat/AutoCollapseMessages'), hint=localization.GetByLabel('UI/Chat/AutoCollapseMessagesHint'), texturePath=eveicon.collapse, subMenuData=autoCollapseMenu) 721 menu.AddCheckbox(text=localization.GetByLabel('UI/Chat/HighlightMyMessages'), setting=highlight_my_messages_setting) 722 menu.AddCheckbox(text=localization.GetByLabel('UI/Chat/ShowTimestamp'), setting=show_message_timestamp_setting) 723 menu.AddEntry(text=localization.GetByLabel('UI/Chat/ConfigureWordFiltersAndHighlights'), func=self.SetWordAndHightlightFilters) 724 725 def _HandleInputOnReturn(self): 726 txt = self.input.GetValue(html=0) 727 txt = StripBreaks(txt) 728 txt = self._GetStrippedText(txt) 729 if not txt: 730 return 731 bannedwords.check_chat_character_allowed(session.charid) 732 bannedwords.check_chat_words_allowed(txt) 733 self._FlushTimedOutPendingMessages() 734 if len(self.myPendingMessages) > 4: 735 logger.warning('Too many pending messages') 736 prompt_player_fsd_dialog('uiwarning03') 737 return 738 self.input.SetValue('') 739 if self.inputs[-1] != txt: 740 self.inputs.append(txt) 741 self.inputIndex = None 742 if txt.startswith(SLASH_ME_STRING): 743 txt = txt.replace(SLASH_ME_STRING, SLASH_EMOTE_STRING, 1) 744 if GetChatService().IsCharacterBanned(): 745 self.OnCharacterBannedNotify() 746 return 747 if txt.startswith('/') and not (txt.startswith(SLASH_EMOTE_STRING) or txt == '/'): 748 self._HandleSlashCommand(txt) 749 else: 750 txt = TruncateVeryLongText(txt) 751 check_banned_message_in_new_player_channel(self.channelCategory, txt) 752 node = self._Output(session.charid, txt) 753 if self.channelCategory == CATEGORY_LOCAL_SUPPRESSED: 754 return 755 msg_id = self._Send(txt) 756 timestamp = blue.os.GetWallclockTime() 757 self.myPendingMessages[msg_id] = (node, timestamp) 758 759 def _FlushTimedOutPendingMessages(self): 760 now = blue.os.GetWallclockTime() 761 stillPending = {} 762 for k, v in self.myPendingMessages.iteritems(): 763 node, timestamp = v 764 if blue.os.TimeDiffInMs(timestamp, now) > 60000: 765 XmppChatEntry.MarkMessageUndelivered(node) 766 else: 767 stillPending[k] = v 768 769 self.myPendingMessages = stillPending 770 771 def _HandleInputCtrlDown(self, editctrl): 772 self._BrowseInputs(1) 773 774 def _HandleInputCtrlUp(self, editctrl): 775 self._BrowseInputs(-1) 776 777 def _BrowseInputs(self, updown): 778 if self.inputIndex is None: 779 self.inputIndex = len(self.inputs) - 1 780 else: 781 self.inputIndex += updown 782 if self.inputIndex < 0: 783 self.inputIndex = len(self.inputs) - 1 784 elif self.inputIndex >= len(self.inputs): 785 self.inputIndex = 0 786 self.input.SetValue(self.inputs[self.inputIndex], cursorPos=-1) 787 788 def _RegisterFocus(self, edit, *args): 789 sm.GetService('focus').SetFocusChannel(self) 790 791 def SetCharFocus(self, char): 792 uicore.registry.SetFocus(self.input) 793 uicore.layer.menu.Flush() 794 if char is not None: 795 self.input.OnChar(char, 0) 796 797 def OnTabSelect(self): 798 uicore.registry.SetFocus(self.input) 799 if self.pendingPortraitReloads: 800 if self.verbose: 801 logger.debug('Triggering reload of %d portraits', len(self.pendingPortraitReloads)) 802 self._ReloadPendingPortraits() 803 804 def _HandleSlashCommand(self, txt): 805 for commandLine in StripTags(txt, ignoredTags=('br',)).split('<br>'): 806 try: 807 sm.GetService('slash').ReportSlashCommandUsage(commandLine) 808 if txt.startswith('/tutorial') and session and session.role & service.ROLE_GML: 809 sm.GetService('tutorial').SlashCmd(commandLine) 810 elif txt.startswith('/advantage') and session and session.role & service.ROLE_GML: 811 sm.GetService('fwAdvantageSvc').HandleSlashCmd(commandLine) 812 else: 813 if txt.startswith('/bypassdestination') and session and session.role & service.ROLE_GML: 814 return self._handle_bypass_destination(commandLine) 815 if txt.startswith('/stationintro') and session and session.role & service.ROLE_GML: 816 return self._handle_station_intro(commandLine) 817 if txt.startswith('/resetstationintro') and session and session.role & service.ROLE_GML: 818 return self._handle_station_intro_reset(commandLine) 819 if txt.startswith('/promptexitstarterspace') and session and session.role & service.ROLE_GML: 820 get_safe_space_exit_dialog_service().prompt_player_to_exit() 821 elif txt.startswith('/resetstarterspaceexitdialog') and session and session.role & service.ROLE_GML: 822 get_safe_space_exit_dialog_service().set_has_seen_exit_dialog(False) 823 elif sm.GetService('publicQaToolsClient').CommandAllowed(commandLine): 824 sm.GetService('publicQaToolsClient').SlashCmd(commandLine) 825 elif session and session.role & ROLE_SLASH: 826 if commandLine.lower().startswith('/mark'): 827 sm.StartService('logger').LogError('SLASHMARKER: ', (session.userid, session.charid), ': ', commandLine) 828 commandLine = self._adjust_commandLine(commandLine) 829 slashRes = sm.GetService('slash').SlashCmd(commandLine) 830 if slashRes is not None: 831 sm.GetService('logger').AddText('slash result: %s' % slashRes, 'slash') 832 self._Output(session.charid, '/slash: ' + commandLine) 833 except: 834 self._Output(session.charid, '/slash failed: ' + commandLine) 835 raise 836 837 def _handle_station_intro_reset(self, command_line): 838 from stationintrocinematic.client import registry, seen_settings 839 parts = command_line.strip().split() 840 if len(parts) > 1: 841 try: 842 station_id = int(parts[1]) 843 except ValueError: 844 self._Output(session.charid, "/resetstationintro: invalid stationID '%s'" % parts[1]) 845 return 846 847 seen_settings.unmark_seen(station_id) 848 self._Output(session.charid, '/resetstationintro: cleared seen flag for %d' % station_id) 849 return 850 for station_id in registry.INTRO_STATIONS_BY_ITEM_ID: 851 seen_settings.unmark_seen(station_id) 852 853 self._Output(session.charid, '/resetstationintro: cleared seen flag for %d station(s)' % len(registry.INTRO_STATIONS_BY_ITEM_ID)) 854 855 def _handle_station_intro(self, command_line): 856 import uthread 857 from eve.client.script.ui.camera.cameraUtil import GetBall 858 from stationintrocinematic.client import registry 859 from stationintrocinematic.client.sequence import run_intro_sequence 860 parts = command_line.strip().split() 861 if len(parts) > 1: 862 try: 863 station_id = int(parts[1]) 864 except ValueError: 865 self._Output(session.charid, "/stationintro: invalid stationID '%s'" % parts[1]) 866 return 867 868 else: 869 station_id = next(iter(registry.INTRO_STATIONS_BY_ITEM_ID)) 870 if GetBall(station_id) is None: 871 self._Output(session.charid, "/stationintro: station %d isn't in the current system" % station_id) 872 return 873 config = registry.INTRO_STATIONS_BY_ITEM_ID.get(station_id) or registry.IntroConfig() 874 uthread.new(run_intro_sequence, station_id, config) 875 876 def _handle_bypass_destination(self, command_line): 877 from eve.client.script.ui.shared.mapView.mapViewUtil import SetBypassDestinationValidation, _bypass_destination_validation 878 parts = command_line.strip().split() 879 if len(parts) > 1 and parts[1].lower() in ('1', 'on', 'true'): 880 SetBypassDestinationValidation(True) 881 elif len(parts) > 1 and parts[1].lower() in ('0', 'off', 'false'): 882 SetBypassDestinationValidation(False) 883 else: 884 SetBypassDestinationValidation(not _bypass_destination_validation) 885 state = 'ON' if _bypass_destination_validation else 'OFF' 886 msg = 'Destination validation bypass: {} '.format(state) 887 self._Output(session.charid, msg) 888 889 def _adjust_commandLine(self, commandLine): 890 COMMANDS_NEEDING_EXTRA_PARAMS = ('/chtban', '/chtunban', '/chtgag', '/chtungag', '/massfleetinvite', '/masstransport') 891 parts = commandLine.split(' ', 1) 892 command = parts[0].strip().lower() 893 if command in COMMANDS_NEEDING_EXTRA_PARAMS: 894 if command in ('/massfleetinvite', '/masstransport'): 895 extra_params = 'windowID={}'.format(self.windowID) 896 else: 897 extra_params = 'channelName={}'.format(self.channelName) 898 parts.insert(1, extra_params) 899 commandLine = ' '.join(parts) 900 return commandLine 901 902 def _Send(self, text): 903 if text == '': 904 return 905 if IsSpam(text): 906 return 907 return GetChatService().SendGroupChat(self.receiver, text) 908 909 def LoadMessages(self): 910 portion = self.output.GetScrollProportion() 911 self.output.Clear() 912 nodes = [] 913 for sender, text, timestamp, colorkey in self.messages: 914 node = self._GetChatEntry(sender, text, timestamp, colorkey) 915 nodes.append(node) 916 917 self.output.AddNodes(-1, nodes) 918 if portion: 919 self.output.ScrollToProportion(portion) 920 921 def OnOutputOrInputKeyUp(self, key, *args): 922 if elevated_chat_highlighting_setting.is_enabled(): 923 self.TurnHighlightOff() 924 925 def IsHilighting(self, *args): 926 return elevated_chat_highlighting_setting.is_enabled() and uicore.uilib.Key(uiconst.VK_CONTROL) 927 928 def HighlightTextInOutput(self, findText = '', *args): 929 if not self.IsHilighting(): 930 return 931 if len(findText.strip().replace('(', '').replace(')', '')) < 2: 932 return 933 if getattr(self, 'highlightedText', '') and self.highlightedText != findText: 934 self.TurnHighlightOff() 935 self.highlightedText = findText 936 nodes = self.output.GetNodes() 937 for eachNode in nodes: 938 if eachNode.panel: 939 eachNode.panel.LoadHighlightedText(findText) 940 941 def TurnHighlightOff(self, *args): 942 highlightedText = getattr(self, 'highlightedText', '') 943 if not highlightedText: 944 return 945 for eachNode in self.output.GetNodes(): 946 if eachNode.panel: 947 eachNode.panel.RemoveHighlightedText() 948 949 self.highlightedText = None 950 951 def _GetChatEntry(self, sender, text, timestamp, colorkey): 952 try: 953 charid = int(sender) 954 except ValueError: 955 charid = sender 956 957 if elevated_chat_highlighting_setting.is_enabled(): 958 highlightFunc = self.HighlightTextInOutput 959 collectWordsInStack = True 960 else: 961 highlightFunc = None 962 collectWordsInStack = False 963 if auto_collapse_messages.get() and charid and charid != session.charid and IsEvePlayerCharacter(charid): 964 maxLines = int(auto_collapse_message_lines.get()) 965 else: 966 maxLines = None 967 node = ScrollEntryNode(decoClass=XmppChatEntry, text=text, timestamp=timestamp, fontsize=self._get_font_size(), letterspace=self._get_letter_space(), sender=sender, charid=charid, mode=self._message_style_setting.get(), colorkey=colorkey, channelMenu=self.GetOutputMenu, mouseOverWordCallback=highlightFunc, collectWordsInStack=collectWordsInStack, channelid=self.receiver, maxLines=maxLines, showExpander=False) 968 return node 969 970 def GetColorKeyForSender(self, sender): 971 try: 972 charid = int(sender) 973 except ValueError: 974 charid = sender 975 976 if charid == session.charid: 977 colorkey = session.role 978 elif charid == const.ownerSystem: 979 colorkey = service.ROLE_ADMIN 980 else: 981 userdata = self.members.get(charid, {}) 982 colorkey = userdata.get('role', None) 983 return colorkey 984 985 def _OnClose(self, *args, **kw): 986 if self.verbose: 987 logger.debug('_OnClose %s', self.receiver) 988 if self == sm.GetService('focus').GetFocusChannel(): 989 sm.GetService('focus').SetFocusChannel() 990 if self.checkConnectionStatusTasklet: 991 self.checkConnectionStatusTasklet.kill() 992 self.checkConnectionStatusTasklet = None 993 if self.removeIdleCharactersTasklet: 994 self.removeIdleCharactersTasklet.kill() 995 self.removeIdleCharactersTasklet = None 996 if not self._has_left_channel: 997 self._has_left_channel = True 998 if self.logfile is not None: 999 self.logfile.Close() 1000 self.logfile = None 1001 if not self.access_lost: 1002 if sm.IsServiceRunning('XmppChat'): 1003 xmppSvc = sm.GetService('XmppChat') 1004 xmppSvc.LeaveChannel(self.receiver) 1005 if xmppSvc.isDoingLogoff is False: 1006 windowName = self.name.replace('chatchannel_', '') 1007 xmppSvc.RemoveChannelFromCurrentChannelsSettings(windowName) 1008 1009 def MaybeAddSettingsOptionsToMenuParent(self, mParent): 1010 if self._should_show_channel_settings_option(): 1011 mParent.AddIconEntry(icon=ACTION_ICON, text=localization.GetByLabel('UI/Chat/OpenChannelSettingsWnd'), callback=self._OpenSettingsWindow) 1012 1013 def _should_show_channel_settings_option(self): 1014 channel_category = GetChannelCategory(self.receiver) 1015 if channel_category in (CATEGORY_CORP, CATEGORY_ALLIANCE): 1016 if IsContentComplianceControlSystemActive(sm.GetService('machoNet')): 1017 return False 1018 else: 1019 return self._HasSuitableCorpRolesForChatManagement(channel_category) 1020 else: 1021 is_owner_or_operator = self._IsOwnerOrOperatorOfChannel_Memoized(self.receiver) 1022 if not is_owner_or_operator and channel_category == CATEGORY_FLEET: 1023 if sm.GetService('fleet').IsBoss(): 1024 return True 1025 return is_owner_or_operator 1026 1027 @Memoize(0.2) 1028 def _HasSuitableCorpRolesForChatManagement(self, channelCategory): 1029 if session.corprole & const.corpRoleChatManager != const.corpRoleChatManager: 1030 return False 1031 if channelCategory == CATEGORY_ALLIANCE: 1032 allianceInfo = sm.GetService('alliance').GetAlliance() 1033 if not allianceInfo or allianceInfo.executorCorpID != session.corpid: 1034 return False 1035 return True 1036 1037 @Memoize(0.2) 1038 def _IsOwnerOrOperatorOfChannel_Memoized(self, channelName): 1039 return GetChatService().IsOwnerOrOperatorOfChannel(channelName) 1040 1041 def _OpenSettingsWindow(self): 1042 ChannelSettingsDlg.Open(channel=self.receiver, windowID='ChannelSettingsDlg_%s' % self.receiver) 1043 1044 def GetUserEntry(self, charID): 1045 for each in self.userlist.GetNodes(): 1046 if each.charID == charID: 1047 return each 1048 1049 def SetWordAndHightlightFilters(self): 1050 ChatFilterSettings.Open() 1051 1052 def _OpenHideMessageSettings(self): 1053 categoryID = self.channelCategory 1054 if categoryID in LOCAL_CHAT_CATEGORIES: 1055 categoryID = ChatCategory.LOCAL 1056 open_hide_message_settings(category_id=categoryID, channel_id=self.receiver, channel_name=GetChatService().GetDetailedDisplayName(self.receiver)) 1057 1058 def SetPictureMode(self, pictureMode): 1059 self._message_style_setting.set(pictureMode) 1060 1061 def GetMenuMoreOptions(self): 1062 menu = super(XmppChatWindow, self).GetMenuMoreOptions() 1063 self._AddChannelMenu(menu) 1064 return menu 1065 1066 def _get_message_style_menu(self): 1067 menu = MenuData() 1068 for mode in self._message_style_setting.options: 1069 menu.AddRadioButton(text=localization.GetByLabel(MESSAGEMODETEXTS[mode]), value=mode, setting=self._message_style_setting) 1070 1071 return menu 1072 1073 def _get_font_size_menu(self): 1074 menu = MenuData() 1075 menu.AddCheckbox(text=localization.GetByLabel('UI/Chat/UseFontSizeInAllChannels'), setting=self._apply_font_size_globally_setting) 1076 menu.AddSeparator() 1077 for font_size in self._effective_font_size_setting.options: 1078 menu.AddRadioButton(text=str(font_size), value=font_size, setting=self._effective_font_size_setting) 1079 1080 return menu 1081 1082 @staticmethod 1083 def _add_elevated_options(menu): 1084 role_mask = service.ROLE_CENTURION | service.ROLE_LEGIONEER | service.ROLEMASK_ELEVATEDPLAYER 1085 if session.role & role_mask: 1086 menu.AddCheckbox(text='Special chat highlighting', setting=elevated_chat_highlighting_setting) 1087 menu.AddSeparator() 1088 1089 def SettingMenu(self, menuParent): 1090 category = self.receiver.split('_')[0] 1091 if category in MOTD_CHANNEL_TYPES: 1092 menuParent.AddIconEntry(icon=ACTION_ICON, text=localization.GetByLabel('UI/Chat/ReloadMOTD'), callback=self.ShowMotdFromMenu) 1093 self.MaybeAddSettingsOptionsToMenuParent(menuParent) 1094 menuParent.AddDivider() 1095 menuParent.AddIconEntry(icon=ACTION_ICON, text=localization.GetByLabel('UI/Chat/ChannelWindow/SetWordFilters'), callback=self.SetWordAndHightlightFilters) 1096 menuParent.AddDivider() 1097 message_style = self._message_style_setting.get() 1098 menuParent.AddRadioButton(text=localization.GetByLabel(MESSAGEMODETEXTS[MESSAGEMODE_TEXTONLY]), checked=message_style == MESSAGEMODE_TEXTONLY, callback=(self.SetPictureMode, MESSAGEMODE_TEXTONLY)) 1099 menuParent.AddRadioButton(text=localization.GetByLabel(MESSAGEMODETEXTS[MESSAGEMODE_SMALLPORTRAIT]), checked=message_style == MESSAGEMODE_SMALLPORTRAIT, callback=(self.SetPictureMode, MESSAGEMODE_SMALLPORTRAIT)) 1100 menuParent.AddRadioButton(text=localization.GetByLabel(MESSAGEMODETEXTS[MESSAGEMODE_BIGPORTRAIT]), checked=message_style == MESSAGEMODE_BIGPORTRAIT, callback=(self.SetPictureMode, MESSAGEMODE_BIGPORTRAIT)) 1101 menuParent.AddDivider() 1102 cont = menuParent.AddContainer(padding=(6, 15, 6, 4), height=18) 1103 Combo(name='fontSizeCombo', parent=cont, align=uiconst.TOTOP, label=localization.GetByLabel('/Carbon/UI/Controls/EditRichText/FontSize'), options=[ (str(size), size) for size in self._font_size_setting.options ], callback=self.OnFontSizeCombo, select=self._get_font_size()) 1104 menuParent.AddCheckBox(text=localization.GetByLabel('UI/Chat/ApplyFontsizeToAllWnds'), checked=self._apply_font_size_globally_setting.is_enabled(), callback=self._apply_font_size_globally_setting.toggle) 1105 menuParent.AddDivider() 1106 menuParent.AddCheckBox(text=localization.GetByLabel('UI/Chat/BlinkOn'), checked=self._chat_notifications_enabled_setting.is_enabled(), callback=self._chat_notifications_enabled_setting.toggle) 1107 menuParent.AddCheckBox(text=localization.GetByLabel('UI/Chat/HighlightMyMessages'), checked=highlight_my_messages_setting.is_enabled(), callback=highlight_my_messages_setting.toggle) 1108 menuParent.AddCheckBox(text=localization.GetByLabel('UI/Chat/ShowTimestamp'), checked=show_message_timestamp_setting.is_enabled(), callback=show_message_timestamp_setting.toggle) 1109 menuParent.AddDivider() 1110 menuParent.AddIconEntry(icon=ACTION_ICON, text=localization.GetByLabel('UI/Chat/ClearAllContent'), callback=self.ClearContent) 1111 menuParent.AddDivider() 1112 roleMask = service.ROLE_CENTURION | service.ROLE_LEGIONEER | service.ROLEMASK_ELEVATEDPLAYER 1113 if session.role & roleMask: 1114 chatHightlighting = elevated_chat_highlighting_setting.is_enabled() 1115 if chatHightlighting: 1116 text = 'Turn chat highlighting off' 1117 else: 1118 text = 'Turn chat highlighting on' 1119 menuParent.AddIconEntry(icon=ACTION_ICON, text=text, callback=elevated_chat_highlighting_setting.toggle) 1120 1121 def UserListMenu(self, menuParent): 1122 if self.channelCategory in CHANNELS_WITH_NO_PRESENCE_BROADCAST: 1123 memberText = localization.GetByLabel('UI/Chat/MemberListRecentText') 1124 hint = localization.GetByLabel('UI/Chat/MemberListRecentSpeakers') 1125 else: 1126 memberText = localization.GetByLabel('UI/Chat/ShowMemberList') 1127 hint = None 1128 menuParent.AddCheckBox(text=memberText, checked=self._show_member_list_setting.is_enabled(), callback=self._show_member_list_setting.toggle, hint=hint) 1129 menuParent.AddCheckBox(text=localization.GetByLabel('UI/Chat/ShowCompactMemberList'), checked=self._compact_member_entries_setting.is_enabled(), callback=self._compact_member_entries_setting.toggle) 1130 1131 def _get_compact_member_entries_setting_key(self): 1132 if self.channelCategory in SINGLE_WND_CATEGORIES: 1133 name_part = self.channelCategory 1134 else: 1135 name_part = self.name 1136 return 'chatCondensedUserList_%s' % name_part 1137 1138 def SetupUserlist(self): 1139 if self.destroyed: 1140 return 1141 if not self._show_member_list_setting.is_enabled(): 1142 self._user_list_cont.state = uiconst.UI_HIDDEN 1143 else: 1144 minW = 50 1145 maxW = 200 1146 self._user_list_cont.width = min(maxW, max(minW, self._user_list_cont.width)) 1147 self._user_list_cont.state = uiconst.UI_PICKCHILDREN 1148 condensed = self._compact_member_entries_setting.is_enabled() 1149 if condensed != getattr(self.userlist, 'isCondensed', False): 1150 self.InitUsers() 1151 1152 def InitUsers(self): 1153 self.userlist.Clear() 1154 nodeListToSort = [] 1155 for char, userdata in self.members.iteritems(): 1156 role = userdata.get('role', 0) 1157 if role & service.ROLE_CHTINVISIBLE: 1158 logger.debug('Ignoring charID %s because role %s includes ROLE_CHTINVISIBLE', char, role) 1159 continue 1160 ownerInfo = cfg.eveowners.Get(char) 1161 node = self._GetUserEntryNode(char, ownerInfo, userdata) 1162 nodeListToSort.append(node) 1163 self.userlistNodesByName[char] = node 1164 1165 nodeList = sorted(nodeListToSort, key=lambda x: x.charIndex) 1166 self.userlist.AddNodes(-1, nodeList) 1167 1168 def OpenChannelWindow(self): 1169 XmppChatChannels.Open() 1170 1171 def ShowMotdFromMenu(self): 1172 if self.motd: 1173 msg = localization.GetByLabel('UI/Chat/ChannelMotd', motd=self.motd) 1174 self._Output(const.ownerSystem, msg) 1175 1176 def GetOutputMenu(self, *args): 1177 m = [(MenuLabel('UI/Common/CopyAll'), self.CopyAll)] 1178 return m 1179 1180 def CopyAll(self): 1181 t = '' 1182 for node in self.output.GetNodes(): 1183 timestr = '' 1184 if show_message_timestamp_setting.is_enabled(): 1185 year, month, wd, day, hour, min, sec, ms = GetTimeParts(node.timestamp) 1186 timestr = '[%02d:%02d:%02d] ' % (hour, min, sec) 1187 who = node.sender 1188 try: 1189 who = cfg.eveowners.Get(who).name 1190 except ValueError: 1191 pass 1192 1193 text = node.text.replace('>', '>').replace('&', '&') 1194 text = CleanText(text) 1195 t += '%s%s > %s\r\n' % (timestr, who, text) 1196 1197 blue.pyos.SetClipboardData(t) 1198 1199 def ClearContent(self): 1200 if self.output: 1201 self.output.Clear() 1202 self.messages = [] 1203 1204 def OnFontSizeCombo(self, combo, label, value): 1205 self._font_size_setting.set(value) 1206 1207 def OnDropCharacter(self, dragObj, nodes): 1208 for node in nodes[:5]: 1209 charID = GetCharIDFromTextLink(node) 1210 if not charID: 1211 guid = getattr(node, '__guid__', None) 1212 if guid not in AllUserEntries(): 1213 return 1214 charID = node.charID 1215 if IsCharacter(charID): 1216 GetChatService().Invite(charID, self.receiver) 1217 1218 def OnPortraitCreated(self, charID, _size): 1219 if self.destroyed or self.output is None: 1220 return 1221 if self.verbose: 1222 logger.debug('OnPortraitCreated %s', charID) 1223 self.pendingPortraitReloads.add(charID) 1224 if self.state == uiconst.UI_HIDDEN: 1225 return 1226 self._ReloadPendingPortraits() 1227 1228 def _ReloadPendingPortraits(self): 1229 if not self.pendingPortraitReloads: 1230 return 1231 for node in self.output.GetNodes(): 1232 if not node.panel or node.panel.state == uiconst.UI_HIDDEN: 1233 continue 1234 if node.charid in self.pendingPortraitReloads and not node.panel.picloaded: 1235 node.panel.LoadPortrait(orderIfMissing=False) 1236 1237 if self.userlist.state != uiconst.UI_HIDDEN: 1238 for charid in self.pendingPortraitReloads: 1239 userNode = self.GetUserEntry(charid) 1240 if userNode and userNode.panel: 1241 if not userNode.panel.picloaded: 1242 userNode.panel.LoadPortrait(orderIfMissing=False) 1243 1244 self.pendingPortraitReloads = set() 1245 1246 def _on_elevated_chat_highlighting_setting_changed(self, value): 1247 if elevated_chat_highlighting_setting.is_enabled(): 1248 if self.input and not self.input.destroyed: 1249 self.input.OnKeyUp = self.OnOutputOrInputKeyUp 1250 if self.output and not self.output.destroyed: 1251 self.output.OnKeyUp = self.OnOutputOrInputKeyUp 1252 else: 1253 if self.input and not self.input.destroyed: 1254 self.input.OnKeyUp = None 1255 if self.output and not self.output.destroyed: 1256 self.output.OnKeyUp = None 1257 self.TurnHighlightOff() 1258 self.LoadMessages() 1259 1260 def _CheckConnectionStatus(self): 1261 while not self.destroyed: 1262 sleepPeriod = GetMaxChatChannelIdleTime(sm.GetService('machoNet')) 1263 if sleepPeriod == 0: 1264 uthread2.Sleep(300) 1265 continue 1266 sleepPeriod += random.random() * 3.0 1267 uthread2.Sleep(sleepPeriod) 1268 if self.destroyed: 1269 break 1270 now = blue.os.GetWallclockTime() 1271 deltaInSec = blue.os.TimeDiffInMs(self.lastActivityTime, now) / 1000.0 1272 if deltaInSec > sleepPeriod: 1273 if self.verbose: 1274 logger.debug('No activity on %s for %s seconds', self.receiver, deltaInSec) 1275 try: 1276 GetChatService().JoinChannel(self.receiver, suppressMessage=True, isConnectionCheck=True, historySeconds=deltaInSec - 2) 1277 except InvalidSessionChatChannelNameError: 1278 continue 1279 1280 def OnMouseUp(self, _): 1281 uthread2.start_tasklet(self.StackSelf) 1282 1283 def StackSelf(self): 1284 if self.stacked: 1285 self.stack.Check() 1286 elif self.FindWindowToStackTo(): 1287 pass 1288 else: 1289 self._ConstructStack([(self, 1)], []) 1290 1291 def ValidatePaste(self, text): 1292 return sm.GetService('chat').validate_paste(text) 1293 1294 def _GetExtraIgnoredTags(self, text): 1295 return sm.GetService('helpPointer').FindTagToIgnore(text) 1296 1297 def UpdateCaption(self): 1298 if self.channelCategory == CATEGORY_PRIVATE: 1299 self.UpdatePrivateChannelCaption() 1300 else: 1301 self.UpdateChannelCaptionCounter() 1302 1303 def GetRecentEntriesFromCharID(self, charID): 1304 all_entries = self.output.GetNodes() 1305 selected_entries = [] 1306 for e in all_entries: 1307 if e.charid == charID: 1308 who = e.sender 1309 try: 1310 who = cfg.eveowners.Get(who).name 1311 except ValueError: 1312 pass 1313 1314 text = e.text.replace('>', '>').replace('&', '&') 1315 time = e.timestamp 1316 selected_entries.append('[%s] %s > %s' % (FmtDate(time, 'nl'), who, text)) 1317 1318 selected_entries = selected_entries[-10:] 1319 selected_entries.reverse() 1320 return selected_entries 1321 1322 def OnCharacterBannedNotify(self): 1323 self.Receive(const.ownerSystem, GetChatService().GetCharacterBannedMessage()) 1324 1325 def _get_font_size(self): 1326 font_size = global_font_size_setting.get() 1327 if font_size is None: 1328 font_size = self._font_size_setting.get() 1329 return font_size 1330 1331 def _get_letter_space(self): 1332 font_size = self._get_font_size() 1333 small_font_size = get_font_size_for_preset(EVE_SMALL_FONTSIZE) 1334 if font_size <= small_font_size: 1335 return 1 1336 else: 1337 return 0 1338 1339 def _update_font_size(self): 1340 self.LoadMessages() 1341 self.input.SetDefaultFontSize(self._get_font_size()) 1342 1343 def _on_font_size_setting_changed(self, value): 1344 if self._apply_font_size_globally_setting.is_enabled(): 1345 global_font_size_setting.set(self._font_size_setting.get()) 1346 else: 1347 self._update_font_size() 1348 1349 def _on_apply_font_size_globally_setting_changed(self, value): 1350 if self._apply_font_size_globally_setting.is_enabled(): 1351 global_font_size_setting.set(self._font_size_setting.get()) 1352 1353 def _on_global_font_size_setting_changed(self, value): 1354 self._update_font_size() 1355 1356 def _on_message_style_setting_changed(self, value): 1357 self._update_message_style() 1358 1359 def _update_message_style(self): 1360 self.LoadMessages() 1361 uicore.registry.SetFocus(self) 1362 1363 def _on_show_member_list_setting_changed(self, value): 1364 self.SetupUserlist() 1365 1366 def _on_compact_member_entries_setting_changed(self, value): 1367 is_compact = self._compact_member_entries_setting.is_enabled() 1368 if self._show_member_list_setting.is_enabled(): 1369 self.userlist.entry_spacing = self._get_user_list_entry_spacing() 1370 self.InitUsers() 1371 self.userlist.isCondensed = is_compact if self.userlist.display else -1 1372 1373 def _get_user_list_entry_spacing(self): 1374 if self._compact_member_entries_setting.is_enabled(): 1375 return 1 1376 else: 1377 return 4