Browse Source

Reworked SteamUser and SteamFriends

* SteamUser is now seperated from SteamFriends, which marely uses it
* added get_user() method which provides instances of SteamUser
* SteamUser instances are weakly cached
* persona state is updated automatically
pull/41/head
Rossen Georgiev 9 years ago
parent
commit
f78c8a0e35
  1. 1
      docs/api/steam.client.rst
  2. 7
      docs/api/steam.client.user.rst
  3. 53
      steam/client/builtins/user.py
  4. 163
      steam/client/mixins/friends.py
  5. 86
      steam/client/user.py

1
docs/api/steam.client.rst

@ -12,4 +12,5 @@ client
steam.client.builtins steam.client.builtins
steam.client.mixins steam.client.mixins
steam.client.gc steam.client.gc
steam.client.user

7
docs/api/steam.client.user.rst

@ -0,0 +1,7 @@
user
====
.. automodule:: steam.client.user
:members:
:show-inheritance:

53
steam/client/builtins/user.py

@ -1,3 +1,5 @@
from weakref import WeakValueDictionary
from steam.client.user import SteamUser
from steam.enums import EPersonaState from steam.enums import EPersonaState
from steam.enums.emsg import EMsg from steam.enums.emsg import EMsg
from steam.core.msg import MsgProto from steam.core.msg import MsgProto
@ -5,14 +7,32 @@ from steam.util import proto_fill_from_dict
class User(object): class User(object):
persona_state = EPersonaState.Online #: current persona state persona_state = EPersonaState.Online #: current persona state
user = None #: :class:`.SteamUser` instance once logged on
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(User, self).__init__(*args, **kwargs) super(User, self).__init__(*args, **kwargs)
self._user_cache = WeakValueDictionary()
self.on(self.EVENT_DISCONNECTED, self.__handle_disconnect)
self.on(self.EVENT_LOGGED_ON, self.__handle_set_persona) self.on(self.EVENT_LOGGED_ON, self.__handle_set_persona)
self.on(EMsg.ClientPersonaState, self.__handle_persona_state)
def __handle_disconnect(self):
self.user = None
def __handle_set_persona(self): def __handle_set_persona(self):
self.change_status(persona_state=self.persona_state) self.change_status(persona_state=self.persona_state)
self.user = self.get_user(self.steam_id)
def __handle_persona_state(self, message):
for friend in message.body.friends:
steamid = friend.friendid
if steamid in self._user_cache:
suser = self._user_cache[steamid]
suser._pstate = friend
suser._pstate_ready.set()
def change_status(self, **kwargs): def change_status(self, **kwargs):
""" """
@ -35,3 +55,36 @@ class User(object):
message = MsgProto(EMsg.ClientChangeStatus) message = MsgProto(EMsg.ClientChangeStatus)
proto_fill_from_dict(message.body, kwargs) proto_fill_from_dict(message.body, kwargs)
self.send(message) self.send(message)
def request_persona_state(self, steam_ids):
"""Request persona state data
:param steam_ids: list of steam ids
:type steam_ids: :class:`list`
"""
m = MsgProto(EMsg.ClientRequestFriendData)
m.body.persona_state_requested = 4294967295 # request all possible flags
m.body.friends.extend(steam_ids)
self.send_job(m)
def get_user(self, steam_id, fetch_persona_state=True):
"""Get :class:`.SteamUser` instance for ``steam id``
:param steam_id: steam id
:type steam_id: :class:`int`, :class:`.SteamID`
:param fetch_persona_state: whether to request person state when necessary
:type fetch_persona_state: :class:`bool`
:return: SteamUser instance
:rtype: :class:`.SteamUser`
"""
steam_id = int(steam_id)
suser = self._user_cache.get(steam_id, None)
if suser is None:
suser = SteamUser(steam_id, self)
self._user_cache[steam_id] = suser
if fetch_persona_state:
self.request_persona_state([steam_id])
return suser

163
steam/client/mixins/friends.py

