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)
+}