afreeca.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711
  1. package platform
  2. import (
  3. "errors"
  4. "fmt"
  5. "git.beejay.kim/WatchDog/ward/internal/tools"
  6. "git.beejay.kim/WatchDog/ward/internal/ws"
  7. "git.beejay.kim/WatchDog/ward/model"
  8. "git.beejay.kim/WatchDog/ward/platform/afreeca"
  9. "github.com/kelindar/bitmap"
  10. "github.com/rs/zerolog/log"
  11. "github.com/spf13/cast"
  12. "github.com/tidwall/gjson"
  13. "google.golang.org/protobuf/proto"
  14. "google.golang.org/protobuf/types/known/timestamppb"
  15. "io"
  16. "net/http"
  17. "net/url"
  18. "strings"
  19. "time"
  20. )
  21. const (
  22. afreecaHeaderReferer = "https://play.afreecatv.com"
  23. afreecaMessageBufferSize = 256
  24. afreecaStateAuthenticated uint32 = iota
  25. afreecaStateQuickView
  26. )
  27. type Afreeca struct {
  28. ForceAuth bool `yaml:"force-auth"`
  29. Username string `yaml:"username"`
  30. Password string `yaml:"password"`
  31. host string
  32. rest *http.Client
  33. info *afreeca.Channel
  34. // credentials
  35. cookies []*http.Cookie
  36. uuid string
  37. state bitmap.Bitmap
  38. bridgeConnector ws.ConnectionOptions
  39. chatConnector ws.ConnectionOptions
  40. }
  41. func (p *Afreeca) Init(bid string) error {
  42. var err error
  43. if p.host = strings.TrimSpace(bid); p.host == "" {
  44. return errors.New("broadcaster ID cannot be empty")
  45. }
  46. // Generate UUID
  47. p.uuid = afreeca.GenerateUID()
  48. // Initiate HTTP Client
  49. p.rest = &http.Client{
  50. Transport: func() *http.Transport {
  51. t := http.DefaultTransport.(*http.Transport)
  52. t.Proxy = http.ProxyFromEnvironment
  53. return t
  54. }(),
  55. Timeout: time.Second * 30,
  56. }
  57. // Authorize if forced
  58. if p.ForceAuth {
  59. if err = p.login(); err != nil {
  60. return err
  61. }
  62. }
  63. // Retrieve and validate channel data
  64. if p.info, err = p.getChannelData(); err != nil {
  65. return err
  66. }
  67. if p.info.Result != 1 {
  68. return errors.New("could not fetch channel data")
  69. }
  70. if p.info.PasswordRequired {
  71. return errors.New("password is required")
  72. }
  73. p.bridgeConnector = ws.ConnectionOptions{
  74. MessageListener: make(chan []byte, afreecaMessageBufferSize),
  75. PingPeriod: time.Second * 20,
  76. PingMessage: afreeca.NewBridgeMessage(afreeca.BridgeCommandKeepAlive).MustMarshall(),
  77. Headers: http.Header{
  78. "Sec-Websocket-Protocol": []string{"bridge"},
  79. "Origin": []string{afreecaHeaderReferer},
  80. },
  81. }
  82. p.chatConnector = ws.ConnectionOptions{
  83. MessageListener: make(chan []byte, afreecaMessageBufferSize),
  84. PingPeriod: time.Minute,
  85. PingMessage: afreeca.NewChatPingMessage(),
  86. Headers: http.Header{
  87. "Sec-Websocket-Protocol": []string{"chat"},
  88. "Origin": []string{afreecaHeaderReferer},
  89. },
  90. }
  91. return nil
  92. }
  93. func (p *Afreeca) push(ch chan proto.Message, m proto.Message) error {
  94. if ch == nil {
  95. return errors.New("could not push message to the closed channel")
  96. }
  97. if m == nil {
  98. return errors.New("could not push empty message")
  99. }
  100. select {
  101. case ch <- m:
  102. }
  103. return nil
  104. }
  105. func (p *Afreeca) Connect(ch chan proto.Message) error {
  106. if p.info == nil {
  107. return errors.New("illegal state: empty channel data")
  108. }
  109. var (
  110. bridge *ws.Connection
  111. chat *ws.Connection
  112. err error
  113. )
  114. // Open bridge connection
  115. if bridge, err = ws.NewConnection("wss://bridge.afreecatv.com/Websocket", p.bridgeConnector); err != nil {
  116. return err
  117. }
  118. if err = p.onBridgeOpened(bridge); err != nil {
  119. return err
  120. }
  121. for {
  122. select {
  123. case buf := <-p.bridgeConnector.MessageListener:
  124. m := afreeca.BridgeMessage{}
  125. if err = m.UnmarshalJSON(buf); err != nil {
  126. log.Error().
  127. Str("raw", string(buf)).
  128. Err(err).
  129. Msg("could not Unmarshall bridge message")
  130. return err
  131. }
  132. switch m.Command {
  133. case afreeca.BridgeCommandLogin:
  134. if err = p.onBridgeAuthenticated(bridge, m.Data); err != nil {
  135. return err
  136. }
  137. case afreeca.BridgeCommandCertTicket:
  138. if err = p.onCertTicketReceived(bridge, m.Data); err != nil {
  139. return err
  140. }
  141. if chat, err = ws.NewConnection(p.info.ChatServerUrl(), p.chatConnector); err != nil {
  142. return err
  143. }
  144. if err = p.onChatOpened(chat); err != nil {
  145. return err
  146. }
  147. case afreeca.BridgeCommandUserCount:
  148. t := model.Online{
  149. At: timestamppb.New(time.Now()),
  150. Platform: p.Id(),
  151. Broadcaster: p.host,
  152. Raw: buf,
  153. DevicePc: cast.ToUint64(m.Data["uiJoinChUser"]),
  154. DeviceMobile: cast.ToUint64(m.Data["uiMbUser"]),
  155. }
  156. t.Total = t.DeviceMobile + t.DevicePc + t.DeviceUnknown
  157. if err = p.push(ch, &t); err != nil {
  158. return err
  159. }
  160. case afreeca.BridgeCommandUserCountExtended:
  161. //t := model.Online{
  162. // At: timestamppb.New(time.Now()),
  163. // Platform: p.Id(),
  164. // Broadcaster: p.host,
  165. // Raw: buf,
  166. // DevicePc: cast.ToUint64(m.Data["uiJoinChPCUser"]),
  167. // DeviceMobile: cast.ToUint64(m.Data["uiJoinChMBUser"]),
  168. //}
  169. //
  170. //t.Total = t.DeviceMobile + t.DevicePc + t.DeviceUnknown
  171. //if err = p.push(ch, &t); err != nil {
  172. // return err
  173. //}
  174. continue
  175. case afreeca.BridgeCommandClosed:
  176. return p.onBridgeClosed(bridge, m.Data)
  177. default:
  178. log.Debug().
  179. Str("command", string(m.Command)).
  180. Any("bridge", m.Data).
  181. Send()
  182. }
  183. case buf := <-p.chatConnector.MessageListener:
  184. m := afreeca.ChatMessage{}
  185. if err = m.Unmarshall(buf); err != nil {
  186. log.Error().
  187. Err(err).
  188. Str("raw", string(buf)).
  189. Msg("could not Unmarshall chat message")
  190. continue
  191. }
  192. if m.Is(afreeca.Kind.Authorize) {
  193. if err = p.onChatAuthenticated(chat); err != nil {
  194. return err
  195. }
  196. } else if m.Is(afreeca.Kind.Authenticate) {
  197. if err = chat.Send(afreeca.NewChatRosterRequestMessage()); err != nil {
  198. return err
  199. }
  200. } else if m.Is(afreeca.Kind.Chat) {
  201. d := m.Data()
  202. if err = p.push(ch, &model.Message{
  203. At: timestamppb.New(time.Now()),
  204. Platform: p.Id(),
  205. Broadcaster: p.host,
  206. Raw: buf,
  207. User: &model.User{
  208. Id: afreeca.NormalizeUserID(d[1]),
  209. Name: d[5],
  210. },
  211. Text: d[0],
  212. }); err != nil {
  213. return err
  214. }
  215. } else if m.Is(afreeca.Kind.ChatEmoticon) {
  216. d := m.Data()
  217. if err = p.push(ch, &model.Message{
  218. At: timestamppb.New(time.Now()),
  219. Platform: p.Id(),
  220. Broadcaster: p.host,
  221. Raw: buf,
  222. User: &model.User{
  223. Id: afreeca.NormalizeUserID(d[5]),
  224. Name: d[6],
  225. },
  226. Text: fmt.Sprintf("%s:%d",
  227. cast.ToString(d[2]),
  228. cast.ToInt(d[3]),
  229. ),
  230. Sticker: true,
  231. }); err != nil {
  232. return err
  233. }
  234. } else if m.Is(afreeca.Kind.Donation) {
  235. d := m.Data()
  236. if err = p.push(ch, &model.Donation{
  237. At: timestamppb.New(time.Now()),
  238. Platform: p.Id(),
  239. Broadcaster: p.host,
  240. Raw: buf,
  241. User: &model.User{
  242. Id: afreeca.NormalizeUserID(d[1]),
  243. Name: d[2],
  244. },
  245. Amount: cast.ToUint64(d[3]),
  246. }); err != nil {
  247. return err
  248. }
  249. } else if m.Is(afreeca.Kind.Roster.List) {
  250. d := m.Data()
  251. if cast.ToInt(d[0]) != 1 {
  252. continue
  253. }
  254. d = d[1:]
  255. for i := 0; i < len(d); i++ {
  256. id := afreeca.NormalizeUserID(d[i])
  257. pass := 2
  258. if !strings.Contains(d[i+pass], `|`) {
  259. pass = 3
  260. }
  261. i += pass
  262. if err = p.push(ch, &model.RosterChange{
  263. At: timestamppb.New(time.Now()),
  264. Platform: p.Id(),
  265. Broadcaster: p.host,
  266. Raw: buf,
  267. User: &model.User{
  268. Id: id,
  269. },
  270. Operation: model.RosterChange_OP_JOINED,
  271. }); err != nil {
  272. return err
  273. }
  274. }
  275. } else if m.Is(afreeca.Kind.Roster.Change) {
  276. d := m.Data()
  277. op := model.RosterChange_OP_JOINED
  278. if cast.ToInt(d[0]) == -1 {
  279. op = model.RosterChange_OP_LEFT
  280. }
  281. if err = p.push(ch, &model.RosterChange{
  282. At: timestamppb.New(time.Now()),
  283. Platform: p.Id(),
  284. Broadcaster: p.host,
  285. Raw: buf,
  286. User: &model.User{
  287. Id: afreeca.NormalizeUserID(d[1]),
  288. },
  289. Operation: op,
  290. }); err != nil {
  291. return err
  292. }
  293. } else {
  294. log.Debug().
  295. Bytes("kind", m.Kind()).
  296. Any("chat", m.Data()).
  297. Send()
  298. }
  299. }
  300. }
  301. }
  302. func (p *Afreeca) Id() string {
  303. return "afreeca"
  304. }
  305. func (p *Afreeca) Host() string {
  306. return p.host
  307. }
  308. func (p *Afreeca) makeRequest(meth string, url string, params url.Values) (*http.Request, error) {
  309. var (
  310. request *http.Request
  311. err error
  312. )
  313. if http.MethodPost == meth {
  314. var body io.Reader
  315. if params != nil {
  316. body = strings.NewReader(params.Encode())
  317. }
  318. request, err = http.NewRequest(meth, url, body)
  319. } else {
  320. if params != nil {
  321. url = fmt.Sprintf("%s?%s", url, params.Encode())
  322. }
  323. request, err = http.NewRequest(meth, url, nil)
  324. }
  325. if err != nil {
  326. return nil, err
  327. }
  328. request.Header.Set("Referer", afreecaHeaderReferer)
  329. if http.MethodPost == meth && params != nil {
  330. request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
  331. }
  332. for i := range p.cookies {
  333. request.AddCookie(p.cookies[i])
  334. }
  335. return request, nil
  336. }
  337. func (p *Afreeca) login() error {
  338. if p.Username == "" || p.Password == "" {
  339. return errors.New("username or password is empty")
  340. }
  341. var (
  342. request *http.Request
  343. response *http.Response
  344. buf []byte
  345. err error
  346. )
  347. if request, err = p.makeRequest(http.MethodPost,
  348. "https://login.afreecatv.com/app/LoginAction.php",
  349. url.Values{
  350. "szWork": []string{"login"},
  351. "szType": []string{"json"},
  352. "szUid": []string{p.Username},
  353. "szPassword": []string{p.Password},
  354. "isSaveId": []string{"true"},
  355. "isSavePw": []string{"false"},
  356. "isSaveJoin": []string{"false"},
  357. "isLoginRetain": []string{"Y"},
  358. }); err != nil {
  359. return err
  360. }
  361. if response, err = p.rest.Do(request); err != nil {
  362. return err
  363. }
  364. //goland:noinspection ALL
  365. defer response.Body.Close()
  366. if buf, err = tools.DecodeHttpResponse(response); err != nil {
  367. return err
  368. }
  369. if gjson.GetBytes(buf, "RESULT").Int() != 1 {
  370. return errors.New("login failed")
  371. }
  372. p.cookies = response.Cookies()
  373. p.state.Set(afreecaStateAuthenticated)
  374. return nil
  375. }
  376. func (p *Afreeca) getHLSKey(preset afreeca.Preset) (string, error) {
  377. var (
  378. request *http.Request
  379. response *http.Response
  380. buf []byte
  381. err error
  382. )
  383. if request, err = p.makeRequest(http.MethodPost,
  384. "https://live.afreecatv.com/afreeca/player_live_api.php",
  385. url.Values{
  386. "bid": []string{p.host},
  387. "bno": []string{cast.ToString(p.info.Id)},
  388. "from_api": []string{"0"},
  389. "mode": []string{"landing"},
  390. "player_type": []string{"html5"},
  391. "pwd": []string{""},
  392. "quality": []string{preset.Name},
  393. "stream_type": []string{"common"},
  394. "type": []string{"aid"},
  395. }); err != nil {
  396. }
  397. if response, err = p.rest.Do(request); err != nil {
  398. return "", err
  399. }
  400. //goland:noinspection ALL
  401. defer response.Body.Close()
  402. if buf, err = tools.DecodeHttpResponse(response); err != nil {
  403. return "", err
  404. }
  405. if gjson.GetBytes(buf, "CHANNEL.RESULT").Int() != 1 {
  406. err = errors.New("could not fetch HLS key")
  407. log.Debug().
  408. Str("json", string(buf)).
  409. Err(err).
  410. Send()
  411. return "", err
  412. }
  413. if aid := gjson.GetBytes(buf, "CHANNEL.AID").String(); aid == "" {
  414. err = errors.New("could not fetch HLS key")
  415. log.Debug().
  416. Str("json", string(buf)).
  417. Err(err).
  418. Send()
  419. return "", err
  420. } else {
  421. return aid, nil
  422. }
  423. }
  424. func (p *Afreeca) HLSStream() (*http.Request, error) {
  425. var (
  426. preset *afreeca.Preset
  427. aid string
  428. uri string
  429. params = url.Values{
  430. "return_type": []string{"gs_cdn_pc_web"},
  431. }
  432. request *http.Request
  433. response *http.Response
  434. buf []byte
  435. err error
  436. )
  437. if preset = p.info.GetPreset("original"); preset == nil {
  438. return nil, errors.New("could not find feasible preset")
  439. }
  440. if aid, err = p.getHLSKey(*preset); err != nil {
  441. return nil, err
  442. }
  443. params.Add("broad_key",
  444. fmt.Sprintf("%d-common-%s-hls", p.info.Id, preset.Name))
  445. if request, err = p.makeRequest(http.MethodGet,
  446. fmt.Sprintf("%s/broad_stream_assign.html", p.info.RMD),
  447. params); err != nil {
  448. return nil, err
  449. }
  450. if response, err = p.rest.Do(request); err != nil {
  451. return nil, err
  452. }
  453. //goland:noinspection ALL
  454. defer response.Body.Close()
  455. if buf, err = tools.DecodeHttpResponse(response); err != nil {
  456. return nil, err
  457. }
  458. if gjson.GetBytes(buf, "result").Int() != 1 {
  459. err = errors.New("could not fetch HLS url")
  460. log.Debug().
  461. Str("json", string(buf)).
  462. Err(err).
  463. Send()
  464. return nil, err
  465. }
  466. if uri = gjson.GetBytes(buf, "view_url").String(); uri == "" {
  467. err = errors.New("could not fetch HLS url")
  468. log.Debug().
  469. Str("json", string(buf)).
  470. Err(err).
  471. Send()
  472. return nil, err
  473. }
  474. return p.makeRequest(http.MethodGet, uri,
  475. url.Values{
  476. "aid": []string{aid},
  477. })
  478. }
  479. func (p *Afreeca) getChannelData() (*afreeca.Channel, error) {
  480. var (
  481. request *http.Request
  482. response *http.Response
  483. ch afreeca.Channel
  484. buf []byte
  485. err error
  486. )
  487. if request, err = p.makeRequest(http.MethodPost,
  488. fmt.Sprintf("https://live.afreecatv.com/afreeca/player_live_api.php?bjid=%s", p.host),
  489. url.Values{
  490. "bid": []string{p.host},
  491. "type": []string{"live"},
  492. "pwd": []string{},
  493. "player_type": []string{"html5"},
  494. "stream_type": []string{"common"},
  495. "quality": []string{"HD"},
  496. "mode": []string{"landing"},
  497. "from_api": []string{"0"},
  498. "is_revive": []string{"false"},
  499. }); err != nil {
  500. return nil, err
  501. }
  502. if response, err = p.rest.Do(request); err != nil {
  503. return nil, err
  504. }
  505. //goland:noinspection ALL
  506. defer response.Body.Close()
  507. if buf, err = tools.DecodeHttpResponse(response); err != nil {
  508. return nil, err
  509. }
  510. if err = ch.Unmarshall(buf); err != nil {
  511. return nil, err
  512. }
  513. if !p.state.Contains(afreecaStateAuthenticated) &&
  514. (ch.IsLoginRequired() || ch.IsGeoRestricted() || ch.AudienceGrade > 0) {
  515. log.Debug().Msg("broadcast is login required / geo restricted / has audience grade restrictions; attempt to login")
  516. if err = p.login(); err != nil {
  517. return nil, err
  518. }
  519. log.Debug().Msg("successfully logged in")
  520. return p.getChannelData()
  521. }
  522. if p.getCookieValue("_au") == "" {
  523. if response, err = p.rest.Get(
  524. fmt.Sprintf("https://play.afreecatv.com/%s/%d", p.host, ch.Id)); err != nil {
  525. return nil, err
  526. }
  527. p.cookies = response.Cookies()
  528. }
  529. return &ch, nil
  530. }
  531. func (p *Afreeca) getCookieValue(k string) string {
  532. for i := range p.cookies {
  533. if p.cookies[i].Name == k {
  534. return p.cookies[i].Value
  535. }
  536. }
  537. return ""
  538. }
  539. func (p *Afreeca) onBridgeOpened(c *ws.Connection) error {
  540. m := afreeca.NewBridgeInitGatewayMessage(p.host, p.uuid, *p.info, p.getCookieValue)
  541. return c.Send(m.MustMarshall())
  542. }
  543. func (p *Afreeca) onBridgeClosed(_ *ws.Connection, data map[string]any) error {
  544. var (
  545. s = cast.ToString(data["pcEndingMsg"])
  546. err error
  547. )
  548. if s, err = url.QueryUnescape(s); err == nil {
  549. log.Debug().Msg(s)
  550. }
  551. return errorClosed
  552. }
  553. func (p *Afreeca) onBridgeAuthenticated(_ *ws.Connection, data map[string]any) error {
  554. // find if we have QuickView
  555. if cast.ToUint(data["iMode"]) == 1 {
  556. p.state.Set(afreecaStateQuickView)
  557. } else {
  558. p.state.Remove(afreecaStateQuickView)
  559. }
  560. return nil
  561. }
  562. func (p *Afreeca) onCertTicketReceived(c *ws.Connection, data map[string]any) error {
  563. m := afreeca.NewBridgeInitBroadcastMessage(p.uuid, *p.info, p.getCookieValue)
  564. m.AddArgument("append_data", data["pcAppendDat"])
  565. m.AddArgument("gw_ticket", data["pcTicket"])
  566. return c.Send(m.MustMarshall())
  567. }
  568. func (p *Afreeca) onChatOpened(c *ws.Connection) error {
  569. m := afreeca.NewChatAuthorizeMessage(
  570. "",
  571. p.state.Contains(afreecaStateQuickView),
  572. )
  573. return c.Send(m.Build())
  574. }
  575. func (p *Afreeca) onChatAuthenticated(c *ws.Connection) error {
  576. joinlog := afreeca.BridgeMessageValue{}
  577. joinlog.AddTuple("log", func() afreeca.BridgeMessageValueParams {
  578. params := afreeca.BridgeMessageValueParams{}
  579. params.Add("set_bps", p.info.Bitrate)
  580. params.Add("view_bps", 500)
  581. params.Add("quality", "sd")
  582. params.Add("uuid", p.uuid)
  583. params.Add("geo_cc", p.info.GeoName)
  584. params.Add("geo_rc", p.info.GeoCode)
  585. params.Add("acpt_lang", p.info.AcceptLanguage)
  586. params.Add("svc_lang", p.info.ServiceLanguage)
  587. params.Add("subscribe", 0)
  588. params.Add("lowlatency", 0)
  589. params.Add("mode", "landing")
  590. return params
  591. }())
  592. joinlog.AddTuple("pwd", "")
  593. joinlog.AddTuple("auth_info", "NULL")
  594. joinlog.AddTuple("pver", 2)
  595. joinlog.AddTuple("access_system", "html5")
  596. m := afreeca.NewChatMessage(afreeca.Kind.Authenticate,
  597. cast.ToString(p.info.ChatId),
  598. p.info.FanToken,
  599. "0",
  600. "",
  601. joinlog.String(),
  602. "")
  603. return c.Send(m.Build())
  604. }