feat: 添加steam上线速报

This commit is contained in:
lixiangwuxian 2024-10-28 02:58:26 +08:00
parent 7e28953a56
commit dce09c6e1f
3 changed files with 626 additions and 48 deletions

View File

@ -0,0 +1,67 @@
package steamplaying
type SteamUser struct {
ID int64 `json:"id" db:"id"`
QQID int64 `json:"qqid" db:"qqid"`
SteamID string `json:"steamid" db:"steamid"`
}
type SteamUserForGroup struct {
ID int64 `json:"id" db:"id"`
GroupID string `json:"group_id" db:"group_id"`
SteamID string `json:"steamid" db:"steamid"`
}
type PlayerSummary struct {
SteamID string `json:"steamid"` // 64位SteamID
PersonaName string `json:"personaname"` // 显示名称
ProfileURL string `json:"profileurl"` // Steam社区个人资料链接
Avatar string `json:"avatar"` // 32x32px头像URL
AvatarMedium string `json:"avatarmedium"` // 64x64px头像URL
AvatarFull string `json:"avatarfull"` // 184x184px头像URL
PersonaState int `json:"personastate"` // 用户状态
CommunityVisibilityState int `json:"communityvisibilitystate"` // 社区可见性状态
ProfileState int `json:"profilestate,omitempty"` // 用户是否配置了社区个人资料
LastLogOff int64 `json:"lastlogoff,omitempty"` // 上次在线时间Unix时间戳
CommentPermission int `json:"commentpermission,omitempty"` // 是否允许评论
// 私有数据,可能根据用户隐私设置而不可见
RealName string `json:"realname,omitempty"` // 真实姓名
PrimaryClanID string `json:"primaryclanid,omitempty"` // 用户的主要组ID
TimeCreated int64 `json:"timecreated,omitempty"` // 账号创建时间
GameID string `json:"gameid,omitempty"` // 当前游戏ID
GameServerIP string `json:"gameserverip,omitempty"` // 游戏服务器IP
GameExtraInfo string `json:"gameextrainfo,omitempty"` // 当前游戏名称
CityID int `json:"cityid,omitempty"` // 已弃用
LocCountryCode string `json:"loccountrycode,omitempty"` // 用户的国家代码
LocStateCode string `json:"locstatecode,omitempty"` // 用户的州/省代码
LocCityID int `json:"loccityid,omitempty"` // 用户的城市ID
}
// 封装返回的数据结构,用于处理 API 的整体返回
type GetPlayerSummariesResponse struct {
Response struct {
Players []PlayerSummary `json:"players"`
} `json:"response"`
}
func (s PlayerSummary) ToGameStatus() string {
UserName := s.PersonaName
GameName := s.GameExtraInfo
if GameName == "" {
return ""
}
if UserName == "" {
UserName = s.RealName
}
if UserName == "" {
UserName = s.SteamID
}
return UserName + "正在玩" + GameName
}
type LastTimeStatus struct {
SteamID string `json:"steamid"`
GameID string `json:"gameid"`
Trigger bool `json:"trigger"`
}

View File

