Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Beta324 #329

Merged
merged 7 commits into from
Feb 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Processor/ProcessGroupMessage.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ func (p *Processors) ProcessGroupMessage(data *dto.WSGroupATMessageData) error {
groupMsg.IsBindedGroupId = IsBindedGroupId
if IsBindedUserId {
groupMsg.Avatar, _ = GenerateAvatarURL(userid64)
} else {
groupMsg.Avatar, _ = GenerateAvatarURLV2(data.Author.ID)
}
}
//根据条件判断是否增加nick和card
Expand Down
8 changes: 8 additions & 0 deletions Processor/Processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -1039,6 +1039,14 @@ func GenerateAvatarURL(userID int64) (string, error) {
return fmt.Sprintf("http://q%d.qlogo.cn/g?b=qq&nk=%d&s=640", qNumber, userID), nil
}

// GenerateAvatarURLV2 生成根据32位ID 和 Appid 组合的 新QQ 头像 URL
func GenerateAvatarURLV2(openid string) (string, error) {

appidstr := config.GetAppIDStr()
// 构建并返回 URL
return fmt.Sprintf("https://q.qlogo.cn/qqapp/%s/%s/640", appidstr, openid), nil
}

// 生成link卡片
func generateMdByConfig() (md *dto.Markdown, kb *keyboard.MessageKeyboard) {
//相关配置获取
Expand Down
8 changes: 8 additions & 0 deletions botgo/dto/websocket.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ type WebsocketAP struct {
SessionStartLimit SessionStartLimit `json:"session_start_limit"`
}

// WebsocketAP wss 单个接入点信息
type WebsocketAPSingle struct {
URL string `json:"url"`
ShardCount uint32 `json:"shards"` //最大值 比如是4个分片就是4
ShardID uint32 `json:"shard_id"` //从0开始的 0 1 2 3 对应上面的
SessionStartLimit SessionStartLimit `json:"session_start_limit"`
}

// SessionStartLimit 链接频控信息
type SessionStartLimit struct {
Total uint32 `json:"total"`
Expand Down
2 changes: 2 additions & 0 deletions botgo/session_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@ var defaultSessionManager SessionManager = local.New()
type SessionManager interface {
// Start 启动连接,默认使用 apInfo 中的 shards 作为 shard 数量,如果有需要自己指定 shard 数,请修 apInfo 中的信息
Start(apInfo *dto.WebsocketAP, token *token.Token, intents *dto.Intent) error
// 自己指定具体的shard
StartSingle(apInfo *dto.WebsocketAPSingle, token *token.Token, intents *dto.Intent) error
}
30 changes: 30 additions & 0 deletions botgo/sessions/local/local.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,36 @@ func (l *ChanManager) Start(apInfo *dto.WebsocketAP, token *token.Token, intents
return nil
}

// Start 启动指定的分片 session manager 适合想要手动指定目前分片的开发者(分片较少)
func (l *ChanManager) StartSingle(apInfo *dto.WebsocketAPSingle, token *token.Token, intents *dto.Intent) error {
defer log.Sync()
if err := manager.CheckSessionLimitSingle(apInfo); err != nil {
log.Errorf("[ws/session/local] session limited apInfo: %+v", apInfo)
return err
}
startInterval := manager.CalcInterval(apInfo.SessionStartLimit.MaxConcurrency)
log.Infof("[ws/session/local] will start %d sessions and per session start interval is %s",
apInfo.ShardCount, startInterval)

// 只启动一个分片

session := dto.Session{
URL: apInfo.URL,
Token: *token,
Intent: *intents,
LastSeq: 0,
Shards: dto.ShardConfig{
ShardID: apInfo.ShardID,
ShardCount: apInfo.ShardCount,
},
}

time.Sleep(startInterval)
go l.newConnect(session)

return nil
}

// newConnect 启动一个新的连接,如果连接在监听过程中报错了,或者被远端关闭了链接,需要识别关闭的原因,能否继续 resume
// 如果能够 resume,则往 sessionChan 中放入带有 sessionID 的 session
// 如果不能,则清理掉 sessionID,将 session 放入 sessionChan 中
Expand Down
8 changes: 8 additions & 0 deletions botgo/sessions/manager/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,11 @@ func CheckSessionLimit(apInfo *dto.WebsocketAP) error {
}
return nil
}

// CheckSessionLimit 检查链接数是否达到限制,如果达到限制需要等待重置
func CheckSessionLimitSingle(apInfo *dto.WebsocketAPSingle) error {
if apInfo.ShardCount > apInfo.SessionStartLimit.Remaining {
return errs.ErrSessionLimit
}
return nil
}
28 changes: 28 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,10 @@ type Settings struct {
LinkBots []string `yaml:"link_bots"`
LinkText string `yaml:"link_text"`
LinkPic string `yaml:"link_pic"`
MusicPrefix string `yaml:"music_prefix"`
DisableWebui bool `yaml:"disable_webui"`
ShardCount int `yaml:"shard_count"`
ShardID int `yaml:"shard_id"`
}

// LoadConfig 从文件中加载配置并初始化单例配置
Expand Down Expand Up @@ -1763,3 +1767,27 @@ func GetLinkPic() string {
}
return instance.Settings.LinkPic
}

