kittenbark/tg — examples

tg-twitter

Full source code is available at https://github.com/kittenbark/tg-twitter.

We'll make a simple Twitter/X media posts downloader, you'll learn how to work with media & simple routing step-by-step. Init project, get dependencies: mkdir tg-twitter --> go mod init tg-twitter --> go get github.com/kittenbark/tg@latest

Your go.mod would look something like this:

module tg-twitter

go 1.24.0

require github.com/kittenbark/tg v0.0.0-20250406125453-06755b99f921

Let's make the basis of our bot, add a scheduler to ensure no 429, add a logging for errors. And let's make sure, users a gently greeted when they /start the bot.

package main

import (
	"github.com/kittenbark/tg"
)

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

Now we need to map urls sent by users to our download logic, first and foremost you could use tg.OnUrl, but we want to allow only Twitter/X urls, so we'll use both tg.OnURL internal logic, and we'll check the link source.

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

To ensure the give url is valid, in the bot declaration, we'll use both filters:

package main

import (
	"context"
	"github.com/kittenbark/tg"
	"strings"
)

func main() {
	tg.NewFromEnv().
		Scheduler().
		OnError(tg.OnErrorLog).
		Command("/start", tg.CommonReactionReply("💅")).
		Branch(tg.All(tg.OnUrl, OnTwitterURL), tg.CommonTextReply("not implemented for now")).
		Branch(tg.OnText, tg.CommonTextReply("This is not a valid URL :(")). // Unmatched link.
		Start()
}

Now we want to start actually downloading media, let's rely on

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

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

	// Classic download-upload cycle.
	for _, mediaURL := range post.MediaURLs {
		if !strings.HasSuffix(mediaURL, "?name=large") {
			mediaURL += "?name=large"
		}
		queryless, _, _ := strings.Cut(mediaURL, "?")
		split := strings.Split(queryless, "/")
		filename := split[len(split)-1]
		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
}

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

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

	var post VxPost
	if err := json.NewDecoder(resp.Body).Decode(&post); err != nil {
		return nil, err
	}
	return &post, nil
}

// I recommend you to use https://github.com/cavaliergopher/grab, but usually default Go is good enough.
func DownloadFile(ctx context.Context, url string, filename string) error {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return err
	}

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

	output, err := os.Create(filename)
	if err != nil {
		return err
	}
	defer output.Close()

	_, err = io.Copy(output, resp.Body)
	return err
}

// VxPost is cut down for this example, bc we care only about urls in this case.
type VxPost struct {
	MediaURLs []string `json:"mediaURLs"`
}

var (
	// Let's be respectful to vxtwitter's free API and do not request them too often.
	vxSync = &sync.Mutex{}
)

Aaaand let's update routing and dependencies:

package main

import (
	"context"
	"encoding/json"
	"github.com/kittenbark/tg"
	"io"
	"net/http"
	"os"
	"strings"
	"sync"
	"time"
)

func main() {
	tg.NewFromEnv().
		Scheduler().
		OnError(tg.OnErrorLog).
		Command("/start", tg.CommonReactionReply("💅")).
		Branch(tg.All(tg.OnUrl, OnTwitterURL), HandleTwitterURL).
		Branch(tg.OnText, tg.CommonTextReply("This is not a valid URL :(")).
		Start()
}

You won.