@ -0,0 +1,376 @@
package steamplaying
import (
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"sync"
"time"
"git.lxtend.com/qqbot/config"
"git.lxtend.com/qqbot/sqlite3"
"golang.org/x/net/proxy"
)
var SteamAPIKey = ""
var ProxyAddr = ""
func init() {
SteamAPIKey = config.ConfigManager.GetProperty("steam_api_key")
ProxyAddr = config.ConfigManager.GetProperty("proxy_addr")
}
func init() {
createSteamUserTable := `CREATE TABLE IF NOT EXISTS steam_user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
qqid TEXT,
steamid TEXT UNIQUE
);`
createSteamUserForGroupTable := `CREATE TABLE IF NOT EXISTS steam_user_for_group (
id INTEGER PRIMARY KEY AUTOINCREMENT,
group_id TEXT,
steamid TEXT
);`
sqlite3.TryCreateTable(createSteamUserTable)
sqlite3.TryCreateTable(createSteamUserForGroupTable)
}
func bindSteamUser(qqid int64, steamid string) error {
tx, err := sqlite3.GetTran()
if err != nil {
return err
}
defer tx.Rollback()
var steamUser []SteamUser
err = tx.Select(&steamUser, "SELECT * FROM steam_user WHERE qqid = ?", qqid)
if err != nil {
return err
}
if len(steamUser) > 0 {
return errors.New("已绑定")
}
_, err = tx.Exec("INSERT INTO steam_user (qqid, steamid) VALUES (:qqid, :steamid)", qqid, steamid)
if err != nil {
return err
}
err = tx.Commit()
if err != nil {
return err
}
return nil
}
func unbindSteamUser(qqid int64) error {
tx, err := sqlite3.GetTran()
if err != nil {
return err
}
defer tx.Rollback()
_, err = tx.Exec("DELETE FROM steam_user WHERE qqid = ?", qqid)
if err != nil {
return err
}
err = tx.Commit()
if err != nil {
return err
}
return nil
}
func bindUserInGroup(groupID int64, steamid string) error {
tx, err := sqlite3.GetTran()
if err != nil {
return err
}
defer tx.Rollback()
var steamUser []SteamUserForGroup
err = tx.Select(&steamUser, "SELECT * FROM steam_user_for_group WHERE group_id = ? AND steamid = ?", groupID, steamid)
if err != nil {
return err
}
if len(steamUser) > 0 {
return errors.New("已绑定")
}
_, err = tx.Exec("INSERT INTO steam_user_for_group (group_id, steamid) VALUES (:group_id, :steamid)", groupID, steamid)
if err != nil {
return err
}
err = tx.Commit()
if err != nil {
return err
}
return nil
}
func unbindUserInGroup(groupID int64, steamid string) error {
tx, err := sqlite3.GetTran()
if err != nil {
return err
}
defer tx.Rollback()
_, err = tx.Exec("DELETE FROM steam_user_for_group WHERE group_id = ? AND steamid = ?", groupID, steamid)
if err != nil {
return err
}
err = tx.Commit()
if err != nil {
return err
}
return nil
}
func unbindUserInAllGroup(steamid string) error {
tx, err := sqlite3.GetTran()
if err != nil {
return err
}
defer tx.Rollback()
_, err = tx.Exec("DELETE FROM steam_user_for_group WHERE steamid = ?", steamid)
if err != nil {
return err
}
err = tx.Commit()
if err != nil {
return err
}
return nil
}
func getSteamUser(qqid int64) (SteamUser, error) {
tx, err := sqlite3.GetTran()
if err != nil {
return SteamUser{}, err
}
defer tx.Rollback()
var steamUser []SteamUser
err = tx.Select(&steamUser, "SELECT * FROM steam_user WHERE qqid = ?", qqid)
if err != nil || len(steamUser) == 0 {
return SteamUser{}, err
}
return steamUser[0], nil
}
func getSteamUsersInGroup(groupID int64) ([]SteamUserForGroup, error) {
tx, err := sqlite3.GetTran()
if err != nil {
return nil, err
}
defer tx.Rollback()
var steamUsers []SteamUserForGroup
err = tx.Select(&steamUsers, "SELECT * FROM steam_user_for_group WHERE group_id = ?", groupID)
if err != nil {
return nil, err
}
return steamUsers, nil
}
func checkSteamGameStatus(steamID []string) (string, error) {
if len(steamID) == 0 {
return "疑似没人在玩游戏", nil
}
var glbErr error
var writeMutex sync.Mutex
var Players []PlayerSummary
var wg sync.WaitGroup
for step := 0; step < len(steamID)/100+1; step++ {
wg.Add(1)
go func() {
defer wg.Done()
url := "https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key=%s&steamids=%s"
fullSteamID := ""
for i := step * 100; i < (step+1)*100 && i < len(steamID); i++ {
fullSteamID += steamID[i] + ","
}
fullSteamID = fullSteamID[:len(fullSteamID)-1]
url = fmt.Sprintf(url, SteamAPIKey, fullSteamID)
dialer, _ := proxy.SOCKS5("tcp", ProxyAddr, nil, proxy.Direct)
httpTransport := &http.Transport{
Dial: func(network, addr string) (net.Conn, error) {
return dialer.Dial(network, addr)
},
}
client := &http.Client{
Transport: httpTransport,
Timeout: 10 * time.Second, // 设置请求超时时间
}
var resp *http.Response
maxRetry := 5
for i := 0; i < maxRetry; i++ {
glbErr = nil
var err error
resp, err = client.Get(url)
if err != nil {
if i == maxRetry-1 {
glbErr = errors.New("Get方法请求Steam API失败")
return
}
continue
}
break
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
glbErr = err
return
}
var userResponse GetPlayerSummariesResponse
if err := json.Unmarshal(body, &userResponse); err != nil {
glbErr = err
return
}
writeMutex.Lock()
Players = append(Players, userResponse.Response.Players...)
writeMutex.Unlock()
}()
}
wg.Wait()
if glbErr != nil {
return "", glbErr
}
gameStatusList := ""
for _, userState := range Players {
if userState.ToGameStatus() != "" {
gameStatusList += userState.ToGameStatus() + "\n"
}
}
if gameStatusList != "" {
gameStatusList = gameStatusList[:len(gameStatusList)-1]
} else {
gameStatusList = "疑似没有人在玩游戏"
}
return gameStatusList, nil
}
func checkDiffSteamGameStatus(steamID []string, lastTimeStat map[string]string) (string, error) {
if len(steamID) == 0 {
return "疑似没人在玩游戏", nil
}
var glbErr error
var writeMutex sync.Mutex
var Players []PlayerSummary
var wg sync.WaitGroup
for step := 0; step < len(steamID)/100+1; step++ {
wg.Add(1)
go func() {
defer wg.Done()
url := "https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key=%s&steamids=%s"
fullSteamID := ""
for i := step * 100; i < (step+1)*100 && i < len(steamID); i++ {
fullSteamID += steamID[i] + ","
}
fullSteamID = fullSteamID[:len(fullSteamID)-1]
url = fmt.Sprintf(url, SteamAPIKey, fullSteamID)
dialer, _ := proxy.SOCKS5("tcp", ProxyAddr, nil, proxy.Direct)
httpTransport := &http.Transport{
Dial: func(network, addr string) (net.Conn, error) {
return dialer.Dial(network, addr)
},
}
client := &http.Client{
Transport: httpTransport,
Timeout: 10 * time.Second, // 设置请求超时时间
}
var resp *http.Response
maxRetry := 5
for i := 0; i < maxRetry; i++ {
glbErr = nil
var err error
resp, err = client.Get(url)
if err != nil {
if i == maxRetry-1 {
glbErr = errors.New("Get方法请求Steam API失败")
return
}
continue
}
break
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
glbErr = err
return
}
var userResponse GetPlayerSummariesResponse
if err := json.Unmarshal(body, &userResponse); err != nil {
glbErr = err
return
}
writeMutex.Lock()
Players = append(Players, userResponse.Response.Players...)
writeMutex.Unlock()
}()
}
wg.Wait()
if glbErr != nil {
return "", glbErr
}
gameStatusListStr := ""
for _, userState := range Players {
if lastTimeStat[userState.SteamID] == userState.GameID {
continue
}
if userState.ToGameStatus() != "" {
gameStatusListStr += userState.ToGameStatus() + "\n"
}
lastTimeStat[userState.SteamID] = userState.GameID
}
if gameStatusListStr != "" {
gameStatusListStr = gameStatusListStr[:len(gameStatusListStr)-1]
}
return gameStatusListStr, nil
}
func checkSteamIDValid(steamID string) (bool, error) {
url := "https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key=%s&steamids=%s"
url = fmt.Sprintf(url, SteamAPIKey, steamID)
dialer, _ := proxy.SOCKS5("tcp", ProxyAddr, nil, proxy.Direct)
httpTransport := &http.Transport{
Dial: func(network, addr string) (net.Conn, error) {
return dialer.Dial(network, addr)
},
}
client := &http.Client{
Transport: httpTransport,
Timeout: 10 * time.Second, // 设置请求超时时间
}
resp, err := client.Get(url)
if err != nil {
return false, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return false, err
}
var userResponse GetPlayerSummariesResponse
if err := json.Unmarshal(body, &userResponse); err != nil {
return false, err
}
if len(userResponse.Response.Players) == 0 {
return false, nil
}
return true, nil
}
func getAllGroupID() ([]int64, error) {
tx, err := sqlite3.GetTran()
if err != nil {
return nil, err
}
defer tx.Rollback()
var groupIDs []int64
err = tx.Select(&groupIDs, "SELECT DISTINCT group_id FROM steam_user_for_group")
if err != nil {
return nil, err
}
return groupIDs, nil
}

