from typing import Dict, List, Set, Any, Tuple, Optional, Iterable from abc import ABCMeta, abstractmethod import asyncio, traceback, settings, smtplib, pprint from collections import defaultdict from enum import IntFlag from util.misc import gen_uuid, first_in_iterable, run_loop, Runner, server_temp_cleanup from .user import UserService from .auth import AuthService, LoginAuthService, GenTokenStr from .stats import Stats from .client import Client from .models import ( User, UserDetail, Group, ContactList, OIM, Contact, ContactDetail, Circle, CircleRole, CircleState, CircleMembership, Chatroom, UserStatus, TextWithData, MessageData, Substatus, LoginOption, _is_blocking, ) from . import error, event class Ack(IntFlag): Zero = 0 NAK = 1 ACK = 2 Full = 3 class Backend: __slots__ = ( 'user_service', 'auth_service', 'login_auth_service', 'loop', 'notify_maintenance', 'maintenance_mode', 'maintenance_mins', '_stats', '_sc', '_chats_by_id', '_cses_by_bs_by_circle_id', '_user_by_uuid', '_worklist_sync_db', '_worklist_sync_circles', '_worklist_notify', '_worklist_notify_self', '_runners', '_linked', '_dev', ) user_service: UserService auth_service: AuthService login_auth_service: LoginAuthService loop: asyncio.AbstractEventLoop notify_maintenance: bool maintenance_mode: bool maintenance_mins: int _stats: Stats _sc: '_SessionCollection' _chats_by_id: Dict[Tuple[str, str], 'Chat'] _cses_by_bs_by_circle_id: Dict[str, Dict['BackendSession', Optional['ChatSession']]] _user_by_uuid: Dict[str, User] _worklist_sync_db: Dict[User, UserDetail] _worklist_sync_circles: Dict[str, Circle] _worklist_notify: List[Tuple['BackendSession', Optional[int], bool, Substatus, Optional[Dict[str, Any]], bool, bool, bool]] _worklist_notify_self: List[Tuple['BackendSession', Substatus, bool, bool]] _runners: List[Runner] _linked: bool _dev: Optional[Any] def __init__(self, loop: asyncio.AbstractEventLoop, *, user_service: UserService, login_auth_service: LoginAuthService, auth_service: AuthService, stats_service: Stats, ) -> None: self.user_service = user_service self.auth_service = auth_service self.login_auth_service = login_auth_service self.loop = loop self.notify_maintenance = False self.maintenance_mode = False self.maintenance_mins = 0 self._stats = stats_service self._sc = _SessionCollection() self._chats_by_id = {} self._cses_by_bs_by_circle_id = {} self._user_by_uuid = {} self._worklist_sync_db = {} self._worklist_sync_circles = {} self._worklist_notify = [] self._worklist_notify_self = [] self._runners = [] self._linked = False self._dev = None server_temp_cleanup() for circle in self.user_service.get_all_circles(): self.chat_create(circle = circle) print(f'[Circle] {circle.name} (ID: {circle.chat_id}) initalized') loop.create_task(self._worker_sync_db()) loop.create_task(self._worker_remove_expired_login_tokens()) loop.create_task(self._worker_sync_circles()) loop.create_task(self._worker_clean_sessions()) loop.create_task(self._worker_sync_stats()) loop.create_task(self._worker_notify()) loop.create_task(self._worker_notify_self()) def push_client_alert(self, icon_url: Optional[str], url: Optional[str], message: str = '') -> None: print(f"[Backend] Pushing system alert") for bs in self._sc.iter_sessions(): bs.evt.on_client_alert(icon_url, url, message) def push_maintenance_message(self, *args: Any, **kwargs: Any) -> None: print(f"[Backend] Server going down for maintenance in {args[1]} minutes. Pushing system alert") for bs in self._sc.iter_sessions(): bs.evt.on_maintenance_message(*args, **kwargs) if isinstance(args[1], int) and args[1] >= 0: self.notify_maintenance = True self.maintenance_mins = args[1] self.loop.create_task(self._worker_set_server_maintenance()) async def _worker_set_server_maintenance(self) -> None: while self.maintenance_mins > 0: await asyncio.sleep(60) self.maintenance_mins -= 1 if self.maintenance_mins <= 0: self.notify_maintenance = False self.maintenance_mode = True for bs in self._sc._sessions.copy(): bs.evt.on_maintenance_boot() server_temp_cleanup() def add_runner(self, runner: Runner) -> None: self._runners.append(runner) def run_forever(self) -> None: run_loop(self.loop, self._runners) def on_leave(self, sess: 'BackendSession', *, sess_id: Optional[int] = None) -> None: user = sess.user old_substatus = user.status.substatus self._stats.on_logout() self._sc.remove_session(sess) print(f"[Presence] BackendSession {str(id(sess))} belonging to {user.username} ({user.email}) has been closed") if self._sc.get_sessions_by_user(user): # There are still other people logged in as this user, # so don't send offline notifications. self._notify_contacts(sess, old_substatus, for_logout = False, update_status = False) self._notify_self(sess, old_substatus, update_status = False) return # User is offline, send notifications user.status.substatus = Substatus.Offline self._sync_contact_statuses(user) self._notify_contacts(sess, old_substatus, for_logout = True) def login( self, uuid: str, client: Client, evt: event.BackendEventHandler, *, option: Optional[LoginOption] = None, only_once: bool = False, ) -> Optional['BackendSession']: user = self._load_user_record(uuid) if user is None: return None bs_others = self._sc.get_sessions_by_user(user) if only_once and bs_others: return None self.user_service.update_date_login(uuid) for bs_other in bs_others: try: if option: bs_other.evt.on_login_elsewhere(option) else: return None except: traceback.print_exc() bs = BackendSession(self, user, client, evt) bs.evt.bs = bs self._stats.on_login() self._stats.on_user_active(user, client) self._sc.add_session(bs) user.detail = self._load_detail(user) print(f"[Presence] {user.username} ({user.email}) has logged in with BackendSession ID {str(id(bs))}") print(f"[Presence] They are using {client.program.upper()} {client.version} via {client.via}") bs.evt.on_open() return bs def _load_user_record(self, uuid: str) -> Optional[User]: if uuid not in self._user_by_uuid: user = self.user_service.get(uuid) if user is None: return None self._user_by_uuid[uuid] = user return self._user_by_uuid[uuid] def _load_detail(self, user: User) -> UserDetail: if user.detail: return user.detail detail = self.user_service.get_detail(user.uuid) assert detail is not None return detail def chat_create(self, *, circle: Optional[Circle] = None) -> 'Chat': return Chat(self, self._stats, circle = circle) def chat_get(self, scope: str, id: str) -> Optional['Chat']: return self._chats_by_id.get((scope, id)) def get_chats_by_scope(self, scope: str) -> Iterable['Chat']: return [chat for (scope_other, _), chat in self._chats_by_id.items() if scope_other is scope] def join_circle( self, chat_id: str, origin: str, bs: 'BackendSession', evt: event.ChatEventHandler, *, pop_id: Optional[str] = None, ) -> Optional['ChatSession']: chat = self.chat_get('persistent', chat_id) if chat is None: return None cs = chat.join(origin, bs, evt, pop_id = pop_id) if chat_id not in self._cses_by_bs_by_circle_id: self._cses_by_bs_by_circle_id[chat_id] = {} self._cses_by_bs_by_circle_id[chat_id][bs] = cs return cs def get_circle_cs(self, chat_id: str, bs: 'BackendSession') -> Optional['ChatSession']: if chat_id not in self._cses_by_bs_by_circle_id: return None return self._cses_by_bs_by_circle_id[chat_id].get(bs) def _sync_contact_statuses(self, user: User) -> None: detail = user.detail if detail is None: return for ctc in detail.contacts.values(): if ctc.lists & ContactList.FL: ctc.compute_visible_status(user) # If the contact lists ever become inconsistent (FL without matching RL), # the contact that's missing the RL will always see the other user as offline. # Because of this, and the fact that most contacts *are* two-way, and it # not being that much extra work, I'm leaving this line commented out. #if not ctc.lists & ContactList.RL: continue if ctc.head.detail is None: continue ctc_rev = ctc.head.detail.contacts.get(user.uuid) if ctc_rev is None: continue ctc_rev.compute_visible_status(ctc.head) def _notify_contacts( self, bs: 'BackendSession', old_substatus: Substatus, *, for_logout: bool = False, sess_id: Optional[int] = None, on_contact_add: bool = False, updated_phone_info: Optional[Dict[str, Any]] = None, update_status: bool = True, update_info_other: bool = True, ) -> None: self._worklist_notify.append(( bs, sess_id, on_contact_add, old_substatus, updated_phone_info, update_status, update_info_other, for_logout, )) def _notify_self(self, bs: 'BackendSession', old_substatus: Substatus, *, update_status: bool = True, update_info: bool = True) -> None: self._worklist_notify_self.append((bs, old_substatus, update_status, update_info)) def _mark_modified(self, user: User, *, detail: Optional[UserDetail] = None) -> None: ud = user.detail or detail if detail: assert ud is detail #assert ud is not None self._worklist_sync_db[user] = ud def _mark_circle_modified(self, circle: Circle) -> None: self._worklist_sync_circles[circle.chat_id] = circle def util_get_uuid_from_email(self, email: str) -> Optional[str]: return self.user_service.get_uuid(email) def util_get_uuid_from_username(self, username: str) -> Optional[str]: return self.user_service.get_uuid_username(username) def util_get_uuid_from_user_id(self, uid: int) -> Optional[str]: return self.user_service.get_uuid_user_id(uid) def util_set_sess_token(self, sess: 'BackendSession', token: str) -> None: self._sc.set_nc_by_token(sess, token) def util_get_sess_by_token(self, token: str) -> Optional['BackendSession']: return self._sc.get_nc_by_token(token) def util_get_sessions_by_user(self, user: User) -> List['BackendSession']: return self._sc.get_sessions_by_user(user) def util_get_circle_memberships_by_role(self, circle: Circle, role: CircleRole) -> Iterable[CircleMembership]: return [membership for membership in circle.memberships.values() if membership.role == role] def util_user_online_in_circle(self, circle: Circle, user: User) -> bool: if user.uuid not in circle.memberships: raise error.MemberNotInCircle() chat = self.chat_get('persistent', circle.chat_id) if chat is None: return False for cs in chat.get_roster_single(): if cs.user is user: return True return False def util_accept_circle_invite(self, circle: Circle, user: User) -> None: if user.uuid not in circle.memberships: raise error.MemberNotInCircle() chat = self.chat_get('persistent', circle.chat_id) membership = circle.memberships[user.uuid] if not (membership.role == CircleRole.StatePendingOutbound and membership.state == CircleState.WaitingResponse): if membership.state == CircleState.Rejected or membership.state == CircleState.Empty: raise error.MemberNotInCircle() raise error.MemberAlreadyInCircle() membership.role = CircleRole.Member membership.state = CircleState.Accepted if membership.invite_message is not None: membership.invite_message = None self._mark_circle_modified(circle) if chat is not None: print(f"[Circle] {user.username} ({user.email}) has joined {circle.name} (ID: {circle.chat_id})") for bs_other in self.util_get_sessions_by_user(user): bs_other.evt.on_circle_role_updated(circle.chat_id, CircleRole.Member) for cs_other in chat.get_roster(): if cs_other is user: continue cs_other.bs.evt.on_circle_updated(circle) def util_decline_circle_invite(self, circle: Circle, user: User) -> None: if user.uuid not in circle.memberships: raise error.MemberNotInCircle() chat = self.chat_get('persistent', circle.chat_id) membership = circle.memberships[user.uuid] if not (membership.role == CircleRole.StatePendingOutbound and membership.state == CircleState.WaitingResponse): if membership.state == CircleState.Rejected or membership.state == CircleState.Empty: raise error.MemberNotInCircle() raise error.MemberAlreadyInCircle() membership.role = CircleRole.Member membership.state = CircleState.Rejected self._mark_circle_modified(circle) if chat is not None: for bs_other in self.util_get_sessions_by_user(user): print(f"[Circle] {user.username} ({user.email}) has declined their invitation to {circle.name} (ID: {circle.chat_id})") bs_other.evt.on_declined_chat_invite(chat, circle = True) chat.send_participant_declined(user, circle = True) def util_revoke_circle_invite(self, circle: Circle, user: User) -> None: if user.uuid not in circle.memberships: raise error.MemberNotInCircle() chat = self.chat_get('persistent', circle.chat_id) membership = circle.memberships[user.uuid] if not (membership.role == CircleRole.StatePendingOutbound and membership.state == CircleState.WaitingResponse): if membership.state == CircleState.Rejected or membership.state == CircleState.Empty: raise error.MemberNotInCircle() raise error.MemberAlreadyInCircle() membership.role = CircleRole.Member membership.state = CircleState.Empty if membership.inviter_uuid is not None: membership.inviter_uuid = None if membership.inviter_email is not None: membership.inviter_email = None if membership.inviter_name is not None: membership.inviter_name = None self._mark_circle_modified(circle) for bs_other in self.util_get_sessions_by_user(user): print(f"[Circle] {user.username} ({user.email})'s invitation to {circle.name} (ID: {circle.chat_id}) has been revoked") if membership_inviter_email is not None: print(f"[Circle] It was sent by {membership.inviter_email}") bs_other.evt.on_circle_invite_revoked(circle.chat_id) if chat is not None: for cs_other in chat.get_roster(): cs_other.bs.evt.on_circle_updated(circle) def util_change_circle_membership_role(self, circle: Circle, user_other: User, role: CircleRole, user_self: Optional[User]) -> None: if user_other.uuid not in circle.memberships or (user_self is not None and user_self.uuid not in circle.memberships): raise error.MemberNotInCircle() chat = self.chat_get('persistent', circle.chat_id) membership = circle.memberships[user_other.uuid] membership_self = None if user_self is not None: assert role is CircleRole.Admin membership_self = circle.memberships[user_self.uuid] old_role = membership.role if old_role == CircleRole.StatePendingOutbound: raise error.CircleMemberIsPending() if membership_self is not None: if membership_self.role != CircleRole.Admin or old_role == CircleRole.Admin: raise error.MemberDoesntHaveSufficientCircleRole() membership.role = role if membership_self is not None: membership_self.role = CircleRole.Member if old_role is not membership.role: self._mark_circle_modified(circle) if chat is not None: for cs_other in chat.get_roster(): if cs_other.user is user_other or (user_self is not None and cs_other.user is user_self): role_user = None if (user_self is not None and cs_other.user is user_self) and membership_self is not None: role_user = membership_self.role elif cs_other.user is user_other: role_user = membership.role if role_user is not None: cs_other.bs.evt.on_circle_role_updated(circle.chat_id, role_user) print(f"[Circle] {user_other.username} ({user_other.email})'s role in {circle.name} (ID: {circle.chat_id}) is now {membership.role}") print(f"[Circle] {user_other.username} ({user_other.email})'s state in {circle.name} (ID: {circle.chat_id}) is now {membership.state}") else: cs_other.bs.evt.on_circle_updated(circle) def util_remove_user_from_circle(self, circle: Circle, user: User) -> None: if user.uuid not in circle.memberships: raise error.MemberNotInCircle() chat = self.chat_get('persistent', circle.chat_id) membership = circle.memberships[user.uuid] if membership.state == CircleState.Empty: raise error.MemberNotInCircle() if membership.role == CircleRole.Admin and len(list(self.util_get_circle_memberships_by_role(circle, CircleRole.Admin))) < 2: raise error.CantLeaveCircle() membership.role = CircleRole.Member membership.state = CircleState.Empty if membership.inviter_uuid is not None: membership.inviter_uuid = None if membership.inviter_email is not None: membership.inviter_email = None if membership.inviter_name is not None: membership.inviter_name = None self._mark_circle_modified(circle) for bs_other in self.util_get_sessions_by_user(user): print(f"[Circle] {user.username} ({user.email}) has been removed from {circle.name} (ID: {circle.chat_id})") bs_other.evt.on_left_circle(circle) if chat is not None: if circle.chat_id in self._cses_by_bs_by_circle_id: for bs, cs in list(self._cses_by_bs_by_circle_id[circle.chat_id].items()): if cs is not None and cs.user is user: cs.close() del self._cses_by_bs_by_circle_id[circle.chat_id][bs] for cs_other in chat.get_roster(): if cs_other.user is not user: cs_other.bs.evt.on_circle_updated(circle) def dev_connect(self, obj: object) -> None: if self._dev is None: return self._dev.connect(obj) def dev_disconnect(self, obj: object) -> None: if self._dev is None: return self._dev.disconnect(obj) async def _worker_sync_db(self) -> None: while True: await asyncio.sleep(0.0125) self._sync_db_impl() def _sync_db_impl(self) -> None: if not self._worklist_sync_db: return try: users = list(self._worklist_sync_db.keys())[:100] batch = [] for user in users: detail = self._worklist_sync_db.pop(user, None) if detail is None: continue batch.append((user, detail)) self.user_service.save_batch(batch) except: traceback.print_exc() async def _worker_remove_expired_login_tokens(self) -> None: while True: await asyncio.sleep(0.0125) self._remove_expired_login_tokens() def _remove_expired_login_tokens(self) -> None: try: self.login_auth_service.remove_expired() except: traceback.print_exc() async def _worker_sync_circles(self) -> None: while True: await asyncio.sleep(0.0125) self._sync_circle_impl() def _sync_circle_impl(self) -> None: if not self._worklist_sync_circles: return try: chat_ids = list(self._worklist_sync_circles.keys())[:100] batch = [] for chat_id in chat_ids: circle = self._worklist_sync_circles.pop(chat_id, None) if circle is None: continue batch.append((chat_id, circle)) self.user_service.save_circle_batch(batch) except: traceback.print_exc() async def _worker_clean_sessions(self) -> None: while True: await asyncio.sleep(0.0125) closed = [] try: for sess in self._sc.iter_sessions(): if sess.closed: closed.append(sess) except: traceback.print_exc() for sess in closed: self._sc.remove_session(sess) async def _worker_sync_stats(self) -> None: while True: await asyncio.sleep(60) try: self._stats.flush() except: traceback.print_exc() async def _worker_notify(self) -> None: # Notify relevant `BackendSession`s of status, name, message, media, etc. changes while True: await asyncio.sleep(0.0125) try: self._handle_worklist_notify() except: traceback.print_exc() self._worklist_notify.clear() def _handle_worklist_notify(self) -> None: worklist = self._worklist_notify for bs, sess_id, on_contact_add, old_substatus, updated_phone_info, update_status, update_info_other, for_logout in worklist: user = bs.user detail = user.detail assert detail is not None for ctc in detail.contacts.values(): for bs_other in self._sc.get_sessions_by_user(ctc.head): if bs_other.user is user: continue detail_other = bs_other.user.detail if detail_other is None: continue ctc_me = detail_other.contacts.get(user.uuid) # This shouldn't be `None`, since every contact should have # an `RL` contact on the other users' list (at the very least). if ctc_me is None: continue if not ctc_me.lists & ContactList.FL or _is_blocking(user, ctc.head): continue bs_other.evt.on_presence_notification( ctc_me, on_contact_add, old_substatus, sess_id = sess_id, updated_phone_info = updated_phone_info, update_status = update_status, update_info_other = update_info_other, ) for circle in self.user_service.get_circle_batch(user): if circle.chat_id not in self._cses_by_bs_by_circle_id: continue if bs not in self._cses_by_bs_by_circle_id[circle.chat_id]: continue cs = self._cses_by_bs_by_circle_id[circle.chat_id][bs] assert cs is not None cs.chat.send_participant_status_updated(cs, old_substatus) if not self._sc.is_session_in_collection(bs): for cs_dict in self._cses_by_bs_by_circle_id.values(): cs = cs_dict.pop(bs, None) if cs is not None: cs.close() if for_logout: if not self._sc.get_sessions_by_user(user): user.detail = None async def _worker_notify_self(self) -> None: # Notify relevant `BackendSession`s of status, name, message, media, etc. changes worklist = self._worklist_notify_self while True: await asyncio.sleep(0.0125) try: for bs, old_substatus, update_status, update_info in worklist: user = bs.user for bs_other in self._sc.get_sessions_by_user(user): bs_other.evt.on_presence_self_notification(old_substatus, update_status = update_status, update_info = update_info) except: traceback.print_exc() worklist.clear() class Session(metaclass = ABCMeta): __slots__ = ('closed',) closed: bool def __init__(self) -> None: self.closed = False def close(self, **kwargs: Any) -> None: if self.closed: return self.closed = True self._on_close(**kwargs) @abstractmethod def _on_close(self, **kwargs: Any) -> None: pass class BackendSession(Session): __slots__ = ('backend', 'user', 'client', 'chat_enabled', 'evt', 'front_data') backend: Backend user: User client: Client chat_enabled: bool evt: event.BackendEventHandler front_data: Dict[str, Any] def __init__(self, backend: Backend, user: User, client: Client, evt: event.BackendEventHandler) -> None: super().__init__() self.backend = backend self.user = user self.client = client self.chat_enabled = True self.evt = evt self.front_data = {} def _on_close(self, **kwargs: Any) -> None: if not kwargs.get('passthrough'): self.evt.on_close() self.backend.on_leave(self, sess_id = kwargs.get('sess_id')) def me_update(self, fields: Dict[str, Any]) -> None: user = self.user needs_mark_modified = False needs_notify = False notify_status = False notify_info_other = False notify_self = False updated_phone_info = {} old_substatus = user.status.substatus if 'message' in fields: if fields['message'] is not None: user.status.message = fields['message'] needs_notify = True notify_info_other = True if 'media' in fields: if fields['media'] is not None: user.status.media = fields['media'] needs_notify = True notify_info_other = True if 'name' in fields: old_name = user.status.name if fields['name'] != old_name: user.status.name = fields['name'] needs_mark_modified = True needs_notify = True notify_status = True if 'home_phone' in fields: if fields['home_phone'] is None and 'PHH' in user.settings: del user.settings['PHH'] else: user.settings['PHH'] = fields['home_phone'] needs_mark_modified = True needs_notify = True updated_phone_info['PHH'] = fields['home_phone'] if 'work_phone' in fields: if fields['work_phone'] is None and 'PHW' in user.settings: del user.settings['PHW'] else: user.settings['PHW'] = fields['work_phone'] needs_mark_modified = True needs_notify = True updated_phone_info['PHW'] = fields['work_phone'] if 'mobile_phone' in fields: if fields['mobile_phone'] is None and 'PHM' in user.settings: del user.settings['PHM'] else: user.settings['PHM'] = fields['mobile_phone'] needs_mark_modified = True needs_notify = True updated_phone_info['PHM'] = fields['mobile_phone'] if 'blp' in fields: user.settings['BLP'] = fields['blp'] needs_mark_modified = True needs_notify = True notify_status = True if 'mob' in fields: user.settings['MOB'] = fields['mob'] needs_mark_modified = True needs_notify = True updated_phone_info['MOB'] = fields['mob'] if 'mbe' in fields: user.settings['MBE'] = fields['mbe'] needs_mark_modified = True needs_notify = True updated_phone_info['MBE'] = fields['mbe'] if 'substatus' in fields: if old_substatus is not fields['substatus']: user.status.substatus = fields['substatus'] needs_notify = True notify_status = True if 'needs_notify' in fields: needs_notify = fields['needs_notify'] if 'notify_self' in fields: notify_self = fields['notify_self'] if 'notify_status' in fields: notify_status = fields['notify_status'] if 'notify_info' in fields: notify_info_other = fields['notify_info'] if 'gtc' in fields: user.settings['GTC'] = fields['gtc'] needs_mark_modified = True if 'rlp' in fields: user.settings['RLP'] = fields['rlp'] needs_mark_modified = True if 'mpop' in fields: user.settings['MPOP'] = fields['mpop'] needs_mark_modified = True print(f"[Presence] {user.username} ({user.email})'s status updated: {user.status.substatus}") if user.status.name: print(f"[Presence] Nickname: {user.status.name}") if user.status.media: print(f"[Presence] Media: {user.status.media}") if user.status.message: print(f"[Presence] Personal Status Message: {user.status.message}") if needs_mark_modified: self.backend._mark_modified(user) if needs_notify and not user.status.substatus is Substatus.Offline: self.backend._sync_contact_statuses(user) self.backend._notify_contacts( self, old_substatus, updated_phone_info = updated_phone_info, update_status = notify_status, update_info_other = notify_info_other, ) if notify_self: self.backend._notify_self(self, old_substatus, update_status = notify_status, update_info = notify_info_other) def me_group_add(self, name: str, *, is_favorite: Optional[bool] = None) -> Group: if len(name) > MAX_GROUP_NAME_LENGTH: raise error.GroupNameTooLong() user = self.user detail = user.detail assert detail is not None if name == '(No Group)': raise error.GroupAlreadyExists() groups = detail.get_groups_by_name(name) if groups: raise error.GroupAlreadyExists() group = Group(_gen_group_id(detail), gen_uuid(), name, is_favorite or False) detail.insert_group(group) print(f'[Contacts] {user.username} ({user.email}) has created group {name} (ID: {group.id})') self.backend._mark_modified(user) return group def me_group_remove(self, group_id: str) -> None: if group_id == '0': raise error.CannotRemoveSpecialGroup() user = self.user detail = user.detail assert detail is not None group = detail.get_group_by_id(group_id) if group is None: raise error.GroupDoesNotExist() detail.delete_group(group) for ctc in detail.contacts.values(): ctc.remove_from_group(group) print(f'[Contacts] {user.username} ({user.email}) has deleted group {name} (ID: {group_id})') self.backend._mark_modified(user) def me_group_edit(self, group_id: str, *, new_name: Optional[str] = None, is_favorite: Optional[bool] = None) -> None: user = self.user detail = user.detail assert detail is not None g = detail.get_group_by_id(group_id) if g is None: raise error.GroupDoesNotExist() print(f'[Contacts] {user.username} ({user.email}) has modified group {g.name} (ID: {g.id})') if new_name is not None: if new_name == '(No Group)': raise error.GroupAlreadyExists() if len(new_name) > MAX_GROUP_NAME_LENGTH: raise error.GroupNameTooLong() groups = detail.get_groups_by_name(new_name) for group in groups: if group.name == new_name and group.id != g.id: raise error.GroupAlreadyExists() g.name = new_name print(f'[Contacts] New name: {new_name}') if is_favorite is not None: g.is_favorite = is_favorite print(f'[Contacts] Is favorites') self.backend._mark_modified(user) def me_group_contact_add(self, group_id: str, contact_uuid: str) -> None: if group_id == '0': return user = self.user detail = user.detail assert detail is not None group = detail.get_group_by_id(group_id) if group is None: raise error.GroupDoesNotExist() ctc = detail.contacts.get(contact_uuid) if ctc is None: raise error.ContactDoesNotExist() if ctc.group_in_entry(group): raise error.ContactAlreadyOnContactList() ctc.add_group_to_entry(group) print(f'[Contacts] {user.username} ({user.email}) has added {ctc.head.username} ({ctc.head.email}) to group {group.name} (ID: {group_id})') self.backend._mark_modified(user) def me_group_contact_remove(self, group_id: str, contact_uuid: str) -> None: user = self.user detail = user.detail assert detail is not None ctc = detail.contacts.get(contact_uuid) if ctc is None: raise error.ContactDoesNotExist() if group_id != '0': group = detail.get_group_by_id(group_id) if group is None: raise error.GroupDoesNotExist() ctc.remove_from_group(group) print(f'[Contacts] {user.username} ({user.email}) has removed {ctc.head.username} ({ctc.head.email}) from group {group.name} (ID: {group_id})') self.backend._mark_modified(user) def me_contact_add( self, contact_uuid: str, lst: ContactList, *, trid: Optional[str] = None, name: Optional[str] = None, nickname: Optional[str] = None, message: Optional[TextWithData] = None, group_id: Optional[str] = None, adder_id: Optional[str] = None, needs_notify: bool = False, ) -> Tuple[Contact, User]: assert not lst & ContactList.PL backend = self.backend user = self.user detail = user.detail assert detail is not None old_lists = None ctc_head = backend._load_user_record(contact_uuid) if ctc_head is None: raise error.UserDoesNotExist() user = self.user old_ctc = detail.contacts.get(ctc_head.uuid) if old_ctc is not None: old_lists = old_ctc.lists for lst2 in [ContactList.FL, ContactList.AL, ContactList.BL]: if old_lists & lst2: if len(detail.get_contacts_by_list(lst2)) >= LST_LIMITS[lst2]: raise error.ContactListIsFull() ctc = self._add_to_list(user, ctc_head, lst, name, group_id, nickname = nickname) if lst & ContactList.FL: # FL needs a matching RL on the contact ctc_me = self._add_to_list(ctc_head, user, ContactList.RL, user.email, None) # type: Optional[Contact] # `ctc_head` was added to `user`'s RL for sess_added in backend._sc.get_sessions_by_user(ctc_head): #if sess_added is self: continue if old_ctc is None or (old_lists is not None and not old_lists & ContactList.FL): sess_added.evt.on_added_me(user, message = message, adder_id = adder_id) else: ctc_detail = backend._load_detail(ctc_head) ctc_me = ctc_detail.contacts.get(user.uuid) if ((lst & ContactList.AL or lst & ContactList.BL) and ctc.lists & ContactList.RL) or needs_notify: for sess_added in backend._sc.get_sessions_by_user(ctc_head): if sess_added is self: continue if not ctc_me: continue if not (ctc_me.lists & ContactList.FL): continue backend._sync_contact_statuses(ctc_head) sess_added.evt.on_presence_notification( ctc_me, False, user.status.substatus, send_status_on_bl = (True if lst & ContactList.BL else False), updated_phone_info = { 'PHH': user.settings.get('PHH'), 'PHW': user.settings.get('PHW'), 'PHM': user.settings.get('PHM'), 'MOB': user.settings.get('MOB'), }, ) print(f'[Contacts] {user.username} ({user.email}) has added {ctc.head.username} ({ctc.head.email}) to their contact list') return ctc, ctc_head def me_contact_rename(self, contact_uuid: str, new_name: str) -> None: user = self.user detail = user.detail assert detail is not None ctc = detail.contacts.get(contact_uuid) if ctc is None: raise error.ContactDoesNotExist() if len(new_name) > 387: raise error.NicknameExceedsLengthLimit() ctc.status.name = new_name print(f"[Contacts] {user.username} ({user.email}) has changed {ctc.head.username} ({ctc.head.email})'s friend nickname to {new_name}") self.backend._mark_modified(user) def me_contact_remove(self, contact_uuid: str, lst: ContactList, *, group_id: Optional[str] = None) -> None: backend = self.backend user = self.user detail = user.detail assert detail is not None ctc = detail.contacts.get(contact_uuid) if ctc is None: raise error.ContactDoesNotExist() assert not lst & ContactList.RL try: print(f'[Contacts] {user.username} ({user.email}) has removed {ctc.head.username} ({ctc.head.email}) from their contact list') ctc_new = self._remove_from_list(user, ctc.head, lst, group_id) except Exception as ex: raise ex if lst & ContactList.FL: # Remove matching RL self._remove_from_list(ctc.head, user, ContactList.RL, None) for sess_added in backend._sc.get_sessions_by_user(ctc.head): sess_added.evt.on_removed_me(user) if lst & ContactList.BL: ctc_detail = backend._load_detail(ctc.head) ctc_me = ctc_detail.contacts.get(user.uuid) for sess_added in backend._sc.get_sessions_by_user(ctc.head): if sess_added is self: continue if ctc_me: if ctc_me.lists & ContactList.FL: sess_added.evt.on_presence_notification(ctc_me, False, Substatus.Offline, updated_phone_info = { 'PHH': user.settings.get('PHH'), 'PHW': user.settings.get('PHW'), 'PHM': user.settings.get('PHM'), 'MOB': user.settings.get('MOB'), }) def me_contact_deny(self, adder_uuid: str, deny_message: Optional[str], *, addee_id: Optional[str] = None) -> None: user_adder = self.backend._load_user_record(adder_uuid) if user_adder is None: raise error.UserDoesNotExist() user = self.user print(f"[Contacts] {user.username} ({user.email}) has denied {user_adder.username} ({user_adder.email})'s friend request") print(f"Attached message: {deny_message}") for sess_adder in self.backend._sc.get_sessions_by_user(user_adder): sess_adder.evt.on_contact_request_denied(user, deny_message or '', contact_id = addee_id) def _add_to_list( self, user: User, ctc_head: User, lst: ContactList, name: Optional[str], group_id: Optional[str], *, nickname: Optional[str] = None, ) -> Contact: # Add `ctc` to `user`'s `lst` detail = self.backend._load_detail(user) contacts = detail.contacts updated = False if ctc_head.uuid not in contacts: contacts[ctc_head.uuid] = Contact(ctc_head, set(), ContactList.Empty, UserStatus(name or ctc_head.email), ContactDetail(_gen_contact_id(detail))) updated = True ctc = contacts[ctc_head.uuid] if (ctc.lists & lst) != lst: ctc.lists |= lst if lst == ContactList.RL: ctc.pending = True updated = True else: if lst == ContactList.FL and group_id is not None: if ctc.is_in_group_id(group_id): raise error.ContactAlreadyOnContactList() orig_name = ctc.status.name if name is not None and (ctc.status.name is None or orig_name != name): ctc.status.name = name updated = True orig_nick = ctc.detail.nickname if nickname is not None and (ctc.detail.nickname is None or orig_nick != nickname): ctc.detail.nickname = nickname updated = True if lst == ContactList.FL: if group_id is not None: try: self.me_group_contact_add(group_id, ctc_head.uuid) except Exception as ex: raise ex if updated: self.backend._mark_modified(user, detail = detail) self.backend._sync_contact_statuses(user) return ctc def _remove_from_list(self, user: User, ctc_head: User, lst: ContactList, group_id: Optional[str]) -> Optional[Contact]: # Remove `ctc_head` from `user`'s `lst` detail = self.backend._load_detail(user) contacts = detail.contacts ctc = contacts.get(ctc_head.uuid) if ctc is None: raise error.ContactDoesNotExist() updated = False if ctc.lists & lst: if lst == ContactList.FL and group_id is not None: try: self.me_group_contact_remove(group_id, ctc.head.uuid) except Exception as ex: raise ex updated = True if (lst == ContactList.FL and group_id is None) or not lst & ContactList.FL: ctc.lists &= ~lst if lst == ContactList.FL: ctc._groups = set() if lst == ContactList.RL and ctc.pending: ctc.pending = False updated = True elif lst == ContactList.PL and ctc.pending: ctc.pending = False updated = True else: raise error.ContactNotOnContactList() if not ctc.lists: del contacts[ctc_head.uuid] ctc = None updated = True if updated: self.backend._mark_modified(user, detail = detail) self.backend._sync_contact_statuses(user) return ctc def me_contact_notify_oim(self, uuid: str, oim: OIM) -> None: ctc_head = self.backend._load_user_record(uuid) if ctc_head is None: raise error.UserDoesNotExist() for sess_notify in self.backend._sc.get_sessions_by_user(ctc_head): if sess_notify is self: continue sess_notify.evt.on_oim_sent(oim) def send_invitation_email(self, email, sender_email, sender_name, message) -> None: if (uuid := self.backend.util_get_uuid_from_email(email)) is None: if sender_name: sender_info = f"{sender_name} ({sender_email})" else: sender_info = sender_email invite_msg = f"""\ Subject: {sender_email} wants to talk to you using CrossTalk! To: {email} From: undergr0und network administration This e-mail was sent to you because {sender_info} wants to chat with you using the CrossTalk service, and you don't have an account. CrossTalk is a chat service that supports many messaging clients, and allows them to seamlessly interoperate. You can learn more here: https://crosstalk.im Create an account here: https://crosstalk.im/register """ if message: invite_msg += f"""\ They've attached this message, as well: "{message.decode('utf-8')}" """ print(f"[Mailer] {sender_email} has sent an e-mail invitation to {email}") print(f"[Mailer] Attached message: {invite_msg}") if settings.DEBUG: print(invite_msg) with smtplib.SMTP(settings.SMTP_HOST, settings.SMTP_PORT) as server: server.starttls() server.login(settings.SMTP_USERNAME, settings.SMTP_PASSWORD) server.sendmail("administration@ugnet.gay", email, invite_msg) else: print(f"[Mailer] {sender_email} already has an account. Not sending an invitation e-mail.") def me_create_circle(self, name: str, owner_friendly: str, membership_access: int) -> Circle: user = self.user backend = self.backend chat_id = backend.user_service.create_circle(user, name, owner_friendly, membership_access) circle = backend.user_service.get_circle(chat_id) assert circle is not None backend.chat_create(circle = circle) print(f"[Circle] {user.username} ({user.email}) has created a new Circle '{circle.name} (ID: {circle.chat_id})'") return circle def me_add_user_to_circle(self, circle: Circle, user_other: User) -> None: user = self.user if user_other.uuid in circle.memberships: raise error.MemberAlreadyInCircle() circle.memberships[user_other.uuid] = CircleMembership( circle.chat_id, user_other, CircleRole.Empty, CircleState.Empty, ) print(f"[Circle] {user.username} ({user.email}) has added {user_other.username} ({user_other.email}) to {circle.name} (ID: {circle.chat_id})") self.backend._mark_circle_modified(circle) def me_invite_user_to_circle(self, circle: Circle, invitee: User, *, invite_message: Optional[str] = None) -> None: backend = self.backend inviter = self.user if invitee.uuid not in circle.memberships: raise error.MemberNotInCircle() membership = circle.memberships[invitee.uuid] chat = backend.chat_get('persistent', circle.chat_id) if chat is None: raise error.CircleDoesNotExist() if membership.state not in (CircleState.Rejected,CircleState.Empty): raise error.MemberAlreadyInvitedToCircle() membership.role = CircleRole.StatePendingOutbound membership.state = CircleState.WaitingResponse membership.inviter_uuid = inviter.uuid membership.inviter_email = inviter.email membership.inviter_name = inviter.status.name or inviter.email membership.invite_message = invite_message self.backend._mark_circle_modified(circle) print(f"[Circle] {inviter.username} ({inviter.email}) has invited {invitee.username} ({invitee.email}) to {circle.name} (ID: {circle.chat_id})") if invite_message: print(f"[Circle] Invitation message: {invite_message}") for bs in backend.util_get_sessions_by_user(invitee): bs.evt.on_chat_invite(chat, inviter, circle = True) for cs in chat.get_roster(): cs.bs.evt.on_circle_updated(circle) def me_change_circle_membership( self, circle: Circle, user_other: User, *, role: Optional[CircleRole] = None, state: Optional[CircleState] = None, ) -> None: user = self.user if user_other.uuid not in circle.memberships: raise error.MemberNotInCircle() chat = self.backend.chat_get('persistent', circle.chat_id) if chat is None: raise error.CircleDoesNotExist() membership = circle.memberships[user_other.uuid] old_role = membership.role old_state = membership.state if role is not None: membership.role = role if state is not None: membership.state = state if old_role is not membership.role or old_state is not membership.state: self.backend._mark_circle_modified(circle) def me_accept_circle_invite(self, circle: Circle, *, send_events: bool = True) -> None: user = self.user backend = self.backend if user.uuid not in circle.memberships: raise error.MemberNotInCircle() chat = self.backend.chat_get('persistent', circle.chat_id) if chat is None: raise error.CircleDoesNotExist() membership = circle.memberships[user.uuid] if not (membership.role == CircleRole.StatePendingOutbound and membership.state == CircleState.WaitingResponse): if membership.state == CircleState.Rejected or membership.state == CircleState.Empty: raise error.MemberNotInCircle() raise error.MemberAlreadyInCircle() membership.role = CircleRole.Member membership.state = CircleState.Accepted if membership.invite_message is not None: membership.invite_message = None self.backend._mark_circle_modified(circle) if send_events: for bs_other in backend.util_get_sessions_by_user(user): if bs_other is self: continue bs_other.evt.on_accepted_circle_invite(circle) for cs_other in chat.get_roster(): if cs_other is user: continue cs_other.bs.evt.on_circle_updated(circle) def me_decline_circle_invite(self, circle: Circle, *, send_events: bool = True) -> None: user = self.user backend = self.backend if user.uuid not in circle.memberships: raise error.MemberNotInCircle() chat = self.backend.chat_get('persistent', circle.chat_id) if chat is None: raise error.CircleDoesNotExist() membership = circle.memberships[user.uuid] if not (membership.role == CircleRole.StatePendingOutbound and membership.state == CircleState.WaitingResponse): if membership.state == CircleState.Rejected or membership.state == CircleState.Empty: raise error.MemberNotInCircle() raise error.MemberAlreadyInCircle() membership.role = CircleRole.Member membership.state = CircleState.Rejected self.backend._mark_circle_modified(circle) if send_events: for bs_other in backend.util_get_sessions_by_user(user): if bs_other is self: continue bs_other.evt.on_declined_chat_invite(chat, circle = True) chat.send_participant_declined(user, circle = True) def me_leave_circle(self, circle: Circle) -> None: user = self.user backend = self.backend if user.uuid not in circle.memberships: raise error.MemberNotInCircle() chat = backend.chat_get('persistent', circle.chat_id) if chat is None: raise error.CircleDoesNotExist() membership = circle.memberships[user.uuid] if membership.state == CircleState.Empty: raise error.MemberNotInCircle() other_owners = False if membership.role == CircleRole.Admin: memberships = backend.util_get_circle_memberships_by_role(circle, CircleRole.Admin) if len(list(memberships)) < 2: raise error.CantLeaveCircle() membership.role = CircleRole.Member membership.state = CircleState.Empty if membership.inviter_uuid is not None: membership.inviter_uuid = None if membership.inviter_email is not None: membership.inviter_email = None if membership.inviter_name is not None: membership.inviter_name = None backend._mark_circle_modified(circle) if circle.chat_id in backend._cses_by_bs_by_circle_id: for bs, cs in list(backend._cses_by_bs_by_circle_id[circle.chat_id].items()): if cs is not None and cs.user is user: cs.close() del backend._cses_by_bs_by_circle_id[circle.chat_id][bs] for cs_other in chat.get_roster(): if cs_other.user is not user: cs_other.bs.evt.on_circle_updated(circle) def me_block_circle(self, circle: Circle) -> None: user = self.user if user.uuid not in circle.memberships: raise error.MemberNotInCircle() if not circle.memberships[user.uuid].blocking: circle.memberships[user.uuid].blocking = True self.backend._mark_circle_modified(circle) sess = first_in_iterable(self.backend.util_get_sessions_by_user(user)) if sess is None: return cs = self.backend.get_circle_cs(circle.chat_id, sess) if cs is None: return cs.chat.send_participant_status_updated(cs, user.status.substatus, send_on_bl = True) def me_unblock_circle(self, circle: Circle) -> None: user = self.user if user.uuid not in circle.memberships: raise error.MemberNotInCircle() if circle.memberships[user.uuid].blocking: circle.memberships[user.uuid].blocking = False self.backend._mark_circle_modified(circle) class _SessionCollection: __slots__ = ('_sessions', '_sessions_by_user', '_sess_by_token', '_tokens_by_sess') _sessions: Set[BackendSession] _sessions_by_user: Dict[User, List[BackendSession]] _sess_by_token: Dict[str, BackendSession] _tokens_by_sess: Dict[BackendSession, Set[str]] def __init__(self) -> None: self._sessions = set() self._sessions_by_user = defaultdict(list) self._sess_by_token = {} self._tokens_by_sess = defaultdict(set) def get_sessions_by_user(self, user: User) -> List[BackendSession]: if user not in self._sessions_by_user: return [] return self._sessions_by_user[user] def is_session_in_collection(self, sess: BackendSession) -> bool: return sess in self._sessions def iter_sessions(self) -> Iterable[BackendSession]: yield from self._sessions def set_nc_by_token(self, sess: BackendSession, token: str) -> None: self._sess_by_token[token] = sess self._tokens_by_sess[sess].add(token) self._sessions.add(sess) def get_nc_by_token(self, token: str) -> Optional[BackendSession]: return self._sess_by_token.get(token) def add_session(self, sess: BackendSession) -> None: if sess.user: self._sessions_by_user[sess.user].append(sess) self._sessions.add(sess) def remove_session(self, sess: BackendSession) -> None: if sess in self._tokens_by_sess: tokens = self._tokens_by_sess.pop(sess) for token in tokens: self._sess_by_token.pop(token, None) self._sessions.discard(sess) if sess.user in self._sessions_by_user: self._sessions_by_user[sess.user].remove(sess) class Chat: __slots__ = ('ids', 'backend', 'circle', 'front_data', '_users_by_sess', '_stats') ids: Dict[str, str] backend: Backend circle: Optional[Circle] # TODO: implement the meat of chatrooms chatroom: Optional[Chatroom] front_data: Dict[str, Any] _users_by_sess: Dict['ChatSession', Tuple[User, Optional[str]]] _stats: Any def __init__(self, backend: Backend, stats: Any, *, circle: Optional[Circle] = None, chatroom: Optional[Chatroom] = None) -> None: super().__init__() self.ids = {} self.backend = backend self.circle = circle self.front_data = {} self._users_by_sess = {} self._stats = stats # 31 characters is all WLM 2009 will allow for chat IDs (RNG); otherwise the receiving end won't have the sender's messages display self.add_id('main', GenTokenStr(trim = 31)) if self.circle is None: return assert circle is not None self.add_id('persistent', circle.chat_id) def add_id(self, scope: str, id: str) -> None: assert id not in self.backend._chats_by_id self.ids[scope] = id self.backend._chats_by_id[(scope, id)] = self def join( self, origin: str, bs: BackendSession, evt: event.ChatEventHandler, *, preferred_name: Optional[str] = None, pop_id: Optional[str] = None, ) -> 'ChatSession': primary_pop = True if self.circle is not None: if bs.user.uuid not in self.circle.memberships: raise error.NotAllowedToJoinCircle() for user_other, pop_id_other in self._users_by_sess.values(): if bs.user is user_other: if pop_id_other is not None: if (pop_id is not None and pop_id_other.lower() == pop_id.lower()): raise error.AuthFail() else: if pop_id is not None: raise error.AuthFail() for other_cs in self.get_roster(): primary_pop = True if other_cs.user is bs.user and other_cs.primary_pop: primary_pop = False cs = ChatSession(origin, bs, self, evt, primary_pop, preferred_name = preferred_name) cs.evt.cs = cs self._users_by_sess[cs] = (cs.user, pop_id) cs.evt.on_open() return cs def add_session(self, cs: 'ChatSession', pop_id: Optional[str] = None) -> None: self._users_by_sess[cs] = (cs.user, pop_id) def get_roster(self) -> Iterable['ChatSession']: return self._users_by_sess.keys() def get_roster_single(self) -> Iterable['ChatSession']: sess_per_user = [] # type: List[ChatSession] for cs in self._users_by_sess.keys(): already_in_roster = False for sess1 in sess_per_user: if cs.primary_pop: if sess1.user is cs.user: already_in_roster = True break if not already_in_roster: sess_per_user.append(cs) return sess_per_user def send_update(self) -> None: for cs in self.get_roster(): cs.evt.on_chat_updated() def send_participant_joined(self, cs: 'ChatSession', *, initial_join: bool = False) -> None: tmp = [] for cs_self in self.get_roster(): if cs_self.user is cs.user and cs_self is not cs: tmp.append(cs_self) if len(tmp) > 0: first_pop = False else: first_pop = True if initial_join and self.circle is not None: for cs_single in self.get_roster_single(): if cs_single is cs: continue cs.evt.on_participant_joined(cs_single, cs_single.primary_pop, initial_join) for cs_other in self.get_roster(): if self.circle is not None: if self.circle.memberships[cs.user.uuid].blocking and cs_other.user is not cs.user: continue if cs_other == cs and cs.origin == 'yahoo': continue cs_other.evt.on_participant_joined(cs, first_pop, initial_join) def send_participant_declined( self, user: User, *, user_id: Optional[str] = None, message: Optional[str] = None, circle: bool = False, ) -> None: for cs_other in self.get_roster(): cs_other.evt.on_chat_invite_declined(self, user, invitee_id = user_id, message = message, circle = circle) def send_participant_status_updated( self, cs: 'ChatSession', old_substatus: Substatus, *, initial: bool = False, send_on_bl: bool = False, ) -> None: tmp = [] for cs_self in self.get_roster(): if cs_self.user is cs.user and cs_self is not cs: tmp.append(cs_self) if len(tmp) > 0: first_pop = False else: first_pop = True for cs_other in self.get_roster(): if cs_other == cs and cs.origin == 'yahoo': continue if self.circle is not None: if self.circle.memberships[cs.user.uuid].blocking and cs_other.user is not cs.user and not send_on_bl: continue if cs.bs.user.status.substatus is Substatus.Offline and cs_other.user is cs.user: continue cs_other.evt.on_participant_status_updated(cs, first_pop, initial, old_substatus) def on_leave(self, sess: 'ChatSession') -> None: su = self._users_by_sess.pop(sess, None) if su is None: return last_pop = False if len(self._users_by_sess) == 1 and self.circle is None: _, last_user = next(iter(self._users_by_sess.values())) if last_user == su[0]: for scope_id in self.ids: del self.backend._chats_by_id[scope_id] last_pop = True sess_others = [sess_other for sess_other in self._users_by_sess.keys() if sess_other.user is su[0]] if sess_others: no_primary_pop = True for sess_other in sess_others: if sess_other.primary_pop: no_primary_pop = False break if no_primary_pop: sess_others[-1].primary_pop = True elif last_pop: last_pop = True # Notify others that `sess` has left for sess1, _ in self._users_by_sess.items(): if sess1 is sess: continue sess1.evt.on_participant_left(sess, last_pop) class ChatSession(Session): __slots__ = ('origin', 'user', 'chat', 'bs', 'evt', 'primary_pop', 'front_data', 'preferred_name') origin: Optional[str] user: User chat: Chat bs: BackendSession evt: event.ChatEventHandler primary_pop: bool front_data: Dict[str, Any] preferred_name: Optional[str] def __init__( self, origin: str, bs: BackendSession, chat: Chat, evt: event.ChatEventHandler, primary_pop: bool, *, preferred_name: Optional[str] = None, ) -> None: super().__init__() self.origin = origin self.user = bs.user self.chat = chat self.bs = bs self.evt = evt self.primary_pop = primary_pop self.front_data = {} self.preferred_name = preferred_name def _on_close(self, **kwargs: Any) -> None: self.evt.on_close() self.chat.on_leave(self) def invite(self, invitee: User, *, invite_msg: Optional[str] = None) -> None: already_invited_sessions = [] # type: List[BackendSession] disabled_sessions = [] # type: List[BackendSession] ctc_sessions = self.bs.backend.util_get_sessions_by_user(invitee) roster = list(self.chat.get_roster()) for cs_other in roster: if cs_other.bs in already_invited_sessions: continue for ctc_sess in ctc_sessions: if cs_other.bs == ctc_sess and self.origin != 'yahoo': already_invited_sessions.append(ctc_sess) for ctc_sess in ctc_sessions: if ctc_sess in already_invited_sessions: continue if not ctc_sess.chat_enabled: disabled_sessions.append(ctc_sess) continue print(f"[Chat] {self.user.username} ({self.user.email}) has invited {invitee.username} ({invitee.email}) to chat session '{self.chat.ids}' from origin {self.origin}") if invite_msg is not None: print(f"[Chat] Invite message: {invite_msg}") ctc_sess.evt.on_chat_invite(self.chat, self.user, invite_msg = invite_msg or '') if len(ctc_sessions) == len(disabled_sessions): raise error.ContactNotOnline() if len(ctc_sessions) == len(already_invited_sessions): raise error.ContactAlreadyOnContactList() def send_message_to_everyone(self, data: MessageData) -> None: stats = self.chat._stats client = self.bs.client if stats: stats.on_message_sent(self.user, client) stats.on_user_active(self.user, client) for cs_other in self.chat._users_by_sess.keys(): if cs_other is self: continue if self.chat.circle is not None and self.chat.circle.memberships[cs_other.user.uuid].blocking: continue cs_other.evt.on_message(data) print(f"[Chat] {self.user.username} ({self.user.email}) has sent a messasge to {cs_other.user.username} ({cs_other.user.email}) of type {data.type}") if stats: stats.on_message_received(cs_other.user, client) def send_message_to_user(self, user_uuid: str, data: MessageData) -> None: stats = self.chat._stats client = self.bs.client if stats: stats.on_message_sent(self.user, client) stats.on_user_active(self.user, client) for cs_other in self.chat._users_by_sess.keys(): if cs_other is self: continue if cs_other.user.uuid != user_uuid: continue print(f"[Chat] {self.user.username} ({self.user.email}) has sent a messasge to {cs_other.user.username} ({cs_other.user.email}) of type {data.type}") cs_other.evt.on_message(data) if stats: stats.on_message_received(cs_other.user, client) def _gen_group_id(detail: UserDetail) -> str: id = 1 s = str(id) while s in detail._groups_by_id: id += 1 s = str(id) return s def _gen_contact_id(detail: UserDetail) -> str: id = 2 for i, _ in enumerate(detail.contacts.values()): if i+2 == id: id += 1 continue s = str(id) return s MAX_GROUP_NAME_LENGTH = 61 # TODO: PL LST_LIMITS = { ContactList.FL: 1000, ContactList.AL: 1500, ContactList.BL: 1200, ContactList.RL: 1500, }