// 获取 GetMusicPrefix
func GetMusicPrefix() string {
mu.Lock()
defer mu.Unlock()

if instance == nil {
mylog.Println("Warning: instance is nil when trying to get MusicPrefix.")
return ""
}
return instance.Settings.MusicPrefix
}

// 获取 GetDisableWebui 的值
func GetDisableWebui() bool {
mu.Lock()
defer mu.Unlock()

if instance == nil {
mylog.Println("Warning: instance is nil when trying to GetDisableWebui value.")
return false
}
return instance.Settings.DisableWebui
}
203 changes: 196 additions & 7 deletions handlers/message_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net"
"net/http"
"path/filepath"
"regexp"
"runtime"
Expand All @@ -19,12 +21,14 @@ import (
"github.com/hoshinonyaruko/gensokyo/config"
"github.com/hoshinonyaruko/gensokyo/echo"
"github.com/hoshinonyaruko/gensokyo/idmap"
"github.com/hoshinonyaruko/gensokyo/images"
"github.com/hoshinonyaruko/gensokyo/mylog"
"github.com/hoshinonyaruko/gensokyo/url"
"github.com/skip2/go-qrcode"
"github.com/tencent-connect/botgo/dto"
"github.com/tencent-connect/botgo/dto/keyboard"
"github.com/tencent-connect/botgo/openapi"
"github.com/tidwall/gjson"
"mvdan.cc/xurls" //xurls是一个从文本提取url的库 适用于多种场景
)

Expand Down Expand Up @@ -240,13 +244,16 @@ func parseMessageContent(paramsMessage callapi.ParamsContent, message callapi.Ac
} else {
localRecordPattern = regexp.MustCompile(`\[CQ:record,file=file://([^\]]+?)\]`)
}
httpUrlImagePattern := regexp.MustCompile(`\[CQ:image,file=http://(.+)\]`)
httpsUrlImagePattern := regexp.MustCompile(`\[CQ:image,file=https://(.+)\]`)
base64ImagePattern := regexp.MustCompile(`\[CQ:image,file=base64://(.+)\]`)
base64RecordPattern := regexp.MustCompile(`\[CQ:record,file=base64://(.+)\]`)
httpUrlRecordPattern := regexp.MustCompile(`\[CQ:record,file=http://(.+)\]`)
httpsUrlRecordPattern := regexp.MustCompile(`\[CQ:record,file=https://(.+)\]`)
mdPattern := regexp.MustCompile(`\[CQ:markdown,data=base64://(.+)\]`)
httpUrlImagePattern := regexp.MustCompile(`\[CQ:image,file=http://(.+?)\]`)
httpsUrlImagePattern := regexp.MustCompile(`\[CQ:image,file=https://(.+?)\]`)
base64ImagePattern := regexp.MustCompile(`\[CQ:image,file=base64://(.+?)\]`)
base64RecordPattern := regexp.MustCompile(`\[CQ:record,file=base64://(.+?)\]`)
httpUrlRecordPattern := regexp.MustCompile(`\[CQ:record,file=http://(.+?)\]`)
httpsUrlRecordPattern := regexp.MustCompile(`\[CQ:record,file=https://(.+?)\]`)
httpUrlVideoPattern := regexp.MustCompile(`\[CQ:video,file=http://(.+?)\]`)
httpsUrlVideoPattern := regexp.MustCompile(`\[CQ:video,file=https://(.+?)\]`)
mdPattern := regexp.MustCompile(`\[CQ:markdown,data=base64://(.+?)\]`)
qqMusicPattern := regexp.MustCompile(`\[CQ:music,type=qq,id=(\d+)\]`)

patterns := []struct {
key string
Expand All @@ -261,6 +268,9 @@ func parseMessageContent(paramsMessage callapi.ParamsContent, message callapi.Ac
{"url_record", httpUrlRecordPattern},
{"url_records", httpsUrlRecordPattern},
{"markdown", mdPattern},
{"qqmusic", qqMusicPattern},
{"url_video", httpUrlVideoPattern},
{"url_videos", httpsUrlVideoPattern},
}

foundItems := make(map[string][]string)
Expand Down Expand Up @@ -1015,3 +1025,182 @@ func parseMDData(mdData []byte) (*dto.Markdown, *keyboard.MessageKeyboard, error

return md, kb, nil
}

func parseQQMuiscMDData(musicid string) (*dto.Markdown, *keyboard.MessageKeyboard, error) {
info, err := QQMusicSongInfo(musicid)
if err != nil {
return nil, nil, err
}
if !info.Get("track_info").Exists() {
return nil, nil, errors.New("song not found")
}
albumMid := info.Get("track_info.album.mid").String()
pinfo, _ := FetchTrackInfo(info.Get("track_info.mid").Str)
jumpURL := "https://i.y.qq.com/v8/playsong.html?platform=11&appshare=android_qq&appversion=10030010&hosteuin=oKnlNenz7i-s7c**&songmid=" + info.Get("track_info.mid").Str + "&type=0&appsongtype=1&_wv=1&source=qq&ADTAG=qfshare"
content := info.Get("track_info.singer.0.name").String()

//专辑图片
PictureUrl := "https://y.gtimg.cn/music/photo_new/T002R180x180M000" + albumMid + ".jpg"
//专辑文字
musicContent := info.Get("track_info.name").Str + "\r" + content
// 处理 Markdown
var md *dto.Markdown
var CustomTemplateID string
//组合 mdParams
var mdParams []*dto.MarkdownParams
CustomTemplateID = config.GetCustomTemplateID()
if CustomTemplateID != "" {
if PictureUrl != "" {
height, width, err := images.GetImageDimensions(PictureUrl)
if err != nil {
mylog.Printf("获取图片宽高出错")
}
imgDesc := fmt.Sprintf("图片 #%dpx #%dpx", width, height)
// 创建 MarkdownParams 的实例
mdParams = []*dto.MarkdownParams{
{Key: "img_dec", Values: []string{imgDesc}},
{Key: "img_url", Values: []string{PictureUrl}},
{Key: "text_end", Values: []string{"\r" + musicContent}},
}
} else {
mdParams = []*dto.MarkdownParams{
{Key: "text_end", Values: []string{"\r" + musicContent}},
}
}
}
// 组合模板 Markdown
md = &dto.Markdown{
CustomTemplateID: CustomTemplateID,
Params: mdParams,
}
// 使用gjson获取musicUrl
musicUrl := gjson.Get(pinfo, "url_mid.data.midurlinfo.0.purl").String()
// 处理 Keyboard
kb := createMusicKeyboard(jumpURL, musicUrl)

return md, kb, nil
}

// QQMusicSongInfo 通过给定id在QQ音乐上查找曲目信息
func QQMusicSongInfo(id string) (gjson.Result, error) {
d, err := FetchSongDetail(id)
if err != nil {
return gjson.Result{}, err
}
return gjson.Get(d, "songinfo.data"), nil
}

func createMusicKeyboard(jumpURL string, musicURL string) *keyboard.MessageKeyboard {
// 初始化自定义键盘
customKeyboard := &keyboard.CustomKeyboard{}
currentRow := &keyboard.Row{} // 创建一个新行

// 创建歌曲页面跳转按钮
songPageButton := &keyboard.Button{
RenderData: &keyboard.RenderData{
Label: "立刻播放",
VisitedLabel: "已播放",
Style: 1, // 蓝色边缘
},
Action: &keyboard.Action{
Type: 0, // 链接类型
Permission: &keyboard.Permission{Type: 2}, // 所有人可操作
Data: jumpURL,
UnsupportTips: "请升级新版手机QQ",
},
}

//这个链接是音乐直链 musicURL 还没有适合能力调用

// // 创建立即播放按钮
// playNowButton := &keyboard.Button{
// RenderData: &keyboard.RenderData{
// Label: "立刻播放",
// VisitedLabel: "立刻播放",
// Style: 1, // 蓝色边缘
// },
// Action: &keyboard.Action{
// Type: 0, // 链接类型
// Permission: &keyboard.Permission{Type: 2}, // 所有人可操作
// Data: musicURL,
// UnsupportTips: "请升级新版手机QQ",
// },
// }

//自传播按钮
musicPrefix := config.GetMusicPrefix()
playNowButton := &keyboard.Button{
RenderData: &keyboard.RenderData{
Label: "我也要点歌",
VisitedLabel: "再次点歌",
Style: 1, // 蓝色边缘
},
Action: &keyboard.Action{
Type: 2, // 链接类型
Permission: &keyboard.Permission{Type: 2}, // 所有人可操作
Data: musicPrefix + " ",
UnsupportTips: "请升级新版手机QQ",
},
}

// 将按钮添加到当前行
currentRow.Buttons = append(currentRow.Buttons, songPageButton, playNowButton)
//currentRow.Buttons = append(currentRow.Buttons, songPageButton)

// 将当前行添加到自定义键盘
customKeyboard.Rows = append(customKeyboard.Rows, currentRow)

// 创建 MessageKeyboard 并设置其 Content
kb := &keyboard.MessageKeyboard{
Content: customKeyboard,
}

return kb
}

// FetchTrackInfo 用于根据trackMid获取QQ音乐的track信息
func FetchTrackInfo(trackMid string) (string, error) {
urlTemplate := "https://u.y.qq.com/cgi-bin/musicu.fcg?g_tk=2034008533&uin=0&format=json&data={\"comm\":{\"ct\":23,\"cv\":0},\"url_mid\":{\"module\":\"vkey.GetVkeyServer\",\"method\":\"CgiGetVkey\",\"param\":{\"guid\":\"4311206557\",\"songmid\":[\"%s\"],\"songtype\":[0],\"uin\":\"0\",\"loginflag\":1,\"platform\":\"23\"}}}&_=1599039471576"
url := fmt.Sprintf(urlTemplate, trackMid)

// 发送HTTP GET请求
resp, err := http.Get(url)
if err != nil {
return "", fmt.Errorf("error making request: %v", err)
}
defer resp.Body.Close()

// 读取并解析响应体
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("error reading response body: %v", err)
}

var result interface{} // 使用interface{}来接收任意的JSON对象
if err := json.Unmarshal(body, &result); err != nil {
return "", fmt.Errorf("error parsing JSON: %v", err)
}

return string(body), nil
}

// FetchSongDetail 发送请求到QQ音乐API并获取歌曲详情
func FetchSongDetail(songID string) (string, error) {
// 构建请求URL
url := fmt.Sprintf("https://u.y.qq.com/cgi-bin/musicu.fcg?format=json&inCharset=utf8&outCharset=utf-8&notice=0&platform=yqq.json&needNewCode=0&data={\"comm\":{\"ct\":24,\"cv\":0},\"songinfo\":{\"method\":\"get_song_detail_yqq\",\"param\":{\"song_type\":0,\"song_mid\":\"\",\"song_id\":%s},\"module\":\"music.pf_song_detail_svr\"}}", songID)

// 发送GET请求
resp, err := http.Get(url)
if err != nil {
return "", fmt.Errorf("error making request: %v", err)
}
defer resp.Body.Close()

// 读取响应体
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("error reading response body: %v", err)
}

return string(body), nil
}
Loading
Loading