From 70fa89cc09caf64315e7a1a8c7a27cc5301005f0 Mon Sep 17 00:00:00 2001
From: Rossen Georgiev <rossen@rgp.io>
Date: Sat, 23 Apr 2016 14:23:28 +0100
Subject: [PATCH] added support for mixins + friends mixin

---
 docs/api/steam.client.mixins.friends.rst |   7 +
 docs/api/steam.client.mixins.rst         |  13 ++
 docs/api/steam.client.rst                |   1 +
 steam/__init__.py                        |   9 +
 steam/client/features/misc.py            |   4 +-
 steam/client/features/user.py            |   4 +-
 steam/client/features/web.py             |   4 +-
 steam/client/mixins/__init__.py          |  50 +++++
 steam/client/mixins/friends.py           | 232 +++++++++++++++++++++++
 steam/enums/common.py                    | 101 +++++++++-
 10 files changed, 410 insertions(+), 15 deletions(-)
 create mode 100644 docs/api/steam.client.mixins.friends.rst
 create mode 100644 docs/api/steam.client.mixins.rst
 create mode 100644 steam/client/mixins/__init__.py
 create mode 100644 steam/client/mixins/friends.py

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 <https://docs.python.org/2/tutorial/classes.html#private-variables-and-class-local-references>`_
+
+.. 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