├── errors.go
├── test_types
└── config.go
├── .idea
├── vcs.xml
└── .gitignore
├── DEV_README.md
├── Makefile
├── go.mod
├── logging.go
├── LICENSE
├── feed_online_test.go
├── wss_online_test.go
├── feed.go
├── .gitignore
├── options.go
├── live_online_test.go
├── go.sum
├── http_proxy.go
├── const.go
├── requests.go
├── tiktok_online_test.go
├── proto
├── enums.proto
├── webcast.proto
└── data.proto
├── wss.go
├── tiktok.go
├── utils.go
├── live.go
├── README.md
└── types.go
/errors.go:
--------------------------------------------------------------------------------
1 | package gotiktoklive
2 |
3 | type UserNotFound struct{}
4 |
5 | func (u UserNotFound) Error() string {
6 | return "User not found"
7 | }
8 |
--------------------------------------------------------------------------------
/test_types/config.go:
--------------------------------------------------------------------------------
1 | package test_types
2 |
3 | const (
4 | USERNAME = "judas_risen"
5 | APIKEY = ""
6 | PROXY = ""
7 | PROXY_INSECURE = true
8 | )
9 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 | # Datasource local storage ignored files
7 | /dataSources/
8 | /dataSources.local.xml
9 |
--------------------------------------------------------------------------------
/DEV_README.md:
--------------------------------------------------------------------------------
1 | ## Generating Protobuf
2 | ```bash
3 | protoc --go_out=. -I .\proto --go-grpc_out=. .\proto\enums.proto
4 | protoc --go_out=. -I .\proto --go-grpc_out=. .\proto\data.proto
5 | protoc --go_out=. -I .\proto --go-grpc_out=. .\proto\webcast.proto
6 | ```
7 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | GOPATH:=$(shell go env GOPATH)
2 |
3 | .PHONY: init
4 | init:
5 | @go get -u google.golang.org/protobuf/proto
6 | @go install github.com/golang/protobuf/protoc-gen-go@latest
7 |
8 | .PHONY: proto
9 | proto:
10 | @protoc \
11 | --proto_path=. \
12 | --go_out=:. \
13 | proto/tiktok.proto
14 |
15 | .PHONY: update
16 | update:
17 | @go get -u
18 |
19 | .PHONY: tidy
20 | tidy:
21 | @go mod tidy
22 |
23 | .PHONY: test
24 | test:
25 | @go test -v ./... -cover -coverprofile coverage.out -count=1
26 |
27 | .PHONY: cov
28 | cov:
29 | @gocovsh --profile coverage.out
30 |
31 |
32 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/steampoweredtaco/gotiktoklive
2 |
3 | go 1.23.0
4 |
5 | require (
6 | github.com/erni27/imcache v1.2.1
7 | github.com/gobwas/ws v1.1.0
8 | github.com/stretchr/testify v1.9.0
9 | go.uber.org/ratelimit v0.3.1
10 | )
11 |
12 | retract (
13 | v1.0.8 // retration only update.
14 | v1.0.7 // Published accidently.
15 | )
16 |
17 | require (
18 | github.com/benbjohnson/clock v1.3.0 // indirect
19 | github.com/davecgh/go-spew v1.1.1 // indirect
20 | github.com/google/go-cmp v0.6.0 // indirect
21 | github.com/pmezard/go-difflib v1.0.0 // indirect
22 | gopkg.in/yaml.v3 v3.0.1 // indirect
23 | )
24 |
25 | require (
26 | github.com/gobwas/httphead v0.1.0 // indirect
27 | github.com/gobwas/pool v0.2.1 // indirect
28 | github.com/pkg/errors v0.9.1
29 | golang.org/x/net v0.25.0
30 | golang.org/x/sys v0.20.0 // indirect
31 | google.golang.org/protobuf v1.33.0
32 | )
33 |
--------------------------------------------------------------------------------
/logging.go:
--------------------------------------------------------------------------------
1 | package gotiktoklive
2 |
3 | import (
4 | "fmt"
5 | "log/slog"
6 | "net/http"
7 | "net/http/httputil"
8 | "strings"
9 | )
10 |
11 | type loggingTransport struct {
12 | Transport http.RoundTripper
13 | }
14 |
15 | func (s *loggingTransport) RoundTrip(r *http.Request) (*http.Response, error) {
16 | bytes, _ := httputil.DumpRequestOut(r, true)
17 |
18 | resp, err := s.Transport.RoundTrip(r)
19 | // err is returned after dumping the response
20 | if err != nil && strings.Contains(err.Error(), "malformed HTTP response") {
21 | httputil.DumpResponse(resp, false)
22 | slog.Debug(fmt.Sprintf("%s\n", bytes))
23 | return resp, err
24 | }
25 | if resp == nil {
26 | return resp, err
27 | }
28 | respBytes, _ := httputil.DumpResponse(resp, true)
29 | bytes = append(bytes, respBytes...)
30 |
31 | slog.Debug(fmt.Sprintf("%s\n", bytes))
32 |
33 | return resp, err
34 | }
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Ahmadreza Zibaei
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/feed_online_test.go:
--------------------------------------------------------------------------------
1 | //go:build requiresOnline
2 |
3 | package gotiktoklive
4 |
5 | import (
6 | "github.com/stretchr/testify/assert"
7 | "sort"
8 | "testing"
9 | )
10 |
11 | func TestFeedItem(t *testing.T) {
12 | tiktok, err := NewTikTok()
13 | if !assert.NoError(t, err) {
14 | return
15 | }
16 | feed := tiktok.NewFeed()
17 |
18 | items := []*LiveStream{}
19 | i := 0
20 | for {
21 | feedItem, err := feed.Next()
22 | if err != nil {
23 | t.Fatal(err)
24 | }
25 | items = append(items, feedItem.LiveStreams...)
26 | i++
27 | t.Logf("%d : %d, %v", feedItem.Extra.MaxTime, len(feedItem.LiveStreams), feedItem.Extra.HasMore)
28 | for _, stream := range feedItem.LiveStreams {
29 | t.Logf("%s : %d viewers, %s", stream.Room.Owner.Nickname, stream.Room.UserCount, stream.LiveReason)
30 | }
31 |
32 | if !feedItem.Extra.HasMore || i > 5 {
33 | break
34 | }
35 | }
36 | t.Logf("Found %d items, over %d requests", len(items), i)
37 |
38 | sort.Slice(items, func(i, j int) bool {
39 | return items[i].Room.UserCount > items[j].Room.UserCount
40 | })
41 |
42 | t.Logf("Setting username to %s", items[0].Room.Owner.Username)
43 | }
44 |
--------------------------------------------------------------------------------
/wss_online_test.go:
--------------------------------------------------------------------------------
1 | //go:build requiresOnline
2 |
3 | package gotiktoklive
4 |
5 | import (
6 | "github.com/stretchr/testify/assert"
7 | "sync"
8 | "testing"
9 | "time"
10 |
11 | "github.com/steampoweredtaco/gotiktoklive/test_types"
12 | "golang.org/x/net/context"
13 | )
14 |
15 | func TestWebsocket(t *testing.T) {
16 | tiktok, err := NewTikTok()
17 | if !assert.NoError(t, err) {
18 | return
19 | }
20 | tiktok.Debug = true
21 | tiktok.debugHandler = func(i ...interface{}) {
22 | t.Log(i...)
23 | }
24 | id, err := tiktok.getRoomID(test_types.USERNAME)
25 | if !assert.NoError(t, err) {
26 | return
27 | }
28 |
29 | live := Live{
30 | t: tiktok,
31 | ID: id,
32 | wg: &sync.WaitGroup{},
33 | Events: make(chan Event, 100),
34 | }
35 |
36 | ctx, cancel := context.WithCancel(context.Background())
37 | live.done = ctx.Done
38 | live.close = func() {
39 | cancel()
40 | close(live.Events)
41 | }
42 |
43 | err = live.getRoomData()
44 | if !assert.NoError(t, err) {
45 | return
46 | }
47 |
48 | if live.wsURL == "" {
49 | t.Fatal("No websocket url provided")
50 | }
51 | t.Logf("Ws url: %s, %+v", live.wsURL, live.wsParams)
52 |
53 | if err := live.connect(live.wsURL, live.wsParams); err != nil {
54 | t.Fatal(err)
55 | }
56 |
57 | tiktok.wg.Add(2)
58 | go live.readSocket()
59 | go live.sendPing()
60 |
61 | timeout := time.After(5 * time.Second)
62 | for {
63 | select {
64 | case <-timeout:
65 | return
66 | case event := <-live.Events:
67 | switch e := event.(type) {
68 | case UserEvent:
69 | t.Logf("%T: %s (%s) %s", e, e.User.Nickname, e.User.Nickname, e.Event)
70 | default:
71 | t.Logf("%T: %+v", e, e)
72 | }
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/feed.go:
--------------------------------------------------------------------------------
1 | package gotiktoklive
2 |
3 | import (
4 | "encoding/json"
5 | "strconv"
6 | )
7 |
8 | // Feed allows you to fetch reccomended livestreams.
9 | type Feed struct {
10 | t *TikTok
11 |
12 | // All collected reccomended livestreams
13 | LiveStreams []*LiveStream
14 |
15 | HasMore bool
16 | maxTime int64
17 | }
18 |
19 | // NewFeed creates a new Feed instance. Start fetching recommended livestreams
20 | // with Feed.Next().
21 | func (t *TikTok) NewFeed() *Feed {
22 | return &Feed{
23 | t: t,
24 | LiveStreams: []*LiveStream{},
25 | HasMore: true,
26 | }
27 | }
28 |
29 | // Next fetches the next couple of recommended live streams, if available.
30 | // You can call this as long as Feed.HasMore = true. All items will be added
31 | //
32 | // to the Feed.LiveStreams list.
33 | func (f *Feed) Next() (*FeedItem, error) {
34 | if !f.HasMore {
35 | return nil, ErrNoMoreFeedItems
36 | }
37 |
38 | params := copyMap(defaultGETParams)
39 | params["channel"] = "tiktok_web"
40 | params["channel_id"] = "86"
41 | if f.maxTime != 0 {
42 | params["max_time"] = strconv.FormatInt(f.maxTime, 10)
43 | }
44 |
45 | body, _, err := f.t.sendRequest(&reqOptions{
46 | Endpoint: urlFeed,
47 | Query: params,
48 | }, nil)
49 | if err != nil {
50 | return nil, err
51 | }
52 |
53 | var rsp FeedItem
54 | if err := json.Unmarshal(body, &rsp); err != nil {
55 | return nil, err
56 | }
57 |
58 | f.HasMore = rsp.Extra.HasMore
59 | f.maxTime = rsp.Extra.MaxTime
60 | for _, s := range rsp.LiveStreams {
61 | s.t = f.t
62 | }
63 |
64 | return &rsp, nil
65 | }
66 |
67 | // Track stars tracking the livestream obtained from the Feed, and returns
68 | // a Live instance, just as if you would start tracking the user with
69 | // tiktok.TrackUser().
70 | func (s *LiveStream) Track() (*Live, error) {
71 | return s.t.TrackRoom(s.Rid)
72 | }
73 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.toptal.com/developers/gitignore/api/goland+iml
2 | # Edit at https://www.toptal.com/developers/gitignore?templates=goland+iml
3 |
4 | ### GoLand+iml ###
5 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
6 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
7 |
8 | # User-specific stuff
9 | .idea/**/workspace.xml
10 | .idea/**/tasks.xml
11 | .idea/**/usage.statistics.xml
12 | .idea/**/dictionaries
13 | .idea/**/shelf
14 |
15 | # AWS User-specific
16 | .idea/**/aws.xml
17 |
18 | # Generated files
19 | .idea/**/contentModel.xml
20 |
21 | # Sensitive or high-churn files
22 | .idea/**/dataSources/
23 | .idea/**/dataSources.ids
24 | .idea/**/dataSources.local.xml
25 | .idea/**/sqlDataSources.xml
26 | .idea/**/dynamic.xml
27 | .idea/**/uiDesigner.xml
28 | .idea/**/dbnavigator.xml
29 |
30 | # Gradle
31 | .idea/**/gradle.xml
32 | .idea/**/libraries
33 |
34 | # Gradle and Maven with auto-import
35 | # When using Gradle or Maven with auto-import, you should exclude module files,
36 | # since they will be recreated, and may cause churn. Uncomment if using
37 | # auto-import.
38 | # .idea/artifacts
39 | # .idea/compiler.xml
40 | # .idea/jarRepositories.xml
41 | # .idea/modules.xml
42 | # .idea/*.iml
43 | # .idea/modules
44 | # *.iml
45 | # *.ipr
46 |
47 | # CMake
48 | cmake-build-*/
49 |
50 | # Mongo Explorer plugin
51 | .idea/**/mongoSettings.xml
52 |
53 | # File-based project format
54 | *.iws
55 |
56 | # IntelliJ
57 | out/
58 |
59 | # mpeltonen/sbt-idea plugin
60 | .idea_modules/
61 |
62 | # JIRA plugin
63 | atlassian-ide-plugin.xml
64 |
65 | # Cursive Clojure plugin
66 | .idea/replstate.xml
67 |
68 | # SonarLint plugin
69 | .idea/sonarlint/
70 |
71 | # Crashlytics plugin (for Android Studio and IntelliJ)
72 | com_crashlytics_export_strings.xml
73 | crashlytics.properties
74 | crashlytics-build.properties
75 | fabric.properties
76 |
77 | # Editor-based Rest Client
78 | .idea/httpRequests
79 |
80 | # Android studio 3.1+ serialized cache file
81 | .idea/caches/build_file_checksums.ser
82 |
83 | ### GoLand+iml Patch ###
84 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
85 |
86 | *.iml
87 | modules.xml
88 | .idea/misc.xml
89 | *.ipr
90 |
91 | # End of https://www.toptal.com/developers/gitignore/api/goland+iml
92 |
93 | vendor/
--------------------------------------------------------------------------------
/options.go:
--------------------------------------------------------------------------------
1 | package gotiktoklive
2 |
3 | import "net/http"
4 |
5 | type TikTokLiveOption func(t *TikTok) error
6 |
7 | // SigningApiKey sets the singer API key.
8 | func SigningApiKey(apiKey string) TikTokLiveOption {
9 | return func(t *TikTok) error {
10 | t.apiKey = apiKey
11 | return nil
12 | }
13 | }
14 |
15 | // SigningUrl defines the signer. The default is https://tiktok.eulerstream.com. Supports any signer that supports the
16 | // signing api as defined by https://www.eulerstream.com/docs/openapi
17 | func SigningUrl(url string) TikTokLiveOption {
18 | return func(t *TikTok) error {
19 | t.signerUrl = url
20 | return nil
21 | }
22 | }
23 |
24 | // DisableSigningLimitsValidation will disable querying the signer for limits and using those as the reasonable limits
25 | // for signing requests per second. Instead, this library will be limited to signing only 5 signing requests per minute
26 | // and may limit functionality compared to the request limit the signer provides.
27 | func DisableSigningLimitsValidation(t *TikTok) error {
28 | t.getLimits = false
29 | return nil
30 | }
31 |
32 | // EnableExperimentalEvents enables experimental events that have not been figured out yet and the API for them is not
33 | // stable. It may also induce additional logging that might be undesirable.
34 | func EnableExperimentalEvents(t *TikTok) error {
35 | t.enableExperimentalEvents = true
36 | return nil
37 | }
38 |
39 | // EnableExtraWebCastDebug an unreasonable amount of debug for library development and troubleshooting. This option
40 | // makes no guarantee of ever having the same output and is only for development and triage purposes.
41 | func EnableExtraWebCastDebug(t *TikTok) error {
42 | t.enableExtraDebug = true
43 | t.c.Transport = &loggingTransport{Transport: http.DefaultTransport}
44 | return nil
45 | }
46 |
47 | // EnableWSTrace will put traces for all websocket messages into the given file. The file will be overwritten so
48 | // if you want multiple traces make sure handle giving a unique filename each startup.
49 | func EnableWSTrace(file string) TikTokLiveOption {
50 | return func(t *TikTok) error {
51 | t.enableWSTrace = true
52 | t.wsTraceFile = file
53 | t.wsTraceChan = make(chan struct{ direction, hex string }, 50)
54 | return nil
55 | }
56 | }
57 |
58 | // Proxy will set a proxy for both the http client and the websocket. You can
59 | // manually set a proxy with option or by using the HTTPS_PROXY environment variable.
60 | // ALL_PROXY can be used to set a proxy only for the websocket.
61 | func Proxy(url string, insecure bool) TikTokLiveOption {
62 | if url == "" {
63 | return func(t *TikTok) error {
64 | return nil
65 | }
66 | }
67 | return func(t *TikTok) error {
68 | return t.setProxy(url, insecure)
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/live_online_test.go:
--------------------------------------------------------------------------------
1 | //go:build requiresOnline
2 |
3 | package gotiktoklive
4 |
5 | import (
6 | "log/slog"
7 | "testing"
8 | "time"
9 |
10 | "github.com/steampoweredtaco/gotiktoklive/test_types"
11 | )
12 |
13 | func TestLiveTrackUser(t *testing.T) {
14 | tiktok, err := NewTikTok()
15 | if err != nil {
16 | t.Fatal(err)
17 | }
18 | live, err := tiktok.TrackUser(test_types.USERNAME)
19 | if err != nil {
20 | t.Fatal(err)
21 | }
22 |
23 | timeout := time.After(5 * time.Second)
24 |
25 | eventCounter := 0
26 | for {
27 | select {
28 | case event := <-live.Events:
29 | t.Logf("%T", event)
30 | eventCounter++
31 | case <-timeout:
32 | if eventCounter < 10 {
33 | t.Fatal("Less than 10 events received.. Something seems off")
34 | }
35 | return
36 | }
37 | }
38 | }
39 |
40 | func TestLivePriceList(t *testing.T) {
41 | tiktok, err := NewTikTok()
42 | if err != nil {
43 | t.Fatal(err)
44 | }
45 | priceList, err := tiktok.GetPriceList()
46 | if err != nil {
47 | t.Fatal(err)
48 | }
49 |
50 | t.Logf("Fetched %d prices", len(priceList.PriceList))
51 | }
52 |
53 | func TestLiveDownload(t *testing.T) {
54 | slog.SetLogLoggerLevel(slog.LevelDebug)
55 | tiktok, err := NewTikTok(SigningApiKey(test_types.APIKEY))
56 | if err != nil {
57 | t.Fatal(err)
58 | }
59 | live, err := tiktok.TrackUser(test_types.USERNAME)
60 | if err != nil {
61 | t.Fatal(err)
62 | }
63 | done := make(chan struct{})
64 | go func() {
65 | defer func() {
66 | done <- struct{}{}
67 | }()
68 | err = live.DownloadStream()
69 | if err != nil {
70 | t.Fatal(err)
71 | }
72 | }()
73 |
74 | select {
75 | case _ = <-done:
76 | t.Log("command exited")
77 | case <-time.After(12 * time.Second):
78 | t.Log("test complete")
79 | }
80 | live.Close()
81 |
82 | live, err = tiktok.TrackUser(test_types.USERNAME)
83 | if err != nil {
84 | t.Fatal(err)
85 | }
86 |
87 | done = make(chan struct{})
88 | go func() {
89 | defer func() {
90 | done <- struct{}{}
91 | }()
92 | err = live.DownloadStream("my-test-download.mkv")
93 | if err != nil {
94 | t.Fatal(err)
95 | }
96 | }()
97 | select {
98 | case _ = <-done:
99 | t.Log("command exited")
100 | case <-time.After(4 * time.Hour):
101 | t.Log("test complete")
102 | }
103 | live.Close()
104 | }
105 |
106 | // func TestRankList(t *testing.T) {
107 | // tiktok := gotiktoklive.NewTikTok()
108 | // live, err := tiktok.TrackUser(USERNAME)
109 | // if err != nil {
110 | // t.Fatal(err)
111 | // }
112 | //
113 | // tiktok.LogRequests = true
114 | // rankList, err := live.GetRankList()
115 | // if err != nil {
116 | // t.Fatal(err)
117 | // }
118 | //
119 | // if len(rankList.Ranks) == 0 {
120 | // t.Fatal("No ranked users found")
121 | // }
122 | //
123 | // topUser := rankList.Ranks[0]
124 | // t.Logf("Top user (%s) has donated %d coins", topUser.User.Nickname, topUser.Score)
125 | // }
126 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
2 | github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5 | github.com/erni27/imcache v1.2.1 h1:hDPesOxGMO8tV+wAUVsC2KVPB3BPjXS2xP+PgdevbRc=
6 | github.com/erni27/imcache v1.2.1/go.mod h1:KNUCBr1U9nOFTyUEC9CsMKZE33NboYTPavsQsuEufoA=
7 | github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
8 | github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
9 | github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
10 | github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
11 | github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA=
12 | github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0=
13 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
14 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
15 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
16 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
17 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
18 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
19 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
20 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
21 | go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
22 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
23 | go.uber.org/ratelimit v0.3.1 h1:K4qVE+byfv/B3tC+4nYWP7v/6SimcO7HzHekoMNBma0=
24 | go.uber.org/ratelimit v0.3.1/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk=
25 | golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
26 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
27 | golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
28 | golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
29 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
30 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
31 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
32 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
33 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
34 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
35 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
36 |
--------------------------------------------------------------------------------
/http_proxy.go:
--------------------------------------------------------------------------------
1 | package gotiktoklive
2 |
3 | // Copied from: https://gist.github.com/jim3ma/3750675f141669ac4702bc9deaf31c6b
4 |
5 | import (
6 | "bufio"
7 | "crypto/tls"
8 | "fmt"
9 | "net"
10 | "net/http"
11 | "net/url"
12 |
13 | "golang.org/x/net/proxy"
14 | )
15 |
16 | type direct struct{}
17 |
18 | // Direct is a direct proxy: one that makes network connections directly.
19 | var Direct = direct{}
20 |
21 | func (direct) Dial(network, addr string) (net.Conn, error) {
22 | return net.Dial(network, addr)
23 | }
24 |
25 | // httpsDialer
26 | type httpsDialer struct{}
27 |
28 | // HTTPSDialer is a https proxy: one that makes network connections on tls.
29 | var HttpsDialer = httpsDialer{}
30 | var TlsConfig = &tls.Config{}
31 |
32 | func (d httpsDialer) Dial(network, addr string) (c net.Conn, err error) {
33 | c, err = tls.Dial("tcp", addr, TlsConfig)
34 | if err != nil {
35 | fmt.Println(err)
36 | }
37 | return
38 | }
39 |
40 | // httpProxy is a HTTP/HTTPS connect proxy.
41 | type httpProxy struct {
42 | host string
43 | haveAuth bool
44 | username string
45 | password string
46 | forward proxy.Dialer
47 | }
48 |
49 | func newHTTPProxy(uri *url.URL, forward proxy.Dialer) (proxy.Dialer, error) {
50 | s := new(httpProxy)
51 | s.host = uri.Host
52 | s.forward = forward
53 | if uri.User != nil {
54 | s.haveAuth = true
55 | s.username = uri.User.Username()
56 | s.password, _ = uri.User.Password()
57 | }
58 |
59 | return s, nil
60 | }
61 |
62 | func (s *httpProxy) Dial(network, addr string) (net.Conn, error) {
63 | // Dial and create the https client connection.
64 | c, err := s.forward.Dial("tcp", s.host)
65 | if err != nil {
66 | return nil, err
67 | }
68 |
69 | // HACK. http.ReadRequest also does this.
70 | reqURL, err := url.Parse("http://" + addr)
71 | if err != nil {
72 | c.Close()
73 | return nil, err
74 | }
75 | reqURL.Scheme = ""
76 |
77 | req, err := http.NewRequest("CONNECT", reqURL.String(), nil)
78 | if err != nil {
79 | c.Close()
80 | return nil, err
81 | }
82 | req.Close = false
83 | if s.haveAuth {
84 | req.SetBasicAuth(s.username, s.password)
85 | }
86 | // req.Header.Set("User-Agent", "Powerby Gota")
87 |
88 | err = req.Write(c)
89 | if err != nil {
90 | c.Close()
91 | return nil, err
92 | }
93 |
94 | resp, err := http.ReadResponse(bufio.NewReader(c), req)
95 | if err != nil {
96 | // TODO close resp body ?
97 | resp.Body.Close()
98 | c.Close()
99 | return nil, err
100 | }
101 | resp.Body.Close()
102 | if resp.StatusCode != 200 {
103 | c.Close()
104 | err = fmt.Errorf("Connect server using proxy error, StatusCode [%d]", resp.StatusCode)
105 | return nil, err
106 | }
107 |
108 | return c, nil
109 | }
110 |
111 | func FromURL(u *url.URL, forward proxy.Dialer) (proxy.Dialer, error) {
112 | return proxy.FromURL(u, forward)
113 | }
114 |
115 | func FromEnvironment() proxy.Dialer {
116 | return proxy.FromEnvironment()
117 | }
118 |
119 | func init() {
120 | proxy.RegisterDialerType("http", newHTTPProxy)
121 | proxy.RegisterDialerType("https", newHTTPProxy)
122 | }
123 |
--------------------------------------------------------------------------------
/const.go:
--------------------------------------------------------------------------------
1 | package gotiktoklive
2 |
3 | import (
4 | "errors"
5 | "regexp"
6 | )
7 |
8 | const (
9 | // Base URL
10 | tiktokBaseUrl = "https://www.tiktok.com/"
11 | tiktokAPIUrl = "https://webcast.tiktok.com/webcast/"
12 |
13 | // Endpoints
14 | urlLive = "live/"
15 | urlFeed = "feed/"
16 | urlRankList = "ranklist/online_audience/"
17 | urlPriceList = "wallet_api/fs/diamond"
18 | urlUser = "@%s/"
19 | // Think this changed to room/enter/
20 | urlRoomInfo = "room/info/"
21 | urlRoomData = "webcast/fetch/"
22 | urlGiftInfo = "gift/list/"
23 | // added slash is intention to simplify the base signer url which is now configurable.
24 | urlSignReq = "/webcast/fetch/"
25 |
26 | urlCheckLive = "room/check_alive/"
27 | clientNameDefault = "gotiktok_live"
28 | apiKeyDefault = ""
29 | // For some reason the user info needs user agent this way but wsUserAgent doesn't want it
30 | userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36"
31 | wsUserAgent = "5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36"
32 | referer = "https://www.tiktok.com/"
33 | // webcast backend will fail if origin is an ending /
34 | origin = "https://www.tiktok.com"
35 | )
36 |
37 | var (
38 | // Used for webcast messages that don't require anything fancy
39 | minGetParams = map[string]string{
40 | "aid": "1988",
41 | "app_language": "en-US",
42 | "app_name": "tiktok_web",
43 | }
44 | defaultGETParams = map[string]string{
45 | "aid": "1988",
46 | "app_language": "en-US",
47 | "app_name": "tiktok_web",
48 | "browser_language": "en",
49 | "browser_name": "Mozilla",
50 | "browser_online": "true",
51 | "browser_platform": "Win32",
52 | "browser_version": userAgent,
53 | "cookie_enabled": "true",
54 | "cursor": "",
55 | "internal_ext": "",
56 | "device_platform": "web",
57 | "focus_state": "true",
58 | "from_page": "user",
59 | "history_len": "4",
60 | "is_fullscreen": "false",
61 | "is_page_visible": "true",
62 | "did_rule": "3",
63 | "fetch_rule": "1",
64 | "last_rtt": "0",
65 | "live_id": "12",
66 | "resp_content_type": "protobuf",
67 | "screen_height": "1152",
68 | "screen_width": "2048",
69 | "tz_name": "Europe/Berlin",
70 | "referer": referer,
71 | "root_referer": origin,
72 | "msToken": "",
73 | "version_code": "180800",
74 | "webcast_sdk_version": "1.3.0",
75 | "update_version_code": "1.3.0",
76 | }
77 | defaultRequestHeaders = map[string]string{
78 | "Connection": "keep-alive",
79 | "Cache-control": "max-age=0",
80 | "User-Agent": userAgent,
81 | "Accept": "text/html,application/json,application/protobuf",
82 | "Referer": referer,
83 | "Origin": origin,
84 | // clientId: = "ttlive-golang"
85 | "Accept-Language": "en-US,en;q=0.9",
86 | "Accept-Encoding": "gzip, deflate",
87 | }
88 | wsOverrideHeaders = map[string]string{
89 | "User-Agent": wsUserAgent,
90 | }
91 | reJsonData = []*regexp.Regexp{
92 | regexp.MustCompile(``),
93 | regexp.MustCompile(`