From 459052060abe3f8d52e6ed2e4901d8e20eb85e5b Mon Sep 17 00:00:00 2001 From: Akegarasu Date: Fri, 25 Apr 2025 20:18:36 +0800 Subject: [PATCH 1/2] fix #26 --- api/info.go | 9 +++++++-- client/client.go | 3 ++- client/handler.go | 9 +++++---- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/api/info.go b/api/info.go index c53ee27..6f96588 100644 --- a/api/info.go +++ b/api/info.go @@ -3,9 +3,10 @@ package api import ( "errors" "fmt" - "github.com/tidwall/gjson" "net/http" "strconv" + + "github.com/tidwall/gjson" ) // RoomInfo @@ -59,6 +60,7 @@ type DanmuInfo struct { func GetUid(cookie string) (int, error) { headers := &http.Header{} headers.Set("cookie", cookie) + headers.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:137.0) Gecko/20100101 Firefox/137.0") resp, err := HttpGet("https://api.bilibili.com/x/web-interface/nav", headers) if err != nil { return 0, err @@ -74,6 +76,7 @@ func GetDanmuInfo(roomID int, cookie string) (*DanmuInfo, error) { result := &DanmuInfo{} headers := &http.Header{} headers.Set("cookie", cookie) + headers.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:137.0) Gecko/20100101 Firefox/137.0") err := GetJsonWithHeader(fmt.Sprintf("https://api.live.bilibili.com/xlive/web-room/v1/index/getDanmuInfo?id=%d&type=0", roomID), headers, result) if err != nil { return nil, err @@ -83,7 +86,9 @@ func GetDanmuInfo(roomID int, cookie string) (*DanmuInfo, error) { func GetRoomInfo(roomID int) (*RoomInfo, error) { result := &RoomInfo{} - err := GetJson(fmt.Sprintf("https://api.live.bilibili.com/room/v1/Room/room_init?id=%d", roomID), result) + headers := &http.Header{} + headers.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:137.0) Gecko/20100101 Firefox/137.0") + err := GetJsonWithHeader(fmt.Sprintf("https://api.live.bilibili.com/room/v1/Room/room_init?id=%d", roomID), headers, result) if err != nil { return nil, err } diff --git a/client/client.go b/client/client.go index 9384161..c6e7d64 100644 --- a/client/client.go +++ b/client/client.go @@ -77,7 +77,8 @@ func (c *Client) init() error { c.RoomID = roomInfo.Data.RoomId if c.host == "" { info, err := api.GetDanmuInfo(c.RoomID, c.Cookie) - if err != nil { + // Workaround for getDanmuInfo API. Error code 352 + if err != nil || info.Code != 0 { c.hostList = []string{"broadcastlv.chat.bilibili.com"} } else { for _, h := range info.Data.HostList { diff --git a/client/handler.go b/client/handler.go index 75d3727..5242941 100644 --- a/client/handler.go +++ b/client/handler.go @@ -1,17 +1,18 @@ package client import ( + "regexp" + "runtime/debug" + "strings" + "github.com/Akegarasu/blivedm-go/message" "github.com/Akegarasu/blivedm-go/packet" "github.com/Akegarasu/blivedm-go/utils" log "github.com/sirupsen/logrus" - "regexp" - "runtime/debug" - "strings" ) var ( - knownCMD = []string{"INTERACT_WORD", "HOT_RANK_SETTLEMENT", "DANMU_GIFT_LOTTERY_START", "WELCOME_GUARD", "PK_PROCESS", "PK_BATTLE_PRO_TYPE", "MATCH_TEAM_GIFT_RANK", "PK_BATTLE_CRIT", "LUCK_GIFT_AWARD_USER", "SCORE_CARD", "ONLINE_RANK_V2", "PK_BATTLE_SPECIAL_GIFT", "SEND_TOP", "SUPER_CHAT_MESSAGE_JPN", "ANIMATION", "GUARD_LOTTERY_START", "WEEK_STAR_CLOCK", "WELCOME", "WIN_ACTIVITY", "ROOM_KICKOUT", "CHANGE_ROOM_INFO", "ROOM_SKIN_MSG", "ROOM_BLOCK_MSG", "SUPER_CHAT_ENTRANCE", "PK_BATTLE_RANK_CHANGE", "ROOM_LOCK", "TV_END", "PK_PRE", "ROOM_SILENT_OFF", "SEND_GIFT", "DANMU_MSG", "ANCHOR_LOT_START", "ROOM_BOX_USER", "ONLINE_RANK_TOP3", "WIDGET_BANNER", "PK_BATTLE_START", "ACTIVITY_MATCH_GIFT", "PK_AGAIN", "PK_MATCH", "RAFFLE_START", "LIVE", "WISH_BOTTLE", "GUARD_ACHIEVEMENT_ROOM", "ONLINE_RANK_COUNT", "COMMON_NOTICE_DANMAKU", "LOL_ACTIVITY", "HOT_RANK_CHANGED", "ROOM_BLOCK_INTO", "ROOM_LIMIT", "PANEL", "RAFFLE_END", "ENTRY_EFFECT", "STOP_LIVE_ROOM_LIST", "TV_START", "WATCH_LPL_EXPIRED", "PK_BATTLE_PRE", "USER_TOAST_MSG", "BOX_ACTIVITY_START", "PK_MIC_END", "LIVE_INTERACTIVE_GAME", "ROOM_BANNER", "PK_BATTLE_GIFT", "MESSAGEBOX_USER_GAIN_MEDAL", "LITTLE_TIPS", "HOUR_RANK_AWARDS", "NOTICE_MSG", "ROOM_REAL_TIME_MESSAGE_UPDATE", "ANCHOR_LOT_END", "PREPARING", "GUARD_BUY", "ROOM_CHANGE", "room_admin_entrance", "CHASE_FRAME_SWITCH", "DANMU_GIFT_LOTTERY_AWARD", "PK_BATTLE_VOTES_ADD", "PK_BATTLE_END", "CUT_OFF", "PK_BATTLE_PROCESS", "PK_BATTLE_SETTLE_USER", "ANCHOR_LOT_AWARD", "WIN_ACTIVITY_USER", "VOICE_JOIN_STATUS", "DANMU_GIFT_LOTTERY_END", "ROOM_RANK", "SUPER_CHAT_MESSAGE", "ACTIVITY_BANNER_UPDATE_V2", "SPECIAL_GIFT", "ROOM_SILENT_ON", "WARNING", "ROOM_ADMINS", "COMBO_SEND", "HOT_RANK_SETTLEMENT_V2", "ANCHOR_LOT_CHECKSTATUS", "HOT_RANK_CHANGED_V2", "SUPER_CHAT_MESSAGE_DELETE", "PK_END", "PK_SETTLE", "ROOM_REFRESH", "PK_START", "COMBO_END", "PK_LOTTERY_START", "GUARD_WINDOWS_OPEN", "REENTER_LIVE_ROOM", "MESSAGEBOX_USER_MEDAL_CHANGE", "MESSAGEBOX_USER_MEDAL_COMPENSATION", "LITTLE_MESSAGE_BOX", "PK_BATTLE_PRE_NEW", "PK_BATTLE_START_NEW", "PK_BATTLE_PROCESS_NEW", "PK_BATTLE_FINAL_PROCESS", "PK_BATTLE_SETTLE_V2", "PK_BATTLE_SETTLE_NEW", "PK_BATTLE_PUNISH_END", "PK_BATTLE_VIDEO_PUNISH_BEGIN", "PK_BATTLE_VIDEO_PUNISH_END", "ENTRY_EFFECT_MUST_RECEIVE", "SUPER_CHAT_AUDIT", "VIDEO_CONNECTION_JOIN_START", "VIDEO_CONNECTION_JOIN_END", "VIDEO_CONNECTION_MSG", "VTR_GIFT_LOTTERY", "RED_POCKET_START", "FULL_SCREEN_SPECIAL_EFFECT", "POPULARITY_RED_POCKET_START", "POPULARITY_RED_POCKET_WINNER_LIST", "USER_PANEL_RED_ALARM", "SHOPPING_CART_SHOW", "THERMAL_STORM_DANMU_BEGIN", "THERMAL_STORM_DANMU_UPDATE", "THERMAL_STORM_DANMU_CANCEL", "THERMAL_STORM_DANMU_OVER", "MILESTONE_UPDATE_EVENT", "WEB_REPORT_CONTROL", "DANMU_TAG_CHANGE", "RANK_REM", "LIVE_PLAYER_LOG_RECYCLE", "LIVE_INTERNAL_ROOM_LOGIN", "LIVE_OPEN_PLATFORM_GAME", "WATCHED_CHANGE", "DANMU_AGGREGATION", "POPULARITY_RED_POCKET_NEW", "LIKE_INFO_V3_CLICK", "POPULAR_RANK_CHANGED", "DM_INTERACTION", "LIKE_INFO_V3_UPDATE", "HOT_ROOM_NOTIFY", "PLAY_TAG"} + knownCMD = []string{"INTERACT_WORD", "HOT_RANK_SETTLEMENT", "DANMU_GIFT_LOTTERY_START", "WELCOME_GUARD", "PK_PROCESS", "PK_BATTLE_PRO_TYPE", "MATCH_TEAM_GIFT_RANK", "PK_BATTLE_CRIT", "LUCK_GIFT_AWARD_USER", "SCORE_CARD", "ONLINE_RANK_V2", "PK_BATTLE_SPECIAL_GIFT", "SEND_TOP", "SUPER_CHAT_MESSAGE_JPN", "ANIMATION", "GUARD_LOTTERY_START", "WEEK_STAR_CLOCK", "WELCOME", "WIN_ACTIVITY", "ROOM_KICKOUT", "CHANGE_ROOM_INFO", "ROOM_SKIN_MSG", "ROOM_BLOCK_MSG", "SUPER_CHAT_ENTRANCE", "PK_BATTLE_RANK_CHANGE", "ROOM_LOCK", "TV_END", "PK_PRE", "ROOM_SILENT_OFF", "SEND_GIFT", "DANMU_MSG", "ANCHOR_LOT_START", "ROOM_BOX_USER", "ONLINE_RANK_TOP3", "WIDGET_BANNER", "PK_BATTLE_START", "ACTIVITY_MATCH_GIFT", "PK_AGAIN", "PK_MATCH", "RAFFLE_START", "LIVE", "WISH_BOTTLE", "GUARD_ACHIEVEMENT_ROOM", "ONLINE_RANK_COUNT", "COMMON_NOTICE_DANMAKU", "LOL_ACTIVITY", "HOT_RANK_CHANGED", "ROOM_BLOCK_INTO", "ROOM_LIMIT", "PANEL", "RAFFLE_END", "ENTRY_EFFECT", "STOP_LIVE_ROOM_LIST", "TV_START", "WATCH_LPL_EXPIRED", "PK_BATTLE_PRE", "USER_TOAST_MSG", "BOX_ACTIVITY_START", "PK_MIC_END", "LIVE_INTERACTIVE_GAME", "ROOM_BANNER", "PK_BATTLE_GIFT", "MESSAGEBOX_USER_GAIN_MEDAL", "LITTLE_TIPS", "HOUR_RANK_AWARDS", "NOTICE_MSG", "ROOM_REAL_TIME_MESSAGE_UPDATE", "ANCHOR_LOT_END", "PREPARING", "GUARD_BUY", "ROOM_CHANGE", "room_admin_entrance", "CHASE_FRAME_SWITCH", "DANMU_GIFT_LOTTERY_AWARD", "PK_BATTLE_VOTES_ADD", "PK_BATTLE_END", "CUT_OFF", "PK_BATTLE_PROCESS", "PK_BATTLE_SETTLE_USER", "ANCHOR_LOT_AWARD", "WIN_ACTIVITY_USER", "VOICE_JOIN_STATUS", "DANMU_GIFT_LOTTERY_END", "ROOM_RANK", "SUPER_CHAT_MESSAGE", "ACTIVITY_BANNER_UPDATE_V2", "SPECIAL_GIFT", "ROOM_SILENT_ON", "WARNING", "ROOM_ADMINS", "COMBO_SEND", "HOT_RANK_SETTLEMENT_V2", "ANCHOR_LOT_CHECKSTATUS", "HOT_RANK_CHANGED_V2", "SUPER_CHAT_MESSAGE_DELETE", "PK_END", "PK_SETTLE", "ROOM_REFRESH", "PK_START", "COMBO_END", "PK_LOTTERY_START", "GUARD_WINDOWS_OPEN", "REENTER_LIVE_ROOM", "MESSAGEBOX_USER_MEDAL_CHANGE", "MESSAGEBOX_USER_MEDAL_COMPENSATION", "LITTLE_MESSAGE_BOX", "PK_BATTLE_PRE_NEW", "PK_BATTLE_START_NEW", "PK_BATTLE_PROCESS_NEW", "PK_BATTLE_FINAL_PROCESS", "PK_BATTLE_SETTLE_V2", "PK_BATTLE_SETTLE_NEW", "PK_BATTLE_PUNISH_END", "PK_BATTLE_VIDEO_PUNISH_BEGIN", "PK_BATTLE_VIDEO_PUNISH_END", "ENTRY_EFFECT_MUST_RECEIVE", "SUPER_CHAT_AUDIT", "VIDEO_CONNECTION_JOIN_START", "VIDEO_CONNECTION_JOIN_END", "VIDEO_CONNECTION_MSG", "VTR_GIFT_LOTTERY", "RED_POCKET_START", "FULL_SCREEN_SPECIAL_EFFECT", "POPULARITY_RED_POCKET_START", "POPULARITY_RED_POCKET_WINNER_LIST", "USER_PANEL_RED_ALARM", "SHOPPING_CART_SHOW", "THERMAL_STORM_DANMU_BEGIN", "THERMAL_STORM_DANMU_UPDATE", "THERMAL_STORM_DANMU_CANCEL", "THERMAL_STORM_DANMU_OVER", "MILESTONE_UPDATE_EVENT", "WEB_REPORT_CONTROL", "DANMU_TAG_CHANGE", "RANK_REM", "LIVE_PLAYER_LOG_RECYCLE", "LIVE_INTERNAL_ROOM_LOGIN", "LIVE_OPEN_PLATFORM_GAME", "WATCHED_CHANGE", "DANMU_AGGREGATION", "POPULARITY_RED_POCKET_NEW", "LIKE_INFO_V3_CLICK", "POPULAR_RANK_CHANGED", "DM_INTERACTION", "LIKE_INFO_V3_UPDATE", "HOT_ROOM_NOTIFY", "PLAY_TAG", "OTHER_SLICE_LOADING_RESULT"} knownCMDMap map[string]int cmdReg = regexp.MustCompile(`"cmd":"([^"]+)"`) ) From 0a012577fa35c5fe0e52d345ac05c053be3756ca Mon Sep 17 00:00:00 2001 From: Akegarasu Date: Tue, 27 May 2025 12:55:30 +0800 Subject: [PATCH 2/2] add sign, fix #27 --- api/info.go | 8 ++- api/sign.go | 177 +++++++++++++++++++++++++++++++++++++++++++++++ client/client.go | 2 +- 3 files changed, 185 insertions(+), 2 deletions(-) create mode 100644 api/sign.go diff --git a/api/info.go b/api/info.go index 6f96588..ed6be1b 100644 --- a/api/info.go +++ b/api/info.go @@ -77,7 +77,13 @@ func GetDanmuInfo(roomID int, cookie string) (*DanmuInfo, error) { headers := &http.Header{} headers.Set("cookie", cookie) headers.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:137.0) Gecko/20100101 Firefox/137.0") - err := GetJsonWithHeader(fmt.Sprintf("https://api.live.bilibili.com/xlive/web-room/v1/index/getDanmuInfo?id=%d&type=0", roomID), headers, result) + + signedUrl, err := WbiKeysSignString(fmt.Sprintf("https://api.live.bilibili.com/xlive/web-room/v1/index/getDanmuInfo?id=%d&type=0", roomID)) + if err != nil { + return nil, err + } + + err = GetJsonWithHeader(signedUrl, headers, result) if err != nil { return nil, err } diff --git a/api/sign.go b/api/sign.go new file mode 100644 index 0000000..f5f4f7f --- /dev/null +++ b/api/sign.go @@ -0,0 +1,177 @@ +package api + +import ( + "bytes" + "crypto/md5" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + "strings" + "time" +) + +// https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/misc/sign/wbi.md + +var wbiKeys WbiKeys + +func WbiKeysSignString(u string) (string, error) { + parsedURL, err := url.Parse(u) + if err != nil { + return "", err + } + + err = wbiKeys.Sign(parsedURL) + if err != nil { + return "", err + } + + return parsedURL.String(), nil +} + +// Sign 为链接签名 +func WbiKeysSign(u *url.URL) error { + return wbiKeys.Sign(u) +} + +// Update 无视过期时间更新 +func WbiKeysUpdate() error { + return wbiKeys.Update() +} + +func WbiKeysGet() (wk WbiKeys, err error) { + if err = wk.update(false); err != nil { + return WbiKeys{}, err + } + return wbiKeys, nil +} + +var mixinKeyEncTab = [...]int{ + 46, 47, 18, 2, 53, 8, 23, 32, + 15, 50, 10, 31, 58, 3, 45, 35, + 27, 43, 5, 49, 33, 9, 42, 19, + 29, 28, 14, 39, 12, 38, 41, 13, + 37, 48, 7, 16, 24, 55, 40, 61, + 26, 17, 0, 1, 60, 51, 30, 4, + 22, 25, 54, 21, 56, 59, 6, 63, + 57, 62, 11, 36, 20, 34, 44, 52, +} + +func removeUnwantedChars(v url.Values, chars ...byte) url.Values { + b := []byte(v.Encode()) + for _, c := range chars { + b = bytes.ReplaceAll(b, []byte{c}, nil) + } + s, err := url.ParseQuery(string(b)) + if err != nil { + panic(err) + } + return s +} + +type Nav struct { + Code int `json:"code"` + Message string `json:"message"` + Ttl int `json:"ttl"` + Data struct { + WbiImg struct { + ImgUrl string `json:"img_url"` + SubUrl string `json:"sub_url"` + } `json:"wbi_img"` + + // ...... + } `json:"data"` +} + +type WbiKeys struct { + Img string + Sub string + Mixin string + lastUpdateTime time.Time +} + +// Sign 为链接签名 +func (wk *WbiKeys) Sign(u *url.URL) (err error) { + if err = wk.update(false); err != nil { + return err + } + + values := u.Query() + + values = removeUnwantedChars(values, '!', '\'', '(', ')', '*') // 必要性存疑? + + values.Set("wts", strconv.FormatInt(time.Now().Unix(), 10)) + + // [url.Values.Encode] 内会对参数排序, + // 且遍历 map 时本身就是无序的 + hash := md5.Sum([]byte(values.Encode() + wk.Mixin)) // Calculate w_rid + values.Set("w_rid", hex.EncodeToString(hash[:])) + u.RawQuery = values.Encode() + return nil +} + +// Update 无视过期时间更新 +func (wk *WbiKeys) Update() (err error) { + return wk.update(true) +} + +// update 按需更新 +func (wk *WbiKeys) update(purge bool) error { + if !purge && time.Since(wk.lastUpdateTime) < time.Hour { + return nil + } + + // 测试下来不用修改 header 也能过 + resp, err := http.Get("https://api.bilibili.com/x/web-interface/nav") + if err != nil { + return err + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + nav := Nav{} + err = json.Unmarshal(body, &nav) + if err != nil { + return err + } + + if nav.Code != 0 && nav.Code != -101 { // -101 未登录时也会返回两个 key + return fmt.Errorf("unexpected code: %d, message: %s", nav.Code, nav.Message) + } + img := nav.Data.WbiImg.ImgUrl + sub := nav.Data.WbiImg.SubUrl + if img == "" || sub == "" { + return fmt.Errorf("empty image or sub url: %s", body) + } + + // https://i0.hdslb.com/bfs/wbi/7cd084941338484aae1ad9425b84077c.png + imgParts := strings.Split(img, "/") + subParts := strings.Split(sub, "/") + + // 7cd084941338484aae1ad9425b84077c.png + imgPng := imgParts[len(imgParts)-1] + subPng := subParts[len(subParts)-1] + + // 7cd084941338484aae1ad9425b84077c + wbiKeys.Img = strings.TrimSuffix(imgPng, ".png") + wbiKeys.Sub = strings.TrimSuffix(subPng, ".png") + + wbiKeys.mixin() + wbiKeys.lastUpdateTime = time.Now() + return nil +} + +func (wk *WbiKeys) mixin() { + var mixin [32]byte + wbi := wk.Img + wk.Sub + for i := range mixin { // for i := 0; i < len(mixin); i++ { + mixin[i] = wbi[mixinKeyEncTab[i]] + } + wk.Mixin = string(mixin[:]) +} diff --git a/client/client.go b/client/client.go index c6e7d64..2fee5f2 100644 --- a/client/client.go +++ b/client/client.go @@ -72,7 +72,7 @@ func (c *Client) init() error { roomInfo, err := api.GetRoomInfo(c.RoomID) // 失败降级 if err != nil || roomInfo.Code != 0 { - log.Errorf("room=%s init GetRoomInfo fialed, %s", c.RoomID, err) + log.Errorf("room=%d init GetRoomInfo fialed, %s", c.RoomID, err) } c.RoomID = roomInfo.Data.RoomId if c.host == "" {