diff --git a/README.md b/README.md index f61238c793..6633165679 100644 --- a/README.md +++ b/README.md @@ -1068,6 +1068,22 @@ NeteaseCloudMusicApi项目地址:https://binaryify.github.io/NeteaseCloudMusicAp - [x] 重置花名册 + +
+ qq空间表白墙 + + `import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/qzone"` + + - [x] 登录QQ空间 (Cookie过期很快, 要经常登录) + + - [x] 发说说[xxx] + + - [x] (匿名)发表白墙[xxx] + + - [x] [ 同意 | 拒绝 ]表白墙 1,2,3 (最后一个参数是表白墙的序号数组, 用英文逗号连接) + + - [x] 查看[ 等待 | 同意 | 拒绝 | 所有 ]表白墙 0 (最后一个参数是页码, 建议私聊审稿) +
Real-CUGAN清晰术 diff --git a/go.mod b/go.mod index 7bb0a65302..1ea1895050 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.19 require ( github.com/Baidu-AIP/golang-sdk v1.1.1 github.com/Coloured-glaze/gg v1.3.4 - github.com/FloatTech/AnimeAPI v1.5.2-0.20221110071402-5672d8466e21 + github.com/FloatTech/AnimeAPI v1.5.2-0.20221112090201-4a200d6330d5 github.com/FloatTech/floatbox v0.0.0-20221110070748-e0d0b3af3e57 github.com/FloatTech/sqlite v0.5.1 github.com/FloatTech/ttl v0.0.0-20220715042055-15612be72f5b diff --git a/go.sum b/go.sum index 01b020e105..787d78dbec 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym github.com/Coloured-glaze/gg v1.3.4 h1:l31zIF/HaVwkzjrj+A56RGQoSKyKuR1IWtIrqXGFStI= github.com/Coloured-glaze/gg v1.3.4/go.mod h1:Ih5NLNNDHOy3RJbB0EPqGTreIzq/H02TGThIagh8HJg= github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= -github.com/FloatTech/AnimeAPI v1.5.2-0.20221110071402-5672d8466e21 h1:Ao45y4vcH2n5Qx1DPyDPc2Wmt7Lol4/MwY1ZknBhGw4= -github.com/FloatTech/AnimeAPI v1.5.2-0.20221110071402-5672d8466e21/go.mod h1:D3VwaTmT25UM+x/0AULJtJw4Mzyhob5YOa90J5QAX/w= +github.com/FloatTech/AnimeAPI v1.5.2-0.20221112090201-4a200d6330d5 h1:1SpesC37urPid5ChljT7fnq+kyJLDrCuHva9usiJptU= +github.com/FloatTech/AnimeAPI v1.5.2-0.20221112090201-4a200d6330d5/go.mod h1:D3VwaTmT25UM+x/0AULJtJw4Mzyhob5YOa90J5QAX/w= github.com/FloatTech/floatbox v0.0.0-20221110070748-e0d0b3af3e57 h1:1H1QSxBPqq7o4S5/xtl0cI/GOqaiajoBg+156cuK1e4= github.com/FloatTech/floatbox v0.0.0-20221110070748-e0d0b3af3e57/go.mod h1:72tI2fKLhrNpuj4AlE2HSjJOAtEnUEKOx/+dEYSc4FE= github.com/FloatTech/sqlite v0.5.1 h1:IjTdnqMVIVIoIEFXhvh/KKBfYxFvG0tk7Rghz65/DAU= diff --git a/main.go b/main.go index dc32421d99..ba184394b3 100644 --- a/main.go +++ b/main.go @@ -114,6 +114,7 @@ import ( _ "github.com/FloatTech/ZeroBot-Plugin/plugin/nsfw" // nsfw图片识别 _ "github.com/FloatTech/ZeroBot-Plugin/plugin/omikuji" // 浅草寺求签 _ "github.com/FloatTech/ZeroBot-Plugin/plugin/qqwife" // 一群一天一夫一妻制群老婆 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/qzone" // qq空间表白墙 _ "github.com/FloatTech/ZeroBot-Plugin/plugin/realcugan" // realcugan清晰术 _ "github.com/FloatTech/ZeroBot-Plugin/plugin/reborn" // 投胎 _ "github.com/FloatTech/ZeroBot-Plugin/plugin/runcode" // 在线运行代码 diff --git a/plugin/aipaint/aipaint.go b/plugin/aipaint/aipaint.go index f87e6ca544..d9ff4ea996 100644 --- a/plugin/aipaint/aipaint.go +++ b/plugin/aipaint/aipaint.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "net/url" + "os" "regexp" "strconv" "strings" @@ -62,6 +63,17 @@ func init() { // 插件主体 PrivateDataFolder: "aipaint", }) datapath = file.BOTPATH + "/" + engine.DataFolder() + if file.IsNotExist(cfg.file) { + s := serverConfig{} + data, err := json.Marshal(s) + if err != nil { + panic(err) + } + err = os.WriteFile(cfg.file, data, 0666) + if err != nil { + panic(err) + } + } engine.OnPrefixGroup([]string{`ai绘图`, `生成色图`, `生成涩图`, `ai画图`}).SetBlock(true). Handle(func(ctx *zero.Ctx) { err := cfg.load() diff --git a/plugin/bilibili/bilibili.go b/plugin/bilibili/bilibili.go index 9b66d61166..1e90e7563b 100644 --- a/plugin/bilibili/bilibili.go +++ b/plugin/bilibili/bilibili.go @@ -40,6 +40,9 @@ var ( 3: "Superchat", 4: "进入直播间", 5: "标题变动", + 6: "分区变动", + 7: "直播中止", + 8: "直播继续", } cfg = bz.NewCookieConfig("data/Bilibili/config.json") ) @@ -492,13 +495,19 @@ func init() { canvas.DrawString(t, moveW, danmuNow) canvas.SetColor(color.Black) moveW += l + dz - case 4, 5: + case 4, 5, 6, 7, 8: t = danmakuTypeMap[danItem.Type] canvas.SetRGB255(0, 128, 0) l, _ = canvas.MeasureString(t) canvas.DrawString(t, moveW, danmuNow) canvas.SetColor(color.Black) moveW += l + dz + default: + canvas.SetRGB255(0, 128, 0) + l, _ = canvas.MeasureString("未知类型" + strconv.Itoa(int(danItem.Type))) + canvas.DrawString(t, moveW, danmuNow) + canvas.SetColor(color.Black) + moveW += l + dz } if moveW > mcw { mcw = moveW diff --git a/plugin/qzone/README.md b/plugin/qzone/README.md new file mode 100644 index 0000000000..2ba724c9f7 --- /dev/null +++ b/plugin/qzone/README.md @@ -0,0 +1,18 @@ +# qq空间表白墙 + +## 参考 + +* [opq-osc/OPQBot](https://github.com/opq-osc/OPQBot) QQ空间发表说说流程 +* [【Ono】QQ空间协议分析----扫码登录----【1】](https://www.52pojie.cn/thread-1022123-1-1.html) QQ空间扫码登录流程 + +## 优化点 +- [ ] 匿名头像背景颜色优化 +- [ ] 转发消息生成图片气泡背景板 +- [x] 查看说说消息分页 (优先) +- [ ] 加zbp水印 (优先) +- [ ] 发表白墙互动优化, 监听对话 +- [ ] 自动审核稿 +- [x] 一次同意多条说说并发送 (优先) +- [ ] 拒绝说说的时候可发送拒绝消息 +- [ ] 表白墙接入钱包 (待定) + diff --git a/plugin/qzone/model.go b/plugin/qzone/model.go new file mode 100644 index 0000000000..0d47e89a73 --- /dev/null +++ b/plugin/qzone/model.go @@ -0,0 +1,132 @@ +package qzone + +import ( + "fmt" + "os" + + _ "github.com/fumiama/sqlite3" // use sql + "github.com/jinzhu/gorm" +) + +// qdb qq空间数据库全局变量 +var qdb *qzonedb + +// qzonedb qq空间数据库结构体 +type qzonedb gorm.DB + +// initialize 初始化 +func initialize(dbpath string) *qzonedb { + var err error + if _, err = os.Stat(dbpath); err != nil || os.IsNotExist(err) { + // 生成文件 + f, err := os.Create(dbpath) + if err != nil { + return nil + } + defer f.Close() + } + qdb, err := gorm.Open("sqlite3", dbpath) + if err != nil { + panic(err) + } + qdb.AutoMigrate(&qzoneConfig{}).AutoMigrate(&emotion{}) + return (*qzonedb)(qdb) +} + +// qzoneConfig qq空间初始化信息 +type qzoneConfig struct { + ID uint `gorm:"primary_key;AUTO_INCREMENT"` + QQ int64 `gorm:"column:qq;unique;not null"` + Cookie string `gorm:"column:cookie;type:varchar(1024)"` +} + +// TableName 表名 +func (qzoneConfig) TableName() string { + return "qzone_config" +} + +func (qdb *qzonedb) insertOrUpdate(qq int64, cookie string) (err error) { + db := (*gorm.DB)(qdb) + qc := qzoneConfig{ + QQ: qq, + Cookie: cookie, + } + var oqc qzoneConfig + err = db.Take(&oqc, "qq = ?", qc.QQ).Error + if err != nil { + if gorm.IsRecordNotFoundError(err) { + err = db.Create(&qc).Error + } + return + } + err = db.Model(&oqc).Updates(qc).Error + return +} + +func (qdb *qzonedb) getByUin(qq int64) (qc qzoneConfig, err error) { + db := (*gorm.DB)(qdb) + err = db.Take(&qc, "qq = ?", qq).Error + return +} + +// emotion 说说信息 +type emotion struct { + gorm.Model + Anonymous bool `gorm:"column:anonymous"` + QQ int64 `gorm:"column:qq"` + Msg string `gorm:"column:msg"` + Status int `gorm:"column:status"` // 1-审核中,2-同意,3-拒绝 + Tag string `gorm:"column:tag"` +} + +func (e emotion) textBrief() (t string) { + t = fmt.Sprintf("序号: %v\nQQ: %v\n创建时间: %v\n", e.ID, e.QQ, e.CreatedAt.Format("2006-01-02 15:04:05")) + switch e.Status { + case 1: + t += "状态: 审核中\n" + case 2: + t += "状态: 同意\n" + case 3: + t += "状态: 拒绝\n" + } + if e.Anonymous { + t += "匿名: 是" + } else { + t += "匿名: 否" + } + return +} + +// TableName 表名 +func (emotion) TableName() string { + return "emotion" +} + +func (qdb *qzonedb) saveEmotion(e emotion) (id int64, err error) { + db := (*gorm.DB)(qdb) + err = db.Create(&e).Error + id = int64(e.ID) + return +} + +func (qdb *qzonedb) getEmotionByIDList(idList []int64) (el []emotion, err error) { + db := (*gorm.DB)(qdb) + err = db.Find(&el, "id in (?)", idList).Error + return +} + +func (qdb *qzonedb) getLoveEmotionByStatus(status int, pageNum int) (el []emotion, err error) { + db := (*gorm.DB)(qdb) + if status == 0 { + err = db.Order("created_at desc").Limit(5).Offset(pageNum*5).Find(&el, "tag like ?", "%"+loveTag+"%").Error + return + } + err = db.Order("created_at desc").Limit(5).Offset(pageNum*5).Find(&el, "status = ? and tag like ?", status, "%"+loveTag+"%").Error + return +} + +func (qdb *qzonedb) updateEmotionStatusByIDList(idList []int64, status int) (err error) { + db := (*gorm.DB)(qdb) + err = db.Model(&emotion{}).Where("id in (?)", idList).Update("status", status).Error + return +} diff --git a/plugin/qzone/qzone.go b/plugin/qzone/qzone.go new file mode 100644 index 0000000000..4a9f9b248b --- /dev/null +++ b/plugin/qzone/qzone.go @@ -0,0 +1,369 @@ +// Package qzone qq空间表白墙 +package qzone + +import ( + "bytes" + "encoding/base64" + "fmt" + "image" + "image/color" + "math/rand" + "strconv" + "strings" + "time" + + "github.com/Coloured-glaze/gg" + "github.com/FloatTech/AnimeAPI/qzone" + "github.com/FloatTech/floatbox/binary" + "github.com/FloatTech/floatbox/img/writer" + "github.com/FloatTech/floatbox/web" + ctrl "github.com/FloatTech/zbpctrl" + "github.com/FloatTech/zbputils/control" + "github.com/FloatTech/zbputils/ctxext" + "github.com/FloatTech/zbputils/img" + "github.com/FloatTech/zbputils/img/text" + "github.com/jinzhu/gorm" + zero "github.com/wdvxdr1123/ZeroBot" + "github.com/wdvxdr1123/ZeroBot/message" +) + +const ( + waitStatus = iota + 1 + agreeStatus + disagreeStatus + loveTag = "表白" + faceURL = "http://q4.qlogo.cn/g?b=qq&nk=%v&s=640" + anonymousURL = "https://gitcode.net/anto_july/avatar/-/raw/master/%v.png" +) + +func init() { + engine := control.Register("qzone", &ctrl.Options[*zero.Ctx]{ + DisableOnDefault: false, + Brief: "QQ空间表白墙", + Help: "- 登录QQ空间 (Cookie过期很快, 要经常登录)\n" + + "- 发说说[xxx]\n" + + "- (匿名)发表白墙[xxx]\n" + + "- [ 同意 | 拒绝 ]表白墙 1,2,3 (最后一个参数是表白墙的序号数组, 用英文逗号连接)\n" + + "- 查看[ 等待 | 同意 | 拒绝 | 所有 ]表白墙 0 (最后一个参数是页码, 建议私聊审稿)", + PrivateDataFolder: "qzone", + }) + go func() { + qdb = initialize(engine.DataFolder() + "qzone.db") + }() + engine.OnFullMatch("登录QQ空间").SetBlock(true). + Handle(func(ctx *zero.Ctx) { + var ( + qrsig string + ptqrtoken string + ptqrloginCookie string + redirectCookie string + data []byte + err error + ) + data, qrsig, ptqrtoken, err = qzone.Ptqrshow() + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return + } + ctx.SendChain(message.Text("请扫描二维码, 登录QQ空间")) + ctx.SendChain(message.ImageBytes(data)) + for { + time.Sleep(2 * time.Second) + data, ptqrloginCookie, err = qzone.Ptqrlogin(qrsig, ptqrtoken) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return + } + text := binary.BytesToString(data) + + switch { + case strings.Contains(text, "二维码已失效"): + ctx.SendChain(message.Text("二维码已失效, 登录失败")) + return + case strings.Contains(text, "登录成功"): + dealedCheckText := strings.ReplaceAll(text, "'", "") + redirectURL := strings.Split(dealedCheckText, ",")[2] + redirectCookie, err = qzone.LoginRedirect(redirectURL) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return + } + m := qzone.NewManager(ptqrloginCookie + redirectCookie) + qq, err := strconv.ParseInt(m.QQ, 10, 64) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return + } + err = qdb.insertOrUpdate(qq, m.Cookie) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return + } + ctx.SendChain(message.Text("登录成功")) + return + } + } + }) + engine.OnRegex(`^发说说.*?([\s\S]*)`, zero.SuperUserPermission).SetBlock(true). + Handle(func(ctx *zero.Ctx) { + regexMatched := ctx.State["regex_matched"].([]string) + text, base64imgs, err := parseTextAndImg(message.UnescapeCQCodeText(regexMatched[1])) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return + } + err = publishEmotion(ctx.Event.SelfID, text, base64imgs) + if err != nil { + if gorm.IsRecordNotFoundError(err) { + ctx.SendChain(message.Text(zero.BotConfig.NickName[0], "(", ctx.Event.SelfID, ")", "未登录QQ空间,请发送\"登录QQ空间\"初始化配置")) + return + } + ctx.SendChain(message.Text("ERROR: ", err)) + return + } + ctx.SendChain(message.Text("发表成功")) + }) + engine.OnRegex(`^(.{0,2})发表白墙.*?([\s\S]*)`).SetBlock(true). + Handle(func(ctx *zero.Ctx) { + regexMatched := ctx.State["regex_matched"].([]string) + if strings.TrimSpace(regexMatched[2]) == "" { + ctx.SendChain(message.Text("请不要发送空内容")) + return + } + qq := ctx.Event.UserID + e := emotion{ + QQ: qq, + Msg: message.UnescapeCQCodeText(regexMatched[2]), + Status: waitStatus, + Tag: loveTag, + Anonymous: false, + } + if regexMatched[1] == "匿名" { + e.Anonymous = true + } + _, err := qdb.saveEmotion(e) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return + } + ctx.SendChain(message.Text("已收稿, 请耐心等待审核")) + }) + engine.OnRegex(`^(同意|拒绝)表白墙\s?((?:\d+,){0,8}\d+)$`, zero.SuperUserPermission).SetBlock(true). + Handle(func(ctx *zero.Ctx) { + var err error + var ti int64 + regexMatched := ctx.State["regex_matched"].([]string) + idStrList := strings.Split(regexMatched[2], ",") + idList := make([]int64, 0, len(idStrList)) + for _, v := range idStrList { + ti, err = strconv.ParseInt(v, 10, 64) + if err != nil { + return + } + idList = append(idList, ti) + } + switch regexMatched[1] { + case "同意": + err = getAndPublishEmotion(ctx.Event.SelfID, idList) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return + } + err = qdb.updateEmotionStatusByIDList(idList, agreeStatus) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return + } + ctx.SendChain(message.Text("同意表白墙", regexMatched[2], ", 发表成功")) + case "拒绝": + err = qdb.updateEmotionStatusByIDList(idList, disagreeStatus) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return + } + ctx.SendChain(message.Text("拒绝表白墙", regexMatched[2])) + } + }) + engine.OnRegex(`^查看(.{0,2})表白墙\s?(\d*)$`, zero.SuperUserPermission).SetBlock(true). + Handle(func(ctx *zero.Ctx) { + var ( + pageNum int + err error + base64Str []byte + ) + regexMatched := ctx.State["regex_matched"].([]string) + if regexMatched[2] == "" { + pageNum = 0 + } else { + pageNum, err = strconv.Atoi(regexMatched[2]) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return + } + } + var status int + switch regexMatched[1] { + case "等待": + status = 1 + case "同意": + status = 2 + case "拒绝": + status = 3 + case "所有": + status = 0 + default: + status = 1 + } + el, err := qdb.getLoveEmotionByStatus(status, pageNum) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return + } + if len(el) == 0 { + ctx.SendChain(message.Text("ERROR: 当前表白墙数量为0")) + return + } + m := message.Message{} + for _, v := range el { + t := v.textBrief() + "\n呢称: " + ctx.CardOrNickName(v.QQ) + base64Str, err = text.RenderToBase64(t, text.FontFile, 400, 20) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return + } + m = append(m, ctxext.FakeSenderForwardNode(ctx, message.Image("base64://"+binary.BytesToString(base64Str)))) + base64Str, err = renderForwardMsg(v.QQ, v.Msg) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return + } + m = append(m, ctxext.FakeSenderForwardNode(ctx, message.Image("base64://"+binary.BytesToString(base64Str)))) + } + time.Sleep(time.Second) + if id := ctx.Send(m).ID(); id == 0 { + ctx.SendChain(message.Text("ERROR: 可能被风控或下载图片用时过长,请耐心等待")) + } + }) +} + +func getAndPublishEmotion(botqq int64, idList []int64) (err error) { + var b []byte + el, err := qdb.getEmotionByIDList(idList) + if err != nil { + return + } + base64imgs := make([]string, 0, 5) + for _, v := range el { + if v.Anonymous { + v.QQ = 0 + } + b, err = renderForwardMsg(v.QQ, v.Msg) + if err != nil { + return + } + base64imgs = append(base64imgs, binary.BytesToString(b)) + } + return publishEmotion(botqq, "", base64imgs) +} + +func publishEmotion(botqq int64, text string, base64imgs []string) (err error) { + qc, err := qdb.getByUin(botqq) + if err != nil { + return + } + m := qzone.NewManager(qc.Cookie) + _, err = m.EmotionPublish(text, base64imgs) + return +} + +func parseTextAndImg(raw string) (text string, base64imgs []string, err error) { + base64imgs = make([]string, 0, 16) + var imgdata []byte + m := message.ParseMessageFromString(raw) + for _, v := range m { + if v.Type == "text" && v.Data["text"] != "" { + text += v.Data["text"] + "\n" + } + if v.Type == "image" && v.Data["url"] != "" { + imgdata, err = web.GetData(v.Data["url"]) + if err != nil { + return + } + encodeStr := base64.StdEncoding.EncodeToString(imgdata) + base64imgs = append(base64imgs, encodeStr) + } + } + return +} + +func renderForwardMsg(qq int64, raw string) (base64Bytes []byte, err error) { + canvas := gg.NewContext(1000, 1000) + canvas.SetRGB255(229, 229, 229) + canvas.Clear() + canvas.SetColor(color.Black) + var ( + maxHeight = 0 + maxWidth = 0 + backX = 200 + backY = 200 + margin = 50 + face []byte + imgdata []byte + msgImg image.Image + faceImg image.Image + t text.Text + ) + if qq != 0 { + face, err = web.GetData(fmt.Sprintf(faceURL, qq)) + } else { + face, err = web.RequestDataWith(web.NewTLS12Client(), fmt.Sprintf(anonymousURL, rand.Intn(4)+1), "GET", "gitcode.net", web.RandUA()) + } + if err != nil { + return + } + faceImg, _, err = image.Decode(bytes.NewReader(face)) + if err != nil { + return + } + back := img.Size(faceImg, backX, backY).Circle(0).Im + m := message.ParseMessageFromString(raw) + maxHeight += margin + + for _, v := range m { + switch { + case v.Type == "text" && strings.TrimSpace(v.Data["text"]) != "": + t, err = text.Render(strings.TrimSuffix(v.Data["text"], "\r\n"), text.FontFile, 400, 40) + if err != nil { + return + } + msgImg = t.Image() + case v.Type == "image" && v.Data["url"] != "": + imgdata, err = web.GetData(v.Data["url"]) + if err != nil { + return + } + msgImg, _, err = image.Decode(bytes.NewReader(imgdata)) + if err != nil { + return + } + default: + continue + } + canvas.DrawImage(back, margin, maxHeight) + if msgImg.Bounds().Dx() > 500 { + msgImg = img.Size(msgImg, 500, msgImg.Bounds().Dy()*500/msgImg.Bounds().Dx()).Im + } + canvas.DrawImage(msgImg, 2*margin+backX, maxHeight) + if 3*margin+backX+msgImg.Bounds().Dx() > maxWidth { + maxWidth = 3*margin + backX + msgImg.Bounds().Dx() + } + if msgImg.Bounds().Dy() > backY { + maxHeight += msgImg.Bounds().Dy() + margin + } else { + maxHeight += backY + margin + } + } + im := canvas.Image().(*image.RGBA) + nim := im.SubImage(image.Rect(0, 0, maxWidth, maxHeight)) + return writer.ToBase64(nim) +}