View File

@ -2,80 +2,215 @@ package steamplaying
import ( import (
"fmt" "fmt"
"io"
"net/http"
"regexp" "regexp"
"time" "time"
"git.lxtend.com/qqbot/action"
"git.lxtend.com/qqbot/constants" "git.lxtend.com/qqbot/constants"
"git.lxtend.com/qqbot/handler" "git.lxtend.com/qqbot/handler"
"git.lxtend.com/qqbot/model" "git.lxtend.com/qqbot/model"
"git.lxtend.com/qqbot/util" "git.lxtend.com/qqbot/util"
"golang.org/x/net/proxy"
) )
func init() { func init() {
// Register the handler with the server // Register the handler with the server
handler.RegisterHandler("绑steam", bindSteam, constants.LEVEL_USER)
handler.RegisterHelpInform("绑steam", "绑定您的steam账号。可以通过右上角-账户明细页面 https://store.steampowered.com/account/ 查看,位于页面左上角")
handler.RegisterHandler("群绑定steam", bindSteamInGroup, constants.LEVEL_USER)
handler.RegisterHelpInform("群绑定steam", "在群内启用你的steam游戏状态查询")
// handler.RegisterHandler("群通报steam", bindSteamInGroupBroadCast, constants.LEVEL_USER)
// handler.RegisterHelpInform("群通报steam", "在群内启用你的steam游戏上线通报")
handler.RegisterHandler("解绑steam", unbindSteam, constants.LEVEL_USER)
handler.RegisterHelpInform("解绑steam", "解绑您的steam账号并解绑所有群监听")
handler.RegisterHandler("群解绑steam", unbindSteamInGroup, constants.LEVEL_USER)
handler.RegisterHelpInform("群解绑steam", "解绑本群群监听steam游戏状态")
handler.RegisterHandler("查房", checkSteamPlaying, constants.LEVEL_USER) handler.RegisterHandler("查房", checkSteamPlaying, constants.LEVEL_USER)
handler.RegisterHelpInform("查房", "查看群内成员的steam游戏状态")
go RoundCheckSteamPlaying()
} }
func checkSteamPlaying(msg model.Message) model.Reply { func bindSteam(msg model.Message) model.Reply {
tokens := util.SplitN(msg.RawMsg, 2) token := util.SplitN(msg.RawMsg, 2)
if len(tokens) < 2 { if len(token) < 2 {
return model.Reply{ return model.Reply{
ReplyMsg: "请输入正确的steamID", ReplyMsg: "请输入steamID",
ReferOriginMsg: true, ReferOriginMsg: true,
FromMsg: msg, FromMsg: msg,
} }
} } else {
status, err := checkSteamGameStatus(tokens[1]) re := regexp.MustCompile(`https://steamcommunity\.com/profiles/([0-9]+)`)
if err != nil { steamIdInUrl := re.FindStringSubmatch(token[1])
return model.Reply{ if steamIdInUrl != nil {
ReplyMsg: fmt.Sprintf("检查失败: %v", err), token[1] = steamIdInUrl[1]
ReferOriginMsg: true, }
FromMsg: msg, if valid, err := checkSteamIDValid(token[1]); !valid {
return model.Reply{
ReplyMsg: fmt.Sprintf("steamID无效: %v", err),
ReferOriginMsg: true,
FromMsg: msg,
}
} else if err != nil {
return model.Reply{
ReplyMsg: fmt.Sprintf("steamID验证失败: %v", err),
ReferOriginMsg: true,
FromMsg: msg,
}
}
if err := bindSteamUser(msg.UserId, token[1]); err != nil {
return model.Reply{
ReplyMsg: fmt.Sprintf("绑定失败: %v", err),
ReferOriginMsg: true,
FromMsg: msg,
}
} }
} }
return model.Reply{ return model.Reply{
ReplyMsg: status, ReplyMsg: fmt.Sprintf("绑定steam用户%s成功", token[1]),
ReferOriginMsg: true, ReferOriginMsg: true,
FromMsg: msg, FromMsg: msg,
} }
} }
func checkSteamGameStatus(steamID string) (string, error) { func bindSteamInGroup(msg model.Message) model.Reply {
dialer, err := proxy.SOCKS5("tcp", "100.94.183.43:2080", nil, proxy.Direct) if user, err := getSteamUser(msg.UserId); err != nil {
if err != nil { return model.Reply{
return "", fmt.Errorf("设置SOCKS5代理失败: %v", err) ReplyMsg: fmt.Sprintf("查询steam绑定失败: %v", err),
ReferOriginMsg: true,
FromMsg: msg,
}
} else if err := bindUserInGroup(msg.GroupInfo.GroupId, user.SteamID); err != nil {
return model.Reply{
ReplyMsg: fmt.Sprintf("绑定至群监听列表失败: %v", err),
ReferOriginMsg: true,
FromMsg: msg,
}
} }
url := fmt.Sprintf("https://steamcommunity.com/id/%s", steamID) return model.Reply{
ReplyMsg: "绑定至群监听列表成功",
httpTransport := &http.Transport{} ReferOriginMsg: true,
httpTransport.Dial = dialer.Dial FromMsg: msg,
client := &http.Client{ }
Transport: httpTransport, }
Timeout: 10 * time.Second, // 设置请求超时时间
func unbindSteam(msg model.Message) model.Reply {
if user, err := getSteamUser(msg.UserId); err != nil {
return model.Reply{
ReplyMsg: fmt.Sprintf("解绑失败: %v", err),
ReferOriginMsg: true,
FromMsg: msg,
}
} else {
if err := unbindUserInAllGroup(user.SteamID); err != nil {
return model.Reply{
ReplyMsg: fmt.Sprintf("解绑所有群监听列表失败: %v", err),
ReferOriginMsg: true,
FromMsg: msg,
}
}
if err := unbindSteamUser(msg.UserId); err != nil {
return model.Reply{
ReplyMsg: fmt.Sprintf("解绑失败: %v", err),
ReferOriginMsg: true,
FromMsg: msg,
}
}
}
return model.Reply{
ReplyMsg: "解绑成功",
ReferOriginMsg: true,
FromMsg: msg,
}
}
func unbindSteamInGroup(msg model.Message) model.Reply {
if user, err := getSteamUser(msg.UserId); err != nil {
return model.Reply{
ReplyMsg: fmt.Sprintf("群监听解绑失败: %v", err),
ReferOriginMsg: true,
FromMsg: msg,
}
} else {
if err := unbindUserInGroup(msg.GroupInfo.GroupId, user.SteamID); err != nil {
return model.Reply{
ReplyMsg: fmt.Sprintf("群监听解绑失败: %v", err),
ReferOriginMsg: true,
FromMsg: msg,
}
}
}
return model.Reply{
ReplyMsg: "群监听解绑成功本群内将不再查询你的steam游戏状态",
ReferOriginMsg: true,
FromMsg: msg,
}
}
func checkSteamPlaying(msg model.Message) model.Reply {
users, err := getSteamUsersInGroup(msg.GroupInfo.GroupId)
if err != nil {
return model.Reply{
ReplyMsg: fmt.Sprintf("获取群成员steam列表失败: %v", err),
ReferOriginMsg: true,
FromMsg: msg,
}
}
var steamIds []string
for _, user := range users {
steamIds = append(steamIds, user.SteamID)
}
gameList, err := checkSteamGameStatus(steamIds)
if err != nil {
return model.Reply{
ReplyMsg: fmt.Sprintf("获取游戏列表失败: %v", err),
ReferOriginMsg: true,
FromMsg: msg,
}
}
return model.Reply{
ReplyMsg: gameList,
ReferOriginMsg: true,
FromMsg: msg,
}
}
func RoundCheckSteamPlaying() {
once := true
playingMap := map[int64]map[string]string{}
for {
time.Sleep(5 * time.Second)
groups, err := getAllGroupID()
if err != nil {
fmt.Println("获取群列表失败: ", err)
continue
}
for _, group := range groups {
if _, ok := playingMap[group]; !ok {
playingMap[group] = map[string]string{}
}
users, err := getSteamUsersInGroup(group)
if err != nil {
fmt.Println("获取群成员steam列表失败: ", err)
continue
}
var steamIds []string
for _, user := range users {
steamIds = append(steamIds, user.SteamID)
}
gameList, err := checkDiffSteamGameStatus(steamIds, playingMap[group])
if err != nil {
fmt.Println("获取游戏列表失败: ", err)
continue
}
if gameList != "" && !once {
msg := model.Reply{
ReplyMsg: "速报:\n" + gameList,
ReferOriginMsg: false,
FromMsg: model.Message{GroupInfo: model.GroupInfo{GroupId: group}},
}
action.ActionManager.SendMsg(msg)
}
}
once = false
} }
resp, err := client.Get(url)
if err != nil {
return "", fmt.Errorf("获取页面失败: %v", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("读取页面内容失败: %v", err)
}
fmt.Print(string(body))
// 提取游戏名称
gameNameRegex := regexp.MustCompile(`(?s)<div\s+class="profile_in_game_name">\s*(.*?)\s*</div>`)
matches := gameNameRegex.FindSubmatch(body)
if len(matches) < 2 {
return "", fmt.Errorf("%s好像没有在玩游戏", steamID)
}
gameName := string(matches[1])
return fmt.Sprintf("该用户正在玩: %s", gameName), nil
} }