├── .gitignore
├── LICENSE
├── README.md
├── bilibili
├── client.go
├── handler.go
├── protocol.go
└── utils.go
├── cmds
└── danmi
│ └── main.go
├── douyu
├── client.go
├── handler.go
├── protocol.go
└── utils.go
└── panda
├── client.go
├── handler.go
├── protocol.go
└── utils.go
/.gitignore:
--------------------------------------------------------------------------------
1 | test
2 | cmds/danmi/danmi
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [2018] [songtianyi]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## danmaku
2 | 各直播平台弹幕协议和开放平台API
3 |
4 | ## 支持列表
5 | * **douyu.com**
6 |
7 | ```go
8 | package main
9 |
10 | import (
11 | "fmt"
12 | "github.com/songtianyi/barrage/douyu"
13 | "github.com/songtianyi/rrframework/logs"
14 | )
15 |
16 | func chatmsg(msg *douyu.Message) {
17 | level := msg.GetStringField("level")
18 | nn := msg.GetStringField("nn")
19 | txt := msg.GetStringField("txt")
20 | logs.Info(fmt.Sprintf("level(%s) - %s >>> %s", level, nn, txt))
21 | }
22 |
23 | func main() {
24 | client, err := douyu.Connect("openbarrage.douyutv.com:8601", nil)
25 | if err != nil {
26 | logs.Error(err)
27 | return
28 | }
29 |
30 | client.HandlerRegister.Add("chatmsg", douyu.Handler(chatmsg), "chatmsg")
31 | if err := client.JoinRoom(288016); err != nil {
32 | logs.Error(fmt.Sprintf("Join room fail, %s", err.Error()))
33 | return
34 | }
35 | client.Serve()
36 | }
37 | ```
38 |
39 | * **live.bilibili.com**
40 |
41 | ```
42 | package main
43 |
44 | import (
45 | "github.com/songtianyi/barrage/bilibili"
46 | "github.com/songtianyi/rrframework/logs"
47 | )
48 |
49 | func danmu(msg *bilibili.Message) {
50 | logs.Debug(">>> ", string(msg.Bytes()))
51 | }
52 |
53 | func main() {
54 | // uri, userid, handlerRegister
55 | client, err := bilibili.Connect("https://live.bilibili.com/43783", -1, nil)
56 | if err != nil {
57 | logs.Error(err)
58 | return
59 | }
60 | client.HandlerRegister.Add(bilibili.DANMU_MSG, bilibili.Handler(danmu), "danmu")
61 | client.Serve()
62 | }
63 | ```
64 |
65 | * **padatv.com**
66 | ```
67 | package main
68 |
69 | import (
70 | "github.com/songtianyi/barrage/panda"
71 | "github.com/songtianyi/rrframework/logs"
72 | )
73 |
74 | func danmu(msg *panda.DecodedMessage) {
75 | logs.Debug("(%s) - %s >>> %s", msg.Type, msg.Nickname, msg.Content)
76 | }
77 |
78 | func main() {
79 | // uri, handlerRegister
80 | client, err := panda.Connect("https://www.panda.tv/66666", nil)
81 | if err != nil {
82 | logs.Error(err)
83 | return
84 | }
85 | client.HandlerRegister.Add(panda.DANMU_MSG, panda.Handler(danmu), "danmu")
86 | client.Serve()
87 | }
88 | ```
89 |
90 | ## demo
91 | 
92 |
--------------------------------------------------------------------------------
/bilibili/client.go:
--------------------------------------------------------------------------------
1 | package bilibili
2 |
3 | import (
4 | "encoding/binary"
5 | "fmt"
6 | "github.com/songtianyi/rrframework/logs"
7 | "io"
8 | "net"
9 | "strconv"
10 | "sync"
11 | "time"
12 | )
13 |
14 | type Client struct {
15 | conn net.Conn
16 | HandlerRegister *HandlerRegister
17 | closed chan struct{}
18 | roomid int
19 | uid int
20 |
21 | rLock sync.Mutex
22 | wLock sync.Mutex
23 | }
24 |
25 | func Connect(uri string, uid int, handlerRegister *HandlerRegister) (*Client, error) {
26 |
27 | roomStr, err := GetRoomId(uri)
28 | if err != nil {
29 | return nil, err
30 | }
31 | server, state, err := GetBarrageServerAndLiveState(roomStr)
32 | if err != nil {
33 | return nil, err
34 | }
35 | server += ":" + SERVER_PORT
36 | conn, err := net.DialTimeout("tcp", server, 10*time.Second)
37 | if err != nil {
38 | return nil, err
39 | }
40 |
41 | logs.Info(fmt.Sprintf("%s connected, live status %s", server, state))
42 |
43 | roomid, err := strconv.Atoi(roomStr)
44 | if err != nil {
45 | return nil, err
46 | }
47 | if uid < 0 {
48 | uid = RandUser()
49 | }
50 |
51 | client := &Client{
52 | conn: conn,
53 | roomid: roomid,
54 | uid: uid,
55 | }
56 | if handlerRegister == nil {
57 | client.HandlerRegister = CreateHandlerRegister()
58 | } else {
59 | client.HandlerRegister = handlerRegister
60 | }
61 |
62 | handshake := NewHandshakeMessage(roomid, uid)
63 | if _, err := client.Send(handshake.Encode()); err != nil {
64 | return nil, err
65 | }
66 |
67 | go client.heartbeat()
68 | return client, nil
69 | }
70 |
71 | func (c *Client) Send(b []byte) (int, error) {
72 | c.wLock.Lock()
73 | defer c.wLock.Unlock()
74 | return c.conn.Write(b)
75 | }
76 |
77 | func (c *Client) Receive() ([]byte, int, error) {
78 | c.rLock.Lock()
79 | defer c.rLock.Unlock()
80 | buf := make([]byte, 512)
81 | if _, err := io.ReadFull(c.conn, buf[:HEADER_LENGTH]); err != nil {
82 | return buf, -1, err
83 | }
84 |
85 | // header
86 | // 4byte for packet length
87 | pl := binary.BigEndian.Uint32(buf[:4])
88 |
89 | // ignore buf[4:6] and buf[6:8]
90 | code := int(binary.BigEndian.Uint32(buf[8:12]))
91 | // ignore buf[12:16]
92 |
93 | // body content length
94 | cl := pl - HEADER_LENGTH
95 |
96 | if cl > 512 {
97 | // expand buffer
98 | buf = make([]byte, cl)
99 | }
100 | if _, err := io.ReadFull(c.conn, buf[:cl]); err != nil {
101 | return buf, code, err
102 | }
103 | return buf[:cl], code, nil
104 | }
105 |
106 | // Close connnection
107 | func (c *Client) Close() error {
108 | c.closed <- struct{}{} // receive
109 | c.closed <- struct{}{} // heartbeat
110 | return c.conn.Close()
111 | }
112 |
113 | func (c *Client) heartbeat() {
114 | tick := time.Tick(30 * time.Second)
115 | loop:
116 | for {
117 | select {
118 | case <-c.closed:
119 | break loop
120 | case <-tick:
121 | heartbeat := NewHeartbeatMessage(c.roomid, c.uid)
122 |
123 | if _, err := c.conn.Write(heartbeat.Encode()); err != nil {
124 | logs.Error("heartbeat failed, " + err.Error())
125 | }
126 | }
127 | }
128 | }
129 |
130 | func (c *Client) Serve() {
131 | loop:
132 | for {
133 | select {
134 | case <-c.closed:
135 | break loop
136 | default:
137 | b, code, err := c.Receive()
138 | if err != nil {
139 | logs.Error(err)
140 | continue
141 | }
142 | switch code {
143 | case 3:
144 | logs.Info("heartbeat ok")
145 | continue
146 | case 8:
147 | logs.Info("handshake ok")
148 | continue
149 | case 5:
150 | msg := NewMessage(b, code).Decode()
151 | err, handlers := c.HandlerRegister.Get(msg.GetCmd())
152 | if err != nil {
153 | logs.Warn(err)
154 | continue
155 | }
156 | for _, v := range handlers {
157 | go v.Run(msg)
158 | }
159 | default:
160 | logs.Warn(fmt.Sprintf("unhandled body type %d", code))
161 |
162 | }
163 |
164 | }
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/bilibili/handler.go:
--------------------------------------------------------------------------------
1 | package bilibili
2 |
3 | import (
4 | "fmt"
5 | "sync"
6 | )
7 |
8 | // Handler: message function wrapper
9 | type Handler func(*Message)
10 |
11 | // HandlerWrapper: message handler wrapper
12 | type HandlerWrapper struct {
13 | handle Handler
14 | enabled bool
15 | name string
16 | }
17 |
18 | // Run: message handler callback
19 | func (s *HandlerWrapper) Run(msg *Message) {
20 | if s.enabled {
21 | s.handle(msg)
22 | }
23 | }
24 |
25 | func (s *HandlerWrapper) getName() string {
26 | return s.name
27 | }
28 |
29 | func (s *HandlerWrapper) enableHandle() {
30 | s.enabled = true
31 | return
32 | }
33 |
34 | func (s *HandlerWrapper) disableHandle() {
35 | s.enabled = false
36 | return
37 | }
38 |
39 | // HandlerRegister: message handler manager
40 | type HandlerRegister struct {
41 | mu sync.RWMutex
42 | hmap map[string][]*HandlerWrapper
43 | }
44 |
45 | // CreateHandlerRegister: create handler register
46 | func CreateHandlerRegister() *HandlerRegister {
47 | return &HandlerRegister{
48 | hmap: make(map[string][]*HandlerWrapper),
49 | }
50 | }
51 |
52 | // Add: add message callback handle to handler register
53 | func (hr *HandlerRegister) Add(key string, h Handler, name string) error {
54 | hr.mu.Lock()
55 | defer hr.mu.Unlock()
56 | for _, v := range hr.hmap {
57 | for _, handle := range v {
58 | if handle.getName() == name {
59 | return fmt.Errorf("handler name %s has been registered", name)
60 | }
61 | }
62 | }
63 | hr.hmap[key] = append(hr.hmap[key], &HandlerWrapper{handle: h, enabled: true, name: name})
64 | return nil
65 | }
66 |
67 | // Get: get message handler
68 | func (hr *HandlerRegister) Get(key string) (error, []*HandlerWrapper) {
69 | hr.mu.RLock()
70 | defer hr.mu.RUnlock()
71 | if v, ok := hr.hmap[key]; ok {
72 | return nil, v
73 | }
74 | return fmt.Errorf("no handlers for key [%s]", key), nil
75 | }
76 |
77 | // EnableByType: enable handler by message type
78 | func (hr *HandlerRegister) EnableByType(key string) error {
79 | err, handles := hr.Get(key)
80 | if err != nil {
81 | return err
82 | }
83 | hr.mu.Lock()
84 | defer hr.mu.Unlock()
85 | // all
86 | for _, v := range handles {
87 | v.enableHandle()
88 | }
89 | return nil
90 | }
91 |
92 | // DisableByType: disable handler by message type
93 | func (hr *HandlerRegister) DisableByType(key string) error {
94 | err, handles := hr.Get(key)
95 | if err != nil {
96 | return err
97 | }
98 | hr.mu.Lock()
99 | defer hr.mu.Unlock()
100 | // all
101 | for _, v := range handles {
102 | v.disableHandle()
103 | }
104 | return nil
105 | }
106 |
107 | // EnableByName: enable message handler by name
108 | func (hr *HandlerRegister) EnableByName(name string) error {
109 | hr.mu.Lock()
110 | defer hr.mu.Unlock()
111 | for _, handles := range hr.hmap {
112 | for _, v := range handles {
113 | if v.getName() == name {
114 | v.enableHandle()
115 | return nil
116 | }
117 | }
118 | }
119 | return fmt.Errorf("cannot find handler %s", name)
120 | }
121 |
122 | // DisableByName: disable message handler by name
123 | func (hr *HandlerRegister) DisableByName(name string) error {
124 | hr.mu.Lock()
125 | defer hr.mu.Unlock()
126 | for _, handles := range hr.hmap {
127 | for _, v := range handles {
128 | if v.getName() == name {
129 | v.disableHandle()
130 | return nil
131 | }
132 | }
133 | }
134 | return fmt.Errorf("cannot find handler %s", name)
135 | }
136 |
137 | // Dump: output all message handlers
138 | func (hr *HandlerRegister) Dump() string {
139 | hr.mu.RLock()
140 | defer hr.mu.RUnlock()
141 | str := "[plugins dump]\n"
142 | for k, handles := range hr.hmap {
143 | for _, v := range handles {
144 | str += fmt.Sprintf("%d %s [%v]\n", k, v.getName(), v.enabled)
145 | }
146 | }
147 | return str
148 | }
149 |
--------------------------------------------------------------------------------
/bilibili/protocol.go:
--------------------------------------------------------------------------------
1 | package bilibili
2 |
3 | import (
4 | "bytes"
5 | "encoding/binary"
6 | "fmt"
7 | "github.com/songtianyi/rrframework/config"
8 | "github.com/songtianyi/rrframework/logs"
9 | )
10 |
11 | const (
12 | HEADER_LENGTH = 16 // in bytes
13 | DEVICE_TYPE = 1
14 | DEVICE = 1
15 | )
16 |
17 | const (
18 | // cmd types
19 | DANMU_MSG = "DANMU_MSG"
20 |
21 | //
22 | SERVER_PORT = "2243"
23 | )
24 |
25 | type Message struct {
26 | body []byte
27 | bodyType int32
28 | }
29 |
30 | func NewHandshakeMessage(roomid, uid int) *Message {
31 |
32 | data := fmt.Sprintf(`{"roomid":%d,"uid":%d}`, roomid, uid)
33 | message := &Message{
34 | body: []byte(data),
35 | bodyType: 7,
36 | }
37 | return message
38 |
39 | }
40 |
41 | func NewHeartbeatMessage(room, uid int) *Message {
42 |
43 | data := fmt.Sprintf(`{"roomid":%d,"uid":%d}`, room, uid)
44 | message := &Message{
45 | body: []byte(data),
46 | bodyType: 2,
47 | }
48 | return message
49 |
50 | }
51 |
52 | func NewMessage(b []byte, btype int) *Message {
53 | return &Message{
54 | body: b,
55 | bodyType: int32(btype),
56 | }
57 |
58 | }
59 |
60 | func (msg *Message) Encode() []byte {
61 | buffer := bytes.NewBuffer([]byte{})
62 | binary.Write(buffer, binary.BigEndian, int32(len(msg.body)+HEADER_LENGTH)) // write package length
63 | binary.Write(buffer, binary.BigEndian, int16(HEADER_LENGTH)) // header length
64 | binary.Write(buffer, binary.BigEndian, int16(DEVICE_TYPE))
65 | binary.Write(buffer, binary.BigEndian, int32(msg.bodyType))
66 | binary.Write(buffer, binary.BigEndian, int32(DEVICE))
67 | binary.Write(buffer, binary.BigEndian, msg.body)
68 | return buffer.Bytes()
69 | }
70 |
71 | func (msg *Message) Decode() *Message {
72 | // TODO
73 | return msg
74 | }
75 |
76 | func (msg *Message) GetCmd() string {
77 | jc, err := rrconfig.LoadJsonConfigFromBytes(msg.body)
78 | if err != nil {
79 | logs.Error(err)
80 | return "INVALID"
81 | }
82 | cmd, err := jc.GetString("cmd")
83 | if err != nil {
84 | logs.Error(err)
85 | return "ERROR"
86 | }
87 | return cmd
88 |
89 | }
90 |
91 | func (msg *Message) Bytes() []byte {
92 | return msg.body
93 | }
94 |
--------------------------------------------------------------------------------
/bilibili/utils.go:
--------------------------------------------------------------------------------
1 | package bilibili
2 |
3 | import (
4 | "fmt"
5 | "io/ioutil"
6 | "math/rand"
7 | "net/http"
8 | "regexp"
9 | )
10 |
11 | var (
12 | roomReg = regexp.MustCompile("var ROOMID = (\\d+)")
13 | serverReg = regexp.MustCompile("(.*?)")
14 | stateReg = regexp.MustCompile("(.*?)")
15 | )
16 |
17 | func doHttp(uri string) ([]byte, error) {
18 | resp, err := http.Get(uri)
19 | if err != nil {
20 | return nil, err
21 | }
22 | defer resp.Body.Close()
23 | body, _ := ioutil.ReadAll(resp.Body)
24 | return body, nil
25 | }
26 |
27 | func GetRoomId(uri string) (string, error) {
28 | body, err := doHttp(uri)
29 | if err != nil {
30 | return "", err
31 | }
32 | fmt.Println(string(body))
33 | matchs := roomReg.FindStringSubmatch(string(body))
34 | if len(matchs) < 2 {
35 | return "", fmt.Errorf("ROOMID submatch %q", matchs)
36 | }
37 | return matchs[1], nil
38 | }
39 |
40 | func GetBarrageServerAndLiveState(room string) (string, string, error) {
41 | uri := "http://live.bilibili.com/api/player?id=cid:" + room
42 | body, err := doHttp(uri)
43 | if err != nil {
44 | return "", "", err
45 | }
46 | matchs := serverReg.FindStringSubmatch(string(body))
47 | if len(matchs) < 2 {
48 | return "", "", fmt.Errorf("server submatch %q", matchs)
49 | }
50 | server := matchs[1]
51 |
52 | matchs = stateReg.FindStringSubmatch(string(body))
53 | if len(matchs) < 2 {
54 | return "", "", fmt.Errorf("state submatch %q", matchs)
55 | }
56 | state := matchs[1]
57 | return server, state, nil
58 | }
59 |
60 | func RandUser() int {
61 | return rand.Intn(4e7-1e5) + 1e5
62 | }
63 |
--------------------------------------------------------------------------------
/cmds/danmi/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "os"
7 | "strings"
8 |
9 | "github.com/songtianyi/danmaku/bilibili"
10 | "github.com/songtianyi/danmaku/douyu"
11 | "github.com/songtianyi/danmaku/panda"
12 | "github.com/songtianyi/rrframework/logs"
13 | "github.com/urfave/cli"
14 | "github.com/yanyiwu/gojieba"
15 | )
16 |
17 | var (
18 | jieba = gojieba.NewJieba()
19 | string2Level = map[string]int{
20 | "EMERGENCY": logs.LevelEmergency,
21 | "ALERT": logs.LevelAlert,
22 | "CRITICAL": logs.LevelCritical,
23 | "ERROR": logs.LevelError,
24 | "WARNING": logs.LevelWarning,
25 | "NOTICE": logs.LevelNotice,
26 | "INFO": logs.LevelInformational,
27 | "DEBUG": logs.LevelDebug,
28 | }
29 | )
30 |
31 | func chatmsg(msg *douyu.Message) {
32 | level := msg.GetStringField("level")
33 | nn := msg.GetStringField("nn")
34 | txt := msg.GetStringField("txt")
35 |
36 | // contents := jieba.CutAll(txt)
37 |
38 | // logs.Info(fmt.Sprintf("level(%s) - %s >>> %s\n%s", level, nn, txt, string(contents)))
39 | // logs.Info(fmt.Sprintf("level(%s) - %s >>> %s\n%s", level, nn, txt, strings.Join(contents, "/")))
40 | logs.Info(fmt.Sprintf("level(%s) - %s >>> %s", level, nn, txt))
41 |
42 | }
43 |
44 | func douyuHandler(ctx *cli.Context) error {
45 | server := ctx.String("s")
46 | client, err := douyu.Connect(server, nil)
47 | if err != nil {
48 | return err
49 | }
50 |
51 | client.HandlerRegister.Add("chatmsg", douyu.Handler(chatmsg), "chatmsg")
52 | if err := client.JoinRoom(ctx.Int("rid")); err != nil {
53 | logs.Error(fmt.Sprintf("Join room fail, %s", err.Error()))
54 | return err
55 | }
56 | client.Serve()
57 | return nil
58 | }
59 |
60 | func danmu(msg *bilibili.Message) {
61 | logs.Debug(">>>", string(msg.Bytes()))
62 | }
63 |
64 | func bilibiliHandler(ctx *cli.Context) error {
65 | prefix := ctx.String("s")
66 | if !strings.HasSuffix(prefix, "/") {
67 | prefix += "/"
68 | }
69 | rid := ctx.String("rid")
70 | uid := ctx.Int("uid")
71 | client, err := bilibili.Connect(prefix+rid, uid, nil)
72 | if err != nil {
73 | return err
74 | }
75 | client.HandlerRegister.Add(bilibili.DANMU_MSG, bilibili.Handler(danmu), "danmu")
76 | client.Serve()
77 | return nil
78 | }
79 |
80 | func pandaTV(msg *panda.DecodedMessage) {
81 | logs.Debug("(%s) - %s >>> %s", msg.Type, msg.Nickname, msg.Content)
82 | }
83 |
84 | func pandaHandler(ctx *cli.Context) error {
85 | // uri, handlerRegister
86 | prefix := ctx.String("s")
87 | if !strings.HasSuffix(prefix, "/") {
88 | prefix += "/"
89 | }
90 | rid := ctx.String("rid")
91 | client, err := panda.Connect(prefix+rid, nil)
92 | if err != nil {
93 | logs.Error(err)
94 | return err
95 | }
96 | client.HandlerRegister.Add(panda.DANMU_MSG, panda.Handler(pandaTV), "danmu")
97 | client.Serve()
98 |
99 | return nil
100 | }
101 |
102 | func main() {
103 | app := cli.NewApp()
104 | app.Usage = "A cli tool to stat danmu messages."
105 | app.Version = "1.0.0"
106 | app.Commands = []cli.Command{
107 | {
108 | Name: "douyu",
109 | Usage: "connect to douyu danmu message server",
110 | Action: douyuHandler,
111 | Flags: []cli.Flag{
112 | cli.StringFlag{
113 | Name: "server, s",
114 | Value: "openbarrage.douyutv.com:8601",
115 | Usage: "douyu danmu message server address",
116 | },
117 | cli.IntFlag{
118 | Name: "room, rid",
119 | Value: 667351,
120 | Usage: "douyu room id",
121 | },
122 | },
123 | },
124 | {
125 | Name: "bilibili",
126 | Usage: "connect to bilibili danmu message api",
127 | Action: bilibiliHandler,
128 | Flags: []cli.Flag{
129 | cli.StringFlag{
130 | Name: "server, s",
131 | Value: "https://live.bilibili.com/",
132 | Usage: "bilibili danmu api prefix",
133 | },
134 | cli.IntFlag{
135 | Name: "room, rid",
136 | Value: 28645,
137 | Usage: "bilibili room id",
138 | },
139 | cli.IntFlag{
140 | Name: "user, uid",
141 | Value: -1,
142 | Usage: "bilibili user id",
143 | },
144 | },
145 | },
146 | {
147 | Name: "panda",
148 | Usage: "connect to pandaTV danmu message api",
149 | Action: pandaHandler,
150 | Flags: []cli.Flag{
151 | cli.StringFlag{
152 | Name: "server, s",
153 | Value: "https://www.panda.tv/",
154 | Usage: "pandaTV danmu api prefix",
155 | },
156 | cli.IntFlag{
157 | Name: "room, rid",
158 | Value: 66666,
159 | Usage: "pandaTV room id",
160 | },
161 | },
162 | },
163 | }
164 | app.Flags = []cli.Flag{
165 | cli.StringFlag{
166 | Name: "log, l",
167 | Value: "DEBUG",
168 | Usage: "log level settings, case insensitive, {" +
169 | "EMERGENCY|ALERT|CRITICAL|ERROR|WARNING|NOTICE|INFO|DEBUG}",
170 | },
171 | }
172 | app.Action = func(ctx *cli.Context) error {
173 | if v, ok := string2Level[ctx.String("l")]; ok {
174 | logs.SetLevel(v)
175 | }
176 | return nil
177 | }
178 | err := app.Run(os.Args)
179 | if err != nil {
180 | log.Fatal(err)
181 | }
182 | return
183 | }
184 |
--------------------------------------------------------------------------------
/douyu/client.go:
--------------------------------------------------------------------------------
1 | package douyu
2 |
3 | import (
4 | "encoding/binary"
5 | "fmt"
6 | "io"
7 | "net"
8 | "sync"
9 | "time"
10 |
11 | "github.com/songtianyi/rrframework/logs"
12 | )
13 |
14 | type Client struct {
15 | conn net.Conn
16 | HandlerRegister *HandlerRegister
17 | closed chan struct{}
18 |
19 | rLock sync.Mutex
20 | wLock sync.Mutex
21 | }
22 |
23 | // Connect to douyu barrage server
24 | func Connect(connStr string, handlerRegister *HandlerRegister) (*Client, error) {
25 | conn, err := net.Dial("tcp", connStr)
26 | if err != nil {
27 | return nil, err
28 | }
29 |
30 | logs.Info(fmt.Sprintf("%s connected.", connStr))
31 |
32 | // server connected
33 | client := &Client{
34 | conn: conn,
35 | }
36 |
37 | if handlerRegister == nil {
38 | client.HandlerRegister = CreateHandlerRegister()
39 | } else {
40 | client.HandlerRegister = handlerRegister
41 | }
42 |
43 | go client.heartbeat()
44 | return client, nil
45 | }
46 |
47 | // Send message to server
48 | func (c *Client) Send(b []byte) (int, error) {
49 | c.wLock.Lock()
50 | defer c.wLock.Unlock()
51 | return c.conn.Write(b)
52 | }
53 |
54 | // Receive message from server
55 | func (c *Client) Receive() ([]byte, int, error) {
56 | c.rLock.Lock()
57 | defer c.rLock.Unlock()
58 | buf := make([]byte, 512)
59 | if _, err := io.ReadFull(c.conn, buf[:12]); err != nil {
60 | return buf, 0, err
61 | }
62 |
63 | // 12 bytes header
64 | // 4byte for packet length
65 | pl := binary.LittleEndian.Uint32(buf[:4])
66 |
67 | // ignore buf[4:8]
68 |
69 | // 2byte for message type
70 | code := binary.LittleEndian.Uint16(buf[8:10])
71 |
72 | // 1byte for secret
73 | // 1byte for reserved
74 |
75 | // body content length(include ENDING)
76 | cl := pl - 8
77 |
78 | if cl > 512 {
79 | // expand buffer
80 | buf = make([]byte, cl)
81 | }
82 | if _, err := io.ReadFull(c.conn, buf[:cl]); err != nil {
83 | return buf, int(code), err
84 | }
85 | // exclude ENDING
86 | return buf[:cl-1], int(code), nil
87 | }
88 |
89 | // Close connnection
90 | func (c *Client) Close() error {
91 | c.closed <- struct{}{} // receive
92 | c.closed <- struct{}{} // heartbeat
93 | return c.conn.Close()
94 | }
95 |
96 | // JoinRoom
97 | func (c *Client) JoinRoom(room int) error {
98 | loginMessage := NewMessage(nil, MESSAGE_TO_SERVER).
99 | SetField("type", MSG_TYPE_LOGINREQ).
100 | SetField("roomid", room)
101 |
102 | logs.Info(fmt.Sprintf("joining room %d...", room))
103 | if _, err := c.Send(loginMessage.Encode()); err != nil {
104 | return err
105 | }
106 |
107 | b, code, err := c.Receive()
108 | if err != nil {
109 | return err
110 | }
111 |
112 | // TODO assert(code == MESSAGE_FROM_SERVER)
113 | logs.Info(fmt.Sprintf("room %d joined", room))
114 | loginRes := NewMessage(nil, MESSAGE_FROM_SERVER).Decode(b, code)
115 | logs.Info(fmt.Sprintf("room %d live status %s", room, loginRes.GetStringField("live_stat")))
116 |
117 | joinMessage := NewMessage(nil, MESSAGE_TO_SERVER).
118 | SetField("type", "joingroup").
119 | SetField("rid", room).
120 | SetField("gid", "-9999")
121 |
122 | logs.Info(fmt.Sprintf("joining group %d...", -9999))
123 | _, err = c.Send(joinMessage.Encode())
124 | if err != nil {
125 | return err
126 | }
127 | logs.Info(fmt.Sprintf("group %d joined", -9999))
128 | return nil
129 | }
130 |
131 | func (c *Client) Serve() {
132 | loop:
133 | for {
134 | select {
135 | case <-c.closed:
136 | break loop
137 | default:
138 | b, code, err := c.Receive()
139 | if err != nil {
140 | logs.Error(err)
141 | break loop
142 | }
143 |
144 | // analize message
145 | msg := NewMessage(nil, MESSAGE_FROM_SERVER).Decode(b, code)
146 | err, handlers := c.HandlerRegister.Get(msg.GetStringField("type"))
147 | if err != nil {
148 | logs.Debug(err)
149 | continue
150 | }
151 | for _, v := range handlers {
152 | go v.Run(msg)
153 | }
154 | }
155 | }
156 | }
157 |
158 | func (c *Client) heartbeat() {
159 | tick := time.Tick(45 * time.Second)
160 | loop:
161 | for {
162 | select {
163 | case <-c.closed:
164 | break loop
165 | case <-tick:
166 | heartbeatMsg := NewMessage(nil, MESSAGE_TO_SERVER).
167 | SetField("type", "keeplive").
168 | SetField("tick", time.Now().Unix())
169 |
170 | _, err := c.Send(heartbeatMsg.Encode())
171 | if err != nil {
172 | logs.Error("heartbeat failed, " + err.Error())
173 | }
174 | }
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/douyu/handler.go:
--------------------------------------------------------------------------------
1 | package douyu
2 |
3 | import (
4 | "fmt"
5 | "sync"
6 | )
7 |
8 | // Handler: message function wrapper
9 | type Handler func(*Message)
10 |
11 | // HandlerWrapper: message handler wrapper
12 | type HandlerWrapper struct {
13 | handle Handler
14 | enabled bool
15 | name string
16 | }
17 |
18 | // Run: message handler callback
19 | func (s *HandlerWrapper) Run(msg *Message) {
20 | if s.enabled {
21 | s.handle(msg)
22 | }
23 | }
24 |
25 | func (s *HandlerWrapper) getName() string {
26 | return s.name
27 | }
28 |
29 | func (s *HandlerWrapper) enableHandle() {
30 | s.enabled = true
31 | return
32 | }
33 |
34 | func (s *HandlerWrapper) disableHandle() {
35 | s.enabled = false
36 | return
37 | }
38 |
39 | // HandlerRegister: message handler manager
40 | type HandlerRegister struct {
41 | mu sync.RWMutex
42 | hmap map[string][]*HandlerWrapper
43 | }
44 |
45 | // CreateHandlerRegister: create handler register
46 | func CreateHandlerRegister() *HandlerRegister {
47 | return &HandlerRegister{
48 | hmap: make(map[string][]*HandlerWrapper),
49 | }
50 | }
51 |
52 | // Add: add message callback handle to handler register
53 | func (hr *HandlerRegister) Add(key string, h Handler, name string) error {
54 | hr.mu.Lock()
55 | defer hr.mu.Unlock()
56 | for _, v := range hr.hmap {
57 | for _, handle := range v {
58 | if handle.getName() == name {
59 | return fmt.Errorf("handler name %s has been registered", name)
60 | }
61 | }
62 | }
63 | hr.hmap[key] = append(hr.hmap[key], &HandlerWrapper{handle: h, enabled: true, name: name})
64 | return nil
65 | }
66 |
67 | // Get: get message handler
68 | func (hr *HandlerRegister) Get(key string) (error, []*HandlerWrapper) {
69 | hr.mu.RLock()
70 | defer hr.mu.RUnlock()
71 | if v, ok := hr.hmap[key]; ok {
72 | return nil, v
73 | }
74 | return fmt.Errorf("no handlers for key [%s]", key), nil
75 | }
76 |
77 | // EnableByType: enable handler by message type
78 | func (hr *HandlerRegister) EnableByType(key string) error {
79 | err, handles := hr.Get(key)
80 | if err != nil {
81 | return err
82 | }
83 | hr.mu.Lock()
84 | defer hr.mu.Unlock()
85 | // all
86 | for _, v := range handles {
87 | v.enableHandle()
88 | }
89 | return nil
90 | }
91 |
92 | // DisableByType: disable handler by message type
93 | func (hr *HandlerRegister) DisableByType(key string) error {
94 | err, handles := hr.Get(key)
95 | if err != nil {
96 | return err
97 | }
98 | hr.mu.Lock()
99 | defer hr.mu.Unlock()
100 | // all
101 | for _, v := range handles {
102 | v.disableHandle()
103 | }
104 | return nil
105 | }
106 |
107 | // EnableByName: enable message handler by name
108 | func (hr *HandlerRegister) EnableByName(name string) error {
109 | hr.mu.Lock()
110 | defer hr.mu.Unlock()
111 | for _, handles := range hr.hmap {
112 | for _, v := range handles {
113 | if v.getName() == name {
114 | v.enableHandle()
115 | return nil
116 | }
117 | }
118 | }
119 | return fmt.Errorf("cannot find handler %s", name)
120 | }
121 |
122 | // DisableByName: disable message handler by name
123 | func (hr *HandlerRegister) DisableByName(name string) error {
124 | hr.mu.Lock()
125 | defer hr.mu.Unlock()
126 | for _, handles := range hr.hmap {
127 | for _, v := range handles {
128 | if v.getName() == name {
129 | v.disableHandle()
130 | return nil
131 | }
132 | }
133 | }
134 | return fmt.Errorf("cannot find handler %s", name)
135 | }
136 |
137 | // Dump: output all message handlers
138 | func (hr *HandlerRegister) Dump() string {
139 | hr.mu.RLock()
140 | defer hr.mu.RUnlock()
141 | str := "[plugins dump]\n"
142 | for k, handles := range hr.hmap {
143 | for _, v := range handles {
144 | str += fmt.Sprintf("%d %s [%v]\n", k, v.getName(), v.enabled)
145 | }
146 | }
147 | return str
148 | }
149 |
--------------------------------------------------------------------------------
/douyu/protocol.go:
--------------------------------------------------------------------------------
1 | package douyu
2 |
3 | import (
4 | "bytes"
5 | "encoding/binary"
6 | "fmt"
7 | "strings"
8 | "sync"
9 | )
10 |
11 | // consts
12 | const (
13 | MESSAGE_TO_SERVER = int16(689)
14 | MESSAGE_FROM_SERVER = int16(690)
15 | MESSAGE_ENDING = int8(0)
16 | )
17 |
18 | // 弹幕服务器端相应消息类型
19 | const (
20 | MSG_TYPE_LOGINREQ = "loginreq"
21 | // 客户端登录请求
22 | // 字段说明
23 | // type 表示为"登录"消息,固定为 loginreq
24 | // roomid 房间id
25 | MSG_TYPE_LOGINRES = "loginres" // 登录响应消息
26 | // 服务端返回登陆响应消息,完整的数据部分应包含的字段如下:
27 | // 字段说明
28 | // type 表示为“登出”消息,固定为 loginres
29 | // userid 用户 ID
30 | // roomgroup 房间权限组
31 | // pg 平台权限组
32 | // sessionid 会话 ID
33 | // username 用户名
34 | // nickname 用户昵称
35 | // is_signed 是否已在房间签到
36 | // signed_count 日总签到次数
37 | // live_stat 直播状态
38 | // npv 是否需要手机验证
39 | // best_dlev 最高酬勤等级
40 | // cur_lev 酬勤等级
41 | MSG_TYPE_KEEPALIVE = "keeplive" // 服务端心跳消息
42 | // 服务端响应客户端心跳的消息,完整的数据部分应包含的字段如下:
43 | // 字段说明
44 | // type 表示为“心跳”消息,固定为 keeplive
45 | // tick 对应客户端心跳请求中的 tick
46 | MSG_TYPE_CHAT_MSG = "chatmsg" // 弹幕消息
47 | // 用户在房间发送弹幕时,服务端发此消息给客户端,完整的数据部分应包含的字 段如下:
48 | // 字段说明
49 | // type 表示为“弹幕”消息,固定为 chatmsg
50 | // gid 弹幕组 id
51 | // rid 房间 id
52 | // uid 发送者 uid
53 | // nn 发送者昵称
54 | // txt 弹幕文本内容
55 | // cid 弹幕唯一 ID
56 | // level 用户等级
57 | // gt 礼物头衔:默认值 0(表示没有头衔)
58 | // col 颜色:默认值 0(表示默认颜色弹幕)
59 | // ct 客户端类型:默认值 0(表示 web 用户)
60 | // rg 房间权限组:默认值 1(表示普通权限用户)
61 | // pg 平台权限组:默认值 1(表示普通权限用户)
62 | // dlv 酬勤等级:默认值 0(表示没有酬勤)
63 | // dc 酬勤数量:默认值 0(表示没有酬勤数量)
64 | // bdlv 最高酬勤等级:默认值 0(表示全站都没有酬勤)
65 | MSG_TYPE_LUCK_GUY = "onlinegift" //领取在线鱼丸暴击消息
66 | // 在线领取鱼丸时,若出现暴击(鱼丸数大于等于 60)服务则发送领取暴击消息 到客户端。完整的数据部分应包含的字段如下:
67 | // 字段说明
68 | // type 表示为“领取在线鱼丸”消息,固定为 onlinegift
69 | // rid 房间 ID
70 | // uid 用户 ID
71 | // gid 弹幕分组 ID
72 | // sil 鱼丸数
73 | // if 领取鱼丸的等级
74 | // ct 客户端类型标识
75 | // nn 用户昵称
76 | MSG_TYPE_NEW_GIFT = "dgb" // 赠送礼物消息
77 | // 用户在房间赠送礼物时,服务端发送此消息给客户端。完整的数据部分应包含的 字段如下:
78 | // 字段说明
79 | // type 表示为“赠送礼物”消息,固定为 dgb
80 | // rid 房间 ID
81 | // gid 弹幕分组 ID
82 | // gfid 礼物 id
83 | // gs 礼物显示样式
84 | // uid 用户 id
85 | // nn 用户昵称
86 | // str 用户战斗力
87 | // level 用户等级
88 | // dw 主播体重
89 | // gfcnt 礼物个数:默认值 1(表示 1 个礼物)
90 | // hits 礼物连击次数:默认值 1(表示 1 连击)
91 | // dlv 酬勤头衔:默认值 0(表示没有酬勤)
92 | // dc 酬勤个数:默认值 0(表示没有酬勤数量)
93 | // bdl 全站最高酬勤等级:默认值 0(表示全站都没有酬勤)
94 | // rg 房间身份组:默认值 1(表示普通权限用户)
95 | // pg 平台身份组:默认值 1(表示普通权限用户)
96 | // rpid 红包 id:默认值 0(表示没有红包)
97 | // slt 红包开启剩余时间:默认值 0(表示没有红包)
98 | // elt 红包销毁剩余时间:默认值 0(表示没有红包)
99 | MSG_TYPE_USER_ENTER = "uenter" // 特殊用户进房通知消息
100 | // 具有特殊属性的用户进入直播间时,服务端发送此消息至客户端。完整的数据部 分应包含的字段如下:
101 | // 字段说明
102 | // type 表示为“用户进房通知”消息,固定为 uenter
103 | // rid 房间 ID
104 | // gid 弹幕分组 ID
105 | // uid 用户 ID
106 | // nn 用户昵称
107 | // str 战斗力
108 | // level 新用户等级
109 | // gt 礼物头衔:默认值 0(表示没有头衔)
110 | // rg 房间权限组:默认值 1(表示普通权限用户)
111 | // pg 平台身份组:默认值 1(表示普通权限用户)
112 | // dlv 酬勤等级:默认值 0(表示没有酬勤)
113 | // dc 酬勤数量:默认值 0(表示没有酬勤数量)
114 | // bdlv 最高酬勤等级:默认值 0(表示全站都没有酬勤)
115 | MSG_TYPE_NEW_DESERVE = "bc_buy_deserve" // 用户赠送酬勤通知消息
116 | // 用户赠送酬勤时,服务端发送此消息至客户端。完整的数据部分应包含的字段如 下:
117 | // 字段说明
118 | // type 表示为“赠送酬勤通知”消息,固定为 bc_buy_deserve
119 | // rid 房间 ID
120 | // gid 弹幕分组 ID
121 | // level 用户等级
122 | // cnt 赠送数量
123 | // hits 赠送连击次数
124 | // lev 酬勤等级
125 | // sui 用户信息序列化字符串,详见下文。注意,此处为嵌套序列化,需注 意符号的转义变换。(转义符号参见 2.2 序列化)
126 | MSG_TYPE_LIVE_STATUS_CHANGE = "rss" // 房间开关播提醒消息
127 | // 房间开播提醒主要部分应包含的字段如下:
128 | // 字段说明
129 | // type 表示为“房间开播提醒”消息,固定为 rss
130 | // rid 房间 id
131 | // gid 弹幕分组 id
132 | // ss 直播状态,0-没有直播,1-正在直播
133 | // code 类型
134 | // rt 开关播原因:0-主播开关播,其他值-其他原因
135 | // notify 通知类型
136 | // endtime 关播时间(仅关播时有效)
137 | MSG_TYPE_RANKLIST = "ranklist" // 广播排行榜消息
138 | MSG_TYPE_SSD = "ssd" // 超级弹幕消息(如,火箭弹幕)
139 | // 超级弹幕主要部分应包含的字段如下:
140 | // 字段说明
141 | // type 表示为“超级弹幕”消息,固定为 ssd
142 | // rid 房间 id
143 | // gid 弹幕分组 id
144 | // sdid 超级弹幕 id
145 | // trid 跳转房间 id
146 | // content 超级弹幕的内容
147 | MSG_TYPE_SPBC = "spbc" // 房间内礼物广播
148 | // 房间内赠送礼物成功后效果主要部分应包含的字段如下:
149 | // 字段说明
150 | // type 表示为“房间内礼物广播”,固定为 spbc
151 | // rid 房间 id
152 | // gid 弹幕分组 id
153 | // sn 赠送者昵称
154 | // dn 受赠者昵称
155 | // gn 礼物名称
156 | // gc 礼物数量
157 | // drid 赠送房间
158 | // gs 广播样式
159 | // gb 是否有礼包(0-无礼包,1-有礼包)
160 | // es 广播展现样式(1-火箭,2-飞机)
161 | // gfid 礼物 id
162 | // eid 特效 id
163 | MSG_TYPE_RED_PACKET = "ggbb" // 房间用户抢红包
164 | // 房间赠送礼物成功后效果(赠送礼物效果,连击数)主要部分应包含的字段如下:
165 | // 字段说明
166 | // type 表示“房间用户抢红包”信息,固定为 ggbb
167 | // rid 房间 id
168 | // gid 弹幕分组 id
169 | // sl 抢到的鱼丸数量
170 | // sid 礼包产生者 id
171 | // did 抢礼包者 id
172 | // snk 礼包产生者昵称
173 | // dnk 抢礼包者昵称
174 | // rpt 礼包类型
175 | MSG_TYPE_RANK_UP = "rankup" // 房间内top10变化消息
176 | // 房间内 top10 排行榜变化后,广播。主要部分应包含的字段如下:
177 | // 字段说明
178 | // type 表示为“房间 top10 排行榜变换”,固定为 rankup
179 | // rid 房间 id
180 | // gid 弹幕分组 id
181 | // uid 用户 id
182 | // drid 目标房间 id
183 | // rt 房间所属栏目类型
184 | // bt 广播类型:1-房间内广播,2-栏目广播,4-全站广播
185 | // sz 展示区域:1-聊天区展示,2-flash 展示,3-都显示
186 | // nk 用户昵称
187 | // rkt top10 榜的类型 1-周榜 2-总榜 4-日榜
188 | // rn 上升后的排名
189 | )
190 |
191 | type Message struct {
192 | body map[string]interface{} // 消息正文map
193 | headerType int16 // 消息类型,2字节,689为客户端发给服务器
194 | headerSecret int8 // 加密字段,1字节,暂时未用,默认为0
195 | headerReserved int8 // 保留字段,1字节,暂时未用,默认为0
196 |
197 | lock sync.RWMutex
198 | }
199 |
200 | // Create a new message
201 | // NewMessage(nil, MESSAGE_TO_SERVER)
202 | // NewMessage(newMap, MESSAGE_FROM_SERVER)
203 | func NewMessage(body map[string]interface{}, mtype int16) *Message {
204 | if body == nil {
205 | body = make(map[string]interface{})
206 | }
207 | return &Message{
208 | body: body,
209 | headerType: mtype,
210 | }
211 | }
212 |
213 | func (msg *Message) SetField(key string, value interface{}) *Message {
214 | msg.lock.Lock()
215 | defer msg.lock.Unlock()
216 | msg.body[key] = value
217 | return msg
218 | }
219 |
220 | // Get certain field
221 | func (msg *Message) GetField(key string) (interface{}, bool) {
222 | msg.lock.RLock()
223 | defer msg.lock.RUnlock()
224 | v, ok := msg.body[key]
225 | return v, ok
226 | }
227 |
228 | // return field value as string
229 | func (msg *Message) GetStringField(key string) string {
230 | value, ok := msg.GetField(key)
231 | if !ok {
232 | return ""
233 | }
234 | return value.(string)
235 | }
236 |
237 | // return field value as int
238 | func (msg *Message) GetIntField(key string) int {
239 | value, ok := msg.GetField(key)
240 | if !ok {
241 | return 0
242 | }
243 | return value.(int)
244 | }
245 |
246 | // return body as string
247 | func (msg *Message) BodyString() string {
248 | values := make([]string, 0, len(msg.body))
249 |
250 | for k, v := range msg.body {
251 | // @ | / in k, v should replaced by @A | @S by package user
252 | values = append(values, fmt.Sprintf("%s@=%v/", k, v))
253 | }
254 | return strings.Join(values, "")
255 | }
256 |
257 | func (msg *Message) Encode() []byte {
258 | content := msg.BodyString()
259 | length := 9 + len(content) // 长度4字节 + 类型2字节 + 加密字段1字节 + 保留字段1字节 + 结尾字段1字节
260 |
261 | buffer := bytes.NewBuffer([]byte{})
262 | binary.Write(buffer, binary.LittleEndian, int32(length))
263 | binary.Write(buffer, binary.LittleEndian, int32(length))
264 | binary.Write(buffer, binary.LittleEndian, int16(msg.headerType))
265 | binary.Write(buffer, binary.LittleEndian, int8(msg.headerSecret))
266 | binary.Write(buffer, binary.LittleEndian, int8(msg.headerReserved))
267 | binary.Write(buffer, binary.LittleEndian, []byte(content))
268 | binary.Write(buffer, binary.LittleEndian, MESSAGE_ENDING)
269 | return buffer.Bytes()
270 | }
271 |
272 | func (msg *Message) Decode(body []byte, mtype int) *Message {
273 | msg.headerType = int16(mtype)
274 | values := strings.Split(strings.Trim(string(body), "/"), "/")
275 |
276 | for _, v := range values {
277 | kv := strings.SplitN(v, "@=", 2)
278 | kk := Unescape(kv[0])
279 | vv := Unescape(kv[1])
280 |
281 | msg.SetField(kk, vv)
282 | }
283 | return msg
284 | }
285 |
--------------------------------------------------------------------------------
/douyu/utils.go:
--------------------------------------------------------------------------------
1 | package douyu
2 |
3 | import (
4 | "strings"
5 | )
6 |
7 | func Escaped(v string) string {
8 | vv := strings.Replace(v, "@", "@A", -1)
9 | vv = strings.Replace(vv, "/", "@S", -1)
10 | return vv
11 | }
12 |
13 | func Unescape(v string) string {
14 | vv := strings.Replace(v, "@A", "@", -1)
15 | vv = strings.Replace(vv, "@S", "/", -1)
16 | return vv
17 | }
18 |
--------------------------------------------------------------------------------
/panda/client.go:
--------------------------------------------------------------------------------
1 | package panda
2 |
3 | import (
4 | //"encoding/binary"
5 | "fmt"
6 | "github.com/songtianyi/rrframework/logs"
7 | "io"
8 | "net"
9 | "strconv"
10 | "sync"
11 | "time"
12 | )
13 |
14 | type Client struct {
15 | conn net.Conn
16 | HandlerRegister *HandlerRegister
17 | closed chan struct{}
18 | roomid int
19 | chatInfo *ChatInfo
20 |
21 | rLock sync.Mutex
22 | wLock sync.Mutex
23 | }
24 |
25 | func Connect(uri string, handlerRegister *HandlerRegister) (*Client, error) {
26 |
27 | roomStr, err := GetRoomId(uri)
28 | if err != nil {
29 | return nil, err
30 | }
31 |
32 | state, err := GetBarrageLiveState(roomStr)
33 | if err != nil {
34 | return nil, err
35 | }
36 | if state == "2" {
37 | return nil, fmt.Errorf("room state %s", state)
38 | }
39 |
40 | chatInfo, err := GetBarrageChatInfo(roomStr)
41 | if err != nil {
42 | return nil, err
43 | }
44 |
45 | conn, err := net.DialTimeout("tcp", chatInfo.Data.ChatAddrList[0], 10*time.Second)
46 | if err != nil {
47 | return nil, err
48 | }
49 |
50 | logs.Info(fmt.Sprintf("%s connected, live status %s", chatInfo.Data.ChatAddrList[0], state))
51 |
52 | roomid, err := strconv.Atoi(roomStr)
53 | if err != nil {
54 | return nil, err
55 | }
56 |
57 | client := &Client{
58 | conn: conn,
59 | roomid: roomid,
60 | chatInfo: chatInfo,
61 | }
62 |
63 | if handlerRegister == nil {
64 | client.HandlerRegister = CreateHandlerRegister()
65 | } else {
66 | client.HandlerRegister = handlerRegister
67 | }
68 |
69 | handshake := NewHandshakeMessage(chatInfo)
70 | if _, err := client.Send(handshake.Encode()); err != nil {
71 | return nil, err
72 | }
73 |
74 | buf := make([]byte, 28)
75 | if _, err := io.ReadFull(client.conn, buf); err != nil {
76 | return nil, err
77 | }
78 | logs.Info("handshake ok")
79 |
80 | go client.heartbeat()
81 | return client, nil
82 | }
83 |
84 | func (c *Client) Send(b []byte) (int, error) {
85 | c.wLock.Lock()
86 | defer c.wLock.Unlock()
87 | return c.conn.Write(b)
88 | }
89 |
90 | func (c *Client) Receive() ([]byte, error) {
91 | c.rLock.Lock()
92 | defer c.rLock.Unlock()
93 | buf := make([]byte, 4096) // big buffer
94 |
95 | n, err := c.conn.Read(buf)
96 | if err != nil {
97 | if err != io.EOF {
98 | return buf[:n], err
99 | }
100 | }
101 | return buf[:n], nil
102 | }
103 |
104 | // Close connnection
105 | func (c *Client) Close() error {
106 | c.closed <- struct{}{} // receive
107 | c.closed <- struct{}{} // heartbeat
108 | return c.conn.Close()
109 | }
110 |
111 | func (c *Client) heartbeat() {
112 | tick := time.Tick(30 * time.Second)
113 | loop:
114 | for {
115 | select {
116 | case <-c.closed:
117 | break loop
118 | case <-tick:
119 | heartbeat := NewHeartbeatMessage()
120 |
121 | if _, err := c.conn.Write(heartbeat.Encode()); err != nil {
122 | logs.Error("heartbeat failed, " + err.Error())
123 | }
124 | }
125 | }
126 | }
127 |
128 | func (c *Client) Serve() {
129 | loop:
130 | for {
131 | select {
132 | case <-c.closed:
133 | break loop
134 | default:
135 | b, err := c.Receive()
136 | if err != nil {
137 | logs.Error("Receive", err)
138 | continue
139 | }
140 | for _, dm := range NewMessage(b).Decode().Decoded {
141 | err, handlers := c.HandlerRegister.Get(dm.Type)
142 | if err != nil {
143 | logs.Warn(err)
144 | continue
145 | }
146 | for _, v := range handlers {
147 | go v.Run(dm)
148 | }
149 | }
150 |
151 | }
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/panda/handler.go:
--------------------------------------------------------------------------------
1 | package panda
2 |
3 | import (
4 | "fmt"
5 | "sync"
6 | )
7 |
8 | // Handler: message function wrapper
9 | type Handler func(*DecodedMessage)
10 |
11 | // HandlerWrapper: message handler wrapper
12 | type HandlerWrapper struct {
13 | handle Handler
14 | enabled bool
15 | name string
16 | }
17 |
18 | // Run: message handler callback
19 | func (s *HandlerWrapper) Run(msg *DecodedMessage) {
20 | if s.enabled {
21 | s.handle(msg)
22 | }
23 | }
24 |
25 | func (s *HandlerWrapper) getName() string {
26 | return s.name
27 | }
28 |
29 | func (s *HandlerWrapper) enableHandle() {
30 | s.enabled = true
31 | return
32 | }
33 |
34 | func (s *HandlerWrapper) disableHandle() {
35 | s.enabled = false
36 | return
37 | }
38 |
39 | // HandlerRegister: message handler manager
40 | type HandlerRegister struct {
41 | mu sync.RWMutex
42 | hmap map[string][]*HandlerWrapper
43 | }
44 |
45 | // CreateHandlerRegister: create handler register
46 | func CreateHandlerRegister() *HandlerRegister {
47 | return &HandlerRegister{
48 | hmap: make(map[string][]*HandlerWrapper),
49 | }
50 | }
51 |
52 | // Add: add message callback handle to handler register
53 | func (hr *HandlerRegister) Add(key string, h Handler, name string) error {
54 | hr.mu.Lock()
55 | defer hr.mu.Unlock()
56 | for _, v := range hr.hmap {
57 | for _, handle := range v {
58 | if handle.getName() == name {
59 | return fmt.Errorf("handler name %s has been registered", name)
60 | }
61 | }
62 | }
63 | hr.hmap[key] = append(hr.hmap[key], &HandlerWrapper{handle: h, enabled: true, name: name})
64 | return nil
65 | }
66 |
67 | // Get: get message handler
68 | func (hr *HandlerRegister) Get(key string) (error, []*HandlerWrapper) {
69 | hr.mu.RLock()
70 | defer hr.mu.RUnlock()
71 | if v, ok := hr.hmap[key]; ok {
72 | return nil, v
73 | }
74 | return fmt.Errorf("no handlers for key [%s]", key), nil
75 | }
76 |
77 | // EnableByType: enable handler by message type
78 | func (hr *HandlerRegister) EnableByType(key string) error {
79 | err, handles := hr.Get(key)
80 | if err != nil {
81 | return err
82 | }
83 | hr.mu.Lock()
84 | defer hr.mu.Unlock()
85 | // all
86 | for _, v := range handles {
87 | v.enableHandle()
88 | }
89 | return nil
90 | }
91 |
92 | // DisableByType: disable handler by message type
93 | func (hr *HandlerRegister) DisableByType(key string) error {
94 | err, handles := hr.Get(key)
95 | if err != nil {
96 | return err
97 | }
98 | hr.mu.Lock()
99 | defer hr.mu.Unlock()
100 | // all
101 | for _, v := range handles {
102 | v.disableHandle()
103 | }
104 | return nil
105 | }
106 |
107 | // EnableByName: enable message handler by name
108 | func (hr *HandlerRegister) EnableByName(name string) error {
109 | hr.mu.Lock()
110 | defer hr.mu.Unlock()
111 | for _, handles := range hr.hmap {
112 | for _, v := range handles {
113 | if v.getName() == name {
114 | v.enableHandle()
115 | return nil
116 | }
117 | }
118 | }
119 | return fmt.Errorf("cannot find handler %s", name)
120 | }
121 |
122 | // DisableByName: disable message handler by name
123 | func (hr *HandlerRegister) DisableByName(name string) error {
124 | hr.mu.Lock()
125 | defer hr.mu.Unlock()
126 | for _, handles := range hr.hmap {
127 | for _, v := range handles {
128 | if v.getName() == name {
129 | v.disableHandle()
130 | return nil
131 | }
132 | }
133 | }
134 | return fmt.Errorf("cannot find handler %s", name)
135 | }
136 |
137 | // Dump: output all message handlers
138 | func (hr *HandlerRegister) Dump() string {
139 | hr.mu.RLock()
140 | defer hr.mu.RUnlock()
141 | str := "[plugins dump]\n"
142 | for k, handles := range hr.hmap {
143 | for _, v := range handles {
144 | str += fmt.Sprintf("%d %s [%v]\n", k, v.getName(), v.enabled)
145 | }
146 | }
147 | return str
148 | }
149 |
--------------------------------------------------------------------------------
/panda/protocol.go:
--------------------------------------------------------------------------------
1 | package panda
2 |
3 | import (
4 | "bytes"
5 | "encoding/binary"
6 | "fmt"
7 | //"strings"
8 | "regexp"
9 | )
10 |
11 | const (
12 | DANMU_MSG = "1"
13 | )
14 |
15 | type DecodedMessage struct {
16 | Type string
17 | Nickname string
18 | Content string
19 | }
20 |
21 | type Message struct {
22 | head []byte
23 | body []byte
24 |
25 | Decoded []*DecodedMessage
26 | }
27 |
28 | func NewHandshakeMessage(chatInfo *ChatInfo) *Message {
29 |
30 | data := fmt.Sprintf("u:%d@%s\nk:1\nt:300\nts:%d\nsign:%s\nauthtype:%s",
31 | chatInfo.Data.Rid, chatInfo.Data.AppId, chatInfo.Data.Ts, chatInfo.Data.Sign, chatInfo.Data.AuthType)
32 |
33 | message := &Message{
34 | head: []byte{0x00, 0x06, 0x00, 0x02},
35 | body: []byte(data),
36 | }
37 | return message
38 |
39 | }
40 |
41 | func NewHeartbeatMessage() *Message {
42 |
43 | message := &Message{
44 | head: []byte{0x00, 0x06, 0x00, 0x00},
45 | }
46 | return message
47 |
48 | }
49 |
50 | func NewMessage(b []byte) *Message {
51 | return &Message{
52 | body: b,
53 | }
54 | }
55 |
56 | func (msg *Message) Encode() []byte {
57 | buffer := bytes.NewBuffer([]byte{})
58 | binary.Write(buffer, binary.BigEndian, msg.head) // write head
59 | if msg.body != nil && len(msg.body) > 0 {
60 | binary.Write(buffer, binary.BigEndian, int16(len(msg.body))) // write body length
61 | binary.Write(buffer, binary.BigEndian, msg.body) // write body
62 | }
63 | return buffer.Bytes()
64 | }
65 |
66 | func (msg *Message) Decode() *Message {
67 | // TODO
68 | s := string(msg.body)
69 | //fmt.Println(s)
70 |
71 | // split by "ack:0"
72 | //raw := strings.Split(s, "ack:0")
73 | //for _, v := range raw {
74 | // if n := strings.Index(v, "{"); n > -1 {
75 | // js := v[n:]
76 | // // unmarshal json
77 | // fmt.Println(js)
78 | // }
79 | //}
80 |
81 | nickNameReg := regexp.MustCompile("\"nickName\":\"([^\"]*)\"")
82 | nickNames := nickNameReg.FindAllStringSubmatch(s, -1)
83 | typeReg := regexp.MustCompile("\"type\":\"([^\"]*)\"")
84 | types := typeReg.FindAllStringSubmatch(s, -1)
85 | contentReg := regexp.MustCompile("\"content\":\"([^\"]*)\"")
86 | contents := contentReg.FindAllStringSubmatch(s, -1)
87 |
88 | msg.Decoded = make([]*DecodedMessage, 0)
89 | for i, v := range types {
90 | if v[1] != "1" {
91 | continue
92 | }
93 | msg.Decoded = append(msg.Decoded, &DecodedMessage{
94 | Type: v[1],
95 | Nickname: nickNames[i][1],
96 | Content: contents[i][1],
97 | })
98 | }
99 | return msg
100 | }
101 |
102 | func (msg *Message) Bytes() []byte {
103 | return msg.body
104 | }
105 |
--------------------------------------------------------------------------------
/panda/utils.go:
--------------------------------------------------------------------------------
1 | package panda
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "github.com/songtianyi/rrframework/config"
7 | "io/ioutil"
8 | "net/http"
9 | "net/url"
10 | "regexp"
11 | "strconv"
12 | "strings"
13 | "time"
14 | )
15 |
16 | var (
17 | videoInfoReg = regexp.MustCompile("'videoinfo': (.*),")
18 | stateReg = regexp.MustCompile("(.*?)")
19 | )
20 |
21 | type ChatData struct {
22 | AppId string `json:"appid"`
23 | Rid int `json:"rid"`
24 | Sign string `json:"sign"`
25 | AuthType string `json:"authType"`
26 | Ts int `json:"ts"`
27 | ChatAddrList []string `json:"chat_addr_list"`
28 | }
29 |
30 | type ChatInfo struct {
31 | Errno int `json:"errno"`
32 | Errmsg string `json:"errmsg"`
33 | Data ChatData `json:"data"`
34 | }
35 |
36 | func doHttp(uri string) ([]byte, error) {
37 | resp, err := http.Get(uri)
38 | if err != nil {
39 | return nil, err
40 | }
41 | defer resp.Body.Close()
42 | body, _ := ioutil.ReadAll(resp.Body)
43 | return body, nil
44 | }
45 |
46 | func GetRoomId(uri string) (string, error) {
47 | uri = strings.Trim(uri, "/")
48 | it := strings.Split(uri, "/")
49 | if len(it) < 2 {
50 | return "", fmt.Errorf("url %s not valid", uri)
51 | }
52 | roomStr := it[len(it)-1]
53 | return roomStr, nil
54 | }
55 |
56 | func GetBarrageLiveState(roomStr string) (string, error) {
57 |
58 | km := url.Values{}
59 | km.Add("roomid", roomStr)
60 | km.Add("pub_key", "")
61 | km.Add("_", strconv.Itoa(int(time.Now().Unix())))
62 |
63 | api := "http://www.panda.tv/api_room?" + km.Encode()
64 | body, err := doHttp(api)
65 | if err != nil {
66 | return "", err
67 | }
68 | jc, err := rrconfig.LoadJsonConfigFromBytes(body)
69 | if err != nil {
70 | return "", err
71 | }
72 | status, _ := jc.GetString("data.videoinfo.status")
73 | return status, nil
74 | }
75 |
76 | func GetBarrageChatInfo(roomStr string) (*ChatInfo, error) {
77 | km := url.Values{}
78 | km.Add("roomid", roomStr)
79 | km.Add("_", strconv.Itoa(int(time.Now().Unix())))
80 |
81 | api := "http://www.panda.tv/ajax_chatinfo?" + km.Encode()
82 | body, err := doHttp(api)
83 | if err != nil {
84 | return nil, err
85 | }
86 | var chatInfo ChatInfo
87 | err = json.Unmarshal(body, &chatInfo)
88 | if err != nil {
89 | return nil, err
90 | }
91 |
92 | km = url.Values{}
93 | km.Add("rid", strconv.Itoa(chatInfo.Data.Rid))
94 | km.Add("roomid", roomStr)
95 | km.Add("retry", "0")
96 | km.Add("sign", chatInfo.Data.Sign)
97 | km.Add("ts", strconv.Itoa(chatInfo.Data.Ts))
98 | km.Add("_", strconv.Itoa(int(time.Now().Unix())))
99 |
100 | api = "http://api.homer.panda.tv/chatroom/getinfo?" + km.Encode()
101 | body, err = doHttp(api)
102 | if err != nil {
103 | return nil, err
104 | }
105 | err = json.Unmarshal(body, &chatInfo)
106 | if err != nil {
107 | return nil, err
108 | }
109 | return &chatInfo, nil
110 | }
111 |
--------------------------------------------------------------------------------