From dce09c6e1ffda70e80ff7015d29322966dae7f10 Mon Sep 17 00:00:00 2001 From: lixiangwuxian Date: Mon, 28 Oct 2024 02:58:26 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0steam=E4=B8=8A?= =?UTF-8?q?=E7=BA=BF=E9=80=9F=E6=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- handler/steamplaying/model.go | 67 +++++ handler/steamplaying/service.go | 376 ++++++++++++++++++++++++++ handler/steamplaying/steam_playing.go | 231 ++++++++++++---- 3 files changed, 626 insertions(+), 48 deletions(-) create mode 100644 handler/steamplaying/model.go create mode 100644 handler/steamplaying/service.go diff --git a/handler/steamplaying/model.go b/handler/steamplaying/model.go new file mode 100644 index 0000000..1214d55 --- /dev/null +++ b/handler/steamplaying/model.go @@ -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"` +} diff --git a/handler/steamplaying/service.go b/handler/steamplaying/service.go new file mode 100644 index 0000000..e5e985d --- /dev/null +++ b/handler/steamplaying/service.go @@ -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 +} diff --git a/handler/steamplaying/steam_playing.go b/handler/steamplaying/steam_playing.go index 9db307d..dfed1e1 100644 --- a/handler/steamplaying/steam_playing.go +++ b/handler/steamplaying/steam_playing.go @@ -2,80 +2,215 @@ package steamplaying import ( "fmt" - "io" - "net/http" "regexp" "time" + "git.lxtend.com/qqbot/action" "git.lxtend.com/qqbot/constants" "git.lxtend.com/qqbot/handler" "git.lxtend.com/qqbot/model" "git.lxtend.com/qqbot/util" - "golang.org/x/net/proxy" ) func init() { // 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.RegisterHelpInform("查房", "查看群内成员的steam游戏状态") + go RoundCheckSteamPlaying() } -func checkSteamPlaying(msg model.Message) model.Reply { - tokens := util.SplitN(msg.RawMsg, 2) - if len(tokens) < 2 { +func bindSteam(msg model.Message) model.Reply { + token := util.SplitN(msg.RawMsg, 2) + if len(token) < 2 { return model.Reply{ - ReplyMsg: "请输入正确的steamID", + ReplyMsg: "请输入steamID", ReferOriginMsg: true, FromMsg: msg, } - } - status, err := checkSteamGameStatus(tokens[1]) - if err != nil { - return model.Reply{ - ReplyMsg: fmt.Sprintf("检查失败: %v", err), - ReferOriginMsg: true, - FromMsg: msg, + } else { + re := regexp.MustCompile(`https://steamcommunity\.com/profiles/([0-9]+)`) + steamIdInUrl := re.FindStringSubmatch(token[1]) + if steamIdInUrl != nil { + token[1] = steamIdInUrl[1] + } + 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{ - ReplyMsg: status, + ReplyMsg: fmt.Sprintf("绑定steam用户%s成功", token[1]), ReferOriginMsg: true, FromMsg: msg, } } -func checkSteamGameStatus(steamID string) (string, error) { - dialer, err := proxy.SOCKS5("tcp", "100.94.183.43:2080", nil, proxy.Direct) - if err != nil { - return "", fmt.Errorf("设置SOCKS5代理失败: %v", err) +func bindSteamInGroup(msg model.Message) model.Reply { + if user, err := getSteamUser(msg.UserId); err != nil { + return model.Reply{ + 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) - - httpTransport := &http.Transport{} - httpTransport.Dial = dialer.Dial - client := &http.Client{ - Transport: httpTransport, - Timeout: 10 * time.Second, // 设置请求超时时间 + return model.Reply{ + ReplyMsg: "绑定至群监听列表成功", + ReferOriginMsg: true, + FromMsg: msg, + } +} + +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)\s*(.*?)\s*`) - matches := gameNameRegex.FindSubmatch(body) - - if len(matches) < 2 { - return "", fmt.Errorf("%s好像没有在玩游戏", steamID) - } - - gameName := string(matches[1]) - return fmt.Sprintf("该用户正在玩: %s", gameName), nil }