Skip to content

Commit

Permalink
telegram-bot-api with custom DNS, v0.1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
i36lib committed Jun 12, 2024
1 parent 4126fa6 commit 92f4a97
Show file tree
Hide file tree
Showing 5 changed files with 241 additions and 12 deletions.
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Golang bindings for the Telegram Bot API

[![Go Reference](https://pkg.go.dev/badge/github.com/go-telegram-bot-api/telegram-bot-api/v5.svg)](https://pkg.go.dev/github.com/go-telegram-bot-api/telegram-bot-api/v5)
[![Test](https://github.com/go-telegram-bot-api/telegram-bot-api/actions/workflows/test.yml/badge.svg)](https://github.com/go-telegram-bot-api/telegram-bot-api/actions/workflows/test.yml)
[![Test](https://github.com/artlibs/telegram-bot-api/actions/workflows/test.yml/badge.svg)](https://github.com/artlibs/telegram-bot-api/actions/workflows/test.yml)

All methods are fairly self-explanatory, and reading the [godoc](https://pkg.go.dev/github.com/go-telegram-bot-api/telegram-bot-api/v5) page should
explain everything. If something isn't clear, open an issue or submit
Expand All @@ -20,7 +20,7 @@ you want to ask questions or discuss development.
## Example

First, ensure the library is installed and up to date by running
`go get -u github.com/go-telegram-bot-api/telegram-bot-api/v5`.
`go get -u github.com/artlibs/telegram-bot-api`.

This is a very simple bot that just displays any gotten updates,
then replies it to that chat.
Expand All @@ -31,11 +31,12 @@ package main
import (
"log"

tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
tgbotapi "github.com/artlibs/telegram-bot-api"
)

func main() {
bot, err := tgbotapi.NewBotAPI("MyAwesomeBotToken")
dnsServers := []string{"8.8.8.8:53",}
bot, err := tgbotapi.NewBotAPIWithDNS("MyAwesomeBotToken", dnsServers)
if err != nil {
log.Panic(err)
}
Expand Down Expand Up @@ -72,11 +73,12 @@ import (
"log"
"net/http"

"github.com/go-telegram-bot-api/telegram-bot-api/v5"
"github.com/artlibs/telegram-bot-api"
)

func main() {
bot, err := tgbotapi.NewBotAPI("MyAwesomeBotToken")
dnsServers := []string{"8.8.8.8:53",}
bot, err := tgbotapi.NewBotAPIWithDNS("MyAwesomeBotToken", dnsServers)
if err != nil {
log.Fatal(err)
}
Expand Down
70 changes: 66 additions & 4 deletions bot.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
package tgbotapi

import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"mime/multipart"
"net"
"net/http"
"net/url"
"strings"
Expand All @@ -32,33 +34,93 @@ type BotAPI struct {
apiEndpoint string
}

func NewHttpClient(dnsServers []string) *http.Client {
if len(dnsServers) == 0 {
return &http.Client{Timeout: 20 * time.Second}
}
dialer := &net.Dialer{
Timeout: 5 * time.Second,
KeepAlive: 10 * time.Second,
Resolver: &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
log.Printf("address: %s\n", address)
for _, server := range dnsServers {
log.Printf("try DNS: %s\n", server)
d := net.Dialer{Timeout: 5 * time.Second}
conn, err := d.DialContext(ctx, "udp", server)
if err == nil {
log.Printf("connected DNS: %s\n", server)
return conn, nil
}
log.Printf("connect DNS failed %s: %v\n", server, err)
}
return nil, fmt.Errorf("all DNS failed")
},
},
}

transport := &http.Transport{
DialContext: dialer.DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 30 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
}

return &http.Client{Transport: transport, Timeout: 20 * time.Second}
}

// NewBotAPI creates a new BotAPI instance.
//
// It requires a token, provided by @BotFather on Telegram.
func NewBotAPI(token string) (*BotAPI, error) {
return NewBotAPIWithClient(token, APIEndpoint, &http.Client{})
return NewBotAPIWithClient(token, APIEndpoint, NewHttpClient(nil))
}

// NewBotAPIWithDNS creates a new BotAPI instance.
//
// It requires a token, provided by @BotFather on Telegram.
func NewBotAPIWithDNS(token string, dnsServers []string) (*BotAPI, error) {
return NewBotAPIWithClient(token, APIEndpoint, NewHttpClient(dnsServers))
}

// NewBotAPIWithAPIEndpoint creates a new BotAPI instance
// and allows you to pass API endpoint.
//
// It requires a token, provided by @BotFather on Telegram and API endpoint.
func NewBotAPIWithAPIEndpoint(token, apiEndpoint string) (*BotAPI, error) {
return NewBotAPIWithClient(token, apiEndpoint, &http.Client{})
return NewBotAPIWithClient(token, apiEndpoint, NewHttpClient(nil))
}

// NewBotAPIWithAPIEndpointAndDNS creates a new BotAPI instance
// and allows you to pass API endpoint.
//
// It requires a token, provided by @BotFather on Telegram and API endpoint.
func NewBotAPIWithAPIEndpointAndDNS(token, apiEndpoint string, dnsServers []string) (*BotAPI, error) {
return NewBotAPIWithClient(token, apiEndpoint, NewHttpClient(dnsServers))
}

// NewBotAPIWithClient creates a new BotAPI instance
// and allows you to pass a http.Client.
//
// It requires a token, provided by @BotFather on Telegram and API endpoint.
func NewBotAPIWithClient(token, apiEndpoint string, client HTTPClient) (*BotAPI, error) {
ip, err := net.LookupHost("api.telegram.org")
if err == nil {
fmt.Printf("ip for api.telegram.org: %s\n", ip)
}
ip, err = net.LookupHost("core.telegram.org")
if err == nil {
fmt.Printf("ip for core.telegram.org: %s\n", ip)
}

bot := &BotAPI{
Token: token,
Client: client,
Buffer: 100,
shutdownChannel: make(chan interface{}),

apiEndpoint: apiEndpoint,
apiEndpoint: apiEndpoint,
}

self, err := bot.GetMe()
Expand Down
86 changes: 86 additions & 0 deletions dnscache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package tgbotapi

// Package tgbotapi caches DNS lookups

import (
"net"
"sync"
"time"
)

type Resolver struct {
lock sync.RWMutex
cache map[string][]net.IP
}

func New(refreshRate time.Duration) *Resolver {
resolver := &Resolver{
cache: make(map[string][]net.IP, 64),
}
if refreshRate > 0 {
go resolver.autoRefresh(refreshRate)
}
return resolver
}

func (r *Resolver) Fetch(address string) ([]net.IP, error) {
r.lock.RLock()
ips, exists := r.cache[address]
r.lock.RUnlock()
if exists {
return ips, nil
}

return r.Lookup(address)
}

func (r *Resolver) FetchOne(address string) (net.IP, error) {
ips, err := r.Fetch(address)
if err != nil || len(ips) == 0 {
return nil, err
}
return ips[0], nil
}

func (r *Resolver) FetchOneString(address string) (string, error) {
ip, err := r.FetchOne(address)
if err != nil || ip == nil {
return "", err
}
return ip.String(), nil
}

func (r *Resolver) Refresh() {
i := 0
r.lock.RLock()
addresses := make([]string, len(r.cache))
for key, _ := range r.cache {
addresses[i] = key
i++
}
r.lock.RUnlock()

for _, address := range addresses {
r.Lookup(address)
time.Sleep(time.Second * 2)
}
}

func (r *Resolver) Lookup(address string) ([]net.IP, error) {
ips, err := net.LookupIP(address)
if err != nil {
return nil, err
}

r.lock.Lock()
r.cache[address] = ips
r.lock.Unlock()
return ips, nil
}

func (r *Resolver) autoRefresh(rate time.Duration) {
for {
time.Sleep(rate)
r.Refresh()
}
}
79 changes: 79 additions & 0 deletions dnscache_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package tgbotapi

import (
"net"
"sort"
"testing"
"time"
)

func TestFetchReturnsAndErrorOnInvalidLookup(t *testing.T) {
ips, err := New(0).Lookup("invalid.viki.io")
if ips != nil {
t.Errorf("Expecting nil ips, got %v", ips)
}
expected := "lookup invalid.viki.io: no such host"
if err.Error() != expected {
t.Errorf("Expecting %q error, got %q", expected, err.Error())
}
}

func TestFetchReturnsAListOfIps(t *testing.T) {
ips, _ := New(0).Lookup("dnscache.go.test.viki.io")
assertIps(t, ips, []string{"1.123.58.13", "31.85.32.110"})
}

func TestCallingLookupAddsTheItemToTheCache(t *testing.T) {
r := New(0)
r.Lookup("dnscache.go.test.viki.io")
assertIps(t, r.cache["dnscache.go.test.viki.io"], []string{"1.123.58.13", "31.85.32.110"})
}

func TestFetchLoadsValueFromTheCache(t *testing.T) {
r := New(0)
r.cache["invalid.viki.io"] = []net.IP{net.ParseIP("1.1.2.3")}
ips, _ := r.Fetch("invalid.viki.io")
assertIps(t, ips, []string{"1.1.2.3"})
}

func TestFetchOneLoadsTheFirstValue(t *testing.T) {
r := New(0)
r.cache["something.viki.io"] = []net.IP{net.ParseIP("1.1.2.3"), net.ParseIP("100.100.102.103")}
ip, _ := r.FetchOne("something.viki.io")
assertIps(t, []net.IP{ip}, []string{"1.1.2.3"})
}

func TestFetchOneStringLoadsTheFirstValue(t *testing.T) {
r := New(0)
r.cache["something.viki.io"] = []net.IP{net.ParseIP("100.100.102.103"), net.ParseIP("100.100.102.104")}
ip, _ := r.FetchOneString("something.viki.io")
if ip != "100.100.102.103" {
t.Errorf("expected 100.100.102.103 but got %v", ip)
}
}

func TestFetchLoadsTheIpAndCachesIt(t *testing.T) {
r := New(0)
ips, _ := r.Fetch("dnscache.go.test.viki.io")
assertIps(t, ips, []string{"1.123.58.13", "31.85.32.110"})
assertIps(t, r.cache["dnscache.go.test.viki.io"], []string{"1.123.58.13", "31.85.32.110"})
}

func TestItReloadsTheIpsAtAGivenInterval(t *testing.T) {
r := New(1)
r.cache["dnscache.go.test.viki.io"] = nil
time.Sleep(time.Second * 2)
assertIps(t, r.cache["dnscache.go.test.viki.io"], []string{"1.123.58.13", "31.85.32.110"})
}

func assertIps(t *testing.T, actuals []net.IP, expected []string) {
if len(actuals) != len(expected) {
t.Errorf("Expecting %d ips, got %d", len(expected), len(actuals))
}
sort.Strings(expected)
for _, ip := range actuals {
if sort.SearchStrings(expected, ip.String()) == -1 {
t.Errorf("Got an unexpected ip: %v:", actuals[0])
}
}
}
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module github.com/go-telegram-bot-api/telegram-bot-api/v5
module github.com/artlibs/telegram-bot-api

go 1.16
go 1.22

0 comments on commit 92f4a97

Please sign in to comment.