Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/golangci-lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ jobs:
with:
version: v2.1
args: --build-tags=lint
install-mode: goinstall # apparently needed since prebuilt doesn't support go1.26 yet
# skip cache to avoid flakes (and avoid using gh-action storage)
skip-cache: true
skip-save-cache: true
8 changes: 4 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM golang:1.25-alpine AS builder
FROM golang:1.26-alpine AS builder

ENV GOCACHE=/root/.cache/go-build

Expand All @@ -8,7 +8,7 @@ RUN --mount=type=cache,target=/var/cache/apk,sharing=locked \
--repository="https://dl-cdn.alpinelinux.org/alpine/edge/main" \
--repository="https://dl-cdn.alpinelinux.org/alpine/edge/community" \
"build-base=0.5-r3" \
"libheif-dev=1.20.2-r1"
"libheif-dev=1.21.2-r1"

WORKDIR /app

Expand Down Expand Up @@ -38,8 +38,8 @@ RUN --mount=type=cache,target=/var/cache/apk,sharing=locked \
apk add --no-cache \
--repository="https://dl-cdn.alpinelinux.org/alpine/edge/main" \
--repository="https://dl-cdn.alpinelinux.org/alpine/edge/community" \
"ffmpeg=8.0.1-r0" \
"libheif=1.20.2-r1"
"ffmpeg=8.0.1-r1" \
"libheif=1.21.2-r1"

COPY --from=builder /app/govd ./govd

Expand Down
6 changes: 3 additions & 3 deletions Dockerfile.dev
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
FROM golang:1.25-alpine
FROM golang:1.26-alpine

RUN --mount=type=cache,target=/var/cache/apk,sharing=locked \
--mount=type=cache,target=/var/lib/apk,sharing=locked \
apk add --no-cache \
--repository="https://dl-cdn.alpinelinux.org/alpine/edge/main" \
--repository="https://dl-cdn.alpinelinux.org/alpine/edge/community" \
"build-base=0.5-r3" \
"libheif-dev=1.20.2-r1" \
"ffmpeg=8.0.1-r0"
"libheif-dev=1.21.2-r1" \
"ffmpeg=8.0.1-r1"

WORKDIR /app

Expand Down
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
module github.com/govdbot/govd

go 1.24.0
go 1.26

toolchain go1.24.2
toolchain go1.26.0

