diff --git a/handler/echo/echo.go b/handler/echo/echo.go index 637768a..f5b89d3 100644 --- a/handler/echo/echo.go +++ b/handler/echo/echo.go @@ -7,6 +7,7 @@ import ( func init() { handler.RegisterHandler("echo", echo) + handler.RegisterHelpInform("echo", "再说一遍") } func echo(msg model.Message) (reply model.Reply) { diff --git a/handler/getweb/getweb.go b/handler/getweb/getweb.go index 4b6b818..dcdc4dc 100644 --- a/handler/getweb/getweb.go +++ b/handler/getweb/getweb.go @@ -8,6 +8,7 @@ import ( func init() { handler.RegisterHandler("上网", getweb) + handler.RegisterHelpInform("上网", "上网 截取网页") } func getweb(msg model.Message) (reply model.Reply) { @@ -15,7 +16,8 @@ func getweb(msg model.Message) (reply model.Reply) { return model.Reply{} } url := msg.RawMsg[len("上网 "):] - if err := util.ScreenshotURL(url, "./tmp/getweb/url.png", 0, 0, 0, 0, 0, 0, ""); err != nil { + url = formatURL(url) + if err := util.ScreenshotURL(url, "./tmp/getweb/url.png", 1620, 1960, 0, 0, 0, 0, ""); err != nil { return model.Reply{ ReplyMsg: err.Error(), ReferOriginMsg: true, @@ -28,3 +30,10 @@ func getweb(msg model.Message) (reply model.Reply) { FromMsg: msg, } } + +func formatURL(url string) string { + if url[:7] != "http://" && url[:8] != "https://" { + return "http://" + url + } + return url +} diff --git a/handler/handler.go b/handler/handler.go index 65c48fd..052515a 100644 --- a/handler/handler.go +++ b/handler/handler.go @@ -8,10 +8,13 @@ import ( ) type Handler func(msg model.Message) (reply model.Reply) +type TryCatchHandler func(msg model.Message) (reply model.Reply, catched bool) var handlers = make(map[string]Handler) var frontMatchHandlers = make(map[string]Handler) -var liveHandlers = make(map[int64]map[int64]Handler) +var liveHandlers = make(map[int64]map[int64]TryCatchHandler) +var livePrivateHandlers = make(map[int64]TryCatchHandler) +var HelpInforms = make(map[string]string) var privateAllHandler Handler func RegisterPrivateHandler(handler Handler) { @@ -26,9 +29,13 @@ func RegisterFrontMatchHandler(trigger string, handler Handler) { frontMatchHandlers[trigger] = handler } -func RegisterLiveHandler(groupID int64, userID int64, handler Handler) { //userID=-1 means for all users in groupID +func RegisterLiveHandler(groupID int64, userID int64, handler TryCatchHandler) { //userID=-1 means for all users in groupID + if groupID == 0 { + livePrivateHandlers[userID] = handler + return + } if _, ok := liveHandlers[groupID]; !ok { - liveHandlers[groupID] = make(map[int64]Handler) + liveHandlers[groupID] = make(map[int64]TryCatchHandler) } liveHandlers[groupID][userID] = handler } @@ -51,8 +58,18 @@ func MsgInHandler(msg model.Message) (reply model.Reply) { }() if handlerUserID, ok := liveHandlers[msg.GroupInfo.GroupId]; ok { if handler, ok := handlerUserID[msg.UserId]; ok { - liveHandlers[msg.GroupInfo.GroupId][msg.UserId] = nil - return handler(msg) + defer func() { liveHandlers[msg.GroupInfo.GroupId][msg.UserId] = nil }() + reply, catched := handler(msg) + if catched { + return reply + } + } + } + if handler, ok := livePrivateHandlers[msg.UserId]; ok { + defer func() { livePrivateHandlers[msg.UserId] = nil }() + reply, catched := handler(msg) + if catched { + return reply } } for trigger, handler := range frontMatchHandlers { @@ -69,3 +86,7 @@ func MsgInHandler(msg model.Message) (reply model.Reply) { } return model.Reply{} } + +func RegisterHelpInform(triggerName string, inform string) { + HelpInforms[triggerName] = inform +} diff --git a/handler/help/help.go b/handler/help/help.go new file mode 100644 index 0000000..bdb7652 --- /dev/null +++ b/handler/help/help.go @@ -0,0 +1,22 @@ +package help + +import ( + "git.lxtend.com/qqbot/handler" + "git.lxtend.com/qqbot/model" +) + +func init() { + handler.RegisterHandler("!help", help) +} + +func help(msg model.Message) (reply model.Reply) { + helpInfo := `以下是支持的功能:` + for k, v := range handler.HelpInforms { + helpInfo += "\n" + k + " : " + v + } + return model.Reply{ + ReplyMsg: helpInfo, + ReferOriginMsg: false, + FromMsg: msg, + } +} diff --git a/handler/jrrp/jrrp.go b/handler/jrrp/jrrp.go index 1cdf199..70b1670 100644 --- a/handler/jrrp/jrrp.go +++ b/handler/jrrp/jrrp.go @@ -14,6 +14,7 @@ func init() { jrrpInstance = jrrp.NewJrrp() handler.RegisterHandler("今日人品", jrrpHandler) handler.RegisterHandler("jrrp", jrrpHandler) + handler.RegisterHelpInform("今日人品/jrrp", "今日人品或jrrp 查询今日人品") } func jrrpHandler(msg model.Message) (reply model.Reply) { diff --git a/handler/roll/roll.go b/handler/roll/roll.go new file mode 100644 index 0000000..e550ea6 --- /dev/null +++ b/handler/roll/roll.go @@ -0,0 +1,85 @@ +package roll + +import ( + "fmt" + "regexp" + "strconv" + "time" + + "git.lxtend.com/qqbot/handler" + "git.lxtend.com/qqbot/model" + "golang.org/x/exp/rand" +) + +func init() { + handler.RegisterHandler("roll", roll) + handler.RegisterHelpInform("roll", "roll点") +} + +func roll(msg model.Message) (reply model.Reply) { + act, err := ParseRollExpression(msg.RawMsg) + if err != nil { + return model.Reply{ + ReplyMsg: "", + ReferOriginMsg: false, + FromMsg: msg, + } + } + result, results := RollDice(act) + return model.Reply{ + ReplyMsg: "掷骰结果: " + strconv.Itoa(result) + "点 " + fmt.Sprint(results), + ReferOriginMsg: true, + FromMsg: msg, + } +} + +// RollExpression 解析结果的结构体 +type RollExpression struct { + Times int // 掷骰次数 + Sides int // 骰子面数 +} + +// ParseRollExpression 解析骰子表达式,例如 "3d6" 或 "1d20" +func ParseRollExpression(input string) (*RollExpression, error) { + // 匹配类似 "3d6" 或 "1d20" 的表达式 + re := regexp.MustCompile(`(\d*)d(\d+)$`) + + matches := re.FindStringSubmatch(input) + if len(matches) != 3 { + return nil, fmt.Errorf("invalid roll expression: %s", input) + } + + // 如果次数为空,默认为 1 + times := 1 + if matches[1] != "" { + t, err := strconv.Atoi(matches[1]) + if err != nil { + return nil, err + } + times = t + } + + // 将面数转换为整数 + sides, err := strconv.Atoi(matches[2]) + if err != nil { + return nil, err + } + + return &RollExpression{Times: times, Sides: sides}, nil +} + +func RollDice(expr *RollExpression) (int, []int) { + rand.Seed(uint64(time.Now().UnixNano())) // 初始化随机种子 + + results := make([]int, expr.Times) + total := 0 + + // 每次掷骰,并累加总和 + for i := 0; i < expr.Times; i++ { + result := rand.Intn(expr.Sides) + 1 // 随机生成 1 到 Sides 的值 + results[i] = result + total += result + } + + return total, results +} diff --git a/handler/scoresaber/score.go b/handler/scoresaber/score.go index f177085..5b0930d 100644 --- a/handler/scoresaber/score.go +++ b/handler/scoresaber/score.go @@ -11,11 +11,18 @@ import ( func init() { handler.RegisterHandler("查ss", getMySS) + handler.RegisterHelpInform("查ss", "查看您的最新分数") handler.RegisterHandler("绑定ss", bindSS) + handler.RegisterHelpInform("绑定ss", "绑定您的scoresaber账号") handler.RegisterHandler("解绑ss", unbindSS) + handler.RegisterHelpInform("解绑ss", "解绑您的scoresaber账号") handler.RegisterHandler("最新ss", getMyRecentScore) + handler.RegisterHelpInform("最新ss", "查看您的最新游戏记录") handler.RegisterHandler("最热ss", getRecentScore) + handler.RegisterHelpInform("最热ss", "查看全大陆的最新游戏记录") handler.RegisterHandler("截ss", screenshotSS) + handler.RegisterHandler("jss", screenshotSS) + handler.RegisterHelpInform("截ss/jss", "scoresaber主页截图") } func getMySS(msg model.Message) (reply model.Reply) { diff --git a/handler/urlparser/url.go b/handler/urlparser/url.go index 191374a..646312e 100644 --- a/handler/urlparser/url.go +++ b/handler/urlparser/url.go @@ -14,7 +14,7 @@ import ( ) func init() { - handler.RegisterFrontMatchHandler("[CQ:json,data=", cqJsonUrlParser) + handler.RegisterFrontMatchHandler("[CQ:json", cqJsonUrlParser) handler.RegisterFrontMatchHandler("http://", plainTextUrlParser) handler.RegisterFrontMatchHandler("https://", plainTextUrlParser) } @@ -50,7 +50,8 @@ func plainTextUrlParser(msg model.Message) (reply model.Reply) { } func cqJsonUrlParser(msg model.Message) (reply model.Reply) { - qqdocurl, err := extractQQDocURL(msg.RawMsg) + newMsg := strings.ReplaceAll(msg.RawMsg, "\n", "") + qqdocurl, err := extractQQDocURL(newMsg) if err != nil { return model.Reply{ ReplyMsg: "", @@ -66,45 +67,67 @@ func cqJsonUrlParser(msg model.Message) (reply model.Reply) { } } +// extractQQDocURL 从字符串中提取 JSON 数据部分 func extractQQDocURL(input string) (string, error) { - // 使用正则表达式提取 JSON 数据部分 + // 使用非贪婪匹配提取 JSON 数据部分 re := regexp.MustCompile(`\{.*\}`) jsonPart := re.FindString(input) if jsonPart == "" { return "", fmt.Errorf("无法找到 JSON 数据部分") } - // 解析 JSON 数据 - var jsonData map[string]interface{} // 替换 HTML 实体为普通字符 jsonPart = strings.ReplaceAll(jsonPart, ",", ",") jsonPart = strings.ReplaceAll(jsonPart, "[", "[") jsonPart = strings.ReplaceAll(jsonPart, "]", "]") jsonPart = strings.ReplaceAll(jsonPart, "&", "&") + url, err := parseQQDocURL(jsonPart) + if err != nil { + return "", fmt.Errorf("解析 JSON 失败: %w", err) + } + return url, nil +} - if err := json.Unmarshal([]byte(jsonPart), &jsonData); err != nil { +// parseQQDocURL 从 JSON 中提取 qqdocurl 字段 +func parseQQDocURL(jsonStr string) (string, error) { + var jsonData map[string]interface{} + + // 解析 JSON 数据 + if err := json.Unmarshal([]byte(jsonStr), &jsonData); err != nil { return "", fmt.Errorf("解析 JSON 失败: %w", err) } - // 定位到 meta -> detail_1 -> qqdocurl - meta, ok := jsonData["meta"].(map[string]interface{}) - if !ok { - return "", fmt.Errorf("找不到 meta 字段") - } - detail, ok := meta["detail_1"].(map[string]interface{}) - if !ok { - return "", fmt.Errorf("找不到 detail_1 字段") - } - qqdocurl, ok := detail["qqdocurl"].(string) - if !ok { - return "", fmt.Errorf("找不到 qqdocurl 字段") + url := "" + + if jsonData["app"] == "com.tencent.miniapp_01" { // 定位到 meta -> detail_1 -> qqdocurl + meta, ok := jsonData["meta"].(map[string]interface{}) + if !ok { + return "", fmt.Errorf("找不到 meta 字段") + } + detail, ok := meta["detail_1"].(map[string]interface{}) + if !ok { + return "", fmt.Errorf("找不到 detail_1 字段") + } + url, ok = detail["qqdocurl"].(string) + if !ok { + return "", fmt.Errorf("找不到 qqdocurl 字段") + } + } else if jsonData["app"] == "com.tencent.structmsg" { // 定位到 meta -> news -> jumpUrl + meta, ok := jsonData["meta"].(map[string]interface{}) + if !ok { + return "", fmt.Errorf("找不到 meta 字段") + } + news, ok := meta["news"].(map[string]interface{}) + if !ok { + return "", fmt.Errorf("找不到 news 字段") + } + url, ok = news["jumpUrl"].(string) + if !ok { + return "", fmt.Errorf("找不到 jumpUrl 字段") + } } - qqdocurl, _ = removeTrackingParams(qqdocurl) - qqdocurl, _ = resolveFinalURL(qqdocurl) - qqdocurl, _ = removeTrackingParams(qqdocurl) - - return qqdocurl, nil + return url, nil } func removeTrackingParams(rawURL string) (string, error) { @@ -144,6 +167,9 @@ func resolveFinalURL(initialURL string) (string, error) { if err != nil { return "", fmt.Errorf("解析重定向地址失败: %w", err) } + if redirectURL.String() == initialURL { + return initialURL, nil + } fmt.Printf("重定向至: %s\n", redirectURL.String()) return resolveFinalURL(redirectURL.String()) } diff --git a/handler/xibao/xibao.go b/handler/xibao/xibao.go index d84ef0b..30d6488 100644 --- a/handler/xibao/xibao.go +++ b/handler/xibao/xibao.go @@ -8,7 +8,9 @@ import ( func init() { handler.RegisterHandler("喜报", xiBao) + handler.RegisterHelpInform("喜报", "喜报 [内容] 生成喜报图片,支持换行") handler.RegisterHandler("悲报", beiBao) + handler.RegisterHelpInform("悲报", "悲报 [内容] 生成悲报图片,支持换行") } func xiBao(msg model.Message) (reply model.Reply) { diff --git a/register.go b/register.go index afaca47..b0ed8e1 100644 --- a/register.go +++ b/register.go @@ -4,8 +4,12 @@ import ( _ "git.lxtend.com/qqbot/handler/echo" _ "git.lxtend.com/qqbot/handler/getweb" _ "git.lxtend.com/qqbot/handler/headmaster" + _ "git.lxtend.com/qqbot/handler/help" _ "git.lxtend.com/qqbot/handler/jrrp" + _ "git.lxtend.com/qqbot/handler/roll" _ "git.lxtend.com/qqbot/handler/scoresaber" _ "git.lxtend.com/qqbot/handler/urlparser" + + // _ "git.lxtend.com/qqbot/handler/wordle" _ "git.lxtend.com/qqbot/handler/xibao" ) diff --git a/util/web_page_shot.go b/util/web_page_shot.go index ed67cfb..3a32828 100644 --- a/util/web_page_shot.go +++ b/util/web_page_shot.go @@ -74,12 +74,12 @@ func ScreenshotURL(url, output string, width, height int, marginTop, marginBotto err := chromedp.Run(ctx, chromedp.Navigate(url), // 打开网页 ignoreErrors(queryAction), // 等待指定元素 - chromedp.Sleep(5*time.Second), + chromedp.Sleep(10*time.Second), chromedp.ActionFunc(func(ctx context.Context) error { // 自定义截图逻辑 - layoutViewport, _, _, _, _, _, _ := page.GetLayoutMetrics().Do(ctx) + _, _, _, _, cssLayoutViewport, _, _ := page.GetLayoutMetrics().Do(ctx) if width == 0 || height == 0 { - width = int(layoutViewport.ClientWidth) - height = int(layoutViewport.ClientHeight) + width = int(cssLayoutViewport.ClientWidth) + height = int(cssLayoutViewport.ClientHeight) } // 计算调整后的截图区域 clip := &page.Viewport{ @@ -125,6 +125,7 @@ func enableRequestInterception() chromedp.Tasks { network.SetBlockedURLS([]string{ "pagead2.googlesyndication.com", "optimizationguide-pa.googleapis.com", + "static.cloudflareinsights.com", }), } } diff --git a/ws_client/client.go b/ws_client/client.go index 509bdad..24c515a 100644 --- a/ws_client/client.go +++ b/ws_client/client.go @@ -2,9 +2,11 @@ package wsclient import ( "encoding/json" + "fmt" "log" "net/url" + "git.lxtend.com/qqbot/action" "git.lxtend.com/qqbot/constants" "git.lxtend.com/qqbot/handler" "git.lxtend.com/qqbot/model" @@ -70,6 +72,9 @@ func (c *WebSocketClient) receiveMessages() { go func() { reply := handler.MsgInHandler(msg) if reply.ReplyMsg != "" { + if reply.ReferOriginMsg { + reply.ReplyMsg = fmt.Sprintf("%s%s", action.GenReply(reply.FromMsg.OriginMsgId, "", 0, 0, 0), reply.ReplyMsg) + } sendPkg := model.GenSendPkg(reply.FromMsg.UserId, reply.FromMsg.GroupInfo.GroupId, reply.ReplyMsg, false) sendPkgJson, err := json.Marshal(sendPkg) if err != nil {