package steam
import (
"bytes"
"encoding/binary"
"io"
"sync"
"time"
. "github.com/Philipp15b/go-steam/protocol"
. "github.com/Philipp15b/go-steam/protocol/protobuf"
. "github.com/Philipp15b/go-steam/protocol/steamlang"
. "github.com/Philipp15b/go-steam/rwu"
"github.com/Philipp15b/go-steam/socialcache"
. "github.com/Philipp15b/go-steam/steamid"
"github.com/golang/protobuf/proto"
)
// Provides access to social aspects of Steam.
type Social struct {
mutex sync.RWMutex
name string
avatar []byte
personaState EPersonaState
Friends *socialcache.FriendsList
Groups *socialcache.GroupsList
Chats *socialcache.ChatsList
client *Client
}
func newSocial(client *Client) *Social {
return &Social{
Friends: socialcache.NewFriendsList(),
Groups: socialcache.NewGroupsList(),
Chats: socialcache.NewChatsList(),
client: client,
}
}
// Gets the local user's avatar
func (s *Social) GetAvatar() []byte {
s.mutex.RLock()
defer s.mutex.RUnlock()
return s.avatar
}
// Gets the local user's persona name
func (s *Social) GetPersonaName() string {
s.mutex.RLock()
defer s.mutex.RUnlock()
return s.name
}
// Sets the local user's persona name and broadcasts it over the network
func (s *Social) SetPersonaName(name string) {
s.mutex.Lock()
defer s.mutex.Unlock()
s.name = name
s.client.Write(NewClientMsgProtobuf(EMsg_ClientChangeStatus, &CMsgClientChangeStatus{
PersonaState: proto.Uint32(uint32(s.personaState)),
PlayerName: proto.String(name),
}))
}
// Gets the local user's persona state
func (s *Social) GetPersonaState() EPersonaState {
s.mutex.RLock()
defer s.mutex.RUnlock()
return s.personaState
}
// Sets the local user's persona state and broadcasts it over the network
func (s *Social) SetPersonaState(state EPersonaState) {
s.mutex.Lock()
defer s.mutex.Unlock()
s.personaState = state
s.client.Write(NewClientMsgProtobuf(EMsg_ClientChangeStatus, &CMsgClientChangeStatus{
PersonaState: proto.Uint32(uint32(state)),
}))
}
// Sends a chat message to ether a room or friend
func (s *Social) SendMessage(to SteamId, entryType EChatEntryType, message string) {
//Friend
if to.GetAccountType() == int32(EAccountType_Individual) || to.GetAccountType() == int32(EAccountType_ConsoleUser) {
s.client.Write(NewClientMsgProtobuf(EMsg_ClientFriendMsg, &CMsgClientFriendMsg{
Steamid: proto.Uint64(to.ToUint64()),
ChatEntryType: proto.Int32(int32(entryType)),
Message: []byte(message),
}))
//Chat room
} else if to.GetAccountType() == int32(EAccountType_Clan) || to.GetAccountType() == int32(EAccountType_Chat) {
chatId := to.ClanToChat()
s.client.Write(NewClientMsg(&MsgClientChatMsg{
ChatMsgType: entryType,
SteamIdChatRoom: chatId,
SteamIdChatter: s.client.SteamId(),
}, []byte(message)))
}
}
// Adds a friend to your friends list or accepts a friend. You'll receive a FriendStateEvent
// for every new/changed friend
func (s *Social) AddFriend(id SteamId) {
s.client.Write(NewClientMsgProtobuf(EMsg_ClientAddFriend, &CMsgClientAddFriend{
SteamidToAdd: proto.Uint64(id.ToUint64()),
}))
}
// Removes a friend from your friends list
func (s *Social) RemoveFriend(id SteamId) {
s.client.Write(NewClientMsgProtobuf(EMsg_ClientRemoveFriend, &CMsgClientRemoveFriend{
Friendid: proto.Uint64(id.ToUint64()),
}))
}
// Ignores or unignores a friend on Steam
func (s *Social) IgnoreFriend(id SteamId, setIgnore bool) {
ignore := uint8(1) //True
if !setIgnore {
ignore = uint8(0) //False
}
s.client.Write(NewClientMsg(&MsgClientSetIgnoreFriend{
MySteamId: s.client.SteamId(),
SteamIdFriend: id,
Ignore: ignore,
}, make([]byte, 0)))
}
// Requests persona state for a list of specified SteamIds
func (s *Social) RequestFriendListInfo(ids []SteamId, requestedInfo EClientPersonaStateFlag) {
var friends []uint64
for _, id := range ids {
friends = append(friends, id.ToUint64())
}
s.client.Write(NewClientMsgProtobuf(EMsg_ClientRequestFriendData, &CMsgClientRequestFriendData{
PersonaStateRequested: proto.Uint32(uint32(requestedInfo)),
Friends: friends,
}))
}
// Requests persona state for a specified SteamId
func (s *Social) RequestFriendInfo(id SteamId, requestedInfo EClientPersonaStateFlag) {
s.RequestFriendListInfo([]SteamId{id}, requestedInfo)
}
// Requests profile information for a specified SteamId
func (s *Social) RequestProfileInfo(id SteamId) {
s.client.Write(NewClientMsgProtobuf(EMsg_ClientFriendProfileInfo, &CMsgClientFriendProfileInfo{
SteamidFriend: proto.Uint64(id.ToUint64()),
}))
}
// Requests all offline messages and marks them as read
func (s *Social) RequestOfflineMessages() {
s.client.Write(NewClientMsgProtobuf(EMsg_ClientChatGetFriendMessageHistoryForOfflineMessages, &CMsgClientChatGetFriendMessageHistoryForOfflineMessages{}))
}
// Attempts to join a chat room
func (s *Social) JoinChat(id SteamId) {
chatId := id.ClanToChat()
s.client.Write(NewClientMsg(&MsgClientJoinChat{
SteamIdChat: chatId,
}, make([]byte, 0)))
}
// Attempts to leave a chat room
func (s *Social) LeaveChat(id SteamId) {
chatId := id.ClanToChat()
payload := new(bytes.Buffer)
binary.Write(payload, binary.LittleEndian, s.client.SteamId().ToUint64()) // ChatterActedOn
binary.Write(payload, binary.LittleEndian, uint32(EChatMemberStateChange_Left)) // StateChange
binary.Write(payload, binary.LittleEndian, s.client.SteamId().ToUint64()) // ChatterActedBy
s.client.Write(NewClientMsg(&MsgClientChatMemberInfo{
SteamIdChat: chatId,
Type: EChatInfoType_StateChange,
}, payload.Bytes()))
}
// Kicks the specified chat member from the given chat room
func (s *Social) KickChatMember(room SteamId, user SteamId) {
chatId := room.ClanToChat()
s.client.Write(NewClientMsg(&MsgClientChatAction{
SteamIdChat: chatId,
SteamIdUserToActOn: user,
ChatAction: EChatAction_Kick,
}, make([]byte, 0)))
}
// Bans the specified chat member from the given chat room
func (s *Social) BanChatMember(room SteamId, user SteamId) {
chatId := room.ClanToChat()
s.client.Write(NewClientMsg(&MsgClientChatAction{
SteamIdChat: chatId,
SteamIdUserToActOn: user,
ChatAction: EChatAction_Ban,
}, make([]byte, 0)))
}
// Unbans the specified chat member from the given chat room
func (s *Social) UnbanChatMember(room SteamId, user SteamId) {
chatId := room.ClanToChat()
s.client.Write(NewClientMsg(&MsgClientChatAction{
SteamIdChat: chatId,
SteamIdUserToActOn: user,
ChatAction: EChatAction_UnBan,
}, make([]byte, 0)))
}
func (s *Social) HandlePacket(packet *Packet) {
switch packet.EMsg {
case EMsg_ClientPersonaState:
s.handlePersonaState(packet)
case EMsg_ClientClanState:
s.handleClanState(packet)
case EMsg_ClientFriendsList:
s.handleFriendsList(packet)
case EMsg_ClientFriendMsgIncoming:
s.handleFriendMsg(packet)
case EMsg_ClientAccountInfo:
s.handleAccountInfo(packet)
case EMsg_ClientAddFriendResponse:
s.handleFriendResponse(packet)
case EMsg_ClientChatEnter:
s.handleChatEnter(packet)
case EMsg_ClientChatMsg:
s.handleChatMsg(packet)
case EMsg_ClientChatMemberInfo:
s.handleChatMemberInfo(packet)
case EMsg_ClientChatActionResult:
s.handleChatActionResult(packet)
case EMsg_ClientChatInvite:
s.handleChatInvite(packet)
case EMsg_ClientSetIgnoreFriendResponse:
s.handleIgnoreFriendResponse(packet)
case EMsg_ClientFriendProfileInfoResponse:
s.handleProfileInfoResponse(packet)
case EMsg_ClientFSGetFriendMessageHistoryResponse:
s.handleFriendMessageHistoryResponse(packet)
}
}
func (s *Social) handleAccountInfo(packet *Packet) {
//Just fire the personainfo, Auth handles the callback
flags := EClientPersonaStateFlag_PlayerName | EClientPersonaStateFlag_Presence | EClientPersonaStateFlag_SourceID
s.RequestFriendInfo(s.client.SteamId(), EClientPersonaStateFlag(flags))
}
func (s *Social) handleFriendsList(packet *Packet) {
list := new(CMsgClientFriendsList)
packet.ReadProtoMsg(list)
var friends []SteamId
for _, friend := range list.GetFriends() {
steamId := SteamId(friend.GetUlfriendid())
isClan := steamId.GetAccountType() == int32(EAccountType_Clan)
if isClan {
rel := EClanRelationship(friend.GetEfriendrelationship())
if rel == EClanRelationship_None {
s.Groups.Remove(steamId)
} else {
s.Groups.Add(socialcache.Group{
SteamId: steamId,
Relationship: rel,
})
}
if list.GetBincremental() {
s.client.Emit(&GroupStateEvent{steamId, rel})
}
} else {
rel := EFriendRelationship(friend.GetEfriendrelationship())
if rel == EFriendRelationship_None {
s.Friends.Remove(steamId)
} else {
s.Friends.Add(socialcache.Friend{
SteamId: steamId,
Relationship: rel,
})
}
if list.GetBincremental() {
s.client.Emit(&FriendStateEvent{steamId, rel})
}
}
if !list.GetBincremental() {
friends = append(friends, steamId)
}
}
if !list.GetBincremental() {
s.RequestFriendListInfo(friends, EClientPersonaStateFlag_DefaultInfoRequest)
s.client.Emit(&FriendsListEvent{})
}
}
func (s *Social) handlePersonaState(packet *Packet) {
list := new(CMsgClientPersonaState)
packet.ReadProtoMsg(list)
flags := EClientPersonaStateFlag(list.GetStatusFlags())
for _, friend := range list.GetFriends() {
id := SteamId(friend.GetFriendid())
if id == s.client.SteamId() { //this is our client id
s.mutex.Lock()
if friend.GetPlayerName() != "" {
s.name = friend.GetPlayerName()
}
avatar := friend.GetAvatarHash()
if ValidAvatar(avatar) {
s.avatar = avatar
}
s.mutex.Unlock()
} else if id.GetAccountType() == int32(EAccountType_Individual) {
if (flags & EClientPersonaStateFlag_PlayerName) == EClientPersonaStateFlag_PlayerName {
if friend.GetPlayerName() != "" {
s.Friends.SetName(id, friend.GetPlayerName())
}
}
if (flags & EClientPersonaStateFlag_Presence) == EClientPersonaStateFlag_Presence {
avatar := friend.GetAvatarHash()
if ValidAvatar(avatar) {
s.Friends.SetAvatar(id, avatar)
}
s.Friends.SetPersonaState(id, EPersonaState(friend.GetPersonaState()))
s.Friends.SetPersonaStateFlags(id, EPersonaStateFlag(friend.GetPersonaStateFlags()))
}
if (flags & EClientPersonaStateFlag_GameDataBlob) == EClientPersonaStateFlag_GameDataBlob {
s.Friends.SetGameAppId(id, friend.GetGamePlayedAppId())
s.Friends.SetGameId(id, friend.GetGameid())
s.Friends.SetGameName(id, friend.GetGameName())
}
} else if id.GetAccountType() == int32(EAccountType_Clan) {
if (flags & EClientPersonaStateFlag_PlayerName) == EClientPersonaStateFlag_PlayerName {
if friend.GetPlayerName() != "" {
s.Groups.SetName(id, friend.GetPlayerName())
}
}
if (flags & EClientPersonaStateFlag_Presence) == EClientPersonaStateFlag_Presence {
avatar := friend.GetAvatarHash()
if ValidAvatar(avatar) {
s.Groups.SetAvatar(id, avatar)
}
}
}
s.client.Emit(&PersonaStateEvent{
StatusFlags: flags,
FriendId: id,
State: EPersonaState(friend.GetPersonaState()),
StateFlags: EPersonaStateFlag(friend.GetPersonaStateFlags()),
GameAppId: friend.GetGamePlayedAppId(),
GameId: friend.GetGameid(),
GameName: friend.GetGameName(),
GameServerIp: friend.GetGameServerIp(),
GameServerPort: friend.GetGameServerPort(),
QueryPort: friend.GetQueryPort(),
SourceSteamId: SteamId(friend.GetSteamidSource()),
GameDataBlob: friend.GetGameDataBlob(),
Name: friend.GetPlayerName(),
Avatar: friend.GetAvatarHash(),
LastLogOff: friend.GetLastLogoff(),
LastLogOn: friend.GetLastLogon(),
ClanRank: friend.GetClanRank(),
ClanTag: friend.GetClanTag(),
OnlineSessionInstances: friend.GetOnlineSessionInstances(),
PublishedSessionId: friend.GetPublishedInstanceId(),
PersonaSetByUser: friend.GetPersonaSetByUser(),
})
}
}
func (s *Social) handleClanState(packet *Packet) {
body := new(CMsgClientClanState)
packet.ReadProtoMsg(body)
var name string
var avatar []byte
if body.GetNameInfo() != nil {
name = body.GetNameInfo().GetClanName()
avatar = body.GetNameInfo().GetShaAvatar()
}
var totalCount, onlineCount, chattingCount, ingameCount uint32
if body.GetUserCounts() != nil {
usercounts := body.GetUserCounts()
totalCount = usercounts.GetMembers()
onlineCount = usercounts.GetOnline()
chattingCount = usercounts.GetChatting()
ingameCount = usercounts.GetInGame()
}
var events, announcements []ClanEventDetails
for _, event := range body.GetEvents() {
events = append(events, ClanEventDetails{
Id: event.GetGid(),
EventTime: event.GetEventTime(),
Headline: event.GetHeadline(),
GameId: event.GetGameId(),
JustPosted: event.GetJustPosted(),
})
}
for _, announce := range body.GetAnnouncements() {
announcements = append(announcements, ClanEventDetails{
Id: announce.GetGid(),
EventTime: announce.GetEventTime(),
Headline: announce.GetHeadline(),
GameId: announce.GetGameId(),
JustPosted: announce.GetJustPosted(),
})
}
//Add stuff to group
clanid := SteamId(body.GetSteamidClan())
if body.NameInfo != nil {
info := body.NameInfo
s.Groups.SetName(clanid, info.GetClanName())
s.Groups.SetAvatar(clanid, info.GetShaAvatar())
}
if body.GetUserCounts() != nil {
s.Groups.SetMemberTotalCount(clanid, totalCount)
s.Groups.SetMemberOnlineCount(clanid, onlineCount)
s.Groups.SetMemberChattingCount(clanid, chattingCount)
s.Groups.SetMemberInGameCount(clanid, ingameCount)
}
s.client.Emit(&ClanStateEvent{
ClanId: clanid,
AccountFlags: EAccountFlags(body.GetClanAccountFlags()),
ClanName: name,
Avatar: avatar,
MemberTotalCount: totalCount,
MemberOnlineCount: onlineCount,
MemberChattingCount: chattingCount,
MemberInGameCount: ingameCount,
Events: events,
Announcements: announcements,
})
}
func (s *Social) handleFriendResponse(packet *Packet) {
body := new(CMsgClientAddFriendResponse)
packet.ReadProtoMsg(body)
s.client.Emit(&FriendAddedEvent{
Result: EResult(body.GetEresult()),
SteamId: SteamId(body.GetSteamIdAdded()),
PersonaName: body.GetPersonaNameAdded(),
})
}
func (s *Social) handleFriendMsg(packet *Packet) {
body := new(CMsgClientFriendMsgIncoming)
packet.ReadProtoMsg(body)
message := string(bytes.Split(body.GetMessage(), []byte{0x0})[0])
s.client.Emit(&ChatMsgEvent{
ChatterId: SteamId(body.GetSteamidFrom()),
Message: message,
EntryType: EChatEntryType(body.GetChatEntryType()),
Timestamp: time.Unix(int64(body.GetRtime32ServerTimestamp()), 0),
})
}
func (s *Social) handleChatMsg(packet *Packet) {
body := new(MsgClientChatMsg)
payload := packet.ReadClientMsg(body).Payload
message := string(bytes.Split(payload, []byte{0x0})[0])
s.client.Emit(&ChatMsgEvent{
ChatRoomId: SteamId(body.SteamIdChatRoom),
ChatterId: SteamId(body.SteamIdChatter),
Message: message,
EntryType: EChatEntryType(body.ChatMsgType),
})
}
func (s *Social) handleChatEnter(packet *Packet) {
body := new(MsgClientChatEnter)
payload := packet.ReadClientMsg(body).Payload
reader := bytes.NewBuffer(payload)
name, _ := ReadString(reader)
ReadByte(reader) //0
count := body.NumMembers
chatId := SteamId(body.SteamIdChat)
clanId := SteamId(body.SteamIdClan)
s.Chats.Add(socialcache.Chat{SteamId: chatId, GroupId: clanId})
for i := 0; i < int(count); i++ {
id, chatPerm, clanPerm := readChatMember(reader)
ReadBytes(reader, 6) //No idea what this is
s.Chats.AddChatMember(chatId, socialcache.ChatMember{
SteamId: SteamId(id),
ChatPermissions: chatPerm,
ClanPermissions: clanPerm,
})
}
s.client.Emit(&ChatEnterEvent{
ChatRoomId: SteamId(body.SteamIdChat),
FriendId: SteamId(body.SteamIdFriend),
ChatRoomType: EChatRoomType(body.ChatRoomType),
OwnerId: SteamId(body.SteamIdOwner),
ClanId: SteamId(body.SteamIdClan),
ChatFlags: byte(body.ChatFlags),
EnterResponse: EChatRoomEnterResponse(body.EnterResponse),
Name: name,
})
}
func (s *Social) handleChatMemberInfo(packet *Packet) {
body := new(MsgClientChatMemberInfo)
payload := packet.ReadClientMsg(body).Payload
reader := bytes.NewBuffer(payload)
chatId := SteamId(body.SteamIdChat)
if body.Type == EChatInfoType_StateChange {
actedOn, _ := ReadUint64(reader)
state, _ := ReadInt32(reader)
actedBy, _ := ReadUint64(reader)
ReadByte(reader) //0
stateChange := EChatMemberStateChange(state)
if stateChange == EChatMemberStateChange_Entered {
_, chatPerm, clanPerm := readChatMember(reader)
s.Chats.AddChatMember(chatId, socialcache.ChatMember{
SteamId: SteamId(actedOn),
ChatPermissions: chatPerm,
ClanPermissions: clanPerm,
})
} else if stateChange == EChatMemberStateChange_Banned || stateChange == EChatMemberStateChange_Kicked ||
stateChange == EChatMemberStateChange_Disconnected || stateChange == EChatMemberStateChange_Left {
s.Chats.RemoveChatMember(chatId, SteamId(actedOn))
}
stateInfo := StateChangeDetails{
ChatterActedOn: SteamId(actedOn),
StateChange: EChatMemberStateChange(stateChange),
ChatterActedBy: SteamId(actedBy),
}
s.client.Emit(&ChatMemberInfoEvent{
ChatRoomId: SteamId(body.SteamIdChat),
Type: EChatInfoType(body.Type),
StateChangeInfo: stateInfo,
})
}
}
func readChatMember(r io.Reader) (SteamId, EChatPermission, EClanPermission) {
ReadString(r) // MessageObject
ReadByte(r) // 7
ReadString(r) //steamid
id, _ := ReadUint64(r)
ReadByte(r) // 2
ReadString(r) //Permissions
chat, _ := ReadInt32(r)
ReadByte(r) // 2
ReadString(r) //Details
clan, _ := ReadInt32(r)
return SteamId(id), EChatPermission(chat), EClanPermission(clan)
}
func (s *Social) handleChatActionResult(packet *Packet) {
body := new(MsgClientChatActionResult)
packet.ReadClientMsg(body)
s.client.Emit(&ChatActionResultEvent{
ChatRoomId: SteamId(body.SteamIdChat),
ChatterId: SteamId(body.SteamIdUserActedOn),
Action: EChatAction(body.ChatAction),
Result: EChatActionResult(body.ActionResult),
})
}
func (s *Social) handleChatInvite(packet *Packet) {
body := new(CMsgClientChatInvite)
packet.ReadProtoMsg(body)
s.client.Emit(&ChatInviteEvent{
InvitedId: SteamId(body.GetSteamIdInvited()),
ChatRoomId: SteamId(body.GetSteamIdChat()),
PatronId: SteamId(body.GetSteamIdPatron()),
ChatRoomType: EChatRoomType(body.GetChatroomType()),
FriendChatId: SteamId(body.GetSteamIdFriendChat()),
ChatRoomName: body.GetChatName(),
GameId: body.GetGameId(),
})
}
func (s *Social) handleIgnoreFriendResponse(packet *Packet) {
body := new(MsgClientSetIgnoreFriendResponse)
packet.ReadClientMsg(body)
s.client.Emit(&IgnoreFriendEvent{
Result: EResult(body.Result),
})
}
func (s *Social) handleProfileInfoResponse(packet *Packet) {
body := new(CMsgClientFriendProfileInfoResponse)
packet.ReadProtoMsg(body)
s.client.Emit(&ProfileInfoEvent{
Result: EResult(body.GetEresult()),
SteamId: SteamId(body.GetSteamidFriend()),
TimeCreated: body.GetTimeCreated(),
RealName: body.GetRealName(),
CityName: body.GetCityName(),
StateName: body.GetStateName(),
CountryName: body.GetCountryName(),
Headline: body.GetHeadline(),
Summary: body.GetSummary(),
})
}
func (s *Social) handleFriendMessageHistoryResponse(packet *Packet) {
body := new(CMsgClientChatGetFriendMessageHistoryResponse)
packet.ReadProtoMsg(body)
steamid := SteamId(body.GetSteamid())
for _, message := range body.GetMessages() {
if !message.GetUnread() {
continue // Skip already read messages
}
s.client.Emit(&ChatMsgEvent{
ChatterId: steamid,
Message: message.GetMessage(),
EntryType: EChatEntryType_ChatMsg,
Timestamp: time.Unix(int64(message.GetTimestamp()), 0),
Offline: true, // GetUnread is true
})
}
}