Files
Athena Funderburg 4b463a3432 init
2026-05-25 07:05:17 +00:00

657 lines
18 KiB
Python

from datetime import datetime
from typing import Dict, Optional, Set, List, Tuple, Any, TypeVar
from enum import Enum, IntEnum, IntFlag
class User:
__slots__ = (
'id',
'uuid',
'email',
'username',
'first_name',
'last_name',
'uin',
'verified_to_login',
'account_verified',
'alias_active',
'status',
'detail',
'settings',
'date_created',
'date_login',
'suspended',
'is_tester',
'is_mvp',
'show_in_dir',
'evil_permanent',
'evil_temporary',
'avatar',
'profile'
)
id: int
uuid: str
email: str
username: str
first_name: str
last_name: str
uin: int
verified_to_login: bool
account_verified: bool
alias_active: bool
status: 'UserStatus'
detail: Optional['UserDetail']
settings: Dict[str, Any]
date_created: datetime
date_login: datetime
suspended: bool
is_tester: bool
is_mvp: bool
show_in_dir: bool
evil_permanent: int
evil_temporary: int
avatar: str
profile: 'UserProfile'
def __init__(
self, id: int, uuid: str, email: str, username: str, first_name: str, last_name: str, uin: int, verified_to_login: bool, account_verified: bool, alias_active: bool, status: 'UserStatus',
settings: Dict[str, Any], date_created: datetime, date_login: datetime, suspended: bool, is_tester: bool, is_mvp: bool, show_in_dir: bool, evil_permanent: int, evil_temporary: int, avatar: str, profile: 'UserProfile'
) -> None:
self.id = id
self.uuid = uuid
self.email = email
self.username = username
self.first_name = first_name
self.last_name = last_name
self.uin = uin
self.verified_to_login = verified_to_login
self.account_verified = account_verified
self.alias_active = alias_active
# `status`: true status of user
self.status = status
self.detail = None
self.settings = settings
self.date_created = date_created
self.date_login = date_login
self.suspended = suspended
self.is_tester = is_tester
self.is_mvp = is_mvp
self.show_in_dir = show_in_dir
self.evil_permanent = evil_permanent
self.evil_temporary = evil_temporary
self.avatar = avatar
self.profile = profile
class Contact:
__slots__ = ('head', '_groups', 'lists', 'status', 'is_messenger_user', 'pending', 'detail')
head: User
_groups: Set['ContactGroupEntry']
lists: 'ContactList'
status: 'UserStatus'
is_messenger_user: bool
pending: bool
detail: 'ContactDetail'
def __init__(
self, user: User, groups: Set['ContactGroupEntry'], lists: 'ContactList', status: 'UserStatus', detail: 'ContactDetail', *,
is_messenger_user: Optional[bool] = None, pending: Optional[bool] = None,
) -> None:
self.head = user
self._groups = groups
self.lists = lists
# `status`: status as known by the contact
self.status = status
self.is_messenger_user = _default_if_none(is_messenger_user, True)
self.pending = _default_if_none(pending, False)
self.detail = detail
def compute_visible_status(self, to_user: User) -> None:
# Set Contact.status based on BLP and Contact.lists
# If not blocked, Contact.status == Contact.head.status
if self.head.detail is None or _is_blocking(self.head, to_user):
self.status.substatus = Substatus.Offline
return
true_status = self.head.status
self.status.substatus = true_status.substatus
self.status.name = true_status.name
self.status.message = true_status.message
self.status.media = true_status.media
def is_in_group_id(self, group_id: str) -> bool:
for group in self._groups:
if group.id == group_id:
return True
return False
def group_in_entry(self, grp: 'Group') -> bool:
for group in self._groups:
if group.id == grp.id or group.uuid == grp.uuid:
return True
return False
def add_group_to_entry(self, grp: 'Group') -> None:
self._groups.add(ContactGroupEntry(
self.head.uuid, grp.id, grp.uuid,
))
def remove_from_group(self, grp: 'Group') -> None:
found_group = None
for group in self._groups:
if group.id == grp.id or group.uuid == grp.uuid:
found_group = group
break
if found_group is not None:
self._groups.discard(group)
def _is_blocking(blocker: User, blockee: User) -> bool:
detail = blocker.detail
assert detail is not None
contact = detail.contacts.get(blockee.uuid)
lists = (contact and contact.lists or 0)
if lists & ContactList.BL: return True
if lists & ContactList.AL: return False
return (blocker.settings.get('BLP', 'AL') == 'BL')
class ContactDetail:
__slots__ = (
'index_id', 'birthdate', 'anniversary', 'notes', 'first_name', 'middle_name', 'last_name',
'nickname', 'primary_email_type', 'personal_email', 'work_email', 'im_email', 'other_email',
'home_phone', 'work_phone', 'fax_phone', 'pager_phone', 'mobile_phone', 'other_phone',
'personal_website', 'business_website', 'locations',
)
index_id: str
birthdate: Optional[datetime]
anniversary: Optional[datetime]
notes: Optional[str]
first_name: Optional[str]
middle_name: Optional[str]
last_name: Optional[str]
nickname: Optional[str]
primary_email_type: Optional[str]
personal_email: Optional[str]
work_email: Optional[str]
im_email: Optional[str]
other_email: Optional[str]
home_phone: Optional[str]
work_phone: Optional[str]
fax_phone: Optional[str]
pager_phone: Optional[str]
mobile_phone: Optional[str]
other_phone: Optional[str]
personal_website: Optional[str]
business_website: Optional[str]
locations: Dict[str, 'ContactLocation']
def __init__(
self, index_id: str, *, birthdate: Optional[datetime] = None, anniversary: Optional[datetime] = None,
notes: Optional[str] = None, first_name: Optional[str] = None, middle_name: Optional[str] = None,
last_name: Optional[str] = None, nickname: Optional[str] = None, primary_email_type: Optional[str] = None,
personal_email: Optional[str] = None, work_email: Optional[str] = None, im_email: Optional[str] = None,
other_email: Optional[str] = None, home_phone: Optional[str] = None, work_phone: Optional[str] = None,
fax_phone: Optional[str] = None, pager_phone: Optional[str] = None, mobile_phone: Optional[str] = None,
other_phone: Optional[str] = None, personal_website: Optional[str] = None, business_website: Optional[str] = None,
):
self.index_id = index_id
self.birthdate = birthdate
self.anniversary = anniversary
self.notes = notes
self.first_name = first_name
self.middle_name = middle_name
self.last_name = last_name
self.nickname = nickname
self.primary_email_type = primary_email_type
self.personal_email = personal_email
self.work_email = work_email
self.im_email = im_email
self.other_email = other_email
self.home_phone = home_phone
self.work_phone = work_phone
self.fax_phone = fax_phone
self.pager_phone = pager_phone
self.mobile_phone = mobile_phone
self.other_phone = other_phone
self.personal_website = personal_website
self.business_website = business_website
self.locations = {}
class ContactGroupEntry:
__slots__ = ('contact_uuid', 'id', 'uuid')
contact_uuid: str
id: str
uuid: str
def __init__(self, contact_uuid: str, id: str, uuid: str) -> None:
self.contact_uuid = contact_uuid
self.id = id
self.uuid = uuid
class ContactLocation:
__slots__ = ('type', 'name', 'street', 'city', 'state', 'country', 'zip_code')
type: str
name: Optional[str]
street: Optional[str]
city: Optional[str]
state: Optional[str]
country: Optional[str]
zip_code: Optional[str]
def __init__(
self, type: str, *, name: Optional[str] = None, street: Optional[str] = None, city: Optional[str] = None,
state: Optional[str] = None, country: Optional[str] = None, zip_code: Optional[str] = None,
) -> None:
self.type = type
self.name = name
self.street = street
self.city = city
self.state = state
self.country = country
self.zip_code = zip_code
class UserProfile:
__slots__ = ('user_id', 'bio', 'pronouns', 'website', 'socials', 'streetaddr', 'city', 'state', 'zip', 'country', 'interests', 'visibility')
user_id: User
bio: Optional[str]
pronouns: Optional[str]
website: Optional[str]
socials: Optional[Dict[str, Any]]
streetaddr: Optional[str]
city: Optional[str]
state: Optional[str]
zip: Optional[int]
country: Optional[str]
interests: Optional[Dict[str, Any]]
visibility: Optional[str]
def __init__(
self, user_id: User, bio: Optional[str], pronouns: Optional[str], website: Optional[str],
socials: Optional[Dict[str, Any]], streetaddr: Optional[str], city: Optional[str], state: Optional[str],
zip: Optional[int], country: Optional[str], interests: Optional[Dict[str, Any]], visibility: Optional[str]
):
self.user_id = user_id
self.bio = bio
self.pronouns = pronouns
self.website = website
self.socials = socials
self.streetaddr = streetaddr
self.city = city
self.state = state
self.zip = zip
self.country = country
self.interests = interests
self.visibility = visibility
class UserStatus:
__slots__ = ('substatus', 'name', 'message', 'media')
substatus: 'Substatus'
name: str
message: str
media: Optional[Any]
def __init__(self, name: str) -> None:
self.substatus = Substatus.Offline
self.name = name
self.message = ''
self.media = None
def is_offlineish(self) -> bool:
return self.substatus is Substatus.Offline or self.substatus is Substatus.Invisible
class UserDetail:
__slots__ = ('_groups_by_id', '_groups_by_uuid', 'contacts')
_groups_by_id: Dict[str, 'Group']
_groups_by_uuid: Dict[str, 'Group']
contacts: Dict[str, 'Contact']
def __init__(self) -> None:
self._groups_by_id = {}
self._groups_by_uuid = {}
self.contacts = {}
def get_contacts_by_list(self, lst: 'ContactList') -> Tuple[Contact, ...]:
return tuple([ctc for ctc in self.contacts.values() if ctc.lists & lst])
def insert_group(self, grp: 'Group') -> None:
self._groups_by_id[grp.id] = grp
self._groups_by_uuid[grp.uuid] = grp
def get_group_by_id(self, id: str) -> Optional['Group']:
group = None
group = self._groups_by_id.get(id)
if group is None:
group = self._groups_by_uuid.get(id)
return group
def get_groups_by_name(self, name: str) -> List['Group']:
groups = [] # type: ContactList[Group]
for group in self._groups_by_id.values():
if group.name == name:
if group not in groups: groups.append(group)
for group in self._groups_by_uuid.values():
if group.name == name:
if group not in groups: groups.append(group)
return groups
def delete_group(self, grp: 'Group') -> None:
if grp.id in self._groups_by_id:
del self._groups_by_id[grp.id]
if grp.uuid in self._groups_by_uuid:
del self._groups_by_uuid[grp.uuid]
class Group:
__slots__ = ('id', 'uuid', 'name', 'is_favorite')
id: str
uuid: str
name: str
is_favorite: bool
def __init__(self, id: str, uuid: str, name: str, is_favorite: bool) -> None:
self.id = id
self.uuid = uuid
self.name = name
self.is_favorite = is_favorite
class MessageType(Enum):
Chat = object()
Nudge = object()
Typing = object()
TypingDone = object()
Webcam = object()
Ink = object()
MSNP2P = object()
MSNP2PInvite = object()
MSNP2PTransBody = object()
Emoticon = object()
Ignored = object()
Other = object()
class MessageData:
__slots__ = ('sender', 'sender_pop_id', 'type', 'text', 'front_cache')
sender: User
sender_pop_id: Optional[str]
type: MessageType
text: Optional[str]
front_cache: Dict[str, Any]
def __init__(self, *, sender: User, sender_pop_id: Optional[str] = None, type: MessageType, text: Optional[str] = None) -> None:
self.sender = sender
self.sender_pop_id = sender_pop_id
self.type = type
self.text = text
self.front_cache = {}
class TextWithData:
__slots__ = ('text', 'yahoo_utf8')
text: str
yahoo_utf8: Any
def __init__(self, text: str, yahoo_utf8: Any) -> None:
self.text = text
self.yahoo_utf8 = yahoo_utf8
class RoamingInfo:
__slots__ = ('name', 'name_last_modified', 'message', 'message_last_modified')
name: Optional[str]
name_last_modified: datetime
message: Optional[str]
message_last_modified: datetime
def __init__(self, name: Optional[str], name_last_modified: datetime, message: Optional[str], message_last_modified: datetime) -> None: #, avatar: Optional[str]) -> None:
self.name = name
self.name_last_modified = name_last_modified
self.message = message
self.message_last_modified = message_last_modified
class Circle:
__slots__ = (
'chat_id', 'name', 'owner_id', 'owner_uuid', 'owner_friendly', 'membership_access',
'request_membership_option', 'memberships',
)
chat_id: str
name: str
owner_id: int
owner_uuid: str
owner_friendly: str
membership_access: int
request_membership_option: int
memberships: Dict[str, 'CircleMembership']
def __init__(
self, chat_id: str, name: str, owner_id: int, owner_uuid: str, owner_friendly: str,
membership_access: int, request_membership_option: int,
) -> None:
self.chat_id = chat_id
self.name = name
self.owner_id = owner_id
self.owner_uuid = owner_uuid
self.owner_friendly = owner_friendly
self.membership_access = membership_access
self.request_membership_option = request_membership_option
self.memberships = {}
class Chatroom:
__slots__ = (
'chat_id', 'name', 'owner', 'topic', 'role', 'is_public'
)
chat_id: str
name: str
owner: User
topic: Optional[str]
role: 'ChatroomRole' # will be used for setting IRC user modes, etc
is_public: bool
def __init__(
self, chat_id: str, name: str, owner: User, role: 'ChatroomRole', topic: Optional[str] = None, is_public: bool = False
):
self.chat_id = chat_id
self.name = name
self.owner = owner
self.topic = topic
self.role = role
self.is_public = is_public
class CircleMembership:
__slots__ = (
'chat_id', 'head', 'role', 'state', 'blocking', 'inviter_uuid', 'inviter_email', 'inviter_name', 'invite_message',
)
chat_id: str
head: User
role: 'CircleRole'
state: 'CircleState'
blocking: bool
inviter_uuid: Optional[str]
inviter_email: Optional[str]
inviter_name: Optional[str]
invite_message: Optional[str]
def __init__(
self, chat_id: str, head: User, role: 'CircleRole', state: 'CircleState', *,
blocking: bool = False, inviter_uuid: Optional[str] = None, inviter_email: Optional[str] = None,
inviter_name: Optional[str] = None, invite_message: Optional[str] = None,
):
self.chat_id = chat_id
self.head = head
self.role = role
self.state = state
self.blocking = blocking
self.inviter_uuid = inviter_uuid
self.inviter_email = inviter_email
self.inviter_name = inviter_name
self.invite_message = invite_message
class OIM:
__slots__ = (
'uuid', 'run_id', 'from_email', 'from_username', 'from_friendly', 'from_friendly_encoding', 'from_friendly_charset',
'from_user_id', 'to_email', 'sent', 'origin_ip', 'oim_proxy', 'headers', 'message', 'utf8',
)
uuid: str
run_id: str
from_email: str
from_username: str
from_friendly: str
from_friendly_encoding: str
from_friendly_charset: str
from_user_id: Optional[str]
to_email: str
sent: datetime
origin_ip: Optional[str]
oim_proxy: Optional[str]
headers: Dict[str, str]
message: str
utf8: bool
def __init__(
self, uuid: str, run_id: str, from_email: str, from_username: str, from_friendly: str, to_email: str, sent: datetime,
message: str, utf8: bool, *, headers: Optional[Dict[str, str]] = None, from_friendly_encoding: Optional[str] = None,
from_friendly_charset: Optional[str] = None, from_user_id: Optional[str] = None, origin_ip: Optional[str] = None,
oim_proxy: Optional[str] = None,
) -> None:
self.uuid = uuid
self.run_id = run_id
self.from_email = from_email
self.from_username = from_username
self.from_friendly = from_friendly
self.from_friendly_encoding = _default_if_none(from_friendly_encoding, 'B')
self.from_friendly_charset = _default_if_none(from_friendly_charset, 'utf-8')
self.from_user_id = from_user_id
self.to_email = to_email
self.sent = sent
self.origin_ip = origin_ip
self.oim_proxy = oim_proxy
self.headers = _default_if_none(headers, {})
self.message = message
self.utf8 = utf8
T = TypeVar('T')
def _default_if_none(x: Optional[T], default: T) -> T:
if x is None: return default
return x
class Substatus(Enum):
Offline = object()
Online = object()
Busy = object()
Idle = object()
BRB = object()
Away = object()
OnPhone = object()
OutToLunch = object()
Invisible = object()
NotAtHome = object()
NotAtDesk = object()
NotInOffice = object()
OnVacation = object()
SteppedOut = object()
def is_offlineish(self) -> bool:
return self is Substatus.Offline or self is Substatus.Invisible
# TODO: Put this in the ContactList class
MembershipLabels = {
# From further discovery, `FL` isn't used officially in any of the membership SOAPs. Skip to `AL`.
0x02: "Allow",
0x04: "Block",
0x08: "Reverse",
0x10: "Pending"
}
class ContactList(IntFlag):
Empty = 0x00
FL = 0x01
AL = 0x02
BL = 0x04
RL = 0x08
PL = 0x10
def __init__(self, id: int) -> None:
super().__init__()
self.label = MembershipLabels.get(id, "Undefined")
@classmethod
def Parse(cls, label: str) -> Optional['ContactList']:
if not hasattr(cls, '_MAP'):
label_map = {v.lower(): k for k, v in MembershipLabels.items()}
setattr(cls, '_MAP', label_map)
return cls._MAP.get(label.lower())
class NetworkID(IntEnum):
WINDOWS_LIVE = 0x01
OFFICE_COMMUNICATOR = 0x02 # E-mail in Skype too
ALIAS = 0x03
TELEPHONE = 0x04
DOMAIN = 0x05
SINK = 0x06
CONTACT = 0x07
MNI = 0x08 # Skype + Mobile Network Interop, used by Vodafone
CIRCLE = 0x09
TEMPORARYGROUP = 0x0A
CID = 0x0B
APPID = 0x0C
CONNECTUSER = 0x0D
CONNECTNETWORKS = 0x0E
SMTP = 0x10 # Jaguire, Japanese mobile interop
LIVEIDSINK = 0x11
MULTICAST = 0x12
THREAD = 0x13 # https://github.com/msndevs/protocol-docs/wiki/Threads-(Groupchats)
SHORTCIRCUIT = 0x14 # Unknown what this was used for
ONETOONETEXT = 0x15
GROUPTEXT = 0x16
BOT = 0x1C
YAHOO = 0x20
PUBSUBTOPIC = 0x21
PUBSUBSUBSCRIBER = 0x22
WNSSID = 0x23
class CircleRole(IntEnum):
Empty = 0
Admin = 1
AssistantAdmin = 2
Member = 3
StatePendingOutbound = 4
class ChatroomRole(IntEnum):
Member = 0
RoomOwner = 1
Admin = 2
Moderator = 3
Bot = 4 # maybe add user flags service-wide instead of doing this?
class CircleState(IntEnum):
Empty = 0
WaitingResponse = 1
Left = 2
Accepted = 3
Rejected = 4
class RelationshipType(IntEnum):
Circle = 5
class Service:
__slots__ = ('host', 'port')
host: str
port: int
def __init__(self, host: str, port: int) -> None:
self.host = host
self.port = port
class LoginOption(Enum):
BootOthers = object()
NotifyOthers = object()
Duplicate = object()