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.