diff --git a/docs/api/steam.client.mixins.friends.rst b/docs/api/steam.client.mixins.friends.rst new file mode 100644 index 0000000..c478695 --- /dev/null +++ b/docs/api/steam.client.mixins.friends.rst @@ -0,0 +1,7 @@ +friends +======= + +.. automodule:: steam.client.mixins.friends + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/api/steam.client.mixins.rst b/docs/api/steam.client.mixins.rst new file mode 100644 index 0000000..90944a0 --- /dev/null +++ b/docs/api/steam.client.mixins.rst @@ -0,0 +1,13 @@ +steam.client.mixins +=================== + +List of mixins: + +.. toctree:: + steam.client.mixins.friends + +---- + +.. automodule:: steam.client.mixins + + diff --git a/docs/api/steam.client.rst b/docs/api/steam.client.rst index d8fc019..bfc3de3 100644 --- a/docs/api/steam.client.rst +++ b/docs/api/steam.client.rst @@ -3,6 +3,7 @@ steam.client .. toctree:: steam.client.features + steam.client.mixins steam.client.gc .. automodule:: steam.client diff --git a/steam/__init__.py b/steam/__init__.py index fc37979..ea870d0 100644 --- a/steam/__init__.py +++ b/steam/__init__.py @@ -12,4 +12,13 @@ from steam.webapi import WebAPI class SteamClient(object): def __new__(cls, *args, **kwargs): from steam.client import SteamClient as SC + + bases = cls.__bases__ + + if bases != (object, ): + if bases[0] != SteamClient: + raise ValueError("SteamClient needs to be the first base for custom classes") + + SC = type("SteamClient", (SC,) + bases[1:], {}) + return SC(*args, **kwargs) diff --git a/steam/client/features/misc.py b/steam/client/features/misc.py index 48d0c59..c12f33c 100644 --- a/steam/client/features/misc.py +++ b/steam/client/features/misc.py @@ -5,8 +5,8 @@ from steam.core.msg import MsgProto from steam.enums.emsg import EMsg class Misc(object): - def __init__(self): - super(Misc, self).__init__() + def __init__(self, *args, **kwargs): + super(Misc, self).__init__(*args, **kwargs) def games_played(self, app_ids): """ diff --git a/steam/client/features/user.py b/steam/client/features/user.py index 479b56d..a588498 100644 --- a/steam/client/features/user.py +++ b/steam/client/features/user.py @@ -3,8 +3,8 @@ from steam.enums.emsg import EMsg from steam.core.msg import MsgProto class User(object): - def __init__(self): - super(User, self).__init__() + def __init__(self, *args, **kwargs): + super(User, self).__init__(*args, **kwargs) def set_persona(self, state=None, name=None): """ diff --git a/steam/client/features/web.py b/steam/client/features/web.py index 3e3042f..08d898a 100644 --- a/steam/client/features/web.py +++ b/steam/client/features/web.py @@ -9,8 +9,8 @@ from steam.util.web import make_requests_session class Web(object): - def __init__(self): - super(Web, self).__init__() + def __init__(self, *args, **kwargs): + super(Web, self).__init__(*args, **kwargs) def get_web_session_cookies(self): """ diff --git a/steam/client/mixins/__init__.py b/steam/client/mixins/__init__.py new file mode 100644 index 0000000..4820bc9 --- /dev/null +++ b/steam/client/mixins/__init__.py @@ -0,0 +1,50 @@ +""" +All optional features are available as mixins for :class:`steam.client.SteamClient`. +Using this approach the client can remain light yet flexible. +Functionallity can be added though inheritance depending on the use case. + + +Here is quick example of how to use one of the available mixins. + +.. code:: python + + from steam import SteamClient + from stema.client.mixins.friends import Friends + + + class MySteamClient(SteamClient, Friends): + pass + + client = MySteamClient() + + + +Making custom mixing is just as simple. + +.. warning:: + Take care not to override existing methods or properties, otherwise bad things will happen + +.. note:: + To avoid name collisions of non-public variables and methods, see `Private Variables `_ + +.. code:: python + + class MyMixin(object): + def __init__(*args, **kwargs): + super(MyMixin, self).__init__(*args, **kwargs) + + self.my_property = 42 + + def my_method(self) + pass + + + class MySteamClient(SteamClient, Friends, MyMixin): + pass + + client = MySteamClient() + + >>> client.my_property + 42 + +""" diff --git a/steam/client/mixins/friends.py b/steam/client/mixins/friends.py new file mode 100644 index 0000000..2af3c6d --- /dev/null +++ b/steam/client/mixins/friends.py @@ -0,0 +1,232 @@ +import logging +from eventemitter import EventEmitter +from steam.steamid import SteamID, intBase +from steam.enums import EResult, EFriendRelationship, EPersonaState +from steam.enums.emsg import EMsg +from steam.core.msg import MsgProto + + +class Friends(object): + def __init__(self, *args, **kwargs): + super(Friends, self).__init__(*args, **kwargs) + + #: SteamFriendlist instance + self.friends = SteamFriendlist(self) + +class SteamFriendlist(EventEmitter): + _LOG = logging.getLogger('SteamClient.friends') + + def __init__(self, client): + self._fr = {} + self._steam = client + + self._steam.on(EMsg.ClientAddFriendResponse, self._handle_add_friend_result) + self._steam.on(EMsg.ClientFriendsList, self._handle_friends_list) + self._steam.on(EMsg.ClientPersonaState, self._handle_persona_state) + + def emit(self, event, *args): + if event is not None: + self._LOG.debug("Emit event: %s" % repr(event)) + EventEmitter.emit(self, event, *args) + + def _handle_add_friend_result(self, message): + eresult = EResult(message.body.eresult) + steam_id = SteamID(message.body.steam_id_added) + self.emit("friend_add_result", eresult, steam_id) + + def _handle_friends_list(self, message): + incremental = message.body.bincremental + if incremental == False: + self._fr.clear() + + pstate_check = set() + + # update internal friends list + for friend in message.body.friends: + steamid = friend.ulfriendid + rel = EFriendRelationship(friend.efriendrelationship) + + if steamid not in self._fr: + suser = SteamUser(steamid, rel) + self._fr[suser] = suser + + if rel in (2,4): + if incremental == False: + pstate_check.add(steamid) + + if rel == EFriendRelationship.RequestRecipient: + self.emit('friend_invite', suser) + else: + oldrel = self._fr[steamid]._data['relationship'] + self._fr[steamid]._data['relationship'] = rel + + if rel == EFriendRelationship.No: + self.emit('friend_removed', self._fr.pop(steamid)) + elif oldrel in (2,4) and rel == EFriendRelationship.Friend: + self.emit('friend_new', self._fr[steamid]) + + # request persona state for any new entries + if pstate_check: + m = MsgProto(EMsg.ClientRequestFriendData) + m.body.persona_state_requested = 4294967295 # request all possible flags + m.body.friends.extend(pstate_check) + self._steam.send(m) + + def _handle_persona_state(self, message): + for friend in message.body.friends: + steamid = friend.friendid + + if steamid == self._steam.steam_id: + continue + + if steamid in self._fr: + self._fr[steamid]._data['pstate'] = friend + + def __repr__(self): + return "<%s %d users>" % ( + self.__class__.__name__, + len(self._fr), + ) + + def __len__(self): + return len(self._fr) + + def __iter__(self): + return iter(self._fr) + + def __list__(self): + return list(self._fr) + + def __getitem__(self, key): + return self._fr[key] + + def __contains__(self, friend): + return friend in self._fr + + def add(self, steamid_or_accountname_or_email): + """ + Add/Accept a steam user to be your friend. + When someone sends you an invite, use this method to accept it. + + :param steamid_or_accountname_or_email: steamid, account name, or email + :type steamid_or_accountname_or_email: :class:`int`, :class:`steam.steamid.SteamID`, :class:`SteamUser`, :class:`str` + + .. note:: + Adding by email doesn't not work. It's only mentioned for the sake of completeness. + """ + m = MsgProto(EMsg.ClientAddFriend) + + if isinstance(steamid_or_accountname_or_email, (intBase, int)): + m.body.steamid_to_add = steamid_or_accountname_or_email + else: + m.body.accountname_or_email_to_add = steamid_or_accountname_or_email + + self._steam.send(m) + + def remove(self, steamid): + """ + Remove a friend + + :param steamid: their steamid + :type steamid: :class:`int`, :class:`steam.steamid.SteamID`, :class:`SteamUser` + """ + m = MsgProto(EMsg.ClientRemoveFriend) + m.body.friendid = steamid + self._steam.send(m) + +class SteamUser(intBase): + def __new__(cls, steam64, *args, **kwargs): + return super(SteamUser, cls).__new__(cls, steam64) + + def __init__(self, steam64, rel): + self._data = { + 'relationship': EFriendRelationship(rel) + } + + @property + def steamid(self): + """SteamID instance + + :rtype: :class:`steam.steamid.SteamID` + """ + return SteamID(int(self)) + + @property + def relationship(self): + """Current relationship with the steam user + + :rtype: :class:`steam.enums.common.EFriendRelationship` + """ + return self._data['relationship'] + + def get_persona_state_value(self, field_name): + """Get a value for field in ``CMsgClientPersonaState.Friend`` + + :param field_name: see example list below + :type field_name: :class:`str` + :return: value for the field, or ``None`` if not available + :rtype: :class:`None`, :class:`int`, :class:`str`, :class:`bytes`, :class:`bool` + + Examples: + - game_played_app_id + - last_logoff + - last_logon + - game_name + - avatar_hash + - facebook_id + ... + + """ + pstate = self._data.get('pstate', None) + if pstate is None or not pstate.HasField(field_name): + return None + return getattr(pstate, field_name) + + @property + def name(self): + """Name of the steam user, or ``None`` if it's not available + + :rtype: :class:`str`, :class:`None` + """ + return self.get_persona_state_value('player_name') + + @property + def state(self): + """State of the steam user + + :rtype: :class:`steam.enums.common.EPersonaState` + """ + state = self.get_persona_state_value('persona_state') + if state: + return EPersonaState(state) + return EPersonaState.Offline + + def get_avatar_url(self, size=2): + """Get url to the steam avatar + + :param size: possible values are ``0``, ``1``, or ``2`` corresponding to small, medium, large + :type size: :class:`int` + :return: url to avatar + :rtype: :class:`str` + """ + ahash = self.get_persona_state_value('avatar_hash') + if ahash is None: + return None + + sizes = { + 0: '', + 1: '_medium', + 2: '_full', + } + url = "http://cdn.akamai.steamstatic.com/steamcommunity/public/images/avatars/%s/%s%s.jpg" + ahash = binascii.hexlify(persona_state_value.avatar_hash).decode('ascii') + + return url % (ahash[:2], ahash, sizes[size]) + + def __repr__(self): + return "<%s (%s) %s %s>" % ( + self.__class__.__name__, + int(self) if self.name is None else repr(self.name), + self.relationship.name, + self.state.name, + ) diff --git a/steam/enums/common.py b/steam/enums/common.py index 8218d6a..3dee805 100644 --- a/steam/enums/common.py +++ b/steam/enums/common.py @@ -1,14 +1,5 @@ from steam.enums.base import SteamIntEnum -__all__ = [ - 'EResult', - 'EUniverse', - 'EType', - 'EServerType', - 'EOSType', - 'EPersonaState', - ] - class EResult(SteamIntEnum): Invalid = 0 @@ -269,3 +260,95 @@ class EPersonaState(SteamIntEnum): LookingToTrade = 5 LookingToPlay = 6 Max = 7 + + +class EFriendRelationship(SteamIntEnum): + No = 0 + Blocked = 1 + RequestRecipient = 2 + Friend = 3 + RequestInitiator = 4 + Ignored = 5 + IgnoredFriend = 6 + SuggestedFriend = 7 + Max = 8 + + +class EAccountFlags(SteamIntEnum): + NormalUser = 0 + PersonaNameSet = 1 + Unbannable = 2 + PasswordSet = 4 + Support = 8 + Admin = 16 + Supervisor = 32 + AppEditor = 64 + HWIDSet = 128 + PersonalQASet = 256 + VacBeta = 512 + Debug = 1024 + Disabled = 2048 + LimitedUser = 4096 + LimitedUserForce = 8192 + EmailValidated = 16384 + MarketingTreatment = 32768 + OGGInviteOptOut = 65536 + ForcePasswordChange = 131072 + ForceEmailVerification = 262144 + LogonExtraSecurity = 524288 + LogonExtraSecurityDisabled = 1048576 + Steam2MigrationComplete = 2097152 + NeedLogs = 4194304 + Lockdown = 8388608 + MasterAppEditor = 16777216 + BannedFromWebAPI = 33554432 + ClansOnlyFromFriends = 67108864 + GlobalModerator = 134217728 + +class EFriendFlags(SteamIntEnum): + No = 0 + Blocked = 1 + FriendshipRequested = 2 + Immediate = 4 + ClanMember = 8 + OnGameServer = 16 + RequestingFriendship = 128 + RequestingInfo = 256 + Ignored = 512 + IgnoredFriend = 1024 + Suggested = 2048 + FlagAll = 65535 + + +class EPersonaStateFlag(SteamIntEnum): + HasRichPresence = 1 + InJoinableGame = 2 + OnlineUsingWeb = 256 + OnlineUsingMobile = 512 + OnlineUsingBigPicture = 1024 + + +class EClientPersonaStateFlag(SteamIntEnum): + Status = 1 + PlayerName = 2 + QueryPort = 4 + SourceID = 8 + Presence = 16 + Metadata = 32 + LastSeen = 64 + ClanInfo = 128 + GameExtraInfo = 256 + GameDataBlob = 512 + ClanTag = 1024 + Facebook = 2048 + + +# Do not remove +from sys import modules +from enum import EnumMeta + +__all__ = map(lambda y: y.__name__, + filter(lambda x: x.__class__ is EnumMeta, modules[__name__].__dict__.values()), + ) + +del modules, EnumMeta