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.mixins
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.emsg import EMsg
from steam.core.msg import MsgProto
@ -5,14 +7,32 @@ from steam.util import proto_fill_from_dict
class User(object):
persona_state = EPersonaState.Online #: current persona state
user = None #: :class:`.SteamUser` instance once logged on
def __init__(self, *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(EMsg.ClientPersonaState, self.__handle_persona_state)
def __handle_disconnect(self):
self.user = None
def __handle_set_persona(self):
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):
"""
@ -35,3 +55,36 @@ class User(object):
message = MsgProto(EMsg.ClientChangeStatus)
proto_fill_from_dict(message.body, kwargs)
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
from eventemitter import EventEmitter
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.core.msg import MsgProto
@ -15,12 +15,9 @@ class Friends(object):
class SteamFriendlist(EventEmitter):
"""SteamFriendlist is an object that keeps state of user's friend list.
Think of it as a list of :class:`SteamUser`.
You can iterate over it, check if it contains a particular steam id, or get :class:`SteamUser` for a steamid.
.. note::
persona state is not updated immediatelly for new user entries
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 ``steam id``.
"""
EVENT_READY = 'ready'
"""Friend list is ready for use
@ -51,14 +48,8 @@ class SteamFriendlist(EventEmitter):
:param steam_id: steam id
: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'):
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.ClientFriendsList, self._handle_friends_list)
self._steam.on(EMsg.ClientPersonaState, self._handle_persona_state)
self._steam.on(self._steam.EVENT_DISCONNECTED, self._handle_disconnect)
def emit(self, event, *args):
@ -89,54 +79,42 @@ class SteamFriendlist(EventEmitter):
if incremental == False:
self._fr.clear()
pstate_check = set()
steamids_to_check = set()
# update internal friends list
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)
if steamid not in self._fr:
suser = SteamUser(steamid, rel)
self._fr[suser] = suser
suser.relationship = rel
steamids_to_check.add(steamid)
if rel in (2,4):
if incremental == False:
pstate_check.add(steamid)
if rel in (2,4): # RequestRecipient = 2, RequestInitiator = 4
if rel == EFriendRelationship.RequestRecipient:
self.emit(self.EVENT_FRIEND_INVITE, suser)
else:
oldrel = self._fr[steamid]._data['relationship']
self._fr[steamid]._data['relationship'] = rel
oldrel, suser.relationship = suser.relationship, rel
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:
self.emit(self.EVENT_FRIEND_NEW, self._fr[steamid])
self.emit(self.EVENT_FRIEND_NEW, suser)
# 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)
if steamids_to_check:
self._steam.request_persona_state(steamids_to_check)
if not self.ready:
self.ready = True
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):
return "<%s %d users>" % (
self.__class__.__name__,
@ -176,7 +154,7 @@ class SteamFriendlist(EventEmitter):
else:
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):
"""
@ -185,102 +163,5 @@ class SteamFriendlist(EventEmitter):
:param steamid: their steamid
:type steamid: :class:`int`, :class:`.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)
}
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`
self._steam.send(MsgProto(EMsg.ClientRemoveFriend), {'friendid': steamid})
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