diff --git a/README.md b/README.md
index 0d890408..b1a594ae 100644
--- a/README.md
+++ b/README.md
@@ -559,7 +559,7 @@ print("run[CQ:image,file="+j["img"]+"]")
- [x] b站推送列表
- - [x] 拉取b站推送 (使用job执行定时任务------记录在"@every 10s"触发的指令)
+ - [x] 拉取b站推送 (使用job执行定时任务------记录在"@every 5m"触发的指令)
@@ -1283,6 +1283,22 @@ print("run[CQ:image,file="+j["img"]+"]")
- [x] 黄油角色[@xxx]
+
+
+ steam
+
+ `import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/steam"`
+
+ - [x] steam[添加|删除]订阅xxxxx
+
+ - [x] steam查看订阅
+
+ - [x] steam绑定 api key xxxxxxx
+
+ - [x] 查看apikey
+
+ - [x] 拉取steam订阅 (使用job执行定时任务------记录在"@every 1m"触发的指令)
+
抽塔罗牌
diff --git a/main.go b/main.go
index 58308ab3..b6edd3cd 100644
--- a/main.go
+++ b/main.go
@@ -131,6 +131,7 @@ import (
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/setutime" // 来份涩图
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/shadiao" // 沙雕app
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/shindan" // 测定
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/steam" // steam相关
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/tarot" // 抽塔罗牌
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/tiangou" // 舔狗日记
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/tracemoe" // 搜番
diff --git a/plugin/bilibili/bilibili.go b/plugin/bilibili/bilibili.go
index f6de28e7..60250b2d 100644
--- a/plugin/bilibili/bilibili.go
+++ b/plugin/bilibili/bilibili.go
@@ -56,7 +56,7 @@ func init() {
"- 查成分 [xxx]\n" +
"- 查弹幕 [xxx]\n" +
"- 设置b站cookie b_ut=7;buvid3=0;i-wanna-go-back=-1;innersign=0;\n" +
- "- 更新vup" +
+ "- 更新vup\n" +
"Tips: (412就是拦截的意思,建议私聊把cookie设全)\n",
PublicDataFolder: "Bilibili",
})
diff --git a/plugin/steam/listenter.go b/plugin/steam/listenter.go
new file mode 100644
index 00000000..4e76f597
--- /dev/null
+++ b/plugin/steam/listenter.go
@@ -0,0 +1,134 @@
+package steam
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/FloatTech/floatbox/binary"
+ "github.com/FloatTech/floatbox/web"
+ ctrl "github.com/FloatTech/zbpctrl"
+ "github.com/tidwall/gjson"
+ zero "github.com/wdvxdr1123/ZeroBot"
+ "github.com/wdvxdr1123/ZeroBot/message"
+)
+
+// ----------------------- 远程调用 ----------------------
+const (
+ URL = "https://api.steampowered.com/" // steam API 调用地址
+ StatusURL = "ISteamUser/GetPlayerSummaries/v2/?key=%+v&steamids=%+v" // 根据用户steamID获取用户状态
+ steamapikeygid = 3
+)
+
+var apiKey string
+
+func init() {
+ engine.OnRegex(`^steam绑定\s*api\s*key\s*(.*)$`, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).Handle(func(ctx *zero.Ctx) {
+ apiKey = ctx.State["regex_matched"].([]string)[1]
+ m := ctx.State["manager"].(*ctrl.Control[*zero.Ctx])
+ _ = m.Manager.Response(steamapikeygid)
+ err := m.Manager.SetExtra(steamapikeygid, apiKey)
+ if err != nil {
+ ctx.SendChain(message.Text("[steam] ERROR: 保存apikey失败!"))
+ return
+ }
+ ctx.SendChain(message.Text("保存apikey成功!"))
+ })
+ engine.OnFullMatch("查看apikey", zero.OnlyPrivate, zero.SuperUserPermission, getDB).SetBlock(true).Handle(func(ctx *zero.Ctx) {
+ ctx.SendChain(message.Text("apikey为: ", apiKey))
+ })
+ engine.OnFullMatch("拉取steam订阅", getDB).SetBlock(true).Handle(func(ctx *zero.Ctx) {
+ su := zero.BotConfig.SuperUsers[0]
+ // 获取所有处于监听状态的用户信息
+ infos, err := database.findAll()
+ if err != nil {
+ // 挂了就给管理员发消息
+ ctx.SendPrivateMessage(su, message.Text("[steam] ERROR: ", err))
+ return
+ }
+ if len(infos) == 0 {
+ return
+ }
+ // 收集这波用户的streamId,然后查当前的状态,并建立信息映射表
+ streamIds := make([]string, len(infos))
+ localPlayerMap := make(map[int64]*player)
+ for i, info := range infos {
+ streamIds[i] = strconv.FormatInt(info.SteamID, 10)
+ localPlayerMap[info.SteamID] = info
+ }
+ // 将所有用户状态查一遍
+ playerStatus, err := getPlayerStatus(streamIds...)
+ if err != nil {
+ // 出错就发消息
+ ctx.SendPrivateMessage(su, message.Text("[steam] ERROR: ", err))
+ return
+ }
+ // 遍历返回的信息做对比,假如信息有变化则发消息
+ now := time.Now()
+ msg := make(message.Message, 0, len(playerStatus))
+ for _, playerInfo := range playerStatus {
+ msg = msg[:0]
+ localInfo := localPlayerMap[playerInfo.SteamID]
+ // 排除不需要处理的情况
+ if localInfo.GameID == 0 && playerInfo.GameID == 0 {
+ continue
+ }
+ // 打开游戏
+ if localInfo.GameID == 0 && playerInfo.GameID != 0 {
+ msg = append(msg, message.Text(playerInfo.PersonaName, "正在玩", playerInfo.GameExtraInfo))
+ localInfo.LastUpdate = now.Unix()
+ }
+ // 更换游戏
+ if localInfo.GameID != 0 && playerInfo.GameID != localInfo.GameID && playerInfo.GameID != 0 {
+ msg = append(msg, message.Text(playerInfo.PersonaName, "玩了", (now.Unix()-localInfo.LastUpdate)/60, "分钟后, 丢下了", localInfo.GameExtraInfo, ", 转头去玩", playerInfo.GameExtraInfo))
+ localInfo.LastUpdate = now.Unix()
+ }
+ // 关闭游戏
+ if playerInfo.GameID != localInfo.GameID && playerInfo.GameID == 0 {
+ msg = append(msg, message.Text(playerInfo.PersonaName, "玩了", (now.Unix()-localInfo.LastUpdate)/60, "分钟后, 关掉了", localInfo.GameExtraInfo))
+ localInfo.LastUpdate = 0
+ }
+ if len(msg) != 0 {
+ groups := strings.Split(localInfo.Target, ",")
+ for _, groupString := range groups {
+ group, err := strconv.ParseInt(groupString, 10, 64)
+ if err != nil {
+ ctx.SendPrivateMessage(su, message.Text("[steam] ERROR: ", err, "\nOTHER: SteamID ", localInfo.SteamID))
+ continue
+ }
+ ctx.SendGroupMessage(group, msg)
+ }
+ }
+ // 更新数据
+ localInfo.GameID = playerInfo.GameID
+ localInfo.GameExtraInfo = playerInfo.GameExtraInfo
+ if err = database.update(localInfo); err != nil {
+ ctx.SendPrivateMessage(su, message.Text("[steam] ERROR: ", err, "\nEXP: 更新数据失败\nOTHER: SteamID ", localInfo.SteamID))
+ }
+ }
+ })
+}
+
+// getPlayerStatus 获取用户状态
+func getPlayerStatus(streamIds ...string) ([]*player, error) {
+ players := make([]*player, 0)
+ // 拼接请求地址
+ url := fmt.Sprintf(URL+StatusURL, apiKey, strings.Join(streamIds, ","))
+ // 拉取并解析数据
+ data, err := web.GetData(url)
+ if err != nil {
+ return players, err
+ }
+ dataStr := binary.BytesToString(data)
+ index := gjson.Get(dataStr, "response.players.#").Uint()
+ for i := uint64(0); i < index; i++ {
+ players = append(players, &player{
+ SteamID: gjson.Get(dataStr, fmt.Sprintf("response.players.%d.steamid", i)).Int(),
+ PersonaName: gjson.Get(dataStr, fmt.Sprintf("response.players.%d.personaname", i)).String(),
+ GameID: gjson.Get(dataStr, fmt.Sprintf("response.players.%d.gameid", i)).Int(),
+ GameExtraInfo: gjson.Get(dataStr, fmt.Sprintf("response.players.%d.gameextrainfo", i)).String(),
+ })
+ }
+ return players, nil
+}
diff --git a/plugin/steam/steam.go b/plugin/steam/steam.go
new file mode 100644
index 00000000..ac9f3f11
--- /dev/null
+++ b/plugin/steam/steam.go
@@ -0,0 +1,158 @@
+// Package steam 获取steam用户状态
+package steam
+
+import (
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/FloatTech/floatbox/binary"
+ "github.com/FloatTech/floatbox/math"
+ ctrl "github.com/FloatTech/zbpctrl"
+ "github.com/FloatTech/zbputils/control"
+ "github.com/FloatTech/zbputils/ctxext"
+ "github.com/FloatTech/zbputils/img/text"
+ zero "github.com/wdvxdr1123/ZeroBot"
+ "github.com/wdvxdr1123/ZeroBot/message"
+)
+
+var (
+ engine = control.Register("steam", &ctrl.Options[*zero.Ctx]{
+ DisableOnDefault: false,
+ Brief: "steam相关插件",
+ Help: "- steam添加订阅 xxxxxxx (可输入需要绑定的 steamid)\n" +
+ "- steam删除订阅 xxxxxxx (删除你创建的对于 steamid 的绑定)\n" +
+ "- steam查询订阅 (查询本群内所有的绑定对象)\n" +
+ "-----------------------\n" +
+ "- steam绑定 api key xxxxxxx (密钥在steam网站申请, 申请地址: https://steamcommunity.com/dev/registerkey)\n" +
+ "- 查看apikey (查询已经绑定的密钥)\n" +
+ "- 拉取steam订阅 (使用插件定时任务开始)\n" +
+ "-----------------------\n" +
+ "Tips: steamID在用户资料页的链接上面, 形如7656119820673xxxx\n" +
+ "需要先私聊绑定apikey, 订阅用户之后使用job插件设置定时, 例: \n" +
+ "记录在\"@every 1m\"触发的指令\n" +
+ "拉取steam订阅",
+ PrivateDataFolder: "steam",
+ }).ApplySingle(ctxext.DefaultSingle)
+)
+
+func init() {
+ // 创建绑定流程
+ engine.OnRegex(`^steam添加订阅\s*(\d+)$`, zero.OnlyGroup, getDB).SetBlock(true).Handle(func(ctx *zero.Ctx) {
+ steamidstr := ctx.State["regex_matched"].([]string)[1]
+ steamID := math.Str2Int64(steamidstr)
+ // 获取用户状态
+ playerStatus, err := getPlayerStatus(steamidstr)
+ if err != nil {
+ ctx.SendChain(message.Text("[steam] ERROR: ", err, "\nEXP: 添加失败, 获取用户信息错误"))
+ return
+ }
+ if len(playerStatus) == 0 {
+ ctx.SendChain(message.Text("[steam] ERROR: 需要添加的用户不存在, 请检查id或url"))
+ return
+ }
+ playerData := playerStatus[0]
+ // 判断用户是否已经初始化:若未初始化,通过用户的steamID获取当前状态并初始化;若已经初始化则更新用户信息
+ info, err := database.find(steamID)
+ if err != nil {
+ ctx.SendChain(message.Text("[steam] ERROR: ", err, "\nEXP: 添加失败,数据库错误"))
+ return
+ }
+ // 处理数据
+ groupID := strconv.FormatInt(ctx.Event.GroupID, 10)
+ if info.Target == "" {
+ info = player{
+ SteamID: steamID,
+ PersonaName: playerData.PersonaName,
+ Target: groupID,
+ GameID: playerData.GameID,
+ GameExtraInfo: playerData.GameExtraInfo,
+ LastUpdate: time.Now().Unix(),
+ }
+ } else if !strings.Contains(info.Target, groupID) {
+ info.Target = strings.Join([]string{info.Target, groupID}, ",")
+ }
+ // 更新数据库
+ if err = database.update(&info); err != nil {
+ ctx.SendChain(message.Text("[steam] ERROR: ", err, "\nEXP: 更新数据库失败"))
+ return
+ }
+ ctx.SendChain(message.Text("添加成功"))
+ })
+ // 删除绑定流程
+ engine.OnRegex(`^steam删除订阅\s*(\d+)$`, zero.OnlyGroup, getDB).SetBlock(true).Handle(func(ctx *zero.Ctx) {
+ steamID := math.Str2Int64(ctx.State["regex_matched"].([]string)[1])
+ groupID := strconv.FormatInt(ctx.Event.GroupID, 10)
+ // 判断是否已经绑定该steamID,若已绑定就将群列表从推送群列表钟去除
+ info, err := database.findWithGroupID(steamID, groupID)
+ if err != nil {
+ ctx.SendChain(message.Text("[steam] ERROR: ", err, "\nEXP: 删除失败,数据库错误"))
+ return
+ }
+ if info.SteamID == 0 {
+ ctx.SendChain(message.Text("[steam] ERROR: 所需要删除的用户不存在。"))
+ return
+ }
+ // 从绑定列表中剔除需要删除的对象
+ targets := strings.Split(info.Target, ",")
+ newTargets := make([]string, 0)
+ for _, target := range targets {
+ if target == groupID {
+ continue
+ }
+ newTargets = append(newTargets, target)
+ }
+ if len(newTargets) == 0 {
+ if err = database.del(steamID); err != nil {
+ ctx.SendChain(message.Text("[steam] ERROR: ", err, "\nEXP: 删除失败,数据库错误"))
+ return
+ }
+ } else {
+ info.Target = strings.Join(newTargets, ",")
+ if err = database.update(&info); err != nil {
+ ctx.SendChain(message.Text("[steam] ERROR: ", err, "\nEXP: 删除失败,数据库错误"))
+ return
+ }
+ }
+ ctx.SendChain(message.Text("删除成功"))
+ })
+ // 查询当前群绑定信息
+ engine.OnFullMatch("steam查询订阅", zero.OnlyGroup, getDB).SetBlock(true).Handle(func(ctx *zero.Ctx) {
+ // 获取群信息
+ groupID := strconv.FormatInt(ctx.Event.GroupID, 10)
+ // 获取所有绑定信息
+ infos, err := database.findAll()
+ if err != nil {
+ ctx.SendChain(message.Text("[steam] ERROR: ", err, "\nEXP: 查询订阅失败, 数据库错误"))
+ return
+ }
+ if len(infos) == 0 {
+ ctx.SendChain(message.Text("[steam] ERROR: 还未订阅过用户关系!"))
+ return
+ }
+ // 遍历所有信息,如果包含该群就收集对应的steamID
+ var sb strings.Builder
+ head := " 查询steam订阅成功, 该群订阅的用户有: \n"
+ sb.WriteString(head)
+ for _, info := range infos {
+ if strings.Contains(info.Target, groupID) {
+ sb.WriteString(" ")
+ sb.WriteString(info.PersonaName)
+ sb.WriteString(":")
+ sb.WriteString(strconv.FormatInt(info.SteamID, 10))
+ sb.WriteString("\n")
+ }
+ }
+ if sb.String() == head {
+ ctx.SendChain(message.Text("查询成功,该群暂时还没有被绑定的用户!"))
+ return
+ }
+ // 组装并返回结果
+ data, err := text.RenderToBase64(sb.String(), text.FontFile, 400, 18)
+ if err != nil {
+ ctx.SendChain(message.Text("[steam] ERROR: ", err))
+ return
+ }
+ ctx.SendChain(message.Image("base64://" + binary.BytesToString(data)))
+ })
+}
diff --git a/plugin/steam/store.go b/plugin/steam/store.go
new file mode 100644
index 00000000..b175fb21
--- /dev/null
+++ b/plugin/steam/store.go
@@ -0,0 +1,117 @@
+package steam
+
+import (
+ "strconv"
+ "sync"
+ "time"
+
+ fcext "github.com/FloatTech/floatbox/ctxext"
+ sql "github.com/FloatTech/sqlite"
+ ctrl "github.com/FloatTech/zbpctrl"
+ zero "github.com/wdvxdr1123/ZeroBot"
+ "github.com/wdvxdr1123/ZeroBot/message"
+)
+
+var (
+ database streamDB
+ // 开启并检查数据库链接
+ getDB = fcext.DoOnceOnSuccess(func(ctx *zero.Ctx) bool {
+ database.db.DBPath = engine.DataFolder() + "steam.db"
+ err := database.db.Open(time.Hour * 24)
+ if err != nil {
+ ctx.SendChain(message.Text("[steam] ERROR: ", err))
+ return false
+ }
+ if err = database.db.Create(TableListenPlayer, &player{}); err != nil {
+ ctx.SendChain(message.Text("[steam] ERROR: ", err))
+ return false
+ }
+ // 校验密钥是否初始化
+ m := ctx.State["manager"].(*ctrl.Control[*zero.Ctx])
+ _ = m.Manager.Response(steamapikeygid)
+ _ = m.Manager.GetExtra(steamapikeygid, &apiKey)
+ if apiKey == "" {
+ ctx.SendChain(message.Text("ERROR: 未设置steam apikey"))
+ return false
+ }
+ return true
+ })
+)
+
+// streamDB 继承方法的存储结构
+type streamDB struct {
+ sync.RWMutex
+ db sql.Sqlite
+}
+
+const (
+ // TableListenPlayer 存储查询用户信息
+ TableListenPlayer = "listen_player"
+)
+
+// player 用户状态存储结构体
+type player struct {
+ SteamID int64 `json:"steam_id"` // 绑定用户标识ID
+ PersonaName string `json:"persona_name"` // 用户昵称
+ Target string `json:"target"` // 信息推送群组
+ GameID int64 `json:"game_id"` // 游戏ID
+ GameExtraInfo string `json:"game_extra_info"` // 游戏信息
+ LastUpdate int64 `json:"last_update"` // 更新时间
+}
+
+// update 如果主键不存在则插入一条新的数据,如果主键存在直接复写
+func (sql *streamDB) update(dbInfo *player) error {
+ sql.Lock()
+ defer sql.Unlock()
+ return sql.db.Insert(TableListenPlayer, dbInfo)
+}
+
+// find 根据主键查信息
+func (sql *streamDB) find(steamID int64) (dbInfo player, err error) {
+ sql.Lock()
+ defer sql.Unlock()
+ condition := "where steam_id = " + strconv.FormatInt(steamID, 10)
+ if !sql.db.CanFind(TableListenPlayer, condition) {
+ return player{}, nil // 规避没有该用户数据的报错
+ }
+ err = sql.db.Find(TableListenPlayer, &dbInfo, condition)
+ return
+}
+
+// findWithGroupID 根据用户steamID和groupID查询信息
+func (sql *streamDB) findWithGroupID(steamID int64, groupID string) (dbInfo player, err error) {
+ sql.Lock()
+ defer sql.Unlock()
+ condition := "where steam_id = " + strconv.FormatInt(steamID, 10) + " AND target LIKE '%" + groupID + "%'"
+ if !sql.db.CanFind(TableListenPlayer, condition) {
+ return player{}, nil // 规避没有该用户数据的报错
+ }
+ err = sql.db.Find(TableListenPlayer, &dbInfo, condition)
+ return
+}
+
+// findAll 查询所有库信息
+func (sql *streamDB) findAll() (dbInfos []*player, err error) {
+ sql.Lock()
+ defer sql.Unlock()
+ var info player
+ num, err := sql.db.Count(TableListenPlayer)
+ if err != nil || num == 0 {
+ return
+ }
+ dbInfos = make([]*player, 0, num)
+ err = sql.db.FindFor(TableListenPlayer, &info, "", func() error {
+ if info.SteamID != 0 {
+ dbInfos = append(dbInfos, &info)
+ }
+ return nil
+ })
+ return
+}
+
+// del 删除指定数据
+func (sql *streamDB) del(steamID int64) error {
+ sql.Lock()
+ defer sql.Unlock()
+ return sql.db.Del(TableListenPlayer, "where steam_id = "+strconv.FormatInt(steamID, 10))
+}