afreeca.go 16 KB

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