@ -1,7 +1,7 @@
import logging import logging
from eventemitter import EventEmitter from eventemitter import EventEmitter
from steam.steamid import SteamID, intBase from steam.steamid import SteamID, intBase
from steam.enums import EResult, EFriendRelationship, EPersonaState from steam.enums import EResult, EFriendRelationship
from steam.enums.emsg import EMsg from steam.enums.emsg import EMsg
from steam.core.msg import MsgProto from steam.core.msg import MsgProto
@ -15,12 +15,9 @@ class Friends(object):
class SteamFriendlist(EventEmitter): class SteamFriendlist(EventEmitter):
"""SteamFriendlist is an object that keeps state of user's friend list. """SteamFriendlist is an object that keeps state of user's friend list.
Think of it as a list of :class:`SteamUser`. It's essentially a :class:`list` of :class:`.SteamUser`.
You can iterate over it, check if it contains a particular steam id, or get :class:`SteamUser` for a steamid. You can iterate over it, check if it contains a particular ``steam id``,
or get :class:`.SteamUser` for a ``steam id``.
.. note::
persona state is not updated immediatelly for new user entries
""" """
EVENT_READY = 'ready' EVENT_READY = 'ready'
"""Friend list is ready for use """Friend list is ready for use
@ -51,14 +48,8 @@ class SteamFriendlist(EventEmitter):
:param steam_id: steam id :param steam_id: steam id
:param type: :class:`.SteamID` :param type: :class:`.SteamID`
""" """
EVENT_PERSONA_STATE_UPDATED = 'persona_state_updated'
"""Upon persona state changes for a user
:param user: steam user instance
:type user: :class:`.SteamUser`
"""
ready = False #: indicates whether friend list is available ready = False #: indicates whether friend list is ready for use
def __init__(self, client, logger_name='SteamFriendList'): def __init__(self, client, logger_name='SteamFriendList'):
self._LOG = logging.getLogger(logger_name) self._LOG = logging.getLogger(logger_name)
@ -67,7 +58,6 @@ class SteamFriendlist(EventEmitter):
self._steam.on(EMsg.ClientAddFriendResponse, self._handle_add_friend_result) self._steam.on(EMsg.ClientAddFriendResponse, self._handle_add_friend_result)
self._steam.on(EMsg.ClientFriendsList, self._handle_friends_list) self._steam.on(EMsg.ClientFriendsList, self._handle_friends_list)
self._steam.on(EMsg.ClientPersonaState, self._handle_persona_state)
self._steam.on(self._steam.EVENT_DISCONNECTED, self._handle_disconnect) self._steam.on(self._steam.EVENT_DISCONNECTED, self._handle_disconnect)
def emit(self, event, *args): def emit(self, event, *args):
@ -89,54 +79,42 @@ class SteamFriendlist(EventEmitter):
if incremental == False: if incremental == False:
self._fr.clear() self._fr.clear()
pstate_check = set() steamids_to_check = set()
# update internal friends list # update internal friends list
for friend in message.body.friends: for friend in message.body.friends:
steamid = friend.ulfriendid steamid = SteamID(friend.ulfriendid)
if steamid.type != steamid.EType.Individual:
continue
suser = self._steam.get_user(steamid, False)
rel = EFriendRelationship(friend.efriendrelationship) rel = EFriendRelationship(friend.efriendrelationship)
if steamid not in self._fr: if steamid not in self._fr:
suser = SteamUser(steamid, rel)
self._fr[suser] = suser self._fr[suser] = suser
suser.relationship = rel
steamids_to_check.add(steamid)
if rel in (2,4): if rel in (2,4): # RequestRecipient = 2, RequestInitiator = 4
if incremental == False:
pstate_check.add(steamid)
if rel == EFriendRelationship.RequestRecipient: if rel == EFriendRelationship.RequestRecipient:
self.emit(self.EVENT_FRIEND_INVITE, suser) self.emit(self.EVENT_FRIEND_INVITE, suser)
else: else:
oldrel = self._fr[steamid]._data['relationship'] oldrel, suser.relationship = suser.relationship, rel
self._fr[steamid]._data['relationship'] = rel
if rel == EFriendRelationship.No: if rel == EFriendRelationship.No:
self.emit(self.EVENT_FRIEND_REMOVED, self._fr.pop(steamid)) self.emit(self.EVENT_FRIEND_REMOVED, self._fr.pop(suder))
elif oldrel in (2,4) and rel == EFriendRelationship.Friend: elif oldrel in (2,4) and rel == EFriendRelationship.Friend:
self.emit(self.EVENT_FRIEND_NEW, self._fr[steamid]) self.emit(self.EVENT_FRIEND_NEW, suser)
# request persona state for any new entries # request persona state for any new entries
if pstate_check: if steamids_to_check:
m = MsgProto(EMsg.ClientRequestFriendData) self._steam.request_persona_state(steamids_to_check)
m.body.persona_state_requested = 4294967295 # request all possible flags
m.body.friends.extend(pstate_check)
self._steam.send(m)
if not self.ready: if not self.ready:
self.ready = True self.ready = True
self.emit(self.EVENT_READY) self.emit(self.EVENT_READY)
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
self.emit('persona_state_updated', self._fr[steamid])
def __repr__(self): def __repr__(self):
return "<%s %d users>" % ( return "<%s %d users>" % (
self.__class__.__name__, self.__class__.__name__,
@ -176,7 +154,7 @@ class SteamFriendlist(EventEmitter):
else: else:
m.body.accountname_or_email_to_add = steamid_or_accountname_or_email m.body.accountname_or_email_to_add = steamid_or_accountname_or_email
self._steam.send(m) self._steam.send_job(m)
def remove(self, steamid): def remove(self, steamid):
""" """
@ -185,102 +163,5 @@ class SteamFriendlist(EventEmitter):
:param steamid: their steamid :param steamid: their steamid
:type steamid: :class:`int`, :class:`.SteamID`, :class:`SteamUser` :type steamid: :class:`int`, :class:`.SteamID`, :class:`SteamUser`
""" """
m = MsgProto(EMsg.ClientRemoveFriend) self._steam.send(MsgProto(EMsg.ClientRemoveFriend), {'friendid': steamid})
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)
}
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,
)
@property
def steamid(self):
"""SteamID instance
:rtype: :class:`.SteamID`
"""
return SteamID(int(self))
@property
def relationship(self):
"""Current relationship with the steam user
:rtype: :class:`.EFriendRelationship`
"""
return self._data['relationship']
def get_ps(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_ps('player_name')
@property
def state(self):
"""State of the steam user
:rtype: :class:`.EPersonaState`
"""
state = self.get_ps('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_ps('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])

86
steam/client/user.py

@ -0,0 +1,86 @@
from datetime import datetime
from binascii import hexlify
from gevent.event import Event
from steam.steamid import SteamID
from steam.enums import EFriendRelationship, EPersonaState
class SteamUser(object):
"""Holds various functionality and data related to a steam user
"""
_pstate = None
steam_id = SteamID()
relationship = EFriendRelationship.No #: friendship status
def __new__(cls, steam_id, *args, **kwargs):
return super(SteamUser, cls).__new__(cls, steam_id)
def __init__(self, steam_id, steam):
self._pstate_ready = Event()
self._steam = steam
self.steam_id = SteamID(steam_id)
def __repr__(self):
return "<%s(%s, %s)>" % (
self.__class__.__name__,
str(self.steam_id),
self.state,
)
def get_ps(self, field_name, wait_pstate=True):
if not wait_pstate or self._pstate_ready.wait(timeout=30):
if self._pstate and self._pstate.HasField(field_name):
return getattr(self._pstate, field_name)
return None
@property
def last_logon(self):
""":rtype: :class:`datetime`, :class:`None`"""
ts = self.get_ps('last_logon')
return datetime.utcfromtimestamp(ts) if ts else None
@property
def last_logoff(self):
""":rtype: :class:`datetime`, :class:`None`"""
ts = self.get_ps('last_logoff')
return datetime.utcfromtimestamp(ts) if ts else None
@property
def name(self):
"""Name of the steam user, or ``None`` if it's not available
:rtype: :class:`str`, :class:`None`
"""
return self.get_ps('player_name')
@property
def state(self):
"""Personsa state (e.g. Online, Offline, Away, Busy, etc)
:rtype: :class:`.EPersonaState`
"""
state = self.get_ps('persona_state', False)
return EPersonaState(state) if state else EPersonaState.Offline
def get_avatar_url(self, size=2):
"""Get URL to avatar picture
: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`
"""
hashbytes = self.get_ps('avatar_hash')
if hashbytes != "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000":
ahash = hexlify(hashbytes).decode('ascii')
else:
ahash = 'fef49e7fa7e1997310d705b2a6158ff8dc1cdfeb'
sizes = {
0: '',
1: '_medium',
2: '_full',
}
url = "http://cdn.akamai.steamstatic.com/steamcommunity/public/images/avatars/%s/%s%s.jpg"
return url % (ahash[:2], ahash, sizes[size])
Loading…
Cancel
Save