Feature danmakusuki (#388)

*  添加查弹幕

* 🎨 添加注释

* 🎨 优化代码

* 🎨 修lint

Co-authored-by: 源文雨 <41315874+fumiama@users.noreply.github.com>
This commit is contained in:
himawari 2022-08-27 12:13:15 +08:00 committed by GitHub
parent 0c637d4c8e
commit e5f186ace1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 393 additions and 53 deletions

View File

@ -388,7 +388,9 @@ print("run[CQ:image,file="+j["img"]+"]")
- [x] 查成分 [xxx]
- [x] 设置b站cookie SESSDATA=82da790d,1663822823,06ecf\*31
- [x] 查弹幕 [xxx] 2 (最后一个参数是页码)
- [x] 设置b站cookie SESSDATA=82da790d,1663822823,06ecf\*31 (最好把cookie设全)
- [x] 更新vup

View File

@ -1,3 +1,4 @@
// package main 主函数
package main
import (

View File

@ -18,19 +18,32 @@ var (
// searchUser 查找b站用户
func searchUser(keyword string) (r []searchResult, err error) {
data, err := web.GetData(fmt.Sprintf(searchUserURL, keyword))
client := &http.Client{}
req, err := http.NewRequest("GET", fmt.Sprintf(searchUserURL, keyword), nil)
if err != nil {
return
}
j := gjson.ParseBytes(data)
if j.Get("data.numResults").Int() == 0 {
err = errors.New("查无此人")
return
}
err = json.Unmarshal(binary.StringToBytes(j.Get("data.result").Raw), &r)
err = reflushBilibiliCookie()
if err != nil {
return
}
req.Header.Add("cookie", cfg.BilibiliCookie)
res, err := client.Do(req)
if err != nil {
return
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
s := fmt.Sprintf("status code: %d", res.StatusCode)
err = errors.New(s)
return
}
var sd searchData
err = json.NewDecoder(res.Body).Decode(&sd)
if err != nil {
return
}
r = sd.Data.Result
return
}
@ -66,8 +79,11 @@ func getMedalwall(uid string) (result []medal, err error) {
if err != nil {
return
}
c := vdb.getBilibiliCookie()
req.Header.Add("cookie", c.Value)
err = reflushBilibiliCookie()
if err != nil {
return
}
req.Header.Add("cookie", cfg.BilibiliCookie)
res, err := client.Do(req)
if err != nil {
return

View File

@ -2,10 +2,13 @@
package bilibili
import (
"crypto/tls"
"encoding/binary"
"encoding/json"
"fmt"
"image"
"image/color"
"net/http"
"os"
"path"
"regexp"
@ -27,16 +30,29 @@ import (
"github.com/wdvxdr1123/ZeroBot/message"
)
var re = regexp.MustCompile(`^\d+$`)
var (
re = regexp.MustCompile(`^\d+$`)
danmakuTypeMap = map[int64]string{
0: "普通消息",
1: "礼物",
2: "上舰",
3: "Superchat",
4: "进入直播间",
5: "标题变动",
}
cfgFile = "data/Bilibili/config.json"
cfg config
)
// 查成分的
func init() {
engine := control.Register("bilibili", &ctrl.Options[*zero.Ctx]{
DisableOnDefault: false,
Help: "bilibili\n" +
Help: "bilibili (412就是拦截的意思,建议私聊把cookie设全)\n" +
"- >vup info [xxx]\n" +
"- >user info [xxx]\n" +
"- 查成分 [xxx]\n" +
"- 查弹幕 [xxx]\n" +
"- 设置b站cookie SESSDATA=82da790d,1663822823,06ecf*31\n" +
"- 更新vup",
PublicDataFolder: "Bilibili",
@ -49,12 +65,11 @@ func init() {
_, _ = engine.GetLazyData("bilibili.db", false)
vdb, err = initializeVup(engine.DataFolder() + "bilibili.db")
if err != nil {
ctx.SendChain(message.Text("ERROR: ", err))
ctx.SendChain(message.Text("ERROR:", err))
return false
}
return true
})
engine.OnRegex(`^>user info\s?(.{1,25})$`, getPara).SetBlock(true).
Handle(func(ctx *zero.Ctx) {
id := ctx.State["uid"].(string)
@ -96,7 +111,7 @@ func init() {
))
})
engine.OnRegex(`^查成分\s?(.{1,25})$`, getdb, getPara).SetBlock(true).
engine.OnRegex(`^查成分\s?(.{1,25})$`, getPara, getdb).SetBlock(true).
Handle(func(ctx *zero.Ctx) {
id := ctx.State["uid"].(string)
today := time.Now().Format("20060102")
@ -254,10 +269,265 @@ func init() {
ctx.SendChain(message.Image("file:///" + file.BOTPATH + "/" + drawedFile))
})
engine.OnRegex(`^设置b站cookie?\s+(.{1,100})$`, zero.SuperUserPermission, getdb).SetBlock(true).
engine.OnRegex(`^查弹幕\s?(\S{1,25})\s?(\d*)$`, getPara).SetBlock(true).Handle(func(ctx *zero.Ctx) {
id := ctx.State["uid"].(string)
pagenum := ctx.State["regex_matched"].([]string)[2]
if pagenum == "" {
pagenum = "0"
}
u, err := getMemberCard(id)
if err != nil {
ctx.SendChain(message.Text("ERROR:", err))
return
}
var danmaku danmakusuki
tr := &http.Transport{
DisableKeepAlives: true,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client := &http.Client{Transport: tr}
data, err := web.RequestDataWith(client, fmt.Sprintf(danmakuAPI, id, pagenum), "GET", "", web.RandUA())
if err != nil {
ctx.SendChain(message.Text("Error:", err))
return
}
err = json.Unmarshal(data, &danmaku)
if err != nil {
ctx.SendChain(message.Text("Error:", err))
return
}
today := time.Now().Format("20060102150415")
drawedFile := cachePath + id + today + "vupLike.png"
facePath := cachePath + id + "vupFace" + path.Ext(u.Face)
backX := 500
backY := 500
var back image.Image
if path.Ext(u.Face) != ".webp" {
err = initFacePic(facePath, u.Face)
if err != nil {
ctx.SendChain(message.Text("ERROR:", err))
return
}
back, err = gg.LoadImage(facePath)
if err != nil {
ctx.SendChain(message.Text("ERROR:", err))
return
}
back = img.Size(back, backX, backY).Im
}
canvas := gg.NewContext(100, 100)
fontSize := 50.0
_, err = file.GetLazyData(text.BoldFontFile, true)
if err != nil {
ctx.SendChain(message.Text("ERROR:", err))
}
if err = canvas.LoadFontFace(text.BoldFontFile, fontSize); err != nil {
ctx.SendChain(message.Text("ERROR:", err))
return
}
dz, h := canvas.MeasureString("好")
danmuH := h * 2
faceH := float64(510)
totalDanmuku := 0
for i := 0; i < len(danmaku.Data.Data); i++ {
totalDanmuku += len(danmaku.Data.Data[i].Danmakus) + 1
}
cw := 10000
mcw := float64(2000)
ch := 550 + len(danmaku.Data.Data)*int(faceH) + totalDanmuku*int(danmuH)
canvas = gg.NewContext(cw, ch)
canvas.SetColor(color.White)
canvas.Clear()
canvas.SetColor(color.Black)
if err = canvas.LoadFontFace(text.BoldFontFile, fontSize); err != nil {
ctx.SendChain(message.Text("ERROR:", err))
return
}
facestart := 100
fontH := h * 1.6
startWidth := float64(700)
startWidth2 := float64(20)
if back != nil {
canvas.DrawImage(back, facestart, 0)
}
length, _ := canvas.MeasureString(u.Mid)
n, _ := canvas.MeasureString(u.Name)
canvas.DrawString(u.Name, startWidth, 122.5)
canvas.DrawRoundedRectangle(900+n-length*0.1, 66, length*1.2, 75, fontSize*0.2)
canvas.SetRGB255(221, 221, 221)
canvas.Fill()
canvas.SetColor(color.Black)
canvas.DrawString(u.Mid, 900+n, 122.5)
canvas.DrawString(fmt.Sprintf("粉丝:%d 关注:%d", u.Fans, u.Attention), startWidth, 222.5)
canvas.DrawString(fmt.Sprintf("页码:[%d/%d]", danmaku.Data.PageNum, (danmaku.Data.Total-1)/5), startWidth, 322.5)
canvas.DrawString("网页链接: "+fmt.Sprintf(danmakuURL, u.Mid), startWidth, 422.5)
var channelStart float64
channelStart = float64(550)
for i := 0; i < len(danmaku.Data.Data); i++ {
item := danmaku.Data.Data[i]
facePath = cachePath + strconv.Itoa(int(item.Channel.UID)) + "vupFace" + path.Ext(item.Channel.FaceURL)
if path.Ext(item.Channel.FaceURL) != ".webp" {
err = initFacePic(facePath, item.Channel.FaceURL)
if err != nil {
ctx.SendChain(message.Text("ERROR:", err))
return
}
back, err = gg.LoadImage(facePath)
if err != nil {
ctx.SendChain(message.Text("ERROR:", err))
return
}
back = img.Size(back, backX, backY).Im
}
if back != nil {
canvas.DrawImage(back, facestart, int(channelStart))
}
canvas.SetRGB255(24, 144, 255)
canvas.DrawString("标题: "+item.Live.Title, startWidth, channelStart+fontH)
canvas.DrawString("主播: "+item.Channel.Name, startWidth, channelStart+fontH*2)
canvas.SetColor(color.Black)
canvas.DrawString("开始时间: "+time.UnixMilli(item.Live.StartDate).Format("2006-01-02 15:04:05"), startWidth, channelStart+fontH*3)
if item.Live.IsFinish {
canvas.DrawString("结束时间: "+time.UnixMilli(item.Live.StopDate).Format("2006-01-02 15:04:05"), startWidth, channelStart+fontH*4)
canvas.DrawString("直播时长: "+strconv.FormatFloat(float64(item.Live.StopDate-item.Live.StartDate)/3600000.0, 'f', 1, 64)+"小时", startWidth, channelStart+fontH*5)
} else {
t := "结束时间:"
l, _ := canvas.MeasureString(t)
canvas.DrawString(t, startWidth, channelStart+fontH*4)
canvas.SetRGB255(0, 128, 0)
t = "正在直播"
canvas.DrawString(t, startWidth+l*1.1, channelStart+fontH*4)
canvas.SetColor(color.Black)
canvas.DrawString("直播时长: "+strconv.FormatFloat(float64(time.Now().UnixMilli()-item.Live.StartDate)/3600000.0, 'f', 1, 64)+"小时", startWidth, channelStart+fontH*5)
}
canvas.DrawString("弹幕数量: "+strconv.Itoa(int(item.Live.DanmakusCount)), startWidth, channelStart+fontH*6)
canvas.DrawString("观看次数: "+strconv.Itoa(int(item.Live.WatchCount)), startWidth, channelStart+fontH*7)
t := "收益:"
l, _ := canvas.MeasureString(t)
canvas.DrawString(t, startWidth, channelStart+fontH*8)
t = "¥" + strconv.Itoa(int(item.Live.TotalIncome))
canvas.SetRGB255(255, 0, 0)
canvas.DrawString(t, startWidth+l*1.1, channelStart+fontH*8)
canvas.SetColor(color.Black)
DanmakuStart := channelStart + faceH
for i := 0; i < len(item.Danmakus); i++ {
moveW := startWidth2
danmuNow := DanmakuStart + danmuH*float64(i+1)
danItem := item.Danmakus[i]
t := time.UnixMilli(danItem.SendDate).Format("15:04:05")
l, _ := canvas.MeasureString(t)
canvas.DrawString(t, moveW, danmuNow)
moveW += l + dz
t = danItem.Name
l, _ = canvas.MeasureString(t)
canvas.SetRGB255(24, 144, 255)
canvas.DrawString(t, moveW, danmuNow)
canvas.SetColor(color.Black)
moveW += l + dz
switch danItem.Type {
case 0:
t = danItem.Message
l, _ = canvas.MeasureString(t)
canvas.DrawString(t, moveW, danmuNow)
moveW += l + dz
case 1:
t = danmakuTypeMap[danItem.Type]
l, _ = canvas.MeasureString(t)
canvas.SetRGB255(255, 0, 0)
canvas.DrawString(t, moveW, danmuNow)
moveW += l + dz
t = danItem.Message
l, _ = canvas.MeasureString(t)
canvas.DrawString(t, moveW, danmuNow)
canvas.SetColor(color.Black)
moveW += l + dz
case 2, 3:
t = danmakuTypeMap[danItem.Type]
l, _ = canvas.MeasureString(t)
if danItem.Type == 3 {
canvas.SetRGB255(0, 85, 255)
} else {
canvas.SetRGB255(128, 0, 128)
}
canvas.DrawString(t, moveW, danmuNow)
moveW += l + dz
t = danItem.Message
l, _ = canvas.MeasureString(t)
canvas.DrawString(t, moveW, danmuNow)
moveW += l
t = "["
l, _ = canvas.MeasureString(t)
canvas.DrawString(t, moveW, danmuNow)
moveW += l
t = "¥" + strconv.FormatFloat(danItem.Price, 'f', 1, 64)
l, _ = canvas.MeasureString(t)
canvas.SetRGB255(255, 0, 0)
canvas.DrawString(t, moveW, danmuNow)
if danItem.Type == 3 {
canvas.SetRGB255(0, 85, 255)
} else {
canvas.SetRGB255(128, 0, 128)
}
moveW += l
t = "]"
l, _ = canvas.MeasureString(t)
canvas.DrawString(t, moveW, danmuNow)
canvas.SetColor(color.Black)
moveW += l + dz
case 4, 5:
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
}
if moveW > mcw {
mcw = moveW
}
}
channelStart = DanmakuStart + float64(len(item.Danmakus)+1)*danmuH
}
im := canvas.Image().(*image.RGBA)
nim := im.SubImage(image.Rect(0, 0, int(mcw), ch))
f, err := os.Create(drawedFile)
if err != nil {
log.Errorln("[bilibili]", err)
data, cl := writer.ToBytes(nim)
ctx.SendChain(message.ImageBytes(data))
cl()
return
}
_, err = writer.WriteTo(nim, f)
_ = f.Close()
if err != nil {
ctx.SendChain(message.Text("ERROR:", err))
return
}
ctx.SendChain(message.Image("file:///" + file.BOTPATH + "/" + drawedFile))
})
engine.OnRegex(`^设置b站cookie?\s+(.*)$`, zero.SuperUserPermission).SetBlock(true).
Handle(func(ctx *zero.Ctx) {
cookie := ctx.State["regex_matched"].([]string)[1]
err := vdb.setBilibiliCookie(cookie)
err := setBilibiliCookie(cookie)
if err != nil {
ctx.SendChain(message.Text("ERROR: ", err))
return
@ -265,7 +535,7 @@ func init() {
ctx.SendChain(message.Text("成功设置b站cookie为" + cookie))
})
engine.OnFullMatch("更新vup", zero.SuperUserPermission, getdb).SetBlock(true).
engine.OnFullMatch("更新vup", zero.SuperUserPermission).SetBlock(true).
Handle(func(ctx *zero.Ctx) {
ctx.SendChain(message.Text("少女祈祷中..."))
err := updateVup()

View File

@ -1,19 +1,18 @@
package bilibili
import (
"encoding/json"
"errors"
"os"
"github.com/FloatTech/floatbox/binary"
"github.com/FloatTech/floatbox/file"
"github.com/FloatTech/floatbox/web"
_ "github.com/fumiama/sqlite3" // use sql
"github.com/jinzhu/gorm"
"github.com/tidwall/gjson"
)
const (
bilibiliCookie = "bilbili_cookie"
)
var (
vtbURLs = [...]string{"https://api.vtbs.moe/v1/short", "https://api.tokyo.vtbs.moe/v1/short", "https://vtbs.musedash.moe/v1/short"}
vdb *vupdb
@ -32,15 +31,6 @@ func (vup) TableName() string {
return "vup"
}
type config struct {
Key string `gorm:"column:key;primary_key"`
Value string `gorm:"column:value"`
}
func (config) TableName() string {
return "config"
}
// initializeVup 初始化vup数据库
func initializeVup(dbpath string) (*vupdb, error) {
if _, err := os.Stat(dbpath); err != nil || os.IsNotExist(err) {
@ -55,7 +45,7 @@ func initializeVup(dbpath string) (*vupdb, error) {
if err != nil {
return nil, err
}
gdb.AutoMigrate(&vup{}).AutoMigrate(&config{})
gdb.AutoMigrate(&vup{})
return (*vupdb)(gdb), nil
}
@ -103,28 +93,31 @@ func updateVup() error {
return nil
}
func (vdb *vupdb) setBilibiliCookie(cookie string) (err error) {
db := (*gorm.DB)(vdb)
c := config{
Key: bilibiliCookie,
Value: cookie,
func setBilibiliCookie(cookie string) (err error) {
cfg = config{
BilibiliCookie: cookie,
}
if err = db.Model(&config{}).First(&c, "key = ? ", bilibiliCookie).Error; err != nil {
// error handling...
if gorm.IsRecordNotFoundError(err) {
err = db.Model(&config{}).Create(&c).Error
}
} else {
err = db.Model(&config{}).Where("key = ? ", bilibiliCookie).Update(
map[string]interface{}{
"value": cookie,
}).Error
}
return
return saveConfig(cfg)
}
func (vdb *vupdb) getBilibiliCookie() (c config) {
db := (*gorm.DB)(vdb)
db.Model(&config{}).First(&c, "key = ?", bilibiliCookie)
return
func reflushBilibiliCookie() (err error) {
if file.IsNotExist(cfgFile) {
err = errors.New("未初始化配置")
return
}
reader, err := os.Open(cfgFile)
if err != nil {
return
}
defer reader.Close()
return json.NewDecoder(reader).Decode(&cfg)
}
func saveConfig(cfg config) (err error) {
reader, err := os.Create(cfgFile)
if err != nil {
return err
}
defer reader.Close()
return json.NewEncoder(reader).Encode(&cfg)
}

View File

@ -31,6 +31,10 @@ const (
spaceHistoryURL = "https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/space_history?host_uid=%v&offset_dynamic_id=%v&need_top=0"
// liveListURL 获得直播状态
liveListURL = "https://api.live.bilibili.com/room/v1/Room/get_status_info_by_uids"
// danmakuAPI 弹幕网获得用户弹幕api
danmakuAPI = "https://danmaku.suki.club/api/search/user/detail?uid=%v&pagenum=%v&pagesize=5"
// danmakuURL 弹幕网链接
danmakuURL = "https://danmaku.suki.club/user/%v"
)
// dynamicCard 总动态结构体,包括desc,card
@ -215,6 +219,14 @@ type roomCard struct {
} `json:"anchor_info"`
}
// searchData 查找b站用户总结构体
type searchData struct {
Data struct {
NumResults int `json:"numResults"`
Result []searchResult `json:"result"`
} `json:"data"`
}
// searchResult 查找b站用户结果
type searchResult struct {
Mid int64 `json:"mid"`
@ -271,3 +283,49 @@ type vtbDetail struct {
GuardNum int `json:"guardNum"`
AreaRank int `json:"areaRank"`
}
// danmakusuki 弹幕网结构体
type danmakusuki struct {
Code int64 `json:"code"`
Message string `json:"message"`
Data struct {
Data []struct {
Channel struct {
Name string `json:"name"`
IsLiving bool `json:"isLiving"`
UID int64 `json:"uId"`
RoomID int64 `json:"roomId"`
FaceURL string `json:"faceUrl"`
LiveCount int64 `json:"liveCount"`
} `json:"channel"`
Live struct {
LiveID string `json:"liveId"`
Title string `json:"title"`
IsFinish bool `json:"isFinish"`
CoverURL string `json:"coverUrl"`
StartDate int64 `json:"startDate"`
StopDate int64 `json:"stopDate"`
DanmakusCount int64 `json:"danmakusCount"`
TotalIncome float64 `json:"totalIncome"`
WatchCount int64 `json:"watchCount"`
} `json:"live"`
Danmakus []struct {
Name string `json:"name"`
Type int64 `json:"type"`
UID int64 `json:"uId"`
SendDate int64 `json:"sendDate"`
Price float64 `json:"price"`
Message string `json:"message"`
} `json:"danmakus"`
} `json:"data"`
Total int64 `json:"total"`
PageNum int64 `json:"pageNum"`
PageSize int64 `json:"pageSize"`
HasMore bool `json:"hasMore"`
} `json:"data"`
}
// 配置结构体
type config struct {
BilibiliCookie string `json:"bilibili_cookie"`
}