NovaBryan/EVE_Dump
Log | Files | 📦 下载最新源码 (.zip) | Refs | README

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('&lt;', '<').replace('&gt;', '>').replace('&amp;', '&')
    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('&gt;', '>').replace('&amp;', '&')
   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('&gt;', '>').replace('&amp;', '&')
   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