channel_controller.py (19507B)
1 #Embedded file name: C:\BuildAgent\work\20d73f1b24eed06e\eve\release\V24.01\packages\chat\client\local_chat\channel_controller.py 2 import datetime 3 from carbon.common.script.sys.serviceConst import ROLE_CHTINVISIBLE 4 import sentry_sdk 5 from collections import deque 6 import httplib 7 import gametime 8 import logging 9 import localization 10 import uthread2 11 import uuid 12 from inventorycommon.const import ownerSystem 13 from eve.common.script.sys.idCheckers import IsEvePlayerCharacter 14 from eveprefs import prefs 15 from chat.common.util import is_local_chat_suppressed, is_local_chat_delayed 16 from chat.client.channel_controller import BaseChatChannelController 17 from chat.client.const import IDLE_MEMBER_REMOVAL_TIME 18 from .messenger import LocalChatMessenger, on_exit_notice, on_message_broadcast_notice, on_membership_list_notice, on_membership_refreshed_notice, on_join_notice 19 from ..hide_messages import open_hide_message_settings 20 from ..util import should_hide_message 21 WAIT_ON_MEMBERSHIP_NOTICE_SECONDS = 4 22 ENSURE_MEMBERS_MAX_DELAY = 60 23 logger = logging.getLogger('chat') 24 25 class LocalChatChannelController(BaseChatChannelController): 26 __notifyevents__ = ['OnSessionChanged', 'ProcessSessionReset'] 27 28 def __init__(self, *args, **kwargs): 29 self._character_id = None 30 self._solar_system_id = None 31 self._detailed_channel_id = None 32 self._is_suppressed = True 33 self._is_delayed = False 34 self._initial_members_received = False 35 self._member_request_tasklet = None 36 self._member_activity_timestamp = {} 37 self._member_activity_queue = deque() 38 self._member_activity_tasklet = None 39 self._ensure_membership_retry_count = 0 40 self._ensure_membership_retry_start_time = None 41 self._pending_member_data = {} 42 self._pending_member_data_timestamp = None 43 self._messenger = LocalChatMessenger.get_instance(sm.GetService('publicGatewaySvc')) 44 super(LocalChatChannelController, self).__init__(*args, **kwargs) 45 if session.charid: 46 uthread2.start_tasklet(self._update_character_and_system, True) 47 48 def close(self): 49 super(LocalChatChannelController, self).close() 50 self._messenger = None 51 self._initial_members_received = True 52 self._kill_member_request_tasklet() 53 54 def _register(self): 55 super(LocalChatChannelController, self)._register() 56 on_message_broadcast_notice.connect(self._on_message_broadcast_notice) 57 on_exit_notice.connect(self._on_exit_notice) 58 on_membership_list_notice.connect(self._on_membership_list_notice) 59 on_membership_refreshed_notice.connect(self._on_membership_refreshed_notice) 60 on_join_notice.connect(self._on_join_notice) 61 62 def _unregister(self): 63 super(LocalChatChannelController, self)._unregister() 64 on_message_broadcast_notice.disconnect(self._on_message_broadcast_notice) 65 on_exit_notice.disconnect(self._on_exit_notice) 66 on_membership_list_notice.disconnect(self._on_membership_list_notice) 67 on_membership_refreshed_notice.disconnect(self._on_membership_refreshed_notice) 68 on_join_notice.disconnect(self._on_join_notice) 69 70 def ProcessSessionReset(self): 71 if self._is_closing: 72 return 73 uthread2.start_tasklet(self._update_character_and_system, False) 74 75 def OnSessionChanged(self, is_remote, session, change): 76 if self._is_closing: 77 return 78 if 'charid' in change: 79 if self._character_id != session.charid: 80 self._update_character_id(ensure_members=True) 81 else: 82 self.chat_service.open_channel_window(self.channel_id) 83 if 'solarsystemid2' in change: 84 self._update_system_id(ensure_members='charid' not in change) 85 86 def _update_character_and_system(self, ensure_members): 87 self._update_character_id(ensure_members=ensure_members) 88 self._update_system_id(ensure_members=False) 89 90 def _update_character_id(self, ensure_members): 91 if self._character_id == session.charid: 92 return 93 self._character_id = session.charid 94 self._kill_member_request_tasklet() 95 self._clear_members() 96 self.clear_messages() 97 self._close_logfile() 98 if self._character_id: 99 self._setup_log_file() 100 if ensure_members: 101 self._ensure_members_for_new_solar_system(wait_time=1) 102 self.chat_service.open_channel_window(self.channel_id) 103 104 def _update_system_id(self, ensure_members): 105 if self._solar_system_id == session.solarsystemid2: 106 return 107 self._clear_member_activity() 108 self._solar_system_id = session.solarsystemid2 109 self._detailed_channel_id = '{}_{}'.format(self._channel_id, self._solar_system_id) 110 self._is_suppressed = self._solar_system_id is None or is_local_chat_suppressed(self._solar_system_id) 111 self._is_delayed = self._solar_system_id is not None and is_local_chat_delayed(self._solar_system_id) 112 if not self._solar_system_id: 113 return 114 if self._is_delayed: 115 self._member_activity_tasklet = uthread2.start_tasklet(self._remove_inactive_members_routine) 116 self._clear_members() 117 self._add_system_changed_message() 118 self._add_invisible_role_message() 119 added_pending = self._process_pending_member_data() 120 if ensure_members and not added_pending: 121 self._ensure_members_for_new_solar_system() 122 123 @property 124 def window_display_name(self): 125 return localization.GetByLabel('UI/Chat/Local') 126 127 @property 128 def member_count(self): 129 if self._is_suppressed or self._is_delayed: 130 return 0 131 return super(LocalChatChannelController, self).member_count 132 133 @property 134 def detailed_channel_id(self): 135 return self._detailed_channel_id 136 137 def set_members(self, members): 138 if self._is_suppressed: 139 return 140 super(LocalChatChannelController, self).set_members(members) 141 142 def add_member(self, character_id, character_info = None): 143 if self._is_suppressed: 144 return 145 super(LocalChatChannelController, self).add_member(character_id, character_info) 146 147 def send_chat_message(self, text): 148 if self._is_suppressed: 149 return None 150 message_id = gametime.GetWallclockTime() 151 self._add_to_messages(session.charid, text) 152 self._add_pending_message(message_id) 153 154 def _response_handler(payload, response): 155 if response is None: 156 self._remove_pending_message(message_id, was_delivered=False) 157 elif response.status_code != httplib.OK: 158 logger.error('LocalChat BroadcastMessageResponse returned with statusCode {}, statusMessage: {}'.format(response.status_code, response.status_message)) 159 self._remove_pending_message(message_id, was_delivered=False) 160 else: 161 self._remove_pending_message(message_id, was_delivered=True) 162 163 def _timeout_handler(*args, **kwargs): 164 self._remove_pending_message(message_id, was_delivered=False) 165 166 self._messenger.broadcast_message(text, _response_handler, _timeout_handler) 167 return message_id 168 169 def should_hide_message(self, text): 170 return should_hide_message(self.detailed_channel_id, self.category_id, text) 171 172 def open_hide_message_settings(self): 173 open_hide_message_settings(category_id=self.category_id, channel_id=self.detailed_channel_id, channel_name=self.channel_id) 174 175 def _on_message_broadcast_notice(self, solar_system_id, sender_id, message_content): 176 if self._is_closing or self._is_suppressed: 177 return 178 if solar_system_id != session.solarsystemid2: 179 logger.warn('LocalChat on_message_broadcast_notice got bad solar_system_id=%d', solar_system_id) 180 return 181 if self._character_id != session.charid or session.charid is None: 182 return 183 self._update_member_activity(sender_id) 184 if sender_id != session.charid: 185 self.message_received(sender_id, message_content) 186 187 def _on_membership_list_notice(self, solar_system_id, members): 188 if self._is_closing: 189 return 190 if solar_system_id != session.solarsystemid2: 191 logger.warn('LocalChat _on_membership_notice received bad solar_system_id=%d', solar_system_id) 192 self._pending_member_data[solar_system_id] = members 193 self._pending_member_data_timestamp = gametime.GetWallclockTime() 194 return 195 self._update_member_list(members) 196 197 def _update_member_list(self, members): 198 if self._is_closing: 199 return 200 chars_missing_corp_id = [] 201 for member_id, member_data in members.iteritems(): 202 if member_data['corporation_id'] == 0: 203 chars_missing_corp_id.append(member_id) 204 205 if len(chars_missing_corp_id) > 0: 206 charSvc = sm.RemoteSvc('charMgr') 207 fallback_data = charSvc.GetOrganizationInfoForCharacters(chars_missing_corp_id) 208 for char_id, data in fallback_data.iteritems(): 209 members[char_id]['corporation_id'] = data.corporationID 210 members[char_id]['alliance_id'] = data.allianceID 211 members[char_id]['war_faction_id'] = data.warFactionID 212 213 with sentry_sdk.configure_scope() as scope: 214 scope.set_tag('solar_system_id', session.solarsystemid2) 215 logger.exception('LocalChat _update_member_list received members with missing corporation_id. Requesting fallback data.', extra={'character_ids': chars_missing_corp_id, 216 'fallback_data': fallback_data}) 217 scope.remove_tag('solar_system_id') 218 self._update_character_id(ensure_members=False) 219 self._update_system_id(ensure_members=False) 220 if session.charid is None: 221 return 222 self._initial_members_received = True 223 self.set_members(members) 224 225 def _on_membership_refreshed_notice(self, solar_system_id, member_id, member_data): 226 if self._is_closing: 227 return 228 if solar_system_id != session.solarsystemid2: 229 logger.warn('LocalChat _on_membership_refreshed_notice received bad solar_system_id=%d', solar_system_id) 230 if solar_system_id in self._pending_member_data and member_id in self._pending_member_data[solar_system_id]: 231 self._pending_member_data[solar_system_id][member_id] = member_data 232 return 233 if not self._is_delayed and member_id not in self._member_ids: 234 return 235 self.chat_service.update_members_info({member_id: member_data}, signal=True) 236 237 def _on_join_notice(self, solar_system_id, character_id, character_info): 238 if self._is_closing: 239 return 240 if solar_system_id != session.solarsystemid2: 241 logger.warn('LocalChat _on_join_notice got bad solar_system_id=%d', solar_system_id) 242 if solar_system_id in self._pending_member_data: 243 self._pending_member_data[solar_system_id][character_id] = character_info 244 return 245 if self._character_id != session.charid or session.charid is None: 246 return 247 self.add_member(character_id, character_info) 248 249 def _on_exit_notice(self, solar_system_id, character_id): 250 if self._is_closing: 251 return 252 if solar_system_id != session.solarsystemid2: 253 logger.warn('LocalChat _on_exit_notice got bad solar_system_id=%d', solar_system_id) 254 if solar_system_id in self._pending_member_data: 255 self._pending_member_data[solar_system_id].pop(character_id, None) 256 return 257 if self._character_id != session.charid or session.charid is None: 258 return 259 self.remove_member(character_id) 260 261 def _process_pending_member_data(self): 262 added = False 263 if self._solar_system_id in self._pending_member_data and gametime.GetSecondsSinceWallclockTime(self._pending_member_data_timestamp) < 5: 264 added = True 265 self._update_member_list(self._pending_member_data[self._solar_system_id]) 266 self._pending_member_data.clear() 267 self._pending_member_data_timestamp = None 268 return added 269 270 def _ensure_members_for_new_solar_system(self, wait_time = None): 271 self._initial_members_received = False 272 self._kill_member_request_tasklet() 273 self._member_request_tasklet = uthread2.StartTasklet(self._ensure_members_for_new_solar_system_tasklet, WAIT_ON_MEMBERSHIP_NOTICE_SECONDS if wait_time is None else wait_time) 274 275 def _ensure_members_for_new_solar_system_tasklet(self, wait_time = None): 276 if wait_time: 277 uthread2.sleep(wait_time) 278 if self._is_closing or session.charid is None or self._initial_members_received: 279 return 280 logger.info('LocalChat timeout waiting on membership notice for solar_system_id=%d', session.solarsystemid2) 281 self._messenger.request_solar_system_membership(self._ensure_members_membership_response_callback, self._ensure_membership_timeout_callback) 282 283 def _ensure_members_membership_response_callback(self, payload, response): 284 if self._is_closing or self._initial_members_received or response is None: 285 return 286 if response.status_code != 200: 287 logger.error('LocalChat error in response for solar system membership request', extra={'status_code': response.status_code, 288 'status_message': response.status_message}) 289 return 290 self._ensure_membership_retry_count = 0 291 self._ensure_membership_retry_start_time = None 292 solar_system_id = payload.solar_system.sequential 293 members = self._messenger.parse_members_from_payload(payload) 294 self._on_membership_list_notice(solar_system_id, members) 295 296 def _ensure_membership_timeout_callback(self, error, request_primitive): 297 if self._is_closing or self._initial_members_received: 298 return 299 public_gateway_service = sm.GetService('publicGatewaySvc') 300 logger.error('LocalChat request for membership list timed out', extra={'error': error, 301 'request_broker_details': public_gateway_service.get_request_broker_details(), 302 'notice_consumer_details': public_gateway_service.get_notice_consumer_details(), 303 'public_gateway_svc_uptime_ms': public_gateway_service.get_gateway_uptime_ms(), 304 'request_correlation_uuid': str(uuid.UUID(bytes=request_primitive.correlation_uuid)) if request_primitive else ''}) 305 if self._ensure_membership_retry_start_time is None: 306 self._ensure_membership_retry_start_time = datetime.datetime.now() 307 308 def get_wait_time(retry_count): 309 if retry_count <= 1: 310 return WAIT_ON_MEMBERSHIP_NOTICE_SECONDS 311 return min(ENSURE_MEMBERS_MAX_DELAY, get_wait_time(retry_count - 1) * 2) 312 313 if datetime.datetime.now() - self._ensure_membership_retry_start_time < datetime.timedelta(minutes=5): 314 self._ensure_membership_retry_count += 1 315 self._ensure_members_for_new_solar_system(wait_time=get_wait_time(self._ensure_membership_retry_count)) 316 else: 317 logger.error('LocalChat failed to get membership list after {} retries'.format(self._ensure_membership_retry_count)) 318 self._ensure_membership_retry_start_time = None 319 self._ensure_membership_retry_count = 0 320 321 def _add_system_changed_message(self): 322 if self._is_suppressed: 323 system_name = localization.GetByLabel('UI/Chat/Unknown') 324 else: 325 system_name = cfg.evelocations.Get(session.solarsystemid2).name 326 channel_name = localization.GetByLabel('UI/Chat/FeatureChannelName', featureType=localization.GetByLabel('UI/Chat/Local'), featureName=system_name) 327 self.add_game_message(localization.GetByLabel('UI/Chat/ChannelWindow/ChannelChangedToChannelName', channelName=channel_name), sender_id=ownerSystem) 328 329 def _add_invisible_role_message(self): 330 if session.role & ROLE_CHTINVISIBLE == ROLE_CHTINVISIBLE: 331 self.add_game_message(localization.GetByLabel('UI/Chat/InvisibleWarning', color='#ffffff00'), sender_id=ownerSystem) 332 333 def _update_member_activity(self, member_id): 334 if not IsEvePlayerCharacter(member_id): 335 return 336 self.add_member(member_id) 337 if self._is_delayed: 338 timestamp = gametime.GetWallclockTime() 339 self._member_activity_timestamp[member_id] = timestamp 340 self._member_activity_queue.append((timestamp, member_id)) 341 342 def _remove_inactive_members_routine(self): 343 while self._is_delayed: 344 now = gametime.GetWallclockTime() 345 inactive_member_ids = [] 346 max_activity_seconds = prefs.GetValue('chat_idle_character_removal_period', IDLE_MEMBER_REMOVAL_TIME) 347 sleep_duration = max_activity_seconds 348 while len(self._member_activity_queue) > 0: 349 activity_timestamp, member_id = self._member_activity_queue[0] 350 if self._member_activity_timestamp[member_id] != activity_timestamp: 351 self._member_activity_queue.popleft() 352 continue 353 time_since_activity = gametime.GetTimeDiff(activity_timestamp, now) / gametime.SEC 354 if time_since_activity > max_activity_seconds: 355 self._member_activity_queue.popleft() 356 inactive_member_ids.append(member_id) 357 else: 358 sleep_duration = max_activity_seconds - time_since_activity + 1 359 break 360 361 for character_id in inactive_member_ids: 362 self.remove_member(character_id) 363 364 uthread2.sleep(sleep_duration) 365 366 def _kill_member_request_tasklet(self): 367 if self._member_request_tasklet: 368 self._member_request_tasklet.kill() 369 self._member_request_tasklet = None 370 371 def _clear_member_activity(self): 372 if self._member_activity_tasklet: 373 self._member_activity_tasklet.kill() 374 self._member_activity_tasklet = None 375 self._member_activity_timestamp.clear() 376 self._member_activity_queue.clear() 377 378 def mute_player(self, character_id, duration, reason): 379 self._messenger.mute_player(character_id, duration, reason, self._mute_player_response_callback, self._mute_player_timeout_callback) 380 381 def _mute_player_response_callback(self, payload, response): 382 if response is None: 383 return 384 if response.status_code != httplib.OK: 385 logger.error('LocalChat MuteResponse returned with statusCode {}, statusMessage: {}'.format(response.status_code, response.status_message)) 386 387 def _mute_player_timeout_callback(self, error, request_primitive): 388 public_gateway_service = sm.GetService('publicGatewaySvc') 389 logger.error('LocalChat request to mute player timed out', extra={'error': error, 390 'request_broker_details': public_gateway_service.get_request_broker_details(), 391 'notice_consumer_details': public_gateway_service.get_notice_consumer_details(), 392 'public_gateway_svc_uptime_ms': public_gateway_service.get_gateway_uptime_ms(), 393 'request_correlation_uuid': str(uuid.UUID(bytes=request_primitive.correlation_uuid)) if request_primitive else ''})