kittenbark/tg — examples

Twitter/X media downloader

Full source: github.com/kittenbark/tg-twitter

A bot that receives a Twitter/X link and replies with the media files. Good walkthrough for routing, HTTP, and file upload.

mkdir tg-twitter && cd tg-twitter
go mod init tg-twitter
go get github.com/kittenbark/tg@latest

Step 1 — skeleton

Start with the bare minimum: scheduler to avoid 429s, error logging, a /start greeting.

package main

import "github.com/kittenbark/tg"

func main() {
    tg.NewFromEnv().
        Scheduler().
        OnError(tg.OnErrorLog).
        Command("/start", tg.CommonReactionReply("💅")).
        Start()
}

Step 2 — routing

We want to handle Twitter/X URLs specifically, so we compose tg.OnUrl (checks for any URL) with our own domain check:

func OnTwitterURL(ctx context.Context, upd *tg.Update) bool {
    for _, prefix := range []string{"https://twitter.com", "https://x.com", "https://vxtwitter.com"} {
        if strings.HasPrefix(upd.Message.Text, prefix) {
            return true
        }
    }
    return false
}

Wire it into the pipeline. Order matters — the Twitter branch runs first, the catch-all text branch handles everything else:

func main() {
    tg.NewFromEnv().
        Scheduler().
        OnError(tg.OnErrorLog).
        Command("/start", tg.CommonReactionReply("💅")).
        Branch(tg.All(tg.OnUrl, OnTwitterURL), tg.CommonTextReply("not implemented yet")).
        Branch(tg.OnText, tg.CommonTextReply("send me a Twitter/X link")).
        Start()
}

Step 3 — download and send

Replace the placeholder with a real handler. Classic pattern: fetch metadata → download each file → upload to Telegram → clean up.

func HandleTwitterURL(ctx context.Context, upd *tg.Update) error {
    msg := upd.Message

    _, urlData, _ := strings.Cut(msg.Text, ".com")
    post, err := fetchVxPost(ctx, "https://api.vxtwitter.com"+urlData)
    if err != nil {
        return err
    }

    for _, mediaURL := range post.MediaURLs {
        if !strings.HasSuffix(mediaURL, "?name=large") {
            mediaURL += "?name=large"
        }
        queryless, _, _ := strings.Cut(mediaURL, "?")
        filename := path.Base(queryless)

        if err := downloadFile(ctx, mediaURL, filename); err != nil {
            return err
        }
        defer os.Remove(filename)

        if _, err := tg.SendDocument(ctx, msg.Chat.Id, tg.FromDisk(filename)); err != nil {
            return err
        }
    }
    return nil
}

The helpers — keep them simple:

type VxPost struct {
    MediaURLs []string `json:"mediaURLs"`
}

// Rate-limit vxtwitter requests — one at a time, 5s cooldown.
var vxMu sync.Mutex

func fetchVxPost(ctx context.Context, url string) (*VxPost, error) {
    vxMu.Lock()
    defer time.AfterFunc(5*time.Second, vxMu.Unlock)

    req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    var post VxPost
    return &post, json.NewDecoder(resp.Body).Decode(&post)
}

func downloadFile(ctx context.Context, url, filename string) error {
    req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    f, err := os.Create(filename)
    if err != nil {
        return err
    }
    defer f.Close()
    _, err = io.Copy(f, resp)
    return err
}

Step 4 — final wiring

package main

import (
    "context"
    "encoding/json"
    "io"
    "net/http"
    "net/url"
    "os"
    "path"
    "strings"
    "sync"
    "time"

    "github.com/kittenbark/tg"
)

func main() {
    tg.NewFromEnv().
        Scheduler().
        OnError(tg.OnErrorLog).
        Command("/start", tg.CommonReactionReply("💅")).
        Branch(tg.All(tg.OnUrl, OnTwitterURL), HandleTwitterURL).
        Branch(tg.OnText, tg.CommonTextReply("send me a Twitter/X link")).
        Start()
}

You won.