feat: add support for gzip & brotli

This commit is contained in:
ayn2op 2025-09-22 19:22:13 -04:00
parent 2ae53a22aa
commit ac669bccae
9 changed files with 172 additions and 133 deletions

View file

@ -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)

1
go.mod
View file

@ -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

2
go.sum
View file

@ -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=

View file

@ -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 {

17
internal/http/client.go Normal file
View file

@ -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)
}

28
internal/http/headers.go Normal file
View file

@ -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
}

65
internal/http/props.go Normal file
View file

@ -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
}

View file

@ -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
}

View file

@ -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