commit 215855af08447b16b4fbe0c0ea88540d4e105596
Author: tursom <tursom@foxmail.com>
Date:   Wed Nov 23 20:30:16 2022 +0800

    create project

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..087c9d4
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,10 @@
+# idea
+.idea/
+
+# python venv
+venv/
+
+# test cookie file
+live/Room_test.cookie.txt
+
+vendor/
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..264acdc
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,13 @@
+module libbili
+
+go 1.19
+
+require (
+	github.com/go-resty/resty/v2 v2.7.0
+	github.com/tursom/GoCollections v0.1.4
+)
+
+require (
+	github.com/timandy/routine v1.0.5 // indirect
+	golang.org/x/net v0.2.0 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..c896f0a
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,18 @@
+github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
+github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
+github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
+github.com/timandy/routine v1.0.5 h1:JBVo2mOZlYVsod/v1jjmjSzxTH84x3vS9b6k4ZBTvdc=
+github.com/timandy/routine v1.0.5/go.mod h1:BT7/+zFFZkugUiUUClwDgZjQ/PNcivY1hqRLXQAftGU=
+github.com/tursom/GoCollections v0.1.4 h1:NV3vK2jmkzseEgpcKv716DlUEl8BzCyk3wM1njr0Cp8=
+github.com/tursom/GoCollections v0.1.4/go.mod h1:kwFG//g3vbmDShsnY8Vm47PsTUBWgk83RcEPYZLgmZU=
+golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
+golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
diff --git a/live/Room.go b/live/Room.go
new file mode 100644
index 0000000..13430e7
--- /dev/null
+++ b/live/Room.go
@@ -0,0 +1,252 @@
+package live
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"mime/multipart"
+	"strconv"
+	"time"
+
+	"github.com/go-resty/resty/v2"
+	"github.com/tursom/GoCollections/exceptions"
+)
+
+type (
+	Room interface {
+		SetCookie(cookie string)
+		ID() uint32
+		Send(msg string) (*DanmuResp, exceptions.Exception)
+		SendDanmu(danmu *Danmu) (*DanmuResp, exceptions.Exception)
+		GetDanmuColors() (*DanmuColors, exceptions.Exception)
+	}
+
+	roomImpl struct {
+		id     uint32
+		cookie string
+	}
+
+	Danmu struct {
+		Bubble    int32  `json:"bubble,omitempty"`
+		Msg       string `json:"msg,omitempty"`
+		Color     string `json:"color,omitempty"`
+		Mode      int32  `json:"mode,omitempty"`
+		Fontsize  int32  `json:"fontsize,omitempty"`
+		Rnd       int64  `json:"rnd,omitempty"`
+		RoomId    uint32 `json:"roomid,omitempty"`
+		Csrf      string `json:"csrf,omitempty"`
+		CsrfToken string `json:"csrf_token,omitempty"`
+	}
+
+	DanmuResp struct {
+		Code int `json:"code"`
+		Data struct {
+			ModeInfo struct {
+				Mode           int    `json:"mode"`
+				ShowPlayerType int    `json:"show_player_type"`
+				Extra          string `json:"extra"`
+			} `json:"mode_info"`
+		} `json:"data"`
+		Message string `json:"message"`
+		Msg     string `json:"msg"`
+	}
+
+	DanmuColors struct {
+		Code int `json:"code"`
+		Data struct {
+			Group []struct {
+				Name  string `json:"name"`
+				Sort  int    `json:"sort"`
+				Color []struct {
+					Name     string `json:"name"`
+					Color    string `json:"color"`
+					ColorHex string `json:"color_hex"`
+					Status   int    `json:"status"`
+					Weight   int    `json:"weight"`
+					ColorId  int    `json:"color_id"`
+					Origin   int    `json:"origin"`
+				} `json:"color"`
+			} `json:"group"`
+			Mode []struct {
+				Name   string `json:"name"`
+				Mode   int    `json:"mode"`
+				Type   string `json:"type"`
+				Status int    `json:"status"`
+			} `json:"mode"`
+		} `json:"data"`
+		Message string `json:"message"`
+		Msg     string `json:"msg"`
+	}
+)
+
+var (
+	client = resty.New()
+)
+
+func NewRoom(id uint32) Room {
+	return &roomImpl{
+		id: id,
+	}
+}
+
+func (r *roomImpl) SetCookie(cookie string) {
+	r.cookie = cookie
+}
+
+func (r *roomImpl) ID() uint32 {
+	return r.id
+}
+
+func (r *roomImpl) Send(msg string) (*DanmuResp, exceptions.Exception) {
+	if r.cookie == "" {
+		// return err that no cookie set
+		return nil, exceptions.NewIllegalParameterException("cookie not set", nil)
+	}
+
+	return r.SendDanmu(&Danmu{
+		Bubble:    0,
+		Msg:       msg,
+		Color:     "16777215",
+		Mode:      1,
+		Fontsize:  25,
+		Rnd:       time.Now().Unix(),
+		RoomId:    r.id,
+		Csrf:      "c1b21617a15daf838f505271ff8f5204",
+		CsrfToken: "c1b21617a15daf838f505271ff8f5204",
+	})
+}
+
+func (r *roomImpl) SendDanmu(danmu *Danmu) (*DanmuResp, exceptions.Exception) {
+	if r.cookie == "" {
+		// return err that no cookie set
+		return nil, exceptions.NewIllegalParameterException("cookie not set", nil)
+	}
+
+	request := client.R()
+
+	form, boundary, exception := multipartForm(danmu)
+	if exception != nil {
+		return nil, exception
+	}
+
+	request.SetBody(form)
+
+	//request, err := http.NewRequest("POST", "https://api.live.bilibili.com/msg/send", form)
+	//if err != nil {
+	//	return nil, exceptions.Package(err)
+	//}
+
+	//request.Header.Add("Accept", "*/*")
+	request.Header.Set("Cookie", r.cookie)
+	//request.Header.Set("Origin", "https://live.bilibili.com")
+	//request.Header.Set("Referer", fmt.Sprintf("https://li|ve.bilibili.com/%d?spm_id_from=444.41.live_users.item.click", r.id))
+	//request.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36")
+	request.Header.Set("Content-Type", fmt.Sprintf("multipart/form-data; boundary=%s", boundary))
+	//request.Header.Set("Sec-Ch-Ua", "\"Google Chrome\";v=\"107\", \"Chromium\";v=\"107\", \"Not=A?Brand\";v=\"24\"")
+	//request.Header.Set("Sec-Ch-Ua-Mobile", "?0")
+	//request.Header.Set("Sec-Ch-Ua-Platform", "\"Windows\"")
+	//request.Header.Set("Sec-Fetch-Dest", "empty")
+	//request.Header.Set("Sec-Fetch-Mode", "cors")
+	//request.Header.Set("Sec-Fetch-Site", "same-site")
+	//request.Header.Set("content-length", strconv.Itoa(len(form)))
+
+	//do, err := http.DefaultClient.Do(request)
+	do, err := request.Post("https://api.live.bilibili.com/msg/send")
+	if do.StatusCode() != 200 {
+		// return err
+		fmt.Println(string(do.Body()))
+		return nil, exceptions.NewPackageException(fmt.Sprintf("send response status failed: %d", do.StatusCode()),
+			exceptions.Cfg().SetCause(do.StatusCode))
+	}
+
+	var resp DanmuResp
+	err = json.Unmarshal(do.Body(), &resp)
+	if err != nil {
+		return nil, exceptions.Package(err)
+	}
+
+	return &resp, nil
+}
+
+func multipartForm(danmu *Danmu) (formData []byte, boundary string, exception exceptions.Exception) {
+	formBuffer := bytes.NewBuffer(nil)
+	formWriter := multipart.NewWriter(formBuffer)
+
+	err := formWriter.WriteField("bubble", strconv.Itoa(int(danmu.Bubble)))
+	if err != nil {
+		return nil, "", exceptions.Package(err)
+	}
+
+	err = formWriter.WriteField("msg", danmu.Msg)
+	if err != nil {
+		return nil, "", exceptions.Package(err)
+	}
+
+	err = formWriter.WriteField("color", danmu.Color)
+	if err != nil {
+		return nil, "", exceptions.Package(err)
+	}
+
+	err = formWriter.WriteField("mode", strconv.Itoa(int(danmu.Mode)))
+	if err != nil {
+		return nil, "", exceptions.Package(err)
+	}
+
+	err = formWriter.WriteField("fontsize", strconv.Itoa(int(danmu.Fontsize)))
+	if err != nil {
+		return nil, "", exceptions.Package(err)
+	}
+
+	err = formWriter.WriteField("rnd", strconv.FormatInt(danmu.Rnd, 10))
+	if err != nil {
+		return nil, "", exceptions.Package(err)
+	}
+
+	err = formWriter.WriteField("roomid", strconv.Itoa(int(danmu.RoomId)))
+	if err != nil {
+		return nil, "", exceptions.Package(err)
+	}
+
+	err = formWriter.WriteField("csrf", danmu.Csrf)
+	if err != nil {
+		return nil, "", exceptions.Package(err)
+	}
+
+	err = formWriter.WriteField("csrf_token", danmu.CsrfToken)
+	if err != nil {
+		return nil, "", exceptions.Package(err)
+	}
+
+	err = formWriter.Close()
+	if err != nil {
+		return nil, "", exceptions.Package(err)
+	}
+
+	formBytes := formBuffer.Bytes()
+	fmt.Println(string(formBytes))
+
+	return formBytes, formWriter.Boundary(), nil
+}
+
+func (r *roomImpl) GetDanmuColors() (*DanmuColors, exceptions.Exception) {
+	url := fmt.Sprintf("https://api.live.bilibili.com/xlive/web-room/v1/dM/GetDMConfigByGroup?room_id=%d", r.id)
+
+	request := client.R()
+
+	if r.cookie != "" {
+		request.Header.Set("Cookie", r.cookie)
+	}
+
+	response, err := request.Get(url)
+	if err != nil {
+		return nil, exceptions.Package(err)
+	}
+
+	var colors DanmuColors
+	err = json.Unmarshal(response.Body(), &colors)
+	if err != nil {
+		return nil, exceptions.Package(err)
+	}
+
+	return &colors, nil
+}
diff --git a/live/Room.py b/live/Room.py
new file mode 100644
index 0000000..27ce3ca
--- /dev/null
+++ b/live/Room.py
@@ -0,0 +1,66 @@
+import http.client
+import logging
+import time
+
+import httpx
+
+logging.basicConfig()
+logging.getLogger().setLevel(logging.DEBUG)
+
+http.client.HTTPConnection.debuglevel = 1
+
+# session.proxies = {
+#     'http': 'http://127.0.0.1:2080',
+#     'https': 'http://127.0.0.1:2080',
+# }
+
+id = 917818
+
+with open("Room_test.cookie.txt") as cookie_txt:
+    cookie = cookie_txt.read()
+
+session = httpx.Client(http2=True)
+
+resp = session.post(
+    "https://api.live.bilibili.com/msg/send",
+    # files=(
+    #     ("bubble", (None, "0")),
+    #     ("msg", (None, "弹幕测试")),
+    #     ("color", (None, "16777215")),
+    #     ("mode", (None, "1")),
+    #     ("fontsize", (None, "25")),
+    #     ("rnd", (None, str(int(time.time())))),
+    #     ("roomid", (None, str(id))),
+    #     ("csrf", (None, "c1b21617a15daf838f505271ff8f5204")),
+    #     ("csrf_token", (None, "c1b21617a15daf838f505271ff8f5204")),
+    # ),
+    data={
+        "bubble", "0",
+        "msg", "弹幕测试",
+        "color", "16777215",
+        "mode", "1",
+        "fontsize", "25",
+        "rnd", str(int(time.time())),
+        "roomid", str(id),
+        "csrf", "c1b21617a15daf838f505271ff8f5204",
+        "csrf_token", "c1b21617a15daf838f505271ff8f5204",
+    },
+    headers={
+        "Accept": "*/*",
+        "Cookie": cookie,
+        # "Origin": "https://live.bilibili.com",
+        # "Referer": f"https://li|ve.bilibili.com/{id}?spm_id_from=444.41.live_users.item.click",
+        # "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36",
+        # "Sec-Ch-Ua": "\"Google Chrome\";v=\"107\", \"Chromium\";v=\"107\", \"Not=A?Brand\";v=\"24\"",
+        # "Sec-Ch-Ua-Mobile": "?0",
+        # "Sec-Ch-Ua-Platform": "\"Windows\"",
+        # "Sec-Fetch-Dest": "empty",
+        # "Sec-Fetch-Mode": "cors",
+        # "Sec-Fetch-Site": "same-site",
+    },
+    # cookies={
+    #     "Cookie": cookie,
+    # },
+)
+
+print(resp)
diff --git a/live/Room_test.go b/live/Room_test.go
new file mode 100644
index 0000000..305de1c
--- /dev/null
+++ b/live/Room_test.go
@@ -0,0 +1,50 @@
+package live
+
+import (
+	"encoding/json"
+	"os"
+	"testing"
+
+	"github.com/tursom/GoCollections/exceptions"
+)
+
+func getRoomImpl() (Room, exceptions.Exception) {
+	cookieBytes, err := os.ReadFile("Room_test.cookie.txt")
+	if err != nil {
+		return nil, exceptions.NewPackageException("read cookie file failed", exceptions.Cfg().SetCause(err))
+	}
+
+	room := NewRoom(917818)
+	room.SetCookie(string(cookieBytes))
+
+	return room, nil
+}
+
+func Test_roomImpl_Send(t *testing.T) {
+	room, err := getRoomImpl()
+	if err != nil {
+		t.Fatalf("get room impl failed: %s", err)
+	}
+
+	resp, err := room.Send("弹幕测试")
+	if err != nil {
+		t.Fatalf("test send danmu failed: %s", exceptions.GetStackTraceString(err.(exceptions.Exception)))
+	}
+
+	t.Logf("send danmu resp: %v", resp)
+}
+
+func Test_roomImpl_GetDanmuColors(t *testing.T) {
+	room, err := getRoomImpl()
+	if err != nil {
+		t.Fatalf("get room impl failed: %s", err)
+	}
+
+	colors, exception := room.GetDanmuColors()
+	if exception != nil {
+		t.Fatalf("get danmu colors failed: %s", exceptions.GetStackTraceString(exception))
+	}
+
+	marshal, _ := json.Marshal(colors)
+	t.Logf("get colors: %s", string(marshal))
+}