diff --git a/cmd/state.go b/cmd/state.go index 24dd2a7..bcf083a 100644 --- a/cmd/state.go +++ b/cmd/state.go @@ -2,16 +2,17 @@ package cmd import ( "context" - "encoding/base64" - "encoding/json" "log/slog" - "net/http" - "github.com/ayn2op/discordo/internal/consts" + "github.com/ayn2op/discordo/internal/http" "github.com/ayn2op/discordo/internal/notifications" "github.com/ayn2op/tview" "github.com/diamondburned/arikawa/v3/api" "github.com/diamondburned/arikawa/v3/gateway" + "github.com/diamondburned/arikawa/v3/session" + "github.com/diamondburned/arikawa/v3/state" + "github.com/diamondburned/arikawa/v3/state/store/defaultstore" + "github.com/diamondburned/arikawa/v3/utils/handler" "github.com/diamondburned/arikawa/v3/utils/httputil" "github.com/diamondburned/arikawa/v3/utils/httputil/httpdriver" "github.com/diamondburned/arikawa/v3/utils/ws" @@ -20,19 +21,20 @@ import ( ) func openState(token string) error { - props := consts.GetIdentifyProps() - if browserUserAgent, ok := props["browser_user_agent"]; ok { - if val, ok := browserUserAgent.(string); ok { - api.UserAgent = val - } - } + identifyProps := http.IdentifyProperties() - gateway.DefaultIdentity = props + api.UserAgent = http.BrowserUserAgent + gateway.DefaultIdentity = identifyProps gateway.DefaultPresence = &gateway.UpdatePresenceCommand{ Status: app.cfg.Status, } - discordState = ningen.New(token) + id := gateway.DefaultIdentifier(token) + id.Compress = false + + session := session.NewCustom(id, http.NewClient(token), handler.New()) + state := state.NewFromSession(session, defaultstore.New()) + discordState = ningen.FromState(state) // Handlers discordState.AddHandler(onRaw) @@ -54,44 +56,10 @@ func openState(token string) error { slog.Error("state log", "err", err) } - discordState.OnRequest = append(discordState.OnRequest, httputil.WithHeaders(getHeaders(props)), onRequest) + discordState.OnRequest = append(discordState.OnRequest, httputil.WithHeaders(http.Headers()), onRequest) return discordState.Open(context.TODO()) } -func getHeaders(props gateway.IdentifyProperties) http.Header { - header := make(http.Header) - - // These properties are only sent when identifying with the gateway and are not included in the X-Super-Properties header. - delete(props, "is_fast_connect") - delete(props, "gateway_connect_reasons") - - if rawProps, err := json.Marshal(props); err == nil { - propsHeader := base64.StdEncoding.EncodeToString(rawProps) - header.Set("X-Super-Properties", propsHeader) - } - - if systemLocale, ok := props["system_locale"]; ok { - if val, ok := systemLocale.(string); ok { - header.Set("X-Discord-Locale", string(val)) - } - } - - header.Set("X-Debug-Options", "bugReporterEnabled") - - // https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers - header.Set("Accept", "*/*") - header.Set("Accept-Encoding", "gzip, deflate, br, zstd") - header.Set("Accept-Language", "en-US,en;q=0.9") - header.Set("Origin", "https://discord.com") - header.Set("Priority", "u=0, i") - header.Set("Referer", "https://discord.com/channels/@me") - header.Set("Sec-Fetch-Dest", "empty") - header.Set("Sec-Fetch-Mode", "cors") - header.Set("Sec-Fetch-Site", "same-origin") - - return header -} - func onRequest(r httpdriver.Request) error { if req, ok := r.(*httpdriver.DefaultRequest); ok { slog.Debug("new HTTP request", "method", req.Method, "url", req.URL) diff --git a/go.mod b/go.mod index 5b00732..0511086 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.25.0 require ( github.com/BurntSushi/toml v1.5.0 + github.com/andybalholm/brotli v1.2.0 github.com/ayn2op/tview v0.0.0-20250720032506-63f04e47b15d github.com/deckarep/gosx-notifier v0.0.0-20180201035817-e127226297fb github.com/diamondburned/arikawa/v3 v3.6.0 diff --git a/go.sum b/go.sum index 92a5c77..46a7815 100644 --- a/go.sum +++ b/go.sum @@ -25,6 +25,8 @@ github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/akavel/rsrc v0.10.2 h1:Zxm8V5eI1hW4gGaYsJQUhxpjkENuG91ki8B4zCrvEsw= github.com/akavel/rsrc v0.10.2/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= +github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= +github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/ayn2op/tview v0.0.0-20250720032506-63f04e47b15d h1:+Q2vA4jMu5/tU8uAyr7mqX63br1jWFEa8q/fZNPP96Y= github.com/ayn2op/tview v0.0.0-20250720032506-63f04e47b15d/go.mod h1:xqhotzuhTSxSudyNSHJfpD6S5C7+FFVv+JzJ8YaFNK4= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= diff --git a/internal/consts/consts.go b/internal/consts/consts.go index 53e60ad..34583cb 100644 --- a/internal/consts/consts.go +++ b/internal/consts/consts.go @@ -1,98 +1,13 @@ package consts import ( - "encoding/json" "log/slog" - "maps" - "net/http" "os" "path/filepath" - - "github.com/diamondburned/arikawa/v3/discord" - "github.com/diamondburned/arikawa/v3/gateway" - "github.com/google/uuid" ) const Name = "discordo" -const identifyPropertiesURL = "https://cordapi.dolfi.es/api/v2/properties/web" - -var defaultIdentifyProps = gateway.IdentifyProperties{ - gateway.IdentifyDevice: "", - - gateway.IdentifyBrowser: "Chrome", - "browser_version": "140.0.0.0", - "browser_user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36", - - gateway.IdentifyOS: "Windows", - "os_version": "10", - - "client_build_number": 439729, - "client_event_source": nil, - "client_launch_id": uuid.NewString(), - "client_app_state": "focused", - - "launch_signature": uuid.NewString(), - "system_locale": discord.EnglishUS, - "release_channel": "stable", - "has_client_mods": false, - "is_fast_connect": false, - "gateway_connect_reasons": "AppSkeleton", - - "referrer": "", - "referrer_current": "", - "referring_domain": "", - "referring_domain_current": "", -} - -type Properties struct { - Client struct { - Type string `json:"type"` - BuildNumber int `json:"build_number"` - BuildHash string `json:"build_hash"` - ReleaseChannel string `json:"release_channel"` - } `json:"client"` - - Browser struct { - Type string `json:"type"` - UserAgent string `json:"user_agent"` - Version string `json:"version"` - OS struct { - Type string `json:"type"` - Version string `json:"version"` - } `json:"os"` - } `json:"browser"` -} - -func GetIdentifyProps() gateway.IdentifyProperties { - resp, err := http.Get(identifyPropertiesURL) - if err != nil { - return defaultIdentifyProps - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return defaultIdentifyProps - } - - var props Properties - if err := json.NewDecoder(resp.Body).Decode(&props); err != nil { - return defaultIdentifyProps - } - - p := maps.Clone(defaultIdentifyProps) - p[gateway.IdentifyBrowser] = props.Browser.Type - p["browser_version"] = props.Browser.Version - p["browser_user_agent"] = props.Browser.UserAgent - - p[gateway.IdentifyOS] = props.Browser.OS.Type - p["os_version"] = props.Browser.OS.Version - - p["release_channel"] = props.Client.ReleaseChannel - p["client_build_number"] = props.Client.BuildNumber - return p -} - var cacheDir string func CacheDir() string { diff --git a/internal/http/client.go b/internal/http/client.go new file mode 100644 index 0000000..dd11239 --- /dev/null +++ b/internal/http/client.go @@ -0,0 +1,17 @@ +package http + +import ( + "net/http" + + "github.com/diamondburned/arikawa/v3/api" + "github.com/diamondburned/arikawa/v3/utils/httputil" + "github.com/diamondburned/arikawa/v3/utils/httputil/httpdriver" +) + +func NewClient(token string) *api.Client { + httpClient := httputil.NewClient() + stdClient := http.DefaultClient + stdClient.Transport = NewTransport() + httpClient.Client = httpdriver.WrapClient(*stdClient) + return api.NewCustomClient(token, httpClient) +} diff --git a/internal/http/headers.go b/internal/http/headers.go new file mode 100644 index 0000000..4c9d9df --- /dev/null +++ b/internal/http/headers.go @@ -0,0 +1,28 @@ +package http + +import ( + stdHttp "net/http" +) + +func Headers() stdHttp.Header { + headers := make(stdHttp.Header) + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers + headers.Set("Accept", "*/*") + headers.Set("Accept-Encoding", "gzip, deflate, br, zstd") + headers.Set("Accept-Language", "en-US,en;q=0.9") + headers.Set("Origin", "https://discord.com") + headers.Set("Priority", "u=0, i") + headers.Set("Referer", "https://discord.com/channels/@me") + headers.Set("Sec-Fetch-Dest", "empty") + headers.Set("Sec-Fetch-Mode", "cors") + headers.Set("Sec-Fetch-Site", "same-origin") + + headers.Set("X-Debug-Options", "bugReporterEnabled") + headers.Set("X-Discord-Locale", string(Locale)) + + if superProps, err := superProps(); err == nil { + headers.Set("X-Super-Properties", superProps) + } + + return headers +} diff --git a/internal/http/props.go b/internal/http/props.go new file mode 100644 index 0000000..b2c12c7 --- /dev/null +++ b/internal/http/props.go @@ -0,0 +1,65 @@ +package http + +import ( + "encoding/base64" + "encoding/json" + + "github.com/diamondburned/arikawa/v3/discord" + "github.com/diamondburned/arikawa/v3/gateway" + "github.com/google/uuid" +) + +const ( + Browser = "Chrome" + BrowserVersion = "140.0.0.0" + BrowserUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36" +) + +var ( + Locale = discord.EnglishUS +) + +func IdentifyProperties() gateway.IdentifyProperties { + return gateway.IdentifyProperties{ + gateway.IdentifyDevice: "", + + gateway.IdentifyOS: "Windows", + "os_version": "10", + + gateway.IdentifyBrowser: Browser, + "browser_version": BrowserVersion, + "browser_user_agent": BrowserUserAgent, + + "client_build_number": 447677, + "client_event_source": nil, + "client_launch_id": uuid.NewString(), + "client_app_state": "focused", + + "launch_signature": uuid.NewString(), + "system_locale": Locale, + "release_channel": "stable", + "has_client_mods": false, + + "referrer": "", + "referrer_current": "", + "referring_domain": "", + "referring_domain_current": "", + + // These properties are only sent when identifying with the gateway and are not included in the X-Super-Properties header. + "is_fast_connect": false, + "gateway_connect_reasons": "AppSkeleton", + } +} + +func superProps() (string, error) { + props := IdentifyProperties() + delete(props, "is_fast_connect") + delete(props, "gateway_connect_reasons") + + raw, err := json.Marshal(props) + if err != nil { + return "", err + } + + return base64.StdEncoding.EncodeToString(raw), nil +} diff --git a/internal/http/transport.go b/internal/http/transport.go new file mode 100644 index 0000000..ee5604b --- /dev/null +++ b/internal/http/transport.go @@ -0,0 +1,42 @@ +package http + +import ( + "compress/gzip" + "io" + "net/http" + + "github.com/andybalholm/brotli" +) + +type Transport struct { + base http.RoundTripper +} + +func NewTransport() *Transport { + return &Transport{ + base: http.DefaultTransport, + } +} + +func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { + resp, err := t.base.RoundTrip(req) + if err != nil { + return nil, err + } + + switch resp.Header.Get("Content-Encoding") { + case "gzip": + resp.Body, err = gzip.NewReader(resp.Body) + if err != nil { + resp.Body.Close() + return nil, err + } + + resp.Header.Del("Content-Encoding") + case "br": + resp.Body = io.NopCloser(brotli.NewReader(resp.Body)) + resp.Header.Del("Content-Encoding") + } + + return resp, nil +} diff --git a/internal/login/form.go b/internal/login/form.go index 4856100..0b3888d 100644 --- a/internal/login/form.go +++ b/internal/login/form.go @@ -6,6 +6,7 @@ import ( "github.com/ayn2op/discordo/internal/config" "github.com/ayn2op/discordo/internal/consts" + "github.com/ayn2op/discordo/internal/http" "github.com/ayn2op/discordo/internal/ui" "github.com/ayn2op/tview" "github.com/diamondburned/arikawa/v3/api" @@ -52,7 +53,7 @@ func (f *Form) login() { // Create an API client without an authentication token. client := api.NewClient("") - props := consts.GetIdentifyProps() + props := http.IdentifyProperties() if browserUserAgent, ok := props["browser_user_agent"]; ok { if val, ok := browserUserAgent.(string); ok { api.UserAgent = val