require (
github.com/BurntSushi/toml v1.5.0
Expand All @@ -20,7 +20,7 @@ require (
github.com/nicksnyder/go-i18n/v2 v2.6.0
github.com/pressly/goose/v3 v3.24.3
github.com/prometheus/client_golang v1.23.2
github.com/strukturag/libheif v1.20.2
github.com/strukturag/libheif v1.21.2
github.com/sunfish-shogi/bufseekio v0.1.0
github.com/titanous/json5 v1.0.0
github.com/u2takey/ffmpeg-go v0.5.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/strukturag/libheif v1.20.2 h1:6te1PczCHlF//Uc9E5xb/mXb5+y67vrwssKwLS0ng30=
github.com/strukturag/libheif v1.20.2/go.mod h1:E/PNRlmVtrtj9j2AvBZlrO4dsBDu6KfwDZn7X1Ce8Ks=
github.com/strukturag/libheif v1.21.2 h1:YFD3crf+d33cFVQh3aTkkVGwJFyWpfqVT4XhzHWU6mA=
github.com/strukturag/libheif v1.21.2/go.mod h1:E/PNRlmVtrtj9j2AvBZlrO4dsBDu6KfwDZn7X1Ce8Ks=
github.com/sunfish-shogi/bufseekio v0.0.0-20210207115823-a4185644b365/go.mod h1:dEzdXgvImkQ3WLI+0KQpmEx8T/C/ma9KeS3AfmU899I=
github.com/sunfish-shogi/bufseekio v0.1.0 h1:zu38kFbv0KuuiwZQeuYeS02U9AM14j0pVA9xkHOCJ2A=
github.com/sunfish-shogi/bufseekio v0.1.0/go.mod h1:dEzdXgvImkQ3WLI+0KQpmEx8T/C/ma9KeS3AfmU899I=
Expand Down
2 changes: 1 addition & 1 deletion internal/extractors/instagram/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ func GetPostFromIGram(ctx *models.ExtractorContext) (*IGramResponse, error) {
}

headers := map[string]string{
"Content-Type": "application/x-www-form-urlencoded",
"Content-Type": "application/json",
}
maps.Copy(headers, igramHeaders)

Expand Down
116 changes: 78 additions & 38 deletions internal/extractors/instagram/util.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package instagram

import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"maps"
"math/big"
"net/http"
"net/url"
Expand All @@ -29,9 +29,10 @@ const (
graphQLEndpoint = "https://www.instagram.com/graphql/query/"
polarisAction = "PolarisPostActionLoadPostQueryQuery"

igramHostname = "api-wh.igram.world"
igramKey = "241c28282e4ce419ce73ca61555a5a0c7faf887c5ccf9305c55484f701ba883a"
igramTimestamp = "1766415734394"
igramHostname = "api-wh.igram.world"
igramAPIBase = "api.igram.world"
igramHMACKey = "75f2d70d3724f98e4a7d1ffd0ba9cfd907f3ae2632ee159980e2c521bff62358"
igramStaticTS = 1771418815381 // parseInt("mls10xp1", 36)
)

var (
Expand Down Expand Up @@ -157,57 +158,96 @@ func ParseEmbedGQL(body []byte) (*Media, error) {
}

func IGramBodyFromURL(contentURL string) (io.Reader, error) {
timestamp := strconv.FormatInt(time.Now().UnixMilli(), 10)
return igramBuildPayload(map[string]string{
"target_url": contentURL,
})
}

hash := sha256.New()
_, err := io.WriteString(hash, contentURL+timestamp+igramKey)
if err != nil {
return nil, fmt.Errorf("error writing to SHA256 hash: %w", err)
}
func IGramBodyFromParams(params map[string]string) (io.Reader, error) {
return igramBuildPayload(params)
}

secretBytes := hash.Sum(nil)
secretString := hex.EncodeToString(secretBytes)
secretString = strings.ToLower(secretString)
func igramBuildPayload(urlParams map[string]string) (io.Reader, error) {
nowMs := time.Now().UnixMilli()
serverMs := getIGramServerTime()

payload := url.Values{}
payload.Set("sf_url", contentURL)
payload.Set("ts", timestamp)
payload.Set("_ts", igramTimestamp)
payload.Set("_tsc", "0") // ?
payload.Set("_s", secretString)
drift := serverMs - nowMs
var correction int64
if drift >= 60000 || drift <= -60000 {
correction = drift
}
ts := nowMs + correction

return strings.NewReader(payload.Encode()), nil
}
// partial payload fields that get signed
partial := map[string]any{
"_sc": 0,
"_ef": 0,
"_df": 0,
}
for k, v := range urlParams {
partial[k] = v
}

func IGramBodyFromParams(params map[string]string) (io.Reader, error) {
timestamp := strconv.FormatInt(time.Now().UnixMilli(), 10)
sig, err := igramSign(partial, ts)
if err != nil {
return nil, err
}

paramsStr, err := sonic.ConfigFastest.Marshal(params)
// assemble final payload
final := make(map[string]any, len(partial)+5)
for k, v := range partial {
final[k] = v
}
final["ts"] = ts
final["_ts"] = igramStaticTS
final["_tsc"] = correction
final["_sv"] = 2
final["_s"] = sig

jsonBytes, err := sonic.ConfigFastest.Marshal(final)
if err != nil {
return nil, fmt.Errorf("failed to marshal payload: %w", err)
}

hash := sha256.New()
_, err = io.WriteString(hash, string(paramsStr)+timestamp+igramKey)
return strings.NewReader(string(jsonBytes)), nil
}

func igramSign(partial map[string]any, ts int64) (string, error) {
// sonic.ConfigStd sorts map keys alphabetically, matching
// the signing: JSON.stringify(sorted_partial) + String(ts)
jsonBytes, err := sonic.ConfigStd.Marshal(partial)
if err != nil {
return nil, fmt.Errorf("error writing to SHA256 hash: %w", err)
return "", fmt.Errorf("failed to marshal partial payload: %w", err)
}

secretBytes := hash.Sum(nil)
secretString := hex.EncodeToString(secretBytes)
secretString = strings.ToLower(secretString)
data := string(jsonBytes) + strconv.FormatInt(ts, 10)

data := map[string]string{
"ts": timestamp,
"_ts": igramTimestamp,
"_tsc": "0", // ?
"_s": secretString,
keyBytes, err := hex.DecodeString(igramHMACKey)
if err != nil {
return "", fmt.Errorf("failed to decode HMAC key: %w", err)
}
maps.Copy(data, params)

parsedData, _ := sonic.ConfigFastest.Marshal(data)
mac := hmac.New(sha256.New, keyBytes)
mac.Write([]byte(data))
return hex.EncodeToString(mac.Sum(nil)), nil
}

func getIGramServerTime() int64 {
apiURL := fmt.Sprintf("https://%s/msec", igramAPIBase)
resp, err := http.Get(apiURL)
if err != nil {
return time.Now().UnixMilli()
}
defer resp.Body.Close()

return strings.NewReader(string(parsedData)), nil
var result struct {
Msec float64 `json:"msec"`
}
decoder := sonic.ConfigFastest.NewDecoder(resp.Body)
if err := decoder.Decode(&result); err != nil {
return time.Now().UnixMilli()
}
return int64(result.Msec * 1000)
}

func ParseIGramResponse(body []byte) (*IGramResponse, error) {
Expand Down
Loading