]> 127.0.0.1 Git - bili_danmu/.git/commitdiff
1
authorqydysky <qydysky@foxmail.com>
Mon, 14 Sep 2020 12:47:00 +0000 (20:47 +0800)
committerqydysky <qydysky@foxmail.com>
Mon, 14 Sep 2020 12:47:00 +0000 (20:47 +0800)
.gitignore [new file with mode: 0644]
B_I.go [new file with mode: 0644]
Msg.go [new file with mode: 0644]
api.go [new file with mode: 0644]
bili_danmu.go [new file with mode: 0644]
bili_danmu_test.go [new file with mode: 0644]
demo/go.mod [new file with mode: 0644]
demo/main.go [new file with mode: 0644]
go.mod [new file with mode: 0644]
go.sum [new file with mode: 0644]
ws.go [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..727bc05
--- /dev/null
@@ -0,0 +1 @@
+.directory
diff --git a/B_I.go b/B_I.go
new file mode 100644 (file)
index 0000000..5c696ec
--- /dev/null
+++ b/B_I.go
@@ -0,0 +1,40 @@
+package bili_danmu
+
+import (
+       "bytes"
+       "encoding/binary"
+
+       p "github.com/qydysky/part"
+)
+/*
+       整数 字节转换区
+       32 4字节
+       16 2字节
+*/
+func Itob32(num int32) []byte {
+       var buffer bytes.Buffer
+       err := binary.Write(&buffer, binary.BigEndian, num)
+       if err != nil {p.Logf().E(err)}
+       return buffer.Bytes()
+}
+
+func Itob16(num int16) []byte {
+       var buffer bytes.Buffer
+       err := binary.Write(&buffer, binary.BigEndian, num)
+       if err != nil {p.Logf().E(err)}
+       return buffer.Bytes()
+}
+
+func Btoi32(b []byte) int32 {
+       var buffer int32
+       err := binary.Read(bytes.NewReader(b), binary.BigEndian, &buffer)
+       if err != nil {p.Logf().E(err)}
+       return buffer
+}
+
+func Btoi16(b []byte) int16 {
+       var buffer int16
+       err := binary.Read(bytes.NewReader(b), binary.BigEndian, &buffer)
+       if err != nil {p.Logf().E(err)}
+       return buffer
+}
\ No newline at end of file
diff --git a/Msg.go b/Msg.go
new file mode 100644 (file)
index 0000000..68cab72
--- /dev/null
+++ b/Msg.go
@@ -0,0 +1,147 @@
+package bili_danmu
+
+import (
+       "bytes"
+       "compress/zlib"
+
+       p "github.com/qydysky/part"
+)
+/*
+       数据为WS_OP_MESSAGE类型的
+*/
+
+var msglog = p.Logf().New().Level(0)
+
+func Msg(b []byte, compress bool) {
+       if compress {
+               readc, err := zlib.NewReader(bytes.NewReader(b[16:]))
+               if err != nil {msglog.E("解压错误");return}
+               defer readc.Close()
+               
+               buf := bytes.NewBuffer(nil)
+               if _, err := buf.ReadFrom(readc);err != nil {msglog.E("解压错误");return}
+               b = buf.Bytes()
+       }
+
+       for len(b) != 0 {
+               
+               var packL int32
+               if ist, packl := headChe(b[:16], len(b), WS_BODY_PROTOCOL_VERSION_NORMAL, WS_OP_MESSAGE, 0, 0); !ist {
+                       msglog.E("头错误");return
+               } else {
+                       packL = packl
+               }
+
+               s := string(b[16:packL])
+               b = b[packL:]
+               if cmd := p.Json().GetValFromS(s, "cmd");cmd == nil {
+                       msglog.E("->", "cmd", s)
+                       return
+               } else {
+                       switch cmd.(string) {
+                       case "INTERACT_WORD":;
+                       case "ACTIVITY_BANNER_UPDATE_V2":;
+                       case "SEND_GIFT":;//礼物
+                       case "NOTICE_MSG":;//礼物公告
+                       case "ROOM_BANNER":;//未知
+                       case "ONLINERANK":;//未知
+                       case "WELCOME":;//进入提示
+                       case "ROOM_SILENT_OFF", "ROOM_SILENT_ON":;
+                       case "HOUR_RANK_AWARDS":;
+                       case "ROOM_RANK":;
+                       case "WELCOME_GUARD":;
+                       case "SUPER_CHAT_MESSAGE", "SUPER_CHAT_MESSAGE_JPN":super_chat_message(s);
+                       case "PANEL":panel(s);
+                       case "ENTRY_EFFECT":entry_effect(s)
+                       case "ROOM_REAL_TIME_MESSAGE_UPDATE":roominfo(s)
+                       case "DANMU_MSG":danmu(s)
+                       default:msglog.I("Unknow cmd", s)
+                       }
+               }
+       }
+
+       return 
+}
+
+func super_chat_message(s string){
+       uname := p.Json().GetValFromS(s, "data.user_info.uname");
+       price := p.Json().GetValFromS(s, "data.price");
+       message := p.Json().GetValFromS(s, "data.message");
+       message_jpn := p.Json().GetValFromS(s, "data.message_jpn");
+
+       var sh []interface{}
+
+       if uname != nil {
+               sh = append(sh, []interface{}{uname.(string)})
+       }
+       if price != nil {
+               sh = append(sh, []interface{}{"¥", int64(price.(float64))})
+       }
+       if message != nil {
+               sh = append(sh, []interface{}{message.(string)})
+       }
+       if message_jpn != nil {
+               sh = append(sh, []interface{}{message_jpn.(string)})
+       }
+
+       if len(sh) != 0 {msglog.I("打赏: ", sh)}
+}
+
+func panel(s string){
+       if note := p.Json().GetValFromS(s, "data.note");note == nil {
+               msglog.E("->", "note", note)
+               return
+       } else {
+               msglog.I(note.(string))
+       }
+
+}
+
+func entry_effect(s string){
+       if copy_writing := p.Json().GetValFromS(s, "data.copy_writing");copy_writing == nil {
+               msglog.E("->", "copy_writing", copy_writing)
+               return
+       } else {
+               msglog.I(copy_writing.(string))
+       }
+
+}
+
+func roomsilent(s string){
+       if level := p.Json().GetValFromS(s, "data.level");level == nil {
+               msglog.E("->", "level", level)
+               return
+       } else {
+               if level.(float64) == 0 {msglog.I("主播关闭了禁言")}
+               msglog.I("主播开启了等级禁言:", int64(level.(float64)))
+       }
+
+}
+
+func roominfo(s string){
+       fans := p.Json().GetValFromS(s, "data.fans");
+       fans_club := p.Json().GetValFromS(s, "data.fans_club");
+
+       var sh []interface{}
+
+       if fans != nil {
+               sh = append(sh, []interface{}{"粉丝总人数:", int64(fans.(float64))})
+       }
+       if fans_club != nil {
+               sh = append(sh, []interface{}{"粉丝团人数:", int64(fans_club.(float64))})
+       }
+
+       if len(sh) != 0 {msglog.I(sh)}
+}
+
+func danmu(s string) {
+       if info := p.Json().GetValFromS(s, "info");info == nil {
+               msglog.E("->", "info", info)
+               return
+       } else {
+               infob := info.([]interface{})
+               msg := infob[1].(string)
+               auth := infob[2].([]interface{})[1].(string)
+               msglog.I(auth, ":", msg)
+       }
+}
diff --git a/api.go b/api.go
new file mode 100644 (file)
index 0000000..df66c68
--- /dev/null
+++ b/api.go
@@ -0,0 +1,122 @@
+package bili_danmu
+
+import (
+       "strconv"
+
+       p "github.com/qydysky/part"
+)
+
+type api struct {
+       Roomid int
+       Uid int
+       Url []string
+       Token string
+}
+
+func New_api(Roomid int) (o *api) {
+       l := p.Logf().New().Level(LogLevel).I("New_api")
+       defer l.Block()
+
+       l.I("->", "ok")
+       o = new(api)
+       o.Roomid = Roomid
+       o.Get_info()
+
+       return
+}
+
+func (i *api) Get_info() (o *api) {
+       o = i 
+       l := p.Logf().New().Level(LogLevel).I("*api.Get_info")
+       defer l.Block()
+
+       if o.Roomid == 0 {
+               l.E("->", "还未New_api")
+               return
+       }
+       Roomid := strconv.Itoa(o.Roomid)
+
+       req := p.Req()
+       if err := req.Reqf(p.Rval{
+               Url:"https://api.live.bilibili.com/room/v1/Room/room_init?id=" + Roomid,
+               Referer:"https://live.bilibili.com/" + Roomid,
+               Timeout:10,
+               Retry:2,
+       });err != nil {
+               l.E("->", err)
+               return
+       }
+       res := string(req.Respon)
+       if msg := p.Json().GetValFrom(res, "msg");msg == nil || msg != "ok" {
+               l.E("->", "msg", msg)
+               return
+       }
+       if Uid := p.Json().GetValFrom(res, "data.uid");Uid == nil {
+               l.E("->", "data.uid", Uid)
+               return
+       } else {
+               o.Uid = int(Uid.(float64))
+       }
+
+       if room_id := p.Json().GetValFrom(res, "data.room_id");room_id == nil {
+               l.E("->", "data.room_id", room_id)
+               return
+       } else {
+               l.I("->", "ok")
+               o.Roomid = int(room_id.(float64))
+       }
+       return
+}
+
+func (i *api) Get_host_Token() (o *api) {
+       o = i
+       l := p.Logf().New().Level(LogLevel).I("*api.Get_host_Token")
+       defer l.Block()
+
+       if o.Roomid == 0 {
+               l.E("->", "还未New_api")
+               return
+       }
+       Roomid := strconv.Itoa(o.Roomid)
+
+
+       req := p.Req()
+       if err := req.Reqf(p.Rval{
+               Url:"https://api.live.bilibili.com/xlive/web-room/v1/index/getDanmuInfo?type=0&id=" + Roomid,
+               Referer:"https://live.bilibili.com/" + Roomid,
+               Timeout:10,
+               Retry:2,
+       });err != nil {
+               l.E("->", err)
+               return
+       }
+       res := string(req.Respon)
+       if msg := p.Json().GetValFrom(res, "message");msg == nil || msg != "0" {
+               l.E("->", "message", msg)
+               return
+       }
+
+       _Token := p.Json().GetValFrom(res, "data.token")
+       if _Token == nil {
+               l.E("->", "data.token", _Token, res)
+               return
+       }
+       o.Token = _Token.(string)
+
+       if host_list := p.Json().GetValFrom(res, "data.host_list");host_list == nil {
+               l.E("->", "data.host_list", host_list)
+               return
+       } else {
+               for k, v := range host_list.([]interface{}) {
+                       if _host := p.Json().GetValFrom(v, "host");_host == nil {
+                               l.E("->", "data.host_list[", k, "].host", _host)
+                               continue
+                       } else {
+                               o.Url = append(o.Url, "wss://" + _host.(string) + "/sub")
+                       }                       
+               }
+               l.I("->", "ok")
+       }
+
+       return
+}
\ No newline at end of file
diff --git a/bili_danmu.go b/bili_danmu.go
new file mode 100644 (file)
index 0000000..6cac1f5
--- /dev/null
@@ -0,0 +1,225 @@
+package bili_danmu
+
+import (
+       "fmt"
+       "bytes"
+       "strconv"
+       "os"
+       "os/signal"
+
+       p "github.com/qydysky/part"
+)
+
+const LogLevel = 3
+
+func Demo() {
+       l:=p.Logf().New().Level(LogLevel)
+       defer l.Block()
+       
+       //ctrl+c退出
+       interrupt := make(chan os.Signal, 1)
+       signal.Notify(interrupt, os.Interrupt)
+
+       {
+               var room int
+               fmt.Printf("输入房间号: ")
+               _, err := fmt.Scanln(&room)
+               if err != nil {
+                       l.E("输入错误", err)
+                       return
+               }
+               
+               var break_sign bool
+               for !break_sign {
+                       //获取房间相关信息
+                       api := New_api(room).Get_host_Token()
+                       if len(api.Url) == 0 || api.Roomid == 0 || api.Token == "" {
+                               l.E("some err")
+                               return
+                       }
+       
+                       //对每个弹幕服务器尝试
+                       for _, v := range api.Url {
+                               //ws启动
+                               ws := New_ws(v).Handle()
+                               go func(){
+                                       <- interrupt
+                                       ws.Close()
+                                       break_sign = true
+                               }()
+       
+                               //SendChan 传入发送[]byte
+                               //RecvChan 接收[]byte
+                               l.I("send hello to", v)
+                               ws.SendChan <- hello_send(api.Roomid, api.Token)
+                               if hello_ok(<- ws.RecvChan) {
+                                       l.I(v, "hello!")
+
+                                       //开始心跳
+                                       go func(){
+                                               p.Sys().MTimeoutf(500)//500ms
+                                               heartbeatmsg, heartinterval := heartbeat()
+                                               ws.Heartbeat(1000 * heartinterval, heartbeatmsg)                                                
+                                       }()
+                               }
+       
+                               for {
+                                       i := <- ws.RecvChan
+                                       if len(i) == 0 && ws.Isclose() {
+                                               break
+                                       } else {
+                                               go Reply(i)
+                                       }
+                               }
+
+                               if break_sign {break}
+
+                               p.Sys().Timeoutf(1)
+                       }
+               }
+       }
+}
+
+//from player-loader-2.0.4.min.js
+const (
+       WS_PACKAGE_HEADER_TOTAL_LENGTH = 16
+       WS_HEADER_DEFAULT_VERSION = 1
+       WS_BODY_PROTOCOL_VERSION_NORMAL = 0
+       WS_BODY_PROTOCOL_VERSION_DEFLATE = 2
+       WS_OP_USER_AUTHENTICATION = 7
+       WS_OP_HEARTBEAT = 2
+       WS_HEADER_DEFAULT_SEQUENCE = 1
+       WS_OP_CONNECT_SUCCESS = 8
+       WS_OP_MESSAGE = 5
+       WS_OP_HEARTBEAT_REPLY = 3
+)
+
+//返回数据分派
+func Reply(b []byte) {
+       l := p.Logf().New().Level(LogLevel)
+       defer l.Block()
+
+       if ist, _ := headChe(b[:16], len(b), WS_BODY_PROTOCOL_VERSION_DEFLATE, WS_OP_MESSAGE, 0, 4); ist {
+               Msg(b, true);return
+       }
+       if ist, _ := headChe(b[:16], len(b), WS_BODY_PROTOCOL_VERSION_NORMAL, WS_OP_MESSAGE, 0, 4); ist {
+               Msg(b, false);return
+       }
+
+       if ist, _ := headChe(b[:16], len(b), WS_HEADER_DEFAULT_VERSION, WS_OP_HEARTBEAT_REPLY, WS_HEADER_DEFAULT_SEQUENCE, 4); ist {
+               l.I("heartbeat replay!");
+               return
+       }
+
+       l.I("unknow reply", b)
+}
+
+//头部生成与检查
+func headGen(datalenght,Opeation,Sequence int) []byte {
+       var buffer bytes.Buffer //Buffer是一个实现了读写方法的可变大小的字节缓冲
+
+       buffer.Write(Itob32(int32(datalenght + WS_PACKAGE_HEADER_TOTAL_LENGTH)))
+       buffer.Write(Itob16(WS_PACKAGE_HEADER_TOTAL_LENGTH))
+       buffer.Write(Itob16(WS_HEADER_DEFAULT_VERSION))
+       buffer.Write(Itob32(int32(Opeation)))
+       buffer.Write(Itob32(int32(Sequence)))
+
+       return buffer.Bytes()
+}
+
+func headChe(head []byte, datalenght,Bodyv,Opeation,Sequence,show int) (bool,int32) {
+       if len(head) != WS_PACKAGE_HEADER_TOTAL_LENGTH {return false, 0}
+       
+       l := p.Logf().New().Level(show)
+       defer l.Block()
+
+       packL := Btoi32(head[:4])
+       headL := Btoi16(head[4:6])
+       BodyV := Btoi16(head[6:8])
+       OpeaT := Btoi32(head[8:12])
+       Seque := Btoi32(head[12:16])
+
+       if packL > int32(datalenght) {l.E("包缺损", packL, datalenght);return false, packL}
+       if headL != WS_PACKAGE_HEADER_TOTAL_LENGTH {l.E("头错误", headL);return false, packL}
+       if OpeaT != int32(Opeation) {l.E("类型错误");return false, packL}
+       if Seque != int32(Sequence) {l.E("Seq错误");return false, packL}
+       if BodyV != int16(Bodyv) {l.E("压缩算法错误");return false, packL}
+       return true, packL
+}
+
+//认证生成与检查
+func hello_send(roomid int, key string) []byte {
+       l := p.Logf().New().Level(LogLevel).I("hello_ws")
+       defer l.Block()
+
+       if roomid == 0 || key == "" {
+               l.E("->", "roomid == 0 || key == \"\"")
+               return []byte("")
+       }
+       
+       //from player-loader-2.0.4.min.js
+       /*
+               customAuthParam
+       */
+       const (
+               _uid = 0
+               _protover = 2
+               _platform = "web"
+               VERSION = "2.0.4"
+               _type = 2
+       )
+
+       var obj = `{"uid":` + strconv.Itoa(_uid) + 
+       `,"roomid":` + strconv.Itoa(roomid) + 
+       `,"protover":` + strconv.Itoa(_protover) + 
+       `,"platform":"`+ _platform + 
+       `","clientver":"` + VERSION + 
+       `","type":` + strconv.Itoa(_type) + 
+       `,"key":"` + key + `"}`
+
+       var buffer bytes.Buffer //Buffer是一个实现了读写方法的可变大小的字节缓冲
+       
+       buffer.Write(headGen(len(obj), WS_OP_USER_AUTHENTICATION, WS_HEADER_DEFAULT_SEQUENCE))
+
+       buffer.Write([]byte(obj))
+
+       return buffer.Bytes()
+}
+
+func hello_ok(r []byte) bool {
+       if len(r) == 0 {return false}
+
+       var obj = `{"code":0}`
+
+       var buffer bytes.Buffer //Buffer是一个实现了读写方法的可变大小的字节缓冲
+
+       buffer.Write(headGen(len(obj), WS_OP_CONNECT_SUCCESS, WS_HEADER_DEFAULT_SEQUENCE))
+
+       buffer.Write([]byte(obj))
+
+       h := buffer.Bytes()
+
+       if len(h) != len(r) {return false}
+
+       for k, v := range r {
+               if v != h[k] {return false}
+       }
+       return true
+}
+
+//心跳生成
+func heartbeat() ([]byte, int) {
+       //from player-loader-2.0.4.min.js
+       const heartBeatInterval = 30
+
+       var obj = `[object Object]`
+
+       var buffer bytes.Buffer //Buffer是一个实现了读写方法的可变大小的字节缓冲
+       
+       buffer.Write(headGen(len(obj), WS_OP_HEARTBEAT, WS_HEADER_DEFAULT_SEQUENCE))
+
+       buffer.Write([]byte(obj))
+
+       return buffer.Bytes(), heartBeatInterval
+
+}
\ No newline at end of file
diff --git a/bili_danmu_test.go b/bili_danmu_test.go
new file mode 100644 (file)
index 0000000..709ebf1
--- /dev/null
@@ -0,0 +1,10 @@
+package bili_danmu
+
+import (
+       "testing"
+)
+
+func Test_bili_danmu(t *testing.T) {
+       
+}
diff --git a/demo/go.mod b/demo/go.mod
new file mode 100644 (file)
index 0000000..9cdf95b
--- /dev/null
@@ -0,0 +1,3 @@
+module github.com/qydysky/bili_danmu/demo
+
+go 1.14
diff --git a/demo/main.go b/demo/main.go
new file mode 100644 (file)
index 0000000..f91f041
--- /dev/null
@@ -0,0 +1,9 @@
+package main
+
+import (
+       q "github.com/qydysky/bili_danmu"
+)
+
+func main() {
+       q.Demo()
+}
\ No newline at end of file
diff --git a/go.mod b/go.mod
new file mode 100644 (file)
index 0000000..621c3c8
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,15 @@
+module github.com/qydysky/bili_danmu
+
+go 1.14
+
+require (
+       github.com/gorilla/websocket v1.4.2
+       github.com/klauspost/compress v1.11.0 // indirect
+       github.com/qydysky/part v0.0.0-20200914123330-afade058e33d
+       github.com/shirou/gopsutil v2.20.8+incompatible // indirect
+       golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a // indirect
+       golang.org/x/net v0.0.0-20200904194848-62affa334b73 // indirect
+       golang.org/x/sys v0.0.0-20200909081042-eff7692f9009 // indirect
+)
+
+//replace github.com/qydysky/part => ../part
diff --git a/go.sum b/go.sum
new file mode 100644 (file)
index 0000000..3461172
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,51 @@
+github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk=
+github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
+github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
+github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
+github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
+github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/klauspost/compress v1.10.10 h1:a/y8CglcM7gLGYmlbP/stPE5sR3hbhFRUjCBfd/0B3I=
+github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
+github.com/klauspost/compress v1.11.0 h1:wJbzvpYMVGG9iTI9VxpnNZfd4DzMPoCWze3GgSqz8yg=
+github.com/klauspost/compress v1.11.0/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
+github.com/miekg/dns v1.1.31 h1:sJFOl9BgwbYAWOGEwr61FU28pqsBNdpRBnhGXtO06Oo=
+github.com/miekg/dns v1.1.31/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
+github.com/qydysky/part v0.0.0-20200912061031-0f3a305b4afb h1:p2R5IUkza8POjdulQWSJrFSxhpjIzSWLX4KMt/YyS9E=
+github.com/qydysky/part v0.0.0-20200912061031-0f3a305b4afb/go.mod h1:+8N3UgJBVyJj8ar31eZtucwrKpLpay854Y5qq0xk3x0=
+github.com/qydysky/part v0.0.0-20200914123330-afade058e33d h1:um1WzsGzwD6h+1W3jqGcWJPVr4kT5gmRRaXXVwO7nb4=
+github.com/qydysky/part v0.0.0-20200914123330-afade058e33d/go.mod h1:+8N3UgJBVyJj8ar31eZtucwrKpLpay854Y5qq0xk3x0=
+github.com/shirou/gopsutil v2.20.7+incompatible h1:Ymv4OD12d6zm+2yONe39VSmp2XooJe8za7ngOLW/o/w=
+github.com/shirou/gopsutil v2.20.7+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
+github.com/shirou/gopsutil v2.20.8+incompatible h1:8c7Atn0FAUZJo+f4wYbN0iVpdWniCQk7IYwGtgdh1mY=
+github.com/shirou/gopsutil v2.20.8+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
+github.com/thedevsaddam/gojsonq v2.3.0+incompatible h1:i2lFTvGY4LvoZ2VUzedsFlRiyaWcJm3Uh6cQ9+HyQA8=
+github.com/thedevsaddam/gojsonq v2.3.0+incompatible/go.mod h1:RBcQaITThgJAAYKH7FNp2onYodRz8URfsuEGpAch0NA=
+github.com/thedevsaddam/gojsonq/v2 v2.5.2 h1:CoMVaYyKFsVj6TjU6APqAhAvC07hTI6IQen8PHzHYY0=
+github.com/thedevsaddam/gojsonq/v2 v2.5.2/go.mod h1:bv6Xa7kWy82uT0LnXPE2SzGqTj33TAEeR560MdJkiXs=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de h1:ikNHVSjEfnvz6sxdSPCaPt572qowuyMDMJLLm3Db3ig=
+golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM=
+golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU=
+golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200904194848-62affa334b73 h1:MXfv8rhZWmFeqX3GNZRsd6vOLoaCHjYEX3qkRo3YBUA=
+golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed h1:WBkVNH1zd9jg/dK4HCM4lNANnmd12EHC9z+LmcCG4ns=
+golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200909081042-eff7692f9009 h1:W0lCpv29Hv0UaM1LXb9QlBHLNP8UFfcKjblhVCWftOM=
+golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/ws.go b/ws.go
new file mode 100644 (file)
index 0000000..ddd0ae3
--- /dev/null
+++ b/ws.go
@@ -0,0 +1,162 @@
+package bili_danmu
+
+import (
+       "time"
+
+       "github.com/gorilla/websocket"
+
+       p "github.com/qydysky/part"
+)
+
+type ws struct {
+       used bool
+       
+       SendChan chan []byte
+       RecvChan chan []byte
+       interrupt chan struct{}
+       url string
+}
+
+func New_ws(url string) (o *ws) {
+       l := p.Logf().New().Level(LogLevel).I("New_ws")
+       defer l.Block()
+
+       l.I("->", "ok")
+       o = new(ws)
+       o.url = url
+       o.SendChan = make(chan []byte, 1e4)
+       o.RecvChan = make(chan []byte, 1e4)
+       return 
+}
+
+func (i *ws) Handle() (o *ws) {
+       o = i
+       l := p.Logf().New().Level(LogLevel).I("*ws.handle")
+       defer l.Block()
+
+       if o.used {
+               l.E("->", "o.used")
+               return
+       }
+
+       if o.url == "" {
+               l.E("->", "o.url == \"\"")
+               return
+       }
+
+       started := make(chan struct{})
+
+       go func() {
+               defer close(o.RecvChan)
+
+               c, _, err := websocket.DefaultDialer.Dial(o.url, nil)
+               if err != nil {
+                       l.E("->", err)
+                       return
+               }
+               defer c.Close()
+
+               l.I("->", "ok")
+               o.interrupt = make(chan struct{})
+               done := make(chan struct{})
+
+               go func() {
+                       defer close(done)
+
+                       for {
+                               _, message, err := c.ReadMessage()
+                               if err != nil {
+                                       if !websocket.IsCloseError(err, websocket.CloseNormalClosure) {
+                                               l.E("->", err)
+                                       }
+                                       o.used = false
+                                       return
+                               }
+                               o.RecvChan <- message
+                       }
+               }()
+
+               close(started)
+
+               for {
+                       select {
+                       case <- done:
+                               o.used = false
+                               return
+                       case t := <- o.SendChan:
+                               err := c.WriteMessage(websocket.TextMessage, t)
+                               if err != nil {
+                                       l.I("->", "write:", err)
+                                       o.used = false
+                                       return
+                               }
+                       case <- o.interrupt:
+                               l.I("->", "interrupt")
+                               // Cleanly close the connection by sending a close message and then
+                               // waiting (with timeout) for the server to close the connection.
+                               err := c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
+                               if err != nil {
+                                       l.E("->", err)
+                               }
+                               select {
+                               case <- done:
+                               case <- time.After(time.Second):
+                               }
+                               o.used = false
+                               return
+                       }
+               }
+       }()
+
+       <- started
+       o.used = true
+       return
+}
+
+func (i *ws) Heartbeat(Millisecond int, msg []byte) (o *ws) {
+       o = i
+       l := p.Logf().New().Level(LogLevel).I("*ws.heartbeat")
+       defer l.Block()
+
+       if !o.used {
+               l.E("->", "!o.used")
+               return
+       }
+       o.SendChan <- msg
+       l.I("->", "ok")
+
+       go func(){
+               ticker := time.NewTicker(time.Duration(Millisecond)*time.Millisecond)
+               defer ticker.Stop()
+
+               for {
+                       select {
+                               case <-ticker.C:
+                                       o.SendChan <- msg
+                               case <- o.interrupt:
+                                       l.I("->", "fin")
+                                       return
+                               }
+               }
+       }()
+
+       return
+}
+
+func (o *ws) Close() {
+       l := p.Logf().New().Level(LogLevel).I("*ws.Close")
+       defer l.Block()
+
+       if !o.used {
+               l.I("->", "!o.used")
+               return
+       }
+       o.used = false
+
+       close(o.interrupt)
+       l.I("->", "ok")
+}
+
+func (o *ws) Isclose() bool {
+       return !o.used
+}
\ No newline at end of file