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.