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, logger_name="%s.friends" % self.__class__.__name__) 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 update immediatelly for new user entries Event: ``ready`` - friendlist is ready to use Event: ``friend_invite`` - new or existing friend invite :param user: steam user instance :type user: :class:`SteamUser` Event: ``friend_new`` - emitted upon accepting a new friend (or being accepted) :param user: steam user instance :type user: :class:`SteamUser` Event: ``friend_removed`` - no longer a friend (removed by either side) :param user: steam user instance :type user: :class:`SteamUser` """ ready = False #: indicates whether friend list is available def __init__(self, client, logger_name='SteamFriendList'): self._LOG = logging.getLogger(logger_name) 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) self._steam.on('disconnected', self._handle_disconnect) 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_disconnect(self): self.ready = False self._fr.clear() 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) if not self.ready: self.ready = True self.emit('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__, 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) } 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:`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_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:`steam.enums.common.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])