diff --git a/README.md b/README.md index 8041d77df2..843969acbf 100644 --- a/README.md +++ b/README.md @@ -655,6 +655,8 @@ print("run[CQ:image,file="+j["img"]+"]") - [x] 齁语解密 [密文] 或 h解密 [密文] - [x] fumo加密 [文本] - [x] fumo解密 [文本] + - [x] qq加密 [文本] + - [x] qq解密 [密文]
@@ -768,7 +770,7 @@ print("run[CQ:image,file="+j["img"]+"]") - [x] 运势 | 抽签 - - [x] 设置底图[车万 DC4 爱因斯坦 星空列车 樱云之恋 富婆妹 李清歌 公主连结 原神 明日方舟 碧蓝航线 碧蓝幻想 战双 阴阳师 赛马娘 东方归言录 奇异恩典 夏日口袋 ASoul Hololive] + - [x] 设置底图[车万 车万2 DC4 爱因斯坦 星空列车 樱云之恋 富婆妹 李清歌 公主连结 原神 明日方舟 碧蓝航线 碧蓝幻想 战双 阴阳师 赛马娘 东方归言录 奇异恩典 夏日口袋 ASoul Hololive]
diff --git a/go.mod b/go.mod index 4b2cf8ab3b..fdf90ee993 100644 --- a/go.mod +++ b/go.mod @@ -59,6 +59,7 @@ require ( github.com/PuerkitoBio/goquery v1.8.0 // indirect github.com/adamzy/cedar-go v0.0.0-20170805034717-80a9c64b256d // indirect github.com/ajstarks/svgo v0.0.0-20200320125537-f189e35d30ca // indirect + github.com/andybalholm/brotli v1.2.0 // indirect github.com/andybalholm/cascadia v1.3.1 // indirect github.com/antchfx/xpath v1.3.5 // indirect github.com/dustin/go-humanize v1.0.1 // indirect @@ -76,6 +77,7 @@ require ( github.com/jfreymuth/vorbis v1.0.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.18.4 // indirect github.com/liuzl/cedar-go v0.0.0-20170805034717-80a9c64b256d // indirect github.com/liuzl/da v0.0.0-20180704015230-14771aad5b1d // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect diff --git a/go.sum b/go.sum index 6ec7775d99..5923022227 100644 --- a/go.sum +++ b/go.sum @@ -31,6 +31,8 @@ github.com/adamzy/cedar-go v0.0.0-20170805034717-80a9c64b256d h1:ir/IFJU5xbja5Ua github.com/adamzy/cedar-go v0.0.0-20170805034717-80a9c64b256d/go.mod h1:PRWNwWq0yifz6XDPZu48aSld8BWwBfr2JKB2bGWiEd4= github.com/ajstarks/svgo v0.0.0-20200320125537-f189e35d30ca h1:kWzLcty5V2rzOqJM7Tp/MfSX0RMSI1x4IOLApEefYxA= github.com/ajstarks/svgo v0.0.0-20200320125537-f189e35d30ca/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= +github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= +github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= @@ -135,6 +137,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kanrichan/resvg-go v0.0.2-0.20231001163256-63db194ca9f5 h1:BXnB1Gz4y/zwQh+ZFNy7rgd+ZfMOrwRr4uZSHEI+ieY= github.com/kanrichan/resvg-go v0.0.2-0.20231001163256-63db194ca9f5/go.mod h1:c9+VS9GaommgIOzNWb5ze4lYwfT8BZ2UDyGiuQTT7yc= +github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= +github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4= github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= diff --git a/plugin/crypter/handlers.go b/plugin/crypter/handlers.go index 45a51b805f..ce0bd19e6b 100644 --- a/plugin/crypter/handlers.go +++ b/plugin/crypter/handlers.go @@ -2,18 +2,105 @@ package crypter import ( + "fmt" + "regexp" + "strconv" + "strings" + "github.com/FloatTech/AnimeAPI/airecord" zero "github.com/wdvxdr1123/ZeroBot" "github.com/wdvxdr1123/ZeroBot/message" ) +var faceTagRe = regexp.MustCompile(`\{\{face:(\d+)\}\}`) + +func parseID(v interface{}) int64 { + n, _ := strconv.ParseInt(fmt.Sprintf("%v", v), 10, 64) + return n +} + +func serializeMsg(segs message.Message) string { + var sb strings.Builder + for _, seg := range segs { + switch seg.Type { + case "text": + sb.WriteString(fmt.Sprintf("%v", seg.Data["text"])) + case "face": + fmt.Fprintf(&sb, "{{face:%v}}", seg.Data["id"]) + } + } + return sb.String() +} + +func deserializeMsg(s string) message.Message { + var msg message.Message + last := 0 + for _, loc := range faceTagRe.FindAllStringSubmatchIndex(s, -1) { + if loc[0] > last { + msg = append(msg, message.Text(s[last:loc[0]])) + } + id, _ := strconv.Atoi(s[loc[2]:loc[3]]) + msg = append(msg, message.Face(id)) + last = loc[1] + } + if last < len(s) { + msg = append(msg, message.Text(s[last:])) + } + return msg +} + +func getInput(ctx *zero.Ctx, cmds ...string) string { + full := serializeMsg(ctx.Event.Message) + for _, cmd := range cmds { + if idx := strings.Index(full, cmd); idx >= 0 { + return strings.TrimSpace(full[idx+len(cmd):]) + } + } + return "" +} + +func getReplyContent(ctx *zero.Ctx) string { + for _, seg := range ctx.Event.Message { + if seg.Type == "reply" { + if msgID := parseID(seg.Data["id"]); msgID > 0 { + if msg := ctx.GetMessage(msgID); msg.Elements != nil { + return serializeMsg(msg.Elements) + } + } + } + } + return "" +} + +func getReplyFaceIDs(ctx *zero.Ctx) []int { + for _, seg := range ctx.Event.Message { + if seg.Type == "reply" { + if msgID := parseID(seg.Data["id"]); msgID > 0 { + return extractFaceIDs(ctx.GetMessage(msgID).Elements) + } + } + } + return nil +} + +func extractFaceIDs(segs message.Message) []int { + var ids []int + for _, seg := range segs { + if seg.Type == "face" { + if id := int(parseID(seg.Data["id"])); id > 0 { + ids = append(ids, id) + } + } + } + return ids +} + // hou func houEncryptHandler(ctx *zero.Ctx) { - text := ctx.State["regex_matched"].([]string)[1] + text := getInput(ctx, "h加密", "齁语加密") result := encodeHou(text) recCfg := airecord.GetConfig() - record := ctx.GetAIRecord(recCfg.ModelID, recCfg.Customgid, result) - if record != "" { + if record := ctx.GetAIRecord(recCfg.ModelID, recCfg.Customgid, result); record != "" { ctx.SendChain(message.Record(record)) } else { ctx.SendChain(message.Text(result)) @@ -21,20 +108,52 @@ func houEncryptHandler(ctx *zero.Ctx) { } func houDecryptHandler(ctx *zero.Ctx) { - text := ctx.State["regex_matched"].([]string)[1] - result := decodeHou(text) - ctx.SendChain(message.Text(result)) + text := getInput(ctx, "h解密", "齁语解密") + if text == "" { + text = getReplyContent(ctx) + } + if text == "" { + ctx.SendChain(message.Text("请输入密文或回复加密消息")) + return + } + ctx.SendChain(deserializeMsg(decodeHou(text))...) } // fumo func fumoEncryptHandler(ctx *zero.Ctx) { - text := ctx.State["regex_matched"].([]string)[1] - result := encryptFumo(text) - ctx.SendChain(message.Text(result)) + ctx.SendChain(message.Text(encryptFumo(getInput(ctx, "fumo加密")))) } func fumoDecryptHandler(ctx *zero.Ctx) { - text := ctx.State["regex_matched"].([]string)[1] - result := decryptFumo(text) - ctx.SendChain(message.Text(result)) + text := getInput(ctx, "fumo解密") + if text == "" { + text = getReplyContent(ctx) + } + if text == "" { + ctx.SendChain(message.Text("请输入密文或回复加密消息")) + return + } + ctx.SendChain(deserializeMsg(decryptFumo(text))...) +} + +// qq表情 +func qqEmojiEncryptHandler(ctx *zero.Ctx) { + text := getInput(ctx, "qq加密") + if text == "" { + ctx.SendChain(message.Text("请输入要加密的文本")) + return + } + ctx.SendChain(encodeQQEmoji(text)...) +} + +func qqEmojiDecryptHandler(ctx *zero.Ctx) { + faceIDs := extractFaceIDs(ctx.Event.Message) + if len(faceIDs) == 0 { + faceIDs = getReplyFaceIDs(ctx) + } + if len(faceIDs) == 0 { + ctx.SendChain(message.Text("请回复QQ表情加密消息进行解密")) + return + } + ctx.SendChain(deserializeMsg(decodeQQEmoji(faceIDs))...) } diff --git a/plugin/crypter/main.go b/plugin/crypter/main.go index caf38668af..c1c539bac8 100644 --- a/plugin/crypter/main.go +++ b/plugin/crypter/main.go @@ -17,15 +17,25 @@ func init() { "- 齁语解密 [密文] 或 h解密 [密文]\n\n" + "- Fumo语加解密:\n" + "- fumo加密 [文本]\n" + - "- fumo解密 [密文]\n\n", + "- fumo解密 [密文]\n\n" + + "- QQ表情加解密:\n" + + "- qq加密 [文本]\n" + + "- qq解密 [密文]\n\n" + + "注意:QQ表情解密建议使用回复,尽量不要复制粘贴\n\n", PublicDataFolder: "Crypter", }) + re := `(?:\[CQ:reply,id=-?\d+\])?` + // hou - engine.OnRegex(`^(?:齁语加密|h加密)\s*(.+)$`).SetBlock(true).Handle(houEncryptHandler) - engine.OnRegex(`^(?:齁语解密|h解密)\s*(.+)$`).SetBlock(true).Handle(houDecryptHandler) + engine.OnRegex(re + `^(?:齁语加密|h加密)\s*(.+)$`).SetBlock(true).Handle(houEncryptHandler) + engine.OnRegex(re + `(?:齁语解密|h解密)\s*(.*)$`).SetBlock(true).Handle(houDecryptHandler) // Fumo - engine.OnRegex(`^fumo加密\s*(.+)$`).SetBlock(true).Handle(fumoEncryptHandler) - engine.OnRegex(`^fumo解密\s*(.+)$`).SetBlock(true).Handle(fumoDecryptHandler) + engine.OnRegex(re + `^fumo加密\s*(.+)$`).SetBlock(true).Handle(fumoEncryptHandler) + engine.OnRegex(re + `fumo解密\s*(.*)$`).SetBlock(true).Handle(fumoDecryptHandler) + + // QQ表情 + engine.OnRegex(re + `^qq加密\s*(.+)$`).SetBlock(true).Handle(qqEmojiEncryptHandler) + engine.OnRegex(re + `qq解密`).SetBlock(true).Handle(qqEmojiDecryptHandler) } diff --git a/plugin/crypter/qqemoji.go b/plugin/crypter/qqemoji.go new file mode 100644 index 0000000000..f59b02c61a --- /dev/null +++ b/plugin/crypter/qqemoji.go @@ -0,0 +1,131 @@ +// Package crypter QQ表情加解密 +package crypter + +import ( + "bytes" + "fmt" + "io" + "strings" + "unicode/utf8" + + "github.com/andybalholm/brotli" + "github.com/klauspost/compress/zstd" + "github.com/wdvxdr1123/ZeroBot/message" +) + +const ( + emojiZeroID = 297 + emojiOneID = 424 +) + +func encodeQQEmoji(text string) message.Message { + if text == "" { + return message.Message{message.Text("请输入要加密的文本")} + } + + data := []byte(text) + best, header := data, "0" + if br := tryCompress(func(w io.Writer) io.WriteCloser { return brotli.NewWriterLevel(w, brotli.BestCompression) }, data); len(br) > 0 && len(br) < len(best) { + best, header = br, "10" + } + if zs := tryCompress(func(w io.Writer) io.WriteCloser { + enc, _ := zstd.NewWriter(w, zstd.WithEncoderLevel(zstd.SpeedBestCompression)) + return enc + }, data); len(zs) > 0 && len(zs) < len(best) { + best, header = zs, "11" + } + + var bin strings.Builder + bin.WriteString(header) + for _, b := range best { + fmt.Fprintf(&bin, "%08b", b) + } + + s := bin.String() + msg := make(message.Message, 0, len(s)) + for _, bit := range s { + if bit == '0' { + msg = append(msg, message.Face(emojiZeroID)) + } else { + msg = append(msg, message.Face(emojiOneID)) + } + } + return msg +} + +func decodeQQEmoji(faceIDs []int) string { + var bin strings.Builder + for _, id := range faceIDs { + if id == emojiZeroID { + bin.WriteByte('0') + } else if id == emojiOneID { + bin.WriteByte('1') + } + } + binary := bin.String() + if len(binary) < 2 { + return "QQ表情密文格式错误" + } + + var header int + switch { + case binary[:2] == "11": + header = 2 + case binary[:2] == "10": + header = 2 + case binary[0] == '0': + header = 1 + default: + return "QQ表情密文格式错误" + } + + dataBin := binary[header:] + if len(dataBin)%8 != 0 { + return fmt.Sprintf("QQ表情解密失败:数据长度不正确(%d位)", len(dataBin)) + } + + data := make([]byte, len(dataBin)/8) + for i := range data { + for j := 0; j < 8; j++ { + if dataBin[i*8+j] == '1' { + data[i] |= 1 << (7 - j) + } + } + } + + var out []byte + var err error + switch binary[:header] { + case "0": + out = data + case "10": + r := brotli.NewReader(bytes.NewReader(data)) + out, err = io.ReadAll(r) + case "11": + var dec *zstd.Decoder + dec, err = zstd.NewReader(bytes.NewReader(data)) + if err == nil { + out, err = io.ReadAll(dec) + dec.Close() + } + } + if err != nil { + return fmt.Sprintf("QQ表情解压失败: %v", err) + } + if !utf8.Valid(out) { + return "QQ表情解密失败:结果不是有效文本" + } + return string(out) +} + +func tryCompress(newWriter func(io.Writer) io.WriteCloser, data []byte) []byte { + var buf bytes.Buffer + w := newWriter(&buf) + if _, err := w.Write(data); err != nil { + return nil + } + if err := w.Close(); err != nil { + return nil + } + return buf.Bytes() +}