├── 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(`