├── .gitignore ├── img ├── show.png └── show2.png ├── login_bili.png ├── errs └── err.go ├── entity ├── robot_reply.go ├── room_init.go ├── login_url.go ├── cmd_text.go └── bullet_info.go ├── config.toml ├── go.mod ├── util └── util.go ├── http ├── cli.go ├── robot.go ├── room.go ├── bullet.go └── user.go ├── bullet_girl ├── send_bullet.go ├── thanks_bullet.go ├── robot_bullet.go ├── timing_bullet.go ├── handle_bullet.go └── catch_bullet.go ├── README.md ├── config └── config.go ├── go.sum └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store -------------------------------------------------------------------------------- /img/show.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k-si/bilibili_live/HEAD/img/show.png -------------------------------------------------------------------------------- /img/show2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k-si/bilibili_live/HEAD/img/show2.png -------------------------------------------------------------------------------- /login_bili.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k-si/bilibili_live/HEAD/login_bili.png -------------------------------------------------------------------------------- /errs/err.go: -------------------------------------------------------------------------------- 1 | package errs 2 | 3 | import "errors" 4 | 5 | var RoomIdNotExistErr = errors.New("房间号不存在") 6 | -------------------------------------------------------------------------------- /entity/robot_reply.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | type QinugyunkeRobotReplay struct { 4 | Result int `json:"result"` 5 | Content string `json:"content"` 6 | } 7 | -------------------------------------------------------------------------------- /config.toml: -------------------------------------------------------------------------------- 1 | # 直播间id 2 | room_id = 25198571 3 | 4 | # 小破站弹幕服务器地址,不用改 5 | ws_server_url = "wss://broadcastlv.chat.bilibili.com:2245/sub" 6 | 7 | # 登录二维码生成到哪个路径,目前是生成在项目根目录下 8 | qr_code_path = "login_bili.png" 9 | 10 | # 触发聊天机器人的前缀,比如"橘子,你好呀",不带"橘子,"就不会触发机器人了 11 | talk_robot_cmd = "橘子," 12 | 13 | # 机器人名称 14 | robot_name = "橘子" -------------------------------------------------------------------------------- /entity/room_init.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | const ( 4 | NotStarted = 0 // 未开播 5 | Live = 1 // 直播中 6 | Carousel = 2 // 轮播中 7 | ) 8 | 9 | type RoomInitStatus struct { 10 | Code int `json:"code"` 11 | } 12 | 13 | type RoomInitInfo struct { 14 | Data struct { 15 | LiveStatus int `json:"live_status"` 16 | } `json:"data"` 17 | } 18 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/k-si/bili_live 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/go-resty/resty/v2 v2.7.0 7 | github.com/gorhill/cronexpr v0.0.0-20180427100037-88b0669f7d75 8 | github.com/gorilla/websocket v1.5.0 9 | github.com/pelletier/go-toml v1.9.5 10 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e 11 | ) 12 | -------------------------------------------------------------------------------- /util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "github.com/k-si/bili_live/config" 5 | "github.com/skip2/go-qrcode" 6 | "log" 7 | ) 8 | 9 | // 根据url生成二维码 10 | func GenerateQrcode(url string) error { 11 | if err := qrcode.WriteFile(url, qrcode.Medium, 256, config.Live.QrCodePath); err != nil { 12 | log.Println("生成二维码失败:", err) 13 | return err 14 | } 15 | return nil 16 | } 17 | -------------------------------------------------------------------------------- /http/cli.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import "github.com/go-resty/resty/v2" 4 | 5 | const ( 6 | //userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36" 7 | userAgent = "" 8 | ) 9 | 10 | // 调用GetLoginInfo后,对全局变量cookie赋值 11 | var CookieStr string 12 | var CookieList = make(map[string]string) 13 | 14 | // 全局客户端对象 15 | var cli *resty.Client 16 | 17 | func InitHttpClient() { 18 | cli = resty.New() 19 | } 20 | -------------------------------------------------------------------------------- /entity/login_url.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | type LoginUrl struct { 4 | Data struct{ 5 | Url string `json:"url"` 6 | OauthKey string `json:"oauthKey"` 7 | } `json:"data"` 8 | } 9 | 10 | type LoginInfoPre struct { 11 | Status bool `json:"status"` 12 | } 13 | 14 | type LoginInfoData struct { 15 | Data struct{ 16 | Url string `json:"url"` 17 | RefreshToken string `json:"refresh_token"` 18 | } `json:"data"` 19 | } 20 | 21 | type LoginInfoCookies struct { 22 | SetCookie []string `json:"Set-Cookie"` 23 | } -------------------------------------------------------------------------------- /entity/cmd_text.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | type CmdText struct { 4 | Cmd string `json:"cmd"` 5 | } 6 | 7 | type DanmuMsgText struct { 8 | Info []interface{} `json:"info"` 9 | } 10 | 11 | type EntryEffectText struct { 12 | Data struct { 13 | CopyWriting string `json:"copy_writing"` 14 | } `json:"data"` 15 | } 16 | 17 | type InteractWordText struct { 18 | Data struct { 19 | Uname string `json:"uname"` 20 | } `json:"data"` 21 | } 22 | 23 | type SendGiftText struct { 24 | Data struct { 25 | Action string `json:"action"` 26 | GiftName string `json:"giftName"` 27 | Uname string `json:"uname"` 28 | Price int `json:"price"` 29 | } `json:"data"` 30 | } 31 | -------------------------------------------------------------------------------- /http/robot.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/go-resty/resty/v2" 6 | "github.com/k-si/bili_live/entity" 7 | "log" 8 | ) 9 | 10 | // 调用青云客机器人api 11 | func RequestQingyunkeRobot(msg string) (string, error) { 12 | var err error 13 | var url = "http://api.qingyunke.com/api.php?key=free&appid=0&msg=" + msg 14 | var resp *resty.Response 15 | 16 | if resp, err = cli.R(). 17 | SetHeader("Content-Type", "utf-8"). 18 | Get(url); err != nil { 19 | log.Println("请求qingyunke机器人接口失败:", err) 20 | return "", err 21 | } 22 | 23 | r := &entity.QinugyunkeRobotReplay{} 24 | err = json.Unmarshal(resp.Body(), r) 25 | 26 | return r.Content, err 27 | } 28 | -------------------------------------------------------------------------------- /bullet_girl/send_bullet.go: -------------------------------------------------------------------------------- 1 | package bullet_girl 2 | 3 | import ( 4 | "context" 5 | "github.com/k-si/bili_live/http" 6 | "log" 7 | "time" 8 | ) 9 | 10 | var sender *BulletSender 11 | 12 | type BulletSender struct { 13 | bulletChan chan string 14 | } 15 | 16 | func PushToBulletSender(bullet string) { 17 | log.Println("PushToBulletSender成功", bullet) 18 | sender.bulletChan <- bullet 19 | } 20 | 21 | func StartSendBullet(ctx context.Context) { 22 | var err error 23 | 24 | sender = &BulletSender{ 25 | bulletChan: make(chan string, 1000), 26 | } 27 | 28 | var msg string 29 | for { 30 | select { 31 | case <-ctx.Done(): 32 | goto END 33 | case msg = <-sender.bulletChan: 34 | if err = http.Send(msg); err != nil { 35 | log.Println("弹幕发送失败:", err, "msg:", msg) 36 | } else { 37 | log.Println("弹幕发送成功:", msg) 38 | } 39 | } 40 | time.Sleep(3 * time.Second) // 防止弹幕发送过快 41 | } 42 | END: 43 | } 44 | -------------------------------------------------------------------------------- /entity/bullet_info.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | // 弹幕服务器信息 4 | type ResponseBulletInfo struct { 5 | Code int `json:"code"` 6 | Message string `json:"message"` 7 | Ttl int `json:"ttl"` 8 | Data *ResponseBulletData `json:"data"` 9 | } 10 | 11 | type ResponseBulletData struct { 12 | BusinessId int `json:"business_id"` 13 | Group string `json:"group"` 14 | HostList []*ResponseBulletHost `json:"host_list"` 15 | MaxDelay int `json:"max_delay"` 16 | RefreshRate int `json:"refresh_rate"` 17 | RefreshRowFactor float64 `json:"refresh_row_factor"` 18 | Token string `json:"token"` 19 | } 20 | 21 | type ResponseBulletHost struct { 22 | Host string `json:"host"` 23 | Port int `json:"port"` 24 | WssPort int `json:"wss_port"` 25 | WsPort int `json:"ws_port"` 26 | } 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bilibili直播弹幕姬来啦! 2 | 3 | 虽然市面上已有很多成熟的直播姬app,但还是想自己手搓一个以获得些许的成就感(说人话就是在家闲的)。 4 | 5 | 话不多说,来看看我搓的"弹幕姬"有啥好玩的。 6 | 7 | ## 功能 8 | 9 | - 定时弹幕;一般用来给主播打call 10 | - 礼物感谢:自动感谢投喂的礼物哟 11 | - 弹幕聊天;程序调用了聊天机器人api,为贵宾排除寂寞 12 | - 欢迎问候;欢迎进入直播间的舰长/小伙伴 13 | - 检测开播:自动检测开播后运行功能,下播后关闭功能 14 | 15 | (目前基础的架子已经搭好了,添加新功能只是人力问题...对,是我懒,哇酷哇酷) 16 | 17 | ps:感兴趣的同学可以直接在此基础上二次开发,增加新功能了(就是对响应包的字段值做判断,然后调接口balabala...),具体查看bullet_girl/handle_bullet.go中的handle()函数 18 | 19 | ## 开始 20 | 21 | 修改配置文件 -> 启动程序 -> 手机小破站扫码登录 -> 程序运行 22 | 23 | 1、首先看config.toml,根据注释,修改成适合自己的参数 24 | 25 | 2、启动程序: 26 | 27 | ```shell 28 | go run main.go -c="./config.toml" 29 | ``` 30 | 31 | 或者,手动build好之后运行 32 | 33 | ```shell 34 | go build main.go 35 | ./main -c="config.toml" 36 | ``` 37 | 38 | 3、根据配置文件中的qr_code_path,对应生成二维码,扫码登录 39 | 40 | 4、程序开始运行,开始体验一下吧! 41 | 42 | ps:查看main.go程序,就能知道看到启动的各个goroutine啦,程序结构还是很清晰哒,bullet_girl目录下的文件都是主要功能的实现。 43 | 44 | ## 展示一下 45 | 46 | **没开播,自己和自己玩的截图...** 47 | 48 | ![avatar](./img/show.png) 49 | ![avatar](./img/show2.png) -------------------------------------------------------------------------------- /http/room.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/go-resty/resty/v2" 6 | "github.com/k-si/bili_live/config" 7 | "github.com/k-si/bili_live/entity" 8 | "github.com/k-si/bili_live/errs" 9 | "log" 10 | "strconv" 11 | ) 12 | 13 | func RoomInit() (*entity.RoomInitInfo, error) { 14 | var err error 15 | var resp *resty.Response 16 | var url = "https://api.live.bilibili.com/room/v1/Room/room_init?id=" + strconv.Itoa(config.Live.RoomId) 17 | 18 | if resp, err = cli.R(). 19 | SetHeader("user-agent", userAgent). 20 | Get(url); err != nil { 21 | log.Println("请求room_init失败:", err) 22 | return nil, err 23 | } 24 | 25 | // 先解析响应状态 26 | status := &entity.RoomInitStatus{} 27 | if err = json.Unmarshal(resp.Body(), status); err != nil { 28 | log.Println("Unmarshal失败:", err, "body:", string(resp.Body())) 29 | return nil, err 30 | } 31 | 32 | // 在解析房间状态 33 | r := &entity.RoomInitInfo{} 34 | if status.Code == 0 { 35 | if err = json.Unmarshal(resp.Body(), r); err != nil { 36 | log.Println("Unmarshal失败:", err, "body:", string(resp.Body())) 37 | return nil, err 38 | } 39 | } 40 | 41 | // 太长时间下播,房间号可能会消失,请求响应的code=60004 42 | if status.Code == 60004 { 43 | return nil, errs.RoomIdNotExistErr 44 | } 45 | 46 | return r, err 47 | } 48 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "flag" 5 | "github.com/pelletier/go-toml" 6 | "io/ioutil" 7 | "log" 8 | ) 9 | 10 | const ( 11 | RoomId = 25198571 12 | WsServerUrl = "wss://broadcastlv.chat.bilibili.com:2245/sub" 13 | QrCodePath = "login_bili.png" 14 | TalkRobotCmd = "橘子," 15 | RobotName = "橘子" 16 | ) 17 | 18 | var Live LiveConfig 19 | 20 | type LiveConfig struct { 21 | RoomId int `toml:"room_id"` 22 | WsServerUrl string `toml:"ws_server_url"` 23 | QrCodePath string `toml:"qr_code_path"` 24 | TalkRobotCmd string `toml:"talk_robot_cmd"` 25 | RobotName string `toml:"robot_name"` 26 | } 27 | 28 | func DefaultLiveConfig() LiveConfig { 29 | return LiveConfig{ 30 | RoomId: RoomId, 31 | WsServerUrl: WsServerUrl, 32 | QrCodePath: QrCodePath, 33 | TalkRobotCmd: TalkRobotCmd, 34 | RobotName: RobotName, 35 | } 36 | } 37 | 38 | func LoadLiveConfig(path string) error { 39 | var data []byte 40 | var err error 41 | if data, err = ioutil.ReadFile(path); err != nil { 42 | return err 43 | } 44 | if err = toml.Unmarshal(data, &Live); err != nil { 45 | return err 46 | } 47 | return err 48 | } 49 | 50 | func InitConfig() error { 51 | var err error 52 | c := flag.String("c", "", "configuration profile path") 53 | flag.Parse() 54 | 55 | // 加载配置文件 56 | if *c == "" { 57 | Live = DefaultLiveConfig() 58 | } else { 59 | if err = LoadLiveConfig(*c); err != nil { 60 | log.Fatal(err) 61 | } 62 | } 63 | return err 64 | } 65 | -------------------------------------------------------------------------------- /http/bullet.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/go-resty/resty/v2" 6 | "github.com/k-si/bili_live/config" 7 | "github.com/k-si/bili_live/entity" 8 | "log" 9 | "strconv" 10 | "time" 11 | ) 12 | 13 | func GetDanmuInfo() (*entity.ResponseBulletInfo, error) { 14 | var err error 15 | var resp *resty.Response 16 | var url = "https://api.live.bilibili.com/xlive/web-room/v1/index/getDanmuInfo?id=" + strconv.Itoa(config.Live.RoomId) + "&type=0" 17 | 18 | r := &entity.ResponseBulletInfo{} 19 | if resp, err = cli.R(). 20 | SetHeader("user-agent", userAgent). 21 | Get(url); err != nil { 22 | log.Println("请求getDanmuInfo失败:", err) 23 | return nil, err 24 | } 25 | if err = json.Unmarshal(resp.Body(), r); err != nil { 26 | log.Println("Unmarshal失败:", err, "body:", string(resp.Body())) 27 | return nil, err 28 | } 29 | 30 | return r, nil 31 | } 32 | 33 | func Send(msg string) error { 34 | var err error 35 | var url = "https://api.live.bilibili.com/msg/send" 36 | var resp *resty.Response 37 | 38 | m := make(map[string]string) 39 | m["bubble"] = "5" 40 | m["msg"] = msg 41 | m["color"] = "4546550" 42 | m["mode"] = "4" 43 | m["fontsize"] = "25" 44 | m["rnd"] = strconv.FormatInt(time.Now().Unix(), 10) 45 | m["roomid"] = strconv.Itoa(config.Live.RoomId) 46 | m["csrf"] = CookieList["bili_jct"] 47 | m["csrf_token"] = CookieList["bili_jct"] 48 | 49 | if resp, err = cli.R(). 50 | SetHeader("user-agent", userAgent). 51 | SetHeader("cookie", CookieStr). 52 | SetFormData(m). 53 | Post(url); err != nil { 54 | log.Println("请求send失败:", err) 55 | return err 56 | } 57 | log.Println("send 弹幕响应:", string(resp.Body())) 58 | 59 | return nil 60 | } 61 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY= 2 | github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I= 3 | github.com/gorhill/cronexpr v0.0.0-20180427100037-88b0669f7d75 h1:f0n1xnMSmBLzVfsMMvriDyA75NB/oBgILX2GcHXIQzY= 4 | github.com/gorhill/cronexpr v0.0.0-20180427100037-88b0669f7d75/go.mod h1:g2644b03hfBX9Ov0ZBDgXXens4rxSxmqFBbhvKv2yVA= 5 | github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= 6 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 7 | github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= 8 | github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= 9 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= 10 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= 11 | golang.org/x/net v0.0.0-20211029224645-99673261e6eb h1:pirldcYWx7rx7kE5r+9WsOXPXK0+WH5+uZ7uPmJ44uM= 12 | golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 13 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 14 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 15 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 16 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 17 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 18 | -------------------------------------------------------------------------------- /bullet_girl/thanks_bullet.go: -------------------------------------------------------------------------------- 1 | package bullet_girl 2 | 3 | import ( 4 | "context" 5 | "github.com/k-si/bili_live/entity" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | // 检测到礼物,push [uname]->[giftName]->[cost],number+1 11 | // 每10s统计一次礼物,并进行感谢,礼物价值高于x元加一句大气 12 | 13 | var thanksGiver *GiftThanksGiver 14 | 15 | type GiftThanksGiver struct { 16 | giftTable map[string]map[string]int 17 | tableMu sync.RWMutex 18 | giftChan chan *entity.SendGiftText 19 | } 20 | 21 | func pushToGiftChan(g *entity.SendGiftText) { 22 | thanksGiver.giftChan <- g 23 | } 24 | 25 | func ThanksGift(ctx context.Context) { 26 | 27 | thanksGiver = &GiftThanksGiver{ 28 | giftTable: make(map[string]map[string]int), 29 | tableMu: sync.RWMutex{}, 30 | giftChan: make(chan *entity.SendGiftText, 1000), 31 | } 32 | 33 | var g *entity.SendGiftText 34 | var w = 10 * time.Second 35 | var t = time.NewTimer(w) 36 | defer t.Stop() 37 | 38 | for { 39 | select { 40 | case <-ctx.Done(): 41 | goto END 42 | case <-t.C: 43 | summarizeGift() 44 | t.Reset(w) 45 | case g = <-thanksGiver.giftChan: 46 | if thanksGiver.giftTable[g.Data.Uname] == nil { 47 | thanksGiver.giftTable[g.Data.Uname] = make(map[string]int) 48 | } 49 | thanksGiver.giftTable[g.Data.Uname][g.Data.GiftName] += g.Data.Price 50 | } 51 | } 52 | END: 53 | } 54 | 55 | func summarizeGift() { 56 | for name, m := range thanksGiver.giftTable { 57 | sumCost := 0 58 | for gift, cost := range m { 59 | 60 | // 名称长度适应 61 | zh := []rune(name) 62 | if len(zh) > 8 { 63 | PushToBulletSender("谢谢" + string(zh)) 64 | } else { 65 | PushToBulletSender("谢谢" + string(zh) + "的" + gift + "~ 爱你") 66 | } 67 | 68 | // 计算打赏金额 69 | sumCost += cost 70 | 71 | // 感谢完后立刻清空map 72 | delete(m, gift) 73 | } 74 | 75 | // 总打赏高于x元,加一句大气 76 | if sumCost >= 2000 { // 2元 77 | PushToBulletSender("老板大气大气") 78 | } 79 | delete(thanksGiver.giftTable, name) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /http/user.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/go-resty/resty/v2" 6 | "github.com/k-si/bili_live/entity" 7 | "log" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | func GetLoginUrl() (*entity.LoginUrl, error) { 13 | var err error 14 | var resp *resty.Response 15 | var url = "https://passport.bilibili.com/qrcode/getLoginUrl" 16 | 17 | r := &entity.LoginUrl{} 18 | if resp, err = cli.R(). 19 | SetHeader("user-agent", userAgent). 20 | Get(url); err != nil { 21 | log.Println("请求getLoginUrl失败:", err) 22 | return nil, err 23 | } 24 | if err = json.Unmarshal(resp.Body(), r); err != nil { 25 | log.Println("Unmarshal失败:", err, "body:", string(resp.Body())) 26 | return nil, err 27 | } 28 | 29 | log.Println("oauthKey:", r.Data.OauthKey) 30 | 31 | return r, err 32 | } 33 | 34 | // 验证登录的同时,将cookie赋值 35 | func GetLoginInfo(oauthKey string) (*entity.LoginInfoData, error) { 36 | var err error 37 | var url = "https://passport.bilibili.com/qrcode/getLoginInfo?oauthKey=" + oauthKey 38 | var resp *resty.Response 39 | var data *entity.LoginInfoData 40 | 41 | pre := &entity.LoginInfoPre{} 42 | 43 | for { 44 | log.Println("等待扫码登录...") 45 | 46 | if resp, err = cli.R(). 47 | SetHeader("user-agent", userAgent). 48 | Post(url); err != nil { 49 | log.Println("请求getLoginInfo失败:", err) 50 | return nil, err 51 | } 52 | 53 | if err = json.Unmarshal(resp.Body(), pre); err != nil { 54 | log.Println("Unmarshal失败:", err, "body:", string(resp.Body())) 55 | return nil, err 56 | } 57 | 58 | if pre.Status { 59 | data = &entity.LoginInfoData{} 60 | if err = json.Unmarshal(resp.Body(), data); err != nil { 61 | log.Println("Unmarshal失败:", err, "body:", string(resp.Body())) 62 | return nil, err 63 | } 64 | log.Println("登录成功!") 65 | break 66 | } 67 | 68 | time.Sleep(5 * time.Second) 69 | } 70 | 71 | for _, v := range resp.Header().Values("Set-Cookie") { 72 | pair := strings.Split(v, ";") 73 | kv := strings.Split(pair[0], "=") 74 | CookieList[kv[0]] = kv[1] 75 | CookieStr += pair[0] + ";" 76 | } 77 | 78 | return data, err 79 | } 80 | -------------------------------------------------------------------------------- /bullet_girl/robot_bullet.go: -------------------------------------------------------------------------------- 1 | package bullet_girl 2 | 3 | import ( 4 | "context" 5 | "github.com/k-si/bili_live/config" 6 | "github.com/k-si/bili_live/http" 7 | "log" 8 | "strings" 9 | ) 10 | 11 | var robot *BulletRobot 12 | 13 | type BulletRobot struct { 14 | bulletRobotChan chan string 15 | } 16 | 17 | func PushToBulletRobot(content string) { 18 | log.Println("PushToBulletRobot成功", content) 19 | robot.bulletRobotChan <- content 20 | } 21 | 22 | func StartBulletRobot(ctx context.Context) { 23 | robot = &BulletRobot{ 24 | bulletRobotChan: make(chan string, 1000), 25 | } 26 | 27 | var content string 28 | 29 | for { 30 | select { 31 | case <-ctx.Done(): 32 | goto END 33 | case content = <-robot.bulletRobotChan: 34 | handleRobotBullet(content) 35 | } 36 | } 37 | END: 38 | } 39 | 40 | func handleRobotBullet(content string) { 41 | var err error 42 | var reply string 43 | if reply, err = http.RequestQingyunkeRobot(content); err != nil { 44 | log.Println("请求机器人失败:", err) 45 | PushToBulletSender("不好意思,机器人坏掉了...") 46 | return 47 | } 48 | 49 | log.Println("机器人回复:", reply) 50 | 51 | bulltes := splitRobotReply(reply) 52 | for _, v := range bulltes { 53 | PushToBulletSender(v) 54 | } 55 | } 56 | 57 | // 将机器人回复语句中的 {br} 进行分割 58 | // b站弹幕一次只能发20个字符,需要切分 59 | func splitRobotReply(content string) []string { 60 | 61 | // 将机器人回复中的菲菲替换为橘子 62 | content = strings.ReplaceAll(content, "菲菲", config.Live.RobotName) 63 | 64 | var res []string 65 | reply := strings.Split(content, "{br}") 66 | 67 | for _, r := range reply { 68 | // 长度大于20再分割 69 | zh := []rune(r) 70 | if len(zh) > 20 { 71 | i := 0 72 | for i < len(zh) { 73 | if i+20 > len(zh) { 74 | res = append(res, string(zh[i:])) 75 | } else { 76 | res = append(res, string(zh[i:i+20])) 77 | } 78 | i += 20 79 | } 80 | } else { 81 | res = append(res, string(zh)) 82 | } 83 | } 84 | return res 85 | } 86 | 87 | // 检查弹幕是否在@我,返回bool和@我要说的内容 88 | func checkIsAtMe(msg string) (bool, string) { 89 | if strings.HasPrefix(msg, config.Live.TalkRobotCmd) { 90 | return true, strings.TrimPrefix(msg, config.Live.TalkRobotCmd) 91 | } else { 92 | return false, "" 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /bullet_girl/timing_bullet.go: -------------------------------------------------------------------------------- 1 | package bullet_girl 2 | 3 | import ( 4 | "context" 5 | "github.com/gorhill/cronexpr" 6 | "log" 7 | "time" 8 | ) 9 | 10 | const ( 11 | Save = iota 12 | Remove 13 | ) 14 | 15 | // 调度表 16 | var scheduler *BulletTaskScheduler 17 | var id = 0 18 | 19 | // 弹幕 20 | type Bullet struct { 21 | id int 22 | msg string 23 | expr string // 定时crontab表达式 24 | } 25 | 26 | // 弹幕定时任务 27 | type BulletTask struct { 28 | bullet *Bullet 29 | expr *cronexpr.Expression 30 | next time.Time // 下次调度时间 31 | } 32 | 33 | // 弹幕事件,删除/创建 定时弹幕 34 | type BulletEvent struct { 35 | spec int 36 | bulletTask *BulletTask 37 | } 38 | 39 | // 定时弹幕任务调度表 40 | type BulletTaskScheduler struct { 41 | table map[int]*BulletTask 42 | eventChan chan *BulletEvent 43 | } 44 | 45 | func NewBullet(msg string, expr string) *Bullet { 46 | id++ 47 | return &Bullet{id: id, msg: msg, expr: expr} 48 | } 49 | 50 | func NewBulletTask(b *Bullet) *BulletTask { 51 | bt := &BulletTask{} 52 | bt.bullet = b 53 | bt.expr = cronexpr.MustParse(b.expr) 54 | bt.next = bt.expr.Next(time.Now()) 55 | return bt 56 | } 57 | 58 | func NewBulletEvent(spec int, bt *BulletTask) *BulletEvent { 59 | return &BulletEvent{ 60 | spec: spec, 61 | bulletTask: bt, 62 | } 63 | } 64 | 65 | func PushToBulletEvent(be *BulletEvent) { 66 | log.Println("PushBulletEvent成功", be.bulletTask.bullet.msg) 67 | scheduler.eventChan <- be 68 | } 69 | 70 | // 定时弹幕任务调度 71 | func StartTimingBullet(ctx context.Context) { 72 | 73 | // 初始化任务表 74 | scheduler = &BulletTaskScheduler{ 75 | table: make(map[int]*BulletTask), 76 | eventChan: make(chan *BulletEvent, 1000), 77 | } 78 | 79 | var be *BulletEvent 80 | 81 | interval := CalculateAndRun() 82 | t := time.NewTimer(interval) 83 | 84 | defer t.Stop() 85 | 86 | for { 87 | select { 88 | // 事件处理 89 | case be = <-scheduler.eventChan: 90 | HandleBulletEvent(be) 91 | // 关闭goroutine 92 | case <-ctx.Done(): 93 | goto END 94 | // 到达等待时间,开始执行定时任务 95 | case <-t.C: 96 | } 97 | interval = CalculateAndRun() 98 | t.Reset(interval) 99 | } 100 | 101 | END: 102 | } 103 | 104 | // 定时弹幕事件处理 105 | func HandleBulletEvent(be *BulletEvent) { 106 | switch be.spec { 107 | case Save: 108 | log.Println("task保存成功", be.bulletTask.bullet.msg) 109 | scheduler.table[be.bulletTask.bullet.id] = be.bulletTask 110 | case Remove: 111 | delete(scheduler.table, be.bulletTask.bullet.id) 112 | } 113 | } 114 | 115 | // 在所有定时任务中计算出需要等待的时间,并执行到期任务 116 | func CalculateAndRun() time.Duration { 117 | 118 | var interval *time.Time 119 | now := time.Now() 120 | 121 | for _, bt := range scheduler.table { 122 | 123 | // 执行到期任务 124 | if bt.next.Before(now) || bt.next.Equal(now) { 125 | PushToBulletSender(bt.bullet.msg) 126 | bt.next = bt.expr.Next(now) // 更新下一次执行时间 127 | } 128 | 129 | // 确定最近任务间隔时间 130 | if interval == nil || bt.next.Before(*interval) { 131 | interval = &bt.next 132 | } 133 | } 134 | 135 | // 没有任务固定等待1s 136 | if interval == nil { 137 | return time.Second 138 | } 139 | 140 | return (*interval).Sub(time.Now()) 141 | } 142 | -------------------------------------------------------------------------------- /bullet_girl/handle_bullet.go: -------------------------------------------------------------------------------- 1 | package bullet_girl 2 | 3 | import ( 4 | "bytes" 5 | "compress/zlib" 6 | "context" 7 | "encoding/binary" 8 | "encoding/json" 9 | "github.com/k-si/bili_live/entity" 10 | "io" 11 | "log" 12 | "strings" 13 | ) 14 | 15 | var handler *BulletHandler 16 | 17 | type BulletHandler struct { 18 | BulletChan chan []byte 19 | } 20 | 21 | func pushToBulletHandler(message []byte) { 22 | handler.BulletChan <- message 23 | } 24 | 25 | func HandleBullet(ctx context.Context) { 26 | handler = &BulletHandler{ 27 | BulletChan: make(chan []byte, 1000), 28 | } 29 | 30 | var message []byte 31 | for { 32 | select { 33 | case <-ctx.Done(): 34 | goto END 35 | case message = <-handler.BulletChan: 36 | handle(message) 37 | } 38 | } 39 | END: 40 | } 41 | 42 | func handle(message []byte) { 43 | var err error 44 | 45 | // 一个正文可能包含多个数据包,需要逐个解析 46 | index := 0 47 | for index < len(message) { 48 | 49 | // 读出包长 50 | var length uint32 51 | if err = binary.Read(bytes.NewBuffer(message[index:index+headLengthOffset]), binary.BigEndian, &length); err != nil { 52 | log.Println("解析包长度失败", err) 53 | return 54 | } 55 | 56 | // 读出正文协议版本 57 | var ver Version 58 | if err = binary.Read(bytes.NewBuffer(message[index+versionOffset:index+opcodeOffset]), binary.BigEndian, &ver); err != nil { 59 | log.Println("解析正文协议版本失败", err) 60 | return 61 | } 62 | 63 | // 读出操作码 64 | var op Opcode 65 | if err = binary.Read(bytes.NewBuffer(message[index+opcodeOffset:index+magicOffset]), binary.BigEndian, &op); err != nil { 66 | log.Println("解析操作码失败", err) 67 | return 68 | } 69 | 70 | // 读出正文内容 71 | body := message[index+packageLength : index+int(length)] 72 | 73 | // 解析正文内容 74 | switch ver { 75 | case normalJson: 76 | log.Println("普通json包:", string(body), ver, op) 77 | text := &entity.CmdText{} 78 | _ = json.Unmarshal(body, text) 79 | if op == command { 80 | switch Cmd(text.Cmd) { 81 | 82 | // 处理弹幕 83 | case DanmuMsg: 84 | danmu := &entity.DanmuMsgText{} 85 | _ = json.Unmarshal(body, danmu) 86 | from := danmu.Info[2].([]interface{}) 87 | 88 | // 如果发现弹幕在@我,那么调用机器人进行回复 89 | y, content := checkIsAtMe(danmu.Info[1].(string)) 90 | if y { 91 | PushToBulletRobot(content) 92 | } 93 | 94 | log.Println(from[0].(float64), from[1], ":", danmu.Info[1]) 95 | 96 | // 欢迎舰长 97 | case entryEffect: 98 | entry := &entity.EntryEffectText{} 99 | _ = json.Unmarshal(body, entry) 100 | PushToBulletSender(welcomeCaptain(entry.Data.CopyWriting)) 101 | 102 | // 欢迎进入房间(该功能会欢迎所有进入房间的人,可能会造成刷屏) 103 | //case interactWord: 104 | // interact := &entity.InteractWordText{} 105 | // _ = json.Unmarshal(body, interact) 106 | // PushToBulletSender(welcomeInteract(interact.Data.Uname)) 107 | 108 | // 感谢礼物 109 | case sendGift: 110 | send := &entity.SendGiftText{} 111 | _ = json.Unmarshal(body, send) 112 | pushToGiftChan(send) 113 | } 114 | } 115 | case heartOrCertification: 116 | log.Println("心跳回复包") 117 | case normalZlib: 118 | b := bytes.NewReader(body) 119 | r, _ := zlib.NewReader(b) 120 | var out bytes.Buffer 121 | _, _ = io.Copy(&out, r) 122 | handle(out.Bytes()) // zlib解压后再进行格式解析 123 | } 124 | index += int(length) 125 | } 126 | } 127 | 128 | // 欢迎舰长语句 129 | func welcomeCaptain(s string) string { 130 | 131 | s = strings.Replace(s, "\u003c%", "", 1) 132 | s = strings.Replace(s, "%\u003e", "", 1) 133 | 134 | return s 135 | } 136 | 137 | func welcomeInteract(name string) string { 138 | if strings.Contains(name, "欢迎") { 139 | return name 140 | } else { 141 | return "欢迎" + name 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /bullet_girl/catch_bullet.go: -------------------------------------------------------------------------------- 1 | package bullet_girl 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/binary" 7 | "encoding/json" 8 | "github.com/gorilla/websocket" 9 | "github.com/k-si/bili_live/config" 10 | "log" 11 | "time" 12 | ) 13 | 14 | type Opcode uint32 // 数据包业务类型 15 | type Version uint16 // 正文类型及压缩方式 16 | type Cmd string // 命令类型 17 | 18 | const ( 19 | normalJson Version = 0 // 正文为json格式的弹幕 20 | heartOrCertification Version = 1 // 心跳或认证包正文不压缩,客户端发送的心跳包无正文,服务队发送的心跳包正文为4字节数据,表示人气值 21 | normalZlib Version = 2 // 普通包正文使用zlib压缩 22 | normalBrotli Version = 3 // 普通包正文使用brotli压缩,解压后为一个普通包(头部协议为0),需要再次解析出正文 23 | heartBeat Opcode = 2 // 心跳包 24 | command Opcode = 5 // 命令包 25 | certification Opcode = 7 // 认证包 26 | enterRoom Opcode = 8 // 进入房间 27 | DanmuMsg Cmd = "DANMU_MSG" // 弹幕消息 28 | welcomeGuard Cmd = "WELCOME_GUARD" // 欢迎xxx老爷 29 | entryEffect Cmd = "ENTRY_EFFECT" // 欢迎舰长进入房间 30 | welcome Cmd = "WELCOME" // 欢迎xxx进入房间 31 | interactWord Cmd = "INTERACT_WORD" // 进入房间 32 | sendGift Cmd = "SEND_GIFT" // 发现送礼物 33 | ) 34 | 35 | // 关于数据包格式的常量 36 | const ( 37 | packageLength = 16 // 包长度 38 | magicNumber = 1 // 包头最后的魔数 39 | 40 | // 包头中,字节位置偏移量 41 | headLengthOffset = 4 42 | versionOffset = 6 43 | opcodeOffset = 8 44 | magicOffset = 12 45 | ) 46 | 47 | type CertificationPackageBody struct { 48 | RoomId int `json:"roomid"` 49 | } 50 | 51 | // 生成数据包头部 52 | func GeneratePackageHead(bodyLength uint32, opcode Opcode) ([]byte, error) { 53 | var err error 54 | head := bytes.NewBuffer([]byte{}) 55 | 56 | // 总长度 该值占4字节 57 | if err = binary.Write(head, binary.BigEndian, bodyLength+uint32(packageLength)); err != nil { 58 | return nil, err 59 | } 60 | // 头部长度 固定16 该值占2字节 61 | if err = binary.Write(head, binary.BigEndian, uint16(packageLength)); err != nil { 62 | return nil, err 63 | } 64 | // 协议版本号 固定1 该值占2字节 65 | if err = binary.Write(head, binary.BigEndian, heartOrCertification); err != nil { 66 | return nil, err 67 | } 68 | // 操作码 该值占4字节 69 | if err = binary.Write(head, binary.BigEndian, opcode); err != nil { 70 | return nil, err 71 | } 72 | // 包序号 可取常数1 该值占4字节 73 | if err = binary.Write(head, binary.BigEndian, uint32(magicNumber)); err != nil { 74 | return nil, err 75 | } 76 | 77 | return head.Bytes(), nil 78 | } 79 | 80 | // 生成请求数据包,由包头和正文组成 81 | func GenerateCertificationPackage() ([]byte, error) { 82 | var err error 83 | var head []byte 84 | var body []byte 85 | 86 | cpb := &CertificationPackageBody{ 87 | RoomId: config.Live.RoomId, 88 | } 89 | body, _ = json.Marshal(cpb) 90 | 91 | if head, err = GeneratePackageHead(uint32(len(body)), certification); err != nil { 92 | log.Println("生成包头失败:", err) 93 | } 94 | 95 | return append(head[:], body[:]...), nil 96 | } 97 | 98 | // 30s发送一次心跳包 99 | func StartHeartBeat(ctx context.Context, conn *websocket.Conn) { 100 | var hb []byte 101 | var err error 102 | t := time.NewTimer(30 * time.Second) 103 | defer t.Stop() 104 | for { 105 | select { 106 | case <-ctx.Done(): 107 | goto END 108 | case <-t.C: 109 | t.Reset(30 * time.Second) 110 | // 心跳包无正文 111 | if hb, err = GeneratePackageHead(0, heartBeat); err != nil { 112 | log.Println("心跳包组装错误:", err) 113 | } 114 | if err = conn.WriteMessage(websocket.BinaryMessage, hb); err != nil { 115 | log.Println("发送心跳包失败:", err) 116 | return 117 | } 118 | } 119 | } 120 | END: 121 | } 122 | 123 | func StartCatchBullet(ctx context.Context) { 124 | var err error 125 | var cert []byte 126 | var conn *websocket.Conn 127 | var message []byte 128 | 129 | // 连接ws服务器 130 | if conn, _, err = websocket.DefaultDialer.Dial(config.Live.WsServerUrl, nil); err != nil { 131 | log.Fatal("websocket连接失败:", err) 132 | return 133 | } 134 | defer conn.Close() 135 | 136 | // 组装认证包 137 | if cert, err = GenerateCertificationPackage(); err != nil { 138 | log.Fatal("组装认证包错误:", err) 139 | return 140 | } 141 | 142 | // 发送认证包 143 | if err = conn.WriteMessage(websocket.BinaryMessage, cert); err != nil { 144 | log.Fatal("发送认证包失败:", err) 145 | return 146 | } 147 | 148 | // 开启心跳包 149 | hbCtx, hbCancel := context.WithCancel(context.Background()) 150 | defer hbCancel() 151 | go StartHeartBeat(hbCtx, conn) 152 | 153 | // 循环接受信息 154 | for { 155 | select { 156 | case <-ctx.Done(): 157 | hbCancel() 158 | goto END 159 | default: 160 | if _, message, err = conn.ReadMessage(); err != nil { 161 | log.Println("websocket读取消息失败", err) 162 | continue 163 | } 164 | pushToBulletHandler(message) 165 | } 166 | } 167 | END: 168 | } 169 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "github.com/k-si/bili_live/bullet_girl" 6 | "github.com/k-si/bili_live/config" 7 | "github.com/k-si/bili_live/entity" 8 | "github.com/k-si/bili_live/errs" 9 | "github.com/k-si/bili_live/http" 10 | "github.com/k-si/bili_live/util" 11 | "log" 12 | "os" 13 | "os/signal" 14 | "syscall" 15 | "time" 16 | ) 17 | 18 | var sendBulletCtx context.Context 19 | var sendBulletCancel context.CancelFunc 20 | var timingBulletCtx context.Context 21 | var timingBulletCancel context.CancelFunc 22 | var robotBulletCtx context.Context 23 | var robotBulletCancel context.CancelFunc 24 | var catchBulletCtx context.Context 25 | var catchBulletCancel context.CancelFunc 26 | var handleBulletCtx context.Context 27 | var handleBulletCancel context.CancelFunc 28 | var thanksGiftCtx context.Context 29 | var thankGiftCancel context.CancelFunc 30 | 31 | func main() { 32 | var err error 33 | 34 | defer func() { 35 | if sendBulletCancel != nil { 36 | sendBulletCancel() 37 | } 38 | if timingBulletCancel != nil { 39 | timingBulletCancel() 40 | } 41 | if robotBulletCancel != nil { 42 | robotBulletCancel() 43 | } 44 | if catchBulletCancel != nil { 45 | catchBulletCancel() 46 | } 47 | if handleBulletCancel != nil { 48 | handleBulletCancel() 49 | } 50 | if thankGiftCancel != nil { 51 | thankGiftCancel() 52 | } 53 | }() 54 | 55 | // 初始化配置文件,http客户端 56 | if err = config.InitConfig(); err != nil { 57 | log.Fatal("配置文件错误:", err) 58 | } 59 | http.InitHttpClient() 60 | 61 | // 扫码登录 62 | if err = UserLogin(); err != nil { 63 | log.Fatal("用户登录失败:", err) 64 | } 65 | 66 | // 准备select中用到的变量 67 | sig := make(chan os.Signal) 68 | signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) 69 | var interval = time.Minute 70 | t := time.NewTimer(interval) 71 | defer t.Stop() 72 | var info *entity.RoomInitInfo 73 | var preStatus int 74 | 75 | log.Println("正在检测直播间是否开播...") 76 | 77 | // 循环监听直播间情况 78 | for { 79 | select { 80 | 81 | // 程序退出 82 | case <-sig: 83 | goto END 84 | 85 | // 每1分钟检查一次直播间是否开播 86 | case <-t.C: 87 | t.Reset(interval) 88 | if info, err = http.RoomInit(); err != nil || err == errs.RoomIdNotExistErr{ 89 | log.Println("RoomInit错误:", err) 90 | continue 91 | } 92 | if info.Data.LiveStatus == entity.Live && preStatus == entity.NotStarted { // 由NotStarted到Live是开播 93 | log.Println("开播啦!") 94 | preStatus = entity.Live 95 | // 弹幕姬各goroutine上下文 96 | sendBulletCtx, sendBulletCancel = context.WithCancel(context.Background()) 97 | timingBulletCtx, timingBulletCancel = context.WithCancel(context.Background()) 98 | robotBulletCtx, robotBulletCancel = context.WithCancel(context.Background()) 99 | catchBulletCtx, catchBulletCancel = context.WithCancel(context.Background()) 100 | handleBulletCtx, handleBulletCancel = context.WithCancel(context.Background()) 101 | thanksGiftCtx, thankGiftCancel = context.WithCancel(context.Background()) 102 | 103 | StartBulletGirl(sendBulletCtx, timingBulletCtx, robotBulletCtx, catchBulletCtx, handleBulletCtx, thanksGiftCtx) // 开启弹幕姬 104 | } else if info.Data.LiveStatus == entity.NotStarted && preStatus == entity.Live { // 由Live到NotStarted是下播 105 | log.Println("下播啦!") 106 | preStatus = entity.NotStarted 107 | sendBulletCancel() 108 | timingBulletCancel() 109 | robotBulletCancel() 110 | catchBulletCancel() 111 | handleBulletCancel() 112 | thankGiftCancel() // 关闭弹幕姬goroutine 113 | } 114 | } 115 | } 116 | END: 117 | } 118 | 119 | func StartBulletGirl(sendBulletCtx, timingBulletCtx, robotBulletCtx, catchBulletCtx, handleBulletCtx, thanksGiftCtx context.Context) { 120 | 121 | // 开启弹幕推送 122 | go bullet_girl.StartSendBullet(sendBulletCtx) 123 | log.Println("弹幕推送已开启...") 124 | 125 | // 开启定时弹幕任务 126 | go bullet_girl.StartTimingBullet(timingBulletCtx) 127 | log.Println("定时弹幕已开启...") 128 | 129 | // 开启弹幕机器人 130 | go bullet_girl.StartBulletRobot(robotBulletCtx) 131 | log.Println("弹幕机器人已开启") 132 | 133 | // 开启弹幕抓取 134 | go bullet_girl.StartCatchBullet(catchBulletCtx) 135 | log.Println("弹幕抓取已开启...") 136 | 137 | // 开启弹幕处理 138 | go bullet_girl.HandleBullet(handleBulletCtx) 139 | log.Println("弹幕处理已开启...") 140 | 141 | // 开启礼物感谢 142 | go bullet_girl.ThanksGift(thanksGiftCtx) 143 | log.Println("礼物感谢已开启") 144 | 145 | // 指定弹幕定时任务 146 | time.Sleep(time.Second) // 现开启定时任务弹幕再推送,这个方法很low,暂且这样吧 147 | bullet_girl.PushToBulletEvent( 148 | bullet_girl.NewBulletEvent( 149 | bullet_girl.Save, bullet_girl.NewBulletTask( 150 | bullet_girl.NewBullet("ios请到哔哩哔哩直播姬公众号投喂哦~", "*/9 * * * * *")))) 151 | bullet_girl.PushToBulletEvent( 152 | bullet_girl.NewBulletEvent( 153 | bullet_girl.Save, bullet_girl.NewBulletTask( 154 | bullet_girl.NewBullet("喜欢主播可以加入粉丝团哦~", "*/7 * * * * *")))) 155 | bullet_girl.PushToBulletEvent( 156 | bullet_girl.NewBulletEvent( 157 | bullet_girl.Save, bullet_girl.NewBulletTask( 158 | bullet_girl.NewBullet("主播今天很可爱哦!干巴爹!", "*/17 * * * * *")))) 159 | bullet_girl.PushToBulletEvent( 160 | bullet_girl.NewBulletEvent( 161 | bullet_girl.Save, bullet_girl.NewBulletTask( 162 | bullet_girl.NewBullet("无聊的同学可以找橘子聊天喔!", "*/23 * * * * *")))) 163 | } 164 | 165 | // 先登录,获取cookie 166 | func UserLogin() error { 167 | var err error 168 | var loginUrl *entity.LoginUrl 169 | 170 | if loginUrl, err = http.GetLoginUrl(); err != nil { 171 | log.Println(err) 172 | return err 173 | } 174 | 175 | if err = util.GenerateQrcode(loginUrl.Data.Url); err != nil { 176 | log.Println(err) 177 | return err 178 | } 179 | 180 | if _, err = http.GetLoginInfo(loginUrl.Data.OauthKey); err != nil { 181 | log.Println(err) 182 | return err 183 | } 184 | 185 | return err 186 | } 187 | 188 | //func main_test() { 189 | // sendBulletCtx, sendBulletCancel = context.WithCancel(context.Background()) 190 | // timingBulletCtx, timingBulletCancel = context.WithCancel(context.Background()) 191 | // robotBulletCtx, robotBulletCancel = context.WithCancel(context.Background()) 192 | // catchBulletCtx, catchBulletCancel = context.WithCancel(context.Background()) 193 | // handleBulletCtx, handleBulletCancel = context.WithCancel(context.Background()) 194 | // thanksGiftCtx, thankGiftCancel = context.WithCancel(context.Background()) 195 | // 196 | // var err error 197 | // 198 | // defer func() { 199 | // if sendBulletCancel != nil { 200 | // sendBulletCancel() 201 | // } 202 | // if timingBulletCancel != nil { 203 | // timingBulletCancel() 204 | // } 205 | // if robotBulletCancel != nil { 206 | // robotBulletCancel() 207 | // } 208 | // if catchBulletCancel != nil { 209 | // catchBulletCancel() 210 | // } 211 | // if handleBulletCancel != nil { 212 | // handleBulletCancel() 213 | // } 214 | // if thankGiftCancel != nil { 215 | // thankGiftCancel() 216 | // } 217 | // }() 218 | // 219 | // // 初始化配置文件,http客户端 220 | // if err = config.InitConfig(); err != nil { 221 | // log.Fatal("配置文件错误:", err) 222 | // } 223 | // http.InitHttpClient() 224 | // 225 | // // 扫码登录 226 | // if err = UserLogin(); err != nil { 227 | // log.Fatal("用户登录失败:", err) 228 | // } 229 | // 230 | // StartBulletGirl(sendBulletCtx, timingBulletCtx, robotBulletCtx, catchBulletCtx, handleBulletCtx, thanksGiftCtx) // 开启弹幕姬 231 | // 232 | // // 准备select中用到的变量 233 | // sig := make(chan os.Signal) 234 | // signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) 235 | // <-sig 236 | //} 237 | --------------------------------------------------------------------------------