initial commit

main
astravexton 2021-10-22 15:24:12 +01:00
commit 6358d0754b
34 changed files with 82616 additions and 0 deletions

3
.gitignore vendored 100644
View File

@ -0,0 +1,3 @@
.idea
/.tdlib
/.cmd

20
LICENSE 100644
View File

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2018-2019 Aleksandr Zelenin
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

19
Makefile 100644
View File

@ -0,0 +1,19 @@
TAG := v1.7.0
schema-update:
curl https://raw.githubusercontent.com/tdlib/td/${TAG}/td/generate/scheme/td_api.tl 2>/dev/null > ./data/td_api.tl
generate-json:
go run ./cmd/generate-json.go \
-version "${TAG}" \
-output "./data/td_api.json"
generate-code:
go run ./cmd/generate-code.go \
-version "${TAG}" \
-outputDir "./client" \
-package client \
-functionFile function.go \
-typeFile type.go \
-unmarshalerFile unmarshaler.go
go fmt ./...

127
README.md 100644
View File

@ -0,0 +1,127 @@
# go-tdlib
Go wrapper for [TDLib (Telegram Database Library)](https://github.com/tdlib/td) with full support of TDLib v1.7.0
## TDLib installation
Use [TDLib build instructions](https://tdlib.github.io/td/build.html)
## Usage
### Client
[Register an application](https://my.telegram.org/apps) to obtain an api_id and api_hash
```go
package main
import (
"log"
"path/filepath"
"github.com/zelenin/go-tdlib/client"
)
func main() {
// client authorizer
authorizer := client.ClientAuthorizer()
go client.CliInteractor(authorizer)
// or bot authorizer
// botToken := "000000000:gsVCGG5YbikxYHC7bP5vRvmBqJ7Xz6vG6td"
// authorizer := client.BotAuthorizer(botToken)
const (
apiId = 00000
apiHash = "8pu9yg32qkuukj83ozaqo5zzjwhkxhnk"
)
authorizer.TdlibParameters <- &client.TdlibParameters{
UseTestDc: false,
DatabaseDirectory: filepath.Join(".tdlib", "database"),
FilesDirectory: filepath.Join(".tdlib", "files"),
UseFileDatabase: true,
UseChatInfoDatabase: true,
UseMessageDatabase: true,
UseSecretChats: false,
ApiId: apiId,
ApiHash: apiHash,
SystemLanguageCode: "en",
DeviceModel: "Server",
SystemVersion: "1.0.0",
ApplicationVersion: "1.0.0",
EnableStorageOptimizer: true,
IgnoreFileNames: false,
}
logVerbosity := client.WithLogVerbosity(&client.SetLogVerbosityLevelRequest{
NewVerbosityLevel: 0,
})
tdlibClient, err := client.NewClient(authorizer, logVerbosity)
if err != nil {
log.Fatalf("NewClient error: %s", err)
}
optionValue, err := tdlibClient.GetOption(&client.GetOptionRequest{
Name: "version",
})
if err != nil {
log.Fatalf("GetOption error: %s", err)
}
log.Printf("TDLib version: %s", optionValue.(*client.OptionValueString).Value)
me, err := tdlibClient.GetMe()
if err != nil {
log.Fatalf("GetMe error: %s", err)
}
log.Printf("Me: %s %s [%s]", me.FirstName, me.LastName, me.Username)
}
```
### Receive updates
```go
tdlibClient, err := client.NewClient(authorizer)
if err != nil {
log.Fatalf("NewClient error: %s", err)
}
listener := tdlibClient.GetListener()
defer listener.Close()
for update := range listener.Updates {
if update.GetClass() == client.ClassUpdate {
log.Printf("%#v", update)
}
}
```
### Proxy support
```go
proxy := client.WithProxy(&client.AddProxyRequest{
Server: "1.1.1.1",
Port: 1080,
Enable: true,
Type: &client.ProxyTypeSocks5{
Username: "username",
Password: "password",
},
})
tdlibClient, err := client.NewClient(authorizer, proxy)
```
## Notes
* WIP. Library API can be changed in the future
* The package includes a .tl-parser and generated [json-schema](https://github.com/zelenin/go-tdlib/tree/master/data) for creating libraries in other languages
## Author
[Aleksandr Zelenin](https://github.com/zelenin/), e-mail: [aleksandr@zelenin.me](mailto:aleksandr@zelenin.me)

View File

@ -0,0 +1,233 @@
package client
import (
"errors"
"fmt"
"time"
"golang.org/x/crypto/ssh/terminal"
)
var ErrNotSupportedAuthorizationState = errors.New("not supported state")
type AuthorizationStateHandler interface {
Handle(client *Client, state AuthorizationState) error
Close()
}
func Authorize(client *Client, authorizationStateHandler AuthorizationStateHandler) error {
defer authorizationStateHandler.Close()
var authorizationError error
for {
state, err := client.GetAuthorizationState()
if err != nil {
return err
}
if state.AuthorizationStateType() == TypeAuthorizationStateClosed {
return authorizationError
}
if state.AuthorizationStateType() == TypeAuthorizationStateReady {
// dirty hack for db flush after authorization
time.Sleep(1 * time.Second)
return nil
}
err = authorizationStateHandler.Handle(client, state)
if err != nil {
authorizationError = err
client.Close()
}
}
}
type clientAuthorizer struct {
TdlibParameters chan *TdlibParameters
PhoneNumber chan string
Code chan string
State chan AuthorizationState
Password chan string
}
func ClientAuthorizer() *clientAuthorizer {
return &clientAuthorizer{
TdlibParameters: make(chan *TdlibParameters, 1),
PhoneNumber: make(chan string, 1),
Code: make(chan string, 1),
State: make(chan AuthorizationState, 10),
Password: make(chan string, 1),
}
}
func (stateHandler *clientAuthorizer) Handle(client *Client, state AuthorizationState) error {
stateHandler.State <- state
switch state.AuthorizationStateType() {
case TypeAuthorizationStateWaitTdlibParameters:
_, err := client.SetTdlibParameters(&SetTdlibParametersRequest{
Parameters: <-stateHandler.TdlibParameters,
})
return err
case TypeAuthorizationStateWaitEncryptionKey:
_, err := client.CheckDatabaseEncryptionKey(&CheckDatabaseEncryptionKeyRequest{})
return err
case TypeAuthorizationStateWaitPhoneNumber:
_, err := client.SetAuthenticationPhoneNumber(&SetAuthenticationPhoneNumberRequest{
PhoneNumber: <-stateHandler.PhoneNumber,
Settings: &PhoneNumberAuthenticationSettings{
AllowFlashCall: false,
IsCurrentPhoneNumber: false,
AllowSmsRetrieverApi: false,
},
})
return err
case TypeAuthorizationStateWaitCode:
_, err := client.CheckAuthenticationCode(&CheckAuthenticationCodeRequest{
Code: <-stateHandler.Code,
})
return err
case TypeAuthorizationStateWaitRegistration:
return ErrNotSupportedAuthorizationState
case TypeAuthorizationStateWaitPassword:
_, err := client.CheckAuthenticationPassword(&CheckAuthenticationPasswordRequest{
Password: <-stateHandler.Password,
})
return err
case TypeAuthorizationStateReady:
return nil
case TypeAuthorizationStateLoggingOut:
return ErrNotSupportedAuthorizationState
case TypeAuthorizationStateClosing:
return nil
case TypeAuthorizationStateClosed:
return nil
}
return ErrNotSupportedAuthorizationState
}
func (stateHandler *clientAuthorizer) Close() {
close(stateHandler.TdlibParameters)
close(stateHandler.PhoneNumber)
close(stateHandler.Code)
close(stateHandler.State)
close(stateHandler.Password)
}
func CliInteractor(clientAuthorizer *clientAuthorizer) {
for {
select {
case state, ok := <-clientAuthorizer.State:
if !ok {
return
}
switch state.AuthorizationStateType() {
case TypeAuthorizationStateWaitPhoneNumber:
fmt.Println("Enter phone number: ")
var phoneNumber string
fmt.Scanln(&phoneNumber)
clientAuthorizer.PhoneNumber <- phoneNumber
case TypeAuthorizationStateWaitCode:
var code string
fmt.Println("Enter code: ")
fmt.Scanln(&code)
clientAuthorizer.Code <- code
case TypeAuthorizationStateWaitPassword:
fmt.Println("Enter password: ")
// var password string
// fmt.Scanln(&password)
bytePassword, _ := terminal.ReadPassword(0)
clientAuthorizer.Password <- string(bytePassword)
// clientAuthorizer.Password <- password
case TypeAuthorizationStateReady:
return
}
}
}
}
type botAuthorizer struct {
TdlibParameters chan *TdlibParameters
Token chan string
State chan AuthorizationState
}
func BotAuthorizer(token string) *botAuthorizer {
botAuthorizer := &botAuthorizer{
TdlibParameters: make(chan *TdlibParameters, 1),
Token: make(chan string, 1),
State: make(chan AuthorizationState, 10),
}
botAuthorizer.Token <- token
return botAuthorizer
}
func (stateHandler *botAuthorizer) Handle(client *Client, state AuthorizationState) error {
stateHandler.State <- state
switch state.AuthorizationStateType() {
case TypeAuthorizationStateWaitTdlibParameters:
_, err := client.SetTdlibParameters(&SetTdlibParametersRequest{
Parameters: <-stateHandler.TdlibParameters,
})
return err
case TypeAuthorizationStateWaitEncryptionKey:
_, err := client.CheckDatabaseEncryptionKey(&CheckDatabaseEncryptionKeyRequest{})
return err
case TypeAuthorizationStateWaitPhoneNumber:
_, err := client.CheckAuthenticationBotToken(&CheckAuthenticationBotTokenRequest{
Token: <-stateHandler.Token,
})
return err
case TypeAuthorizationStateWaitCode:
return ErrNotSupportedAuthorizationState
case TypeAuthorizationStateWaitPassword:
return ErrNotSupportedAuthorizationState
case TypeAuthorizationStateReady:
return nil
case TypeAuthorizationStateLoggingOut:
return ErrNotSupportedAuthorizationState
case TypeAuthorizationStateClosing:
return ErrNotSupportedAuthorizationState
case TypeAuthorizationStateClosed:
return ErrNotSupportedAuthorizationState
}
return ErrNotSupportedAuthorizationState
}
func (stateHandler *botAuthorizer) Close() {
close(stateHandler.TdlibParameters)
close(stateHandler.Token)
close(stateHandler.State)
}

154
client/client.go 100644
View File

@ -0,0 +1,154 @@
package client
import (
"errors"
"sync"
"time"
)
type Client struct {
jsonClient *JsonClient
extraGenerator ExtraGenerator
catcher chan *Response
listenerStore *listenerStore
catchersStore *sync.Map
updatesTimeout time.Duration
catchTimeout time.Duration
}
type Option func(*Client)
func WithExtraGenerator(extraGenerator ExtraGenerator) Option {
return func(client *Client) {
client.extraGenerator = extraGenerator
}
}
func WithCatchTimeout(timeout time.Duration) Option {
return func(client *Client) {
client.catchTimeout = timeout
}
}
func WithUpdatesTimeout(timeout time.Duration) Option {
return func(client *Client) {
client.updatesTimeout = timeout
}
}
func WithProxy(req *AddProxyRequest) Option {
return func(client *Client) {
client.AddProxy(req)
}
}
func WithLogVerbosity(req *SetLogVerbosityLevelRequest) Option {
return func(client *Client) {
client.SetLogVerbosityLevel(req)
}
}
func NewClient(authorizationStateHandler AuthorizationStateHandler, options ...Option) (*Client, error) {
catchersListener := make(chan *Response, 1000)
client := &Client{
jsonClient: NewJsonClient(),
catcher: catchersListener,
listenerStore: newListenerStore(),
catchersStore: &sync.Map{},
}
client.extraGenerator = UuidV4Generator()
client.catchTimeout = 60 * time.Second
client.updatesTimeout = 60 * time.Second
for _, option := range options {
option(client)
}
go client.receive()
go client.catch(catchersListener)
err := Authorize(client, authorizationStateHandler)
if err != nil {
return nil, err
}
return client, nil
}
func (client *Client) receive() {
for {
resp, err := client.jsonClient.Receive(client.updatesTimeout)
if err != nil {
continue
}
client.catcher <- resp
typ, err := UnmarshalType(resp.Data)
if err != nil {
continue
}
needGc := false
for _, listener := range client.listenerStore.Listeners() {
if listener.IsActive() {
listener.Updates <- typ
} else {
needGc = true
}
}
if needGc {
client.listenerStore.gc()
}
}
}
func (client *Client) catch(updates chan *Response) {
for update := range updates {
if update.Extra != "" {
value, ok := client.catchersStore.Load(update.Extra)
if ok {
value.(chan *Response) <- update
}
}
}
}
func (client *Client) Send(req Request) (*Response, error) {
req.Extra = client.extraGenerator()
catcher := make(chan *Response, 1)
client.catchersStore.Store(req.Extra, catcher)
defer func() {
close(catcher)
client.catchersStore.Delete(req.Extra)
}()
client.jsonClient.Send(req)
select {
case response := <-catcher:
return response, nil
case <-time.After(client.catchTimeout):
return nil, errors.New("response catching timeout")
}
}
func (client *Client) GetListener() *Listener {
listener := &Listener{
isActive: true,
Updates: make(chan Type, 1000),
}
client.listenerStore.Add(listener)
return listener
}
func (client *Client) Stop() {
client.Destroy()
client.jsonClient.Destroy()
}

20
client/extra.go 100644
View File

@ -0,0 +1,20 @@
package client
import (
"fmt"
"math/rand"
)
type ExtraGenerator func() string
func UuidV4Generator() ExtraGenerator {
return func() string {
var uuid [16]byte
rand.Read(uuid[:])
uuid[6] = (uuid[6] & 0x0f) | 0x40
uuid[8] = (uuid[8] & 0x3f) | 0x80
return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x", uuid[:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:])
}
}

11390
client/function.go 100755

File diff suppressed because it is too large Load Diff

66
client/listener.go 100644
View File

@ -0,0 +1,66 @@
package client
import (
"sync"
)
func newListenerStore() *listenerStore {
return &listenerStore{
listeners: []*Listener{},
}
}
type listenerStore struct {
sync.Mutex
listeners []*Listener
}
func (store *listenerStore) Add(listener *Listener) {
store.Lock()
defer store.Unlock()
store.listeners = append(store.listeners, listener)
}
func (store *listenerStore) Listeners() []*Listener {
store.Lock()
defer store.Unlock()
return store.listeners
}
func (store *listenerStore) gc() {
store.Lock()
defer store.Unlock()
oldListeners := store.listeners
store.listeners = []*Listener{}
for _, listener := range oldListeners {
if listener.IsActive() {
store.listeners = append(store.listeners, listener)
}
}
}
type Listener struct {
mu sync.Mutex
isActive bool
Updates chan Type
}
func (listener *Listener) Close() {
listener.mu.Lock()
defer listener.mu.Unlock()
listener.isActive = false
close(listener.Updates)
}
func (listener *Listener) IsActive() bool {
listener.mu.Lock()
defer listener.mu.Unlock()
return listener.isActive
}

View File

@ -0,0 +1,52 @@
package puller
import (
"github.com/astravexton/go-tdlib/client"
)
func ChatHistory(tdlibClient *client.Client, chatId int64) (chan *client.Message, chan error) {
messageChan := make(chan *client.Message, 10)
errChan := make(chan error, 1)
var fromMessageId int64 = 0
var offset int32 = 0
var limit int32 = 100
go chatHistory(tdlibClient, messageChan, errChan, chatId, fromMessageId, offset, limit, false)
return messageChan, errChan
}
func chatHistory(tdlibClient *client.Client, messageChan chan *client.Message, errChan chan error, chatId int64, fromMessageId int64, offset int32, limit int32, onlyLocal bool) {
defer func() {
close(messageChan)
close(errChan)
}()
for {
messages, err := tdlibClient.GetChatHistory(&client.GetChatHistoryRequest{
ChatId: chatId,
FromMessageId: fromMessageId,
Offset: offset,
Limit: limit,
OnlyLocal: onlyLocal,
})
if err != nil {
errChan <- err
return
}
if len(messages.Messages) == 0 {
errChan <- EOP
break
}
for _, message := range messages.Messages {
fromMessageId = message.Id
messageChan <- message
}
}
}

View File

@ -0,0 +1,62 @@
package puller
import (
"math"
"github.com/astravexton/go-tdlib/client"
)
func Chats(tdlibClient *client.Client) (chan *client.Chat, chan error) {
chatChan := make(chan *client.Chat, 10)
errChan := make(chan error, 1)
var offsetOrder client.JsonInt64 = math.MaxInt64
var offsetChatId int64 = 0
var limit int32 = 100
go chats(tdlibClient, chatChan, errChan, offsetOrder, offsetChatId, limit)
return chatChan, errChan
}
func chats(tdlibClient *client.Client, chatChan chan *client.Chat, errChan chan error, offsetOrder client.JsonInt64, offsetChatId int64, limit int32) {
defer func() {
close(chatChan)
close(errChan)
}()
for {
chats, err := tdlibClient.GetChats(&client.GetChatsRequest{
OffsetOrder: offsetOrder,
OffsetChatId: offsetChatId,
Limit: limit,
})
if err != nil {
errChan <- err
return
}
if len(chats.ChatIds) == 0 {
errChan <- EOP
break
}
for _, chatId := range chats.ChatIds {
chat, err := tdlibClient.GetChat(&client.GetChatRequest{
ChatId: chatId,
})
if err != nil {
errChan <- err
return
}
offsetOrder = chat.Order
offsetChatId = chat.Id
chatChan <- chat
}
}
}

View File

@ -0,0 +1,7 @@
package puller
import (
"errors"
)
var EOP = errors.New("end of pull")

View File

@ -0,0 +1,53 @@
package puller
import (
"github.com/astravexton/go-tdlib/client"
)
func SupergroupMembers(tdlibClient *client.Client, supergroupId int32) (chan *client.ChatMember, chan error) {
chatMemberChan := make(chan *client.ChatMember, 10)
errChan := make(chan error, 1)
var filter client.SupergroupMembersFilter = nil
var offset int32 = 0
var limit int32 = 200
go supergroupMembers(tdlibClient, chatMemberChan, errChan, supergroupId, filter, offset, limit)
return chatMemberChan, errChan
}
func supergroupMembers(tdlibClient *client.Client, chatMemberChan chan *client.ChatMember, errChan chan error, supergroupId int32, filter client.SupergroupMembersFilter, offset int32, limit int32) {
defer func() {
close(chatMemberChan)
close(errChan)
}()
var page int32 = 0
for {
chatMembers, err := tdlibClient.GetSupergroupMembers(&client.GetSupergroupMembersRequest{
SupergroupId: supergroupId,
Filter: filter,
Offset: page*limit + offset,
Limit: limit,
})
if err != nil {
errChan <- err
return
}
if len(chatMembers.Members) == 0 {
errChan <- EOP
break
}
for _, member := range chatMembers.Members {
chatMemberChan <- member
}
page++
}
}

9
client/tdjson.go 100644
View File

@ -0,0 +1,9 @@
// +build darwin
package client
/*
#cgo darwin CFLAGS: -I/usr/local/include
#cgo darwin LDFLAGS: -L/usr/local/lib -L/usr/local/opt/openssl/lib -ltdjson_static -ltdjson_private -ltdclient -ltdcore -ltdactor -ltddb -ltdsqlite -ltdnet -ltdutils -lstdc++ -lssl -lcrypto -ldl -lz -lm
*/
import "C"

View File

@ -0,0 +1,14 @@
// +build libtdjson
// +build linux darwin windows
package client
/*
#cgo linux CFLAGS: -I/usr/local/include
#cgo linux LDFLAGS: -L/usr/local/lib -ltdjson -lstdc++ -lssl -lcrypto -ldl -lz -lm
#cgo darwin CFLAGS: -I/usr/local/include
#cgo darwin LDFLAGS: -L/usr/local/lib -ltdjson -lstdc++ -lssl -lcrypto -ldl -lz -lm
#cgo windows CFLAGS: -Ic:/td -Ic:/td/example/csharp/build
#cgo windows LDFLAGS: -Lc:/td/example/csharp/build/Release -ltdjson
*/
import "C"

View File

@ -0,0 +1,12 @@
// +build !libtdjson
// +build linux darwin
package client
/*
#cgo linux CFLAGS: -I/usr/local/include
#cgo linux LDFLAGS: -L/usr/local/lib -ltdjson_static -ltdjson_private -ltdclient -ltdcore -ltdactor -ltdapi -ltddb -ltdsqlite -ltdnet -ltdutils -lstdc++ -lssl -lcrypto -ldl -lz -lm
#cgo darwin CFLAGS: -I/usr/local/include
#cgo darwin LDFLAGS: -L/usr/local/lib -ltdjson_static -ltdjson_private -ltdclient -ltdcore -ltdactor -ltdapi -ltddb -ltdsqlite -ltdnet -ltdutils -lstdc++ -lssl -lcrypto -ldl -lz -lm
*/
import "C"

164
client/tdlib.go 100644
View File

@ -0,0 +1,164 @@
package client
/*
#include <stdlib.h>
#include <td/telegram/td_json_client.h>
*/
import "C"
import (
"encoding/json"
"errors"
"fmt"
"strconv"
"time"
"unsafe"
)
type JsonClient struct {
jsonClient unsafe.Pointer
}
func NewJsonClient() *JsonClient {
return &JsonClient{
jsonClient: C.td_json_client_create(),
}
}
// Sends request to the TDLib client. May be called from any thread.
func (jsonClient *JsonClient) Send(req Request) {
data, _ := json.Marshal(req)
query := C.CString(string(data))
defer C.free(unsafe.Pointer(query))
C.td_json_client_send(jsonClient.jsonClient, query)
}
// Receives incoming updates and request responses from the TDLib client. May be called from any thread, but
// shouldn't be called simultaneously from two different threads.
// Returned pointer will be deallocated by TDLib during next call to td_json_client_receive or td_json_client_execute
// in the same thread, so it can't be used after that.
func (jsonClient *JsonClient) Receive(timeout time.Duration) (*Response, error) {
result := C.td_json_client_receive(jsonClient.jsonClient, C.double(float64(timeout)/float64(time.Second)))
if result == nil {
return nil, errors.New("update receiving timeout")
}
data := []byte(C.GoString(result))
var resp Response
err := json.Unmarshal(data, &resp)
if err != nil {
return nil, err
}
resp.Data = data
return &resp, nil
}
// Synchronously executes TDLib request. May be called from any thread.
// Only a few requests can be executed synchronously.
// Returned pointer will be deallocated by TDLib during next call to td_json_client_receive or td_json_client_execute
// in the same thread, so it can't be used after that.
func (jsonClient *JsonClient) Execute(req Request) (*Response, error) {
data, _ := json.Marshal(req)
query := C.CString(string(data))
defer C.free(unsafe.Pointer(query))
result := C.td_json_client_execute(jsonClient.jsonClient, query)
if result == nil {
return nil, errors.New("request can't be parsed")
}
data = []byte(C.GoString(result))
var resp Response
err := json.Unmarshal(data, &resp)
if err != nil {
return nil, err
}
resp.Data = data
return &resp, nil
}
// Destroys the TDLib client instance. After this is called the client instance shouldn't be used anymore.
func (jsonClient *JsonClient) Destroy() {
C.td_json_client_destroy(jsonClient.jsonClient)
}
type meta struct {
Type string `json:"@type"`
Extra string `json:"@extra"`
}
type Request struct {
meta
Data map[string]interface{}
}
func (req Request) MarshalJSON() ([]byte, error) {
req.Data["@type"] = req.Type
req.Data["@extra"] = req.Extra
return json.Marshal(req.Data)
}
type Response struct {
meta
Data json.RawMessage
}
type ResponseError struct {
Err *Error
}
func (responseError ResponseError) Error() string {
return fmt.Sprintf("%d %s", responseError.Err.Code, responseError.Err.Message)
}
func buildResponseError(data json.RawMessage) error {
respErr, err := UnmarshalError(data)
if err != nil {
return err
}
return ResponseError{
Err: respErr,
}
}
// JsonInt64 alias for int64, in order to deal with json big number problem
type JsonInt64 int64
// MarshalJSON marshals to json
func (jsonInt64 JsonInt64) MarshalJSON() ([]byte, error) {
return []byte(`"` + strconv.FormatInt(int64(jsonInt64), 10) + `"`), nil
}
// UnmarshalJSON unmarshals from json
func (jsonInt64 *JsonInt64) UnmarshalJSON(data []byte) error {
if len(data) > 2 && data[0] == '"' && data[len(data)-1] == '"' {
data = data[1 : len(data)-1]
}
jsonBigInt, err := strconv.ParseInt(string(data), 10, 64)
if err != nil {
return err
}
*jsonInt64 = JsonInt64(jsonBigInt)
return nil
}
type Type interface {
GetType() string
GetClass() string
}

28587
client/type.go 100755

File diff suppressed because it is too large Load Diff

13356
client/unmarshaler.go 100755

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,86 @@
package main
import (
"bufio"
"flag"
"log"
"net/http"
"os"
"path/filepath"
"github.com/astravexton/go-tdlib/codegen"
"github.com/astravexton/go-tdlib/tlparser"
)
type config struct {
version string
outputDirPath string
packageName string
functionFileName string
typeFileName string
unmarshalerFileName string
}
func main() {
var config config
flag.StringVar(&config.version, "version", "", "TDLib version")
flag.StringVar(&config.outputDirPath, "outputDir", "./tdlib", "output directory")
flag.StringVar(&config.packageName, "package", "tdlib", "package name")
flag.StringVar(&config.functionFileName, "functionFile", "function.go", "functions filename")
flag.StringVar(&config.typeFileName, "typeFile", "type.go", "types filename")
flag.StringVar(&config.unmarshalerFileName, "unmarshalerFile", "unmarshaler.go", "unmarshalers filename")
flag.Parse()
resp, err := http.Get("https://raw.githubusercontent.com/tdlib/td/" + config.version + "/td/generate/scheme/td_api.tl")
if err != nil {
log.Fatalf("http.Get error: %s", err)
return
}
defer resp.Body.Close()
schema, err := tlparser.Parse(resp.Body)
if err != nil {
log.Fatalf("schema parse error: %s", err)
return
}
err = os.MkdirAll(config.outputDirPath, 0755)
if err != nil {
log.Fatalf("error creating %s: %s", config.outputDirPath, err)
}
functionFilePath := filepath.Join(config.outputDirPath, config.functionFileName)
os.Remove(functionFilePath)
functionFile, err := os.OpenFile(functionFilePath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, os.ModePerm)
if err != nil {
log.Fatalf("functionFile open error: %s", err)
}
defer functionFile.Close()
bufio.NewWriter(functionFile).Write(codegen.GenerateFunctions(schema, config.packageName))
typeFilePath := filepath.Join(config.outputDirPath, config.typeFileName)
os.Remove(typeFilePath)
typeFile, err := os.OpenFile(typeFilePath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, os.ModePerm)
if err != nil {
log.Fatalf("typeFile open error: %s", err)
}
defer typeFile.Close()
bufio.NewWriter(typeFile).Write(codegen.GenerateTypes(schema, config.packageName))
unmarshalerFilePath := filepath.Join(config.outputDirPath, config.unmarshalerFileName)
os.Remove(unmarshalerFilePath)
unmarshalerFile, err := os.OpenFile(unmarshalerFilePath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, os.ModePerm)
if err != nil {
log.Fatalf("unmarshalerFile open error: %s", err)
}
defer unmarshalerFile.Close()
bufio.NewWriter(unmarshalerFile).Write(codegen.GenerateUnmarshalers(schema, config.packageName))
}

View File

@ -0,0 +1,68 @@
package main
import (
"bufio"
"encoding/json"
"flag"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/astravexton/go-tdlib/tlparser"
)
func main() {
var version string
var outputFilePath string
flag.StringVar(&version, "version", "", "TDLib version")
flag.StringVar(&outputFilePath, "output", "./td_api.json", "json schema file")
flag.Parse()
resp, err := http.Get("https://raw.githubusercontent.com/tdlib/td/" + version + "/td/generate/scheme/td_api.tl")
if err != nil {
log.Fatalf("http.Get error: %s", err)
return
}
defer resp.Body.Close()
schema, err := tlparser.Parse(resp.Body)
if err != nil {
log.Fatalf("schema parse error: %s", err)
return
}
resp, err = http.Get("https://raw.githubusercontent.com/tdlib/td/" + version + "/td/telegram/Td.cpp")
if err != nil {
log.Fatalf("http.Get error: %s", err)
return
}
defer resp.Body.Close()
err = tlparser.ParseCode(resp.Body, schema)
if err != nil {
log.Fatalf("parse code error: %s", err)
return
}
err = os.MkdirAll(filepath.Dir(outputFilePath), os.ModePerm)
if err != nil {
log.Fatalf("make dir error: %s", filepath.Dir(outputFilePath))
}
file, err := os.OpenFile(outputFilePath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, os.ModePerm)
if err != nil {
log.Fatalf("open file error: %s", err)
return
}
data, err := json.MarshalIndent(schema, "", strings.Repeat(" ", 4))
if err != nil {
log.Fatalf("json marshal error: %s", err)
return
}
bufio.NewWriter(file).Write(data)
}

115
codegen/function.go 100644
View File

@ -0,0 +1,115 @@
package codegen
import (
"bytes"
"fmt"
"github.com/astravexton/go-tdlib/tlparser"
)
func GenerateFunctions(schema *tlparser.Schema, packageName string) []byte {
buf := bytes.NewBufferString("")
buf.WriteString(fmt.Sprintf("%s\n\npackage %s\n\n", header, packageName))
buf.WriteString(`import (
"errors"
)`)
buf.WriteString("\n")
for _, function := range schema.Functions {
tdlibFunction := TdlibFunction(function.Name, schema)
tdlibFunctionReturn := TdlibFunctionReturn(function.Class, schema)
if len(function.Properties) > 0 {
buf.WriteString("\n")
buf.WriteString(fmt.Sprintf("type %sRequest struct { \n", tdlibFunction.ToGoName()))
for _, property := range function.Properties {
tdlibTypeProperty := TdlibTypeProperty(property.Name, property.Type, schema)
buf.WriteString(fmt.Sprintf(" // %s\n", property.Description))
buf.WriteString(fmt.Sprintf(" %s %s `json:\"%s\"`\n", tdlibTypeProperty.ToGoName(), tdlibTypeProperty.ToGoType(), property.Name))
}
buf.WriteString("}\n")
}
buf.WriteString("\n")
buf.WriteString("// " + function.Description)
buf.WriteString("\n")
requestArgument := ""
if len(function.Properties) > 0 {
requestArgument = fmt.Sprintf("req *%sRequest", tdlibFunction.ToGoName())
}
buf.WriteString(fmt.Sprintf("func (client *Client) %s(%s) (%s, error) {\n", tdlibFunction.ToGoName(), requestArgument, tdlibFunctionReturn.ToGoReturn()))
sendMethod := "Send"
if function.IsSynchronous {
sendMethod = "jsonClient.Execute"
}
if len(function.Properties) > 0 {
buf.WriteString(fmt.Sprintf(` result, err := client.%s(Request{
meta: meta{
Type: "%s",
},
Data: map[string]interface{}{
`, sendMethod, function.Name))
for _, property := range function.Properties {
tdlibTypeProperty := TdlibTypeProperty(property.Name, property.Type, schema)
buf.WriteString(fmt.Sprintf(" \"%s\": req.%s,\n", property.Name, tdlibTypeProperty.ToGoName()))
}
buf.WriteString(` },
})
`)
} else {
buf.WriteString(fmt.Sprintf(` result, err := client.%s(Request{
meta: meta{
Type: "%s",
},
Data: map[string]interface{}{},
})
`, sendMethod, function.Name))
}
buf.WriteString(` if err != nil {
return nil, err
}
if result.Type == "error" {
return nil, buildResponseError(result.Data)
}
`)
if tdlibFunctionReturn.IsClass() {
buf.WriteString(" switch result.Type {\n")
for _, subType := range tdlibFunctionReturn.GetClass().GetSubTypes() {
buf.WriteString(fmt.Sprintf(` case %s:
return Unmarshal%s(result.Data)
`, subType.ToTypeConst(), subType.ToGoType()))
}
buf.WriteString(` default:
return nil, errors.New("invalid type")
`)
buf.WriteString(" }\n")
} else {
buf.WriteString(fmt.Sprintf(` return Unmarshal%s(result.Data)
`, tdlibFunctionReturn.ToGoType()))
}
buf.WriteString("}\n")
}
return buf.Bytes()
}

View File

@ -0,0 +1,3 @@
package codegen
const header = "// AUTOGENERATED"

26
codegen/string.go 100644
View File

@ -0,0 +1,26 @@
package codegen
import (
"strings"
"unicode"
)
func firstUpper(str string) string {
for i, r := range str {
return string(unicode.ToUpper(r)) + str[i+1:]
}
return str
}
func firstLower(str string) string {
for i, r := range str {
return string(unicode.ToLower(r)) + str[i+1:]
}
return str
}
func underscoreToCamelCase(s string) string {
return strings.Replace(strings.Title(strings.Replace(strings.ToLower(s), "_", " ", -1)), " ", "", -1)
}

487
codegen/tdlib.go 100644
View File

@ -0,0 +1,487 @@
package codegen
import (
"log"
"strings"
"github.com/astravexton/go-tdlib/tlparser"
)
type tdlibFunction struct {
name string
schema *tlparser.Schema
}
func TdlibFunction(name string, schema *tlparser.Schema) *tdlibFunction {
return &tdlibFunction{
name: name,
schema: schema,
}
}
func (entity *tdlibFunction) ToGoName() string {
return firstUpper(entity.name)
}
type tdlibFunctionReturn struct {
name string
schema *tlparser.Schema
}
func TdlibFunctionReturn(name string, schema *tlparser.Schema) *tdlibFunctionReturn {
return &tdlibFunctionReturn{
name: name,
schema: schema,
}
}
func (entity *tdlibFunctionReturn) IsType() bool {
return isType(entity.name, func(entity *tlparser.Type) string {
return entity.Class
}, entity.schema)
}
func (entity *tdlibFunctionReturn) GetType() *tdlibType {
return getType(entity.name, func(entity *tlparser.Type) string {
return entity.Class
}, entity.schema)
}
func (entity *tdlibFunctionReturn) IsClass() bool {
return isClass(entity.name, func(entity *tlparser.Class) string {
return entity.Name
}, entity.schema)
}
func (entity *tdlibFunctionReturn) GetClass() *tdlibClass {
return getClass(entity.name, func(entity *tlparser.Class) string {
return entity.Name
}, entity.schema)
}
func (entity *tdlibFunctionReturn) ToGoReturn() string {
if strings.HasPrefix(entity.name, "vector<") {
log.Fatal("vectors are not supported")
}
if entity.IsClass() {
return entity.GetClass().ToGoType()
}
if entity.GetType().IsInternal() {
return entity.GetType().ToGoType()
}
return "*" + entity.GetType().ToGoType()
}
func (entity *tdlibFunctionReturn) ToGoType() string {
if strings.HasPrefix(entity.name, "vector<") {
log.Fatal("vectors are not supported")
}
if entity.IsClass() {
return entity.GetClass().ToGoType()
}
return entity.GetType().ToGoType()
}
type tdlibFunctionProperty struct {
name string
propertyType string
schema *tlparser.Schema
}
func TdlibFunctionProperty(name string, propertyType string, schema *tlparser.Schema) *tdlibFunctionProperty {
return &tdlibFunctionProperty{
name: name,
propertyType: propertyType,
schema: schema,
}
}
func (entity *tdlibFunctionProperty) GetPrimitive() string {
primitive := entity.propertyType
for strings.HasPrefix(primitive, "vector<") {
primitive = strings.TrimSuffix(strings.TrimPrefix(primitive, "vector<"), ">")
}
return primitive
}
func (entity *tdlibFunctionProperty) IsType() bool {
primitive := entity.GetPrimitive()
return isType(primitive, func(entity *tlparser.Type) string {
return entity.Name
}, entity.schema)
}
func (entity *tdlibFunctionProperty) GetType() *tdlibType {
primitive := entity.GetPrimitive()
return getType(primitive, func(entity *tlparser.Type) string {
return entity.Name
}, entity.schema)
}
func (entity *tdlibFunctionProperty) IsClass() bool {
primitive := entity.GetPrimitive()
return isClass(primitive, func(entity *tlparser.Class) string {
return entity.Name
}, entity.schema)
}
func (entity *tdlibFunctionProperty) GetClass() *tdlibClass {
primitive := entity.GetPrimitive()
return getClass(primitive, func(entity *tlparser.Class) string {
return entity.Name
}, entity.schema)
}
func (entity *tdlibFunctionProperty) ToGoName() string {
name := firstLower(underscoreToCamelCase(entity.name))
if name == "type" {
name += "Param"
}
return name
}
func (entity *tdlibFunctionProperty) ToGoType() string {
tdlibType := entity.propertyType
goType := ""
for strings.HasPrefix(tdlibType, "vector<") {
goType = goType + "[]"
tdlibType = strings.TrimSuffix(strings.TrimPrefix(tdlibType, "vector<"), ">")
}
if entity.IsClass() {
return goType + entity.GetClass().ToGoType()
}
if entity.GetType().IsInternal() {
return goType + entity.GetType().ToGoType()
}
return goType + "*" + entity.GetType().ToGoType()
}
type tdlibType struct {
name string
schema *tlparser.Schema
}
func TdlibType(name string, schema *tlparser.Schema) *tdlibType {
return &tdlibType{
name: name,
schema: schema,
}
}
func (entity *tdlibType) IsInternal() bool {
switch entity.name {
case "double":
return true
case "string":
return true
case "int32":
return true
case "int53":
return true
case "int64":
return true
case "bytes":
return true
case "boolFalse":
return true
case "boolTrue":
return true
case "vector<t>":
return true
}
return false
}
func (entity *tdlibType) GetType() *tlparser.Type {
name := normalizeEntityName(entity.name)
for _, typ := range entity.schema.Types {
if typ.Name == name {
return typ
}
}
return nil
}
func (entity *tdlibType) ToGoType() string {
if strings.HasPrefix(entity.name, "vector<") {
log.Fatal("vectors are not supported")
}
switch entity.name {
case "double":
return "float64"
case "string":
return "string"
case "int32":
return "int32"
case "int53":
return "int64"
case "int64":
return "JsonInt64"
case "bytes":
return "[]byte"
case "boolFalse":
return "bool"
case "boolTrue":
return "bool"
}
return firstUpper(entity.name)
}
func (entity *tdlibType) ToType() string {
return entity.ToGoType() + "Type"
}
func (entity *tdlibType) HasClass() bool {
className := entity.GetType().Class
for _, class := range entity.schema.Classes {
if class.Name == className {
return true
}
}
return false
}
func (entity *tdlibType) GetClass() *tlparser.Class {
className := entity.GetType().Class
for _, class := range entity.schema.Classes {
if class.Name == className {
return class
}
}
return nil
}
func (entity *tdlibType) HasClassProperties() bool {
for _, prop := range entity.GetType().Properties {
tdlibTypeProperty := TdlibTypeProperty(prop.Name, prop.Type, entity.schema)
if tdlibTypeProperty.IsClass() {
return true
}
}
return false
}
func (entity *tdlibType) IsList() bool {
return strings.HasPrefix(entity.name, "vector<")
}
func (entity *tdlibType) ToClassConst() string {
if entity.HasClass() {
return "Class" + TdlibClass(entity.GetType().Class, entity.schema).ToGoType()
}
return "Class" + entity.ToGoType()
}
func (entity *tdlibType) ToTypeConst() string {
return "Type" + entity.ToGoType()
}
type tdlibClass struct {
name string
schema *tlparser.Schema
}
func TdlibClass(name string, schema *tlparser.Schema) *tdlibClass {
return &tdlibClass{
name: name,
schema: schema,
}
}
func (entity *tdlibClass) ToGoType() string {
return firstUpper(entity.name)
}
func (entity *tdlibClass) ToType() string {
return entity.ToGoType() + "Type"
}
func (entity *tdlibClass) GetSubTypes() []*tdlibType {
types := []*tdlibType{}
for _, t := range entity.schema.Types {
if t.Class == entity.name {
types = append(types, TdlibType(t.Name, entity.schema))
}
}
return types
}
func (entity *tdlibClass) ToClassConst() string {
return "Class" + entity.ToGoType()
}
type tdlibTypeProperty struct {
name string
propertyType string
schema *tlparser.Schema
}
func TdlibTypeProperty(name string, propertyType string, schema *tlparser.Schema) *tdlibTypeProperty {
return &tdlibTypeProperty{
name: name,
propertyType: propertyType,
schema: schema,
}
}
func (entity *tdlibTypeProperty) IsList() bool {
return strings.HasPrefix(entity.propertyType, "vector<")
}
func (entity *tdlibTypeProperty) GetPrimitive() string {
primitive := entity.propertyType
for strings.HasPrefix(primitive, "vector<") {
primitive = strings.TrimSuffix(strings.TrimPrefix(primitive, "vector<"), ">")
}
return primitive
}
func (entity *tdlibTypeProperty) IsType() bool {
primitive := entity.GetPrimitive()
return isType(primitive, func(entity *tlparser.Type) string {
return entity.Name
}, entity.schema)
}
func (entity *tdlibTypeProperty) GetType() *tdlibType {
primitive := entity.GetPrimitive()
return getType(primitive, func(entity *tlparser.Type) string {
return entity.Name
}, entity.schema)
}
func (entity *tdlibTypeProperty) IsClass() bool {
primitive := entity.GetPrimitive()
return isClass(primitive, func(entity *tlparser.Class) string {
return entity.Name
}, entity.schema)
}
func (entity *tdlibTypeProperty) GetClass() *tdlibClass {
primitive := entity.GetPrimitive()
return getClass(primitive, func(entity *tlparser.Class) string {
return entity.Name
}, entity.schema)
}
func (entity *tdlibTypeProperty) ToGoName() string {
return firstUpper(underscoreToCamelCase(entity.name))
}
func (entity *tdlibTypeProperty) ToGoFunctionPropertyName() string {
name := firstLower(underscoreToCamelCase(entity.name))
if name == "type" {
name += "Param"
}
return name
}
func (entity *tdlibTypeProperty) ToGoType() string {
tdlibType := entity.propertyType
goType := ""
for strings.HasPrefix(tdlibType, "vector<") {
goType = goType + "[]"
tdlibType = strings.TrimSuffix(strings.TrimPrefix(tdlibType, "vector<"), ">")
}
if entity.IsClass() {
return goType + entity.GetClass().ToGoType()
}
if entity.GetType().IsInternal() {
return goType + entity.GetType().ToGoType()
}
return goType + "*" + entity.GetType().ToGoType()
}
func isType(name string, field func(entity *tlparser.Type) string, schema *tlparser.Schema) bool {
name = normalizeEntityName(name)
for _, entity := range schema.Types {
if name == field(entity) {
return true
}
}
return false
}
func getType(name string, field func(entity *tlparser.Type) string, schema *tlparser.Schema) *tdlibType {
name = normalizeEntityName(name)
for _, entity := range schema.Types {
if name == field(entity) {
return TdlibType(entity.Name, schema)
}
}
return nil
}
func isClass(name string, field func(entity *tlparser.Class) string, schema *tlparser.Schema) bool {
name = normalizeEntityName(name)
for _, entity := range schema.Classes {
if name == field(entity) {
return true
}
}
return false
}
func getClass(name string, field func(entity *tlparser.Class) string, schema *tlparser.Schema) *tdlibClass {
name = normalizeEntityName(name)
for _, entity := range schema.Classes {
if name == field(entity) {
return TdlibClass(entity.Name, schema)
}
}
return nil
}
func normalizeEntityName(name string) string {
if name == "Bool" {
name = "boolFalse"
}
return name
}

186
codegen/type.go 100644
View File

@ -0,0 +1,186 @@
package codegen
import (
"bytes"
"fmt"
"github.com/astravexton/go-tdlib/tlparser"
)
func GenerateTypes(schema *tlparser.Schema, packageName string) []byte {
buf := bytes.NewBufferString("")
buf.WriteString(fmt.Sprintf("%s\n\npackage %s\n\n", header, packageName))
buf.WriteString(`import (
"encoding/json"
)
`)
buf.WriteString("const (\n")
for _, entity := range schema.Classes {
tdlibClass := TdlibClass(entity.Name, schema)
buf.WriteString(fmt.Sprintf(" %s = %q\n", tdlibClass.ToClassConst(), entity.Name))
}
for _, entity := range schema.Types {
tdlibType := TdlibType(entity.Name, schema)
if tdlibType.IsInternal() || tdlibType.HasClass() {
continue
}
buf.WriteString(fmt.Sprintf(" %s = %q\n", tdlibType.ToClassConst(), entity.Class))
}
buf.WriteString(")")
buf.WriteString("\n\n")
buf.WriteString("const (\n")
for _, entity := range schema.Types {
tdlibType := TdlibType(entity.Name, schema)
if tdlibType.IsInternal() {
continue
}
buf.WriteString(fmt.Sprintf(" %s = %q\n", tdlibType.ToTypeConst(), entity.Name))
}
buf.WriteString(")")
buf.WriteString("\n\n")
for _, class := range schema.Classes {
tdlibClass := TdlibClass(class.Name, schema)
buf.WriteString(fmt.Sprintf(`// %s
type %s interface {
%sType() string
}
`, class.Description, tdlibClass.ToGoType(), tdlibClass.ToGoType()))
}
for _, typ := range schema.Types {
tdlibType := TdlibType(typ.Name, schema)
if tdlibType.IsInternal() {
continue
}
buf.WriteString("// " + typ.Description + "\n")
if len(typ.Properties) > 0 {
buf.WriteString(`type ` + tdlibType.ToGoType() + ` struct {
meta
`)
for _, property := range typ.Properties {
tdlibTypeProperty := TdlibTypeProperty(property.Name, property.Type, schema)
buf.WriteString(fmt.Sprintf(" // %s\n", property.Description))
buf.WriteString(fmt.Sprintf(" %s %s `json:\"%s\"`\n", tdlibTypeProperty.ToGoName(), tdlibTypeProperty.ToGoType(), property.Name))
}
buf.WriteString("}\n\n")
} else {
buf.WriteString(`type ` + tdlibType.ToGoType() + ` struct{
meta
}
`)
}
buf.WriteString(fmt.Sprintf(`func (entity *%s) MarshalJSON() ([]byte, error) {
entity.meta.Type = entity.GetType()
type stub %s
return json.Marshal((*stub)(entity))
}
`, tdlibType.ToGoType(), tdlibType.ToGoType()))
buf.WriteString(fmt.Sprintf(`func (*%s) GetClass() string {
return %s
}
func (*%s) GetType() string {
return %s
}
`, tdlibType.ToGoType(), tdlibType.ToClassConst(), tdlibType.ToGoType(), tdlibType.ToTypeConst()))
if tdlibType.HasClass() {
tdlibClass := TdlibClass(tdlibType.GetClass().Name, schema)
buf.WriteString(fmt.Sprintf(`func (*%s) %sType() string {
return %s
}
`, tdlibType.ToGoType(), tdlibClass.ToGoType(), tdlibType.ToTypeConst()))
}
if tdlibType.HasClassProperties() {
buf.WriteString(fmt.Sprintf(`func (%s *%s) UnmarshalJSON(data []byte) error {
var tmp struct {
`, typ.Name, tdlibType.ToGoType()))
var countSimpleProperties int
for _, property := range typ.Properties {
tdlibTypeProperty := TdlibTypeProperty(property.Name, property.Type, schema)
if !tdlibTypeProperty.IsClass() {
buf.WriteString(fmt.Sprintf(" %s %s `json:\"%s\"`\n", tdlibTypeProperty.ToGoName(), tdlibTypeProperty.ToGoType(), property.Name))
countSimpleProperties++
} else {
if tdlibTypeProperty.IsList() {
buf.WriteString(fmt.Sprintf(" %s %s `json:\"%s\"`\n", tdlibTypeProperty.ToGoName(), "[]json.RawMessage", property.Name))
} else {
buf.WriteString(fmt.Sprintf(" %s %s `json:\"%s\"`\n", tdlibTypeProperty.ToGoName(), "json.RawMessage", property.Name))
}
}
}
buf.WriteString(` }
err := json.Unmarshal(data, &tmp)
if err != nil {
return err
}
`)
for _, property := range typ.Properties {
tdlibTypeProperty := TdlibTypeProperty(property.Name, property.Type, schema)
if !tdlibTypeProperty.IsClass() {
buf.WriteString(fmt.Sprintf(" %s.%s = tmp.%s\n", typ.Name, tdlibTypeProperty.ToGoName(), tdlibTypeProperty.ToGoName()))
}
}
if countSimpleProperties > 0 {
buf.WriteString("\n")
}
for _, property := range typ.Properties {
tdlibTypeProperty := TdlibTypeProperty(property.Name, property.Type, schema)
if tdlibTypeProperty.IsClass() && !tdlibTypeProperty.IsList() {
buf.WriteString(fmt.Sprintf(` field%s, _ := Unmarshal%s(tmp.%s)
%s.%s = field%s
`, tdlibTypeProperty.ToGoName(), tdlibTypeProperty.ToGoType(), tdlibTypeProperty.ToGoName(), typ.Name, tdlibTypeProperty.ToGoName(), tdlibTypeProperty.ToGoName()))
}
if tdlibTypeProperty.IsClass() && tdlibTypeProperty.IsList() {
buf.WriteString(fmt.Sprintf(` field%s, _ := UnmarshalListOf%s(tmp.%s)
%s.%s = field%s
`, tdlibTypeProperty.ToGoName(), tdlibTypeProperty.GetClass().ToGoType(), tdlibTypeProperty.ToGoName(), typ.Name, tdlibTypeProperty.ToGoName(), tdlibTypeProperty.ToGoName()))
}
}
buf.WriteString(` return nil
}
`)
}
}
return buf.Bytes()
}

View File

@ -0,0 +1,120 @@
package codegen
import (
"bytes"
"fmt"
"github.com/astravexton/go-tdlib/tlparser"
)
func GenerateUnmarshalers(schema *tlparser.Schema, packageName string) []byte {
buf := bytes.NewBufferString("")
buf.WriteString(fmt.Sprintf("%s\n\npackage %s\n\n", header, packageName))
buf.WriteString(`import (
"encoding/json"
"fmt"
)
`)
for _, class := range schema.Classes {
tdlibClass := TdlibClass(class.Name, schema)
buf.WriteString(fmt.Sprintf(`func Unmarshal%s(data json.RawMessage) (%s, error) {
var meta meta
err := json.Unmarshal(data, &meta)
if err != nil {
return nil, err
}
switch meta.Type {
`, tdlibClass.ToGoType(), tdlibClass.ToGoType()))
for _, subType := range tdlibClass.GetSubTypes() {
buf.WriteString(fmt.Sprintf(` case %s:
return Unmarshal%s(data)
`, subType.ToTypeConst(), subType.ToGoType()))
}
buf.WriteString(` default:
return nil, fmt.Errorf("Error unmarshaling. Unknown type: " + meta.Type)
}
}
`)
buf.WriteString(fmt.Sprintf(`func UnmarshalListOf%s(dataList []json.RawMessage) ([]%s, error) {
list := []%s{}
for _, data := range dataList {
entity, err := Unmarshal%s(data)
if err != nil {
return nil, err
}
list = append(list, entity)
}
return list, nil
}
`, tdlibClass.ToGoType(), tdlibClass.ToGoType(), tdlibClass.ToGoType(), tdlibClass.ToGoType()))
}
for _, typ := range schema.Types {
tdlibType := TdlibType(typ.Name, schema)
if tdlibType.IsList() || tdlibType.IsInternal() {
continue
}
buf.WriteString(fmt.Sprintf(`func Unmarshal%s(data json.RawMessage) (*%s, error) {
var resp %s
err := json.Unmarshal(data, &resp)
return &resp, err
}
`, tdlibType.ToGoType(), tdlibType.ToGoType(), tdlibType.ToGoType()))
}
buf.WriteString(`func UnmarshalType(data json.RawMessage) (Type, error) {
var meta meta
err := json.Unmarshal(data, &meta)
if err != nil {
return nil, err
}
switch meta.Type {
`)
for _, typ := range schema.Types {
tdlibType := TdlibType(typ.Name, schema)
if tdlibType.IsList() || tdlibType.IsInternal() {
continue
}
buf.WriteString(fmt.Sprintf(` case %s:
return Unmarshal%s(data)
`, tdlibType.ToTypeConst(), tdlibType.ToGoType()))
}
buf.WriteString(` default:
return nil, fmt.Errorf("Error unmarshaling. Unknown type: " + meta.Type)
}
}
`)
return buf.Bytes()
}

21961
data/td_api.json 100755

File diff suppressed because it is too large Load Diff

4910
data/td_api.tl 100644

File diff suppressed because it is too large Load Diff

5
go.mod 100644
View File

@ -0,0 +1,5 @@
module github.com/astravexton/go-tdlib
go 1.16
require golang.org/x/crypto v0.0.0-20210921155107-089bfa567519

10
go.sum 100644
View File

@ -0,0 +1,10 @@
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

74
tlparser/code.go 100644
View File

@ -0,0 +1,74 @@
package tlparser
import (
"bufio"
"fmt"
"io"
"strings"
)
func ParseCode(reader io.Reader, schema *Schema) error {
var prevLine string
var curLine string
userMethods := map[string]bool{}
botMethods := map[string]bool{}
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
prevLine = curLine
curLine = scanner.Text()
if strings.Contains(curLine, "CHECK_IS_USER();") {
fields := strings.Fields(prevLine)
for _, field := range fields {
var methodName string
n, err := fmt.Sscanf(field, "td_api::%s", &methodName)
if err == nil && n > 0 {
userMethods[methodName] = true
}
}
}
if strings.Contains(curLine, "CHECK_IS_BOT();") {
fields := strings.Fields(prevLine)
for _, field := range fields {
var methodName string
n, err := fmt.Sscanf(field, "td_api::%s", &methodName)
if err == nil && n > 0 {
botMethods[methodName] = true
}
}
}
}
err := scanner.Err()
if err != nil {
return err
}
var ok bool
for index, _ := range schema.Functions {
hasType := false
_, ok = userMethods[schema.Functions[index].Name]
if ok {
schema.Functions[index].Type = FUNCTION_TYPE_USER
hasType = true
}
_, ok = botMethods[schema.Functions[index].Name]
if ok {
schema.Functions[index].Type = FUNCTION_TYPE_BOT
hasType = true
}
if !hasType {
schema.Functions[index].Type = FUNCTION_TYPE_COMMON
}
ok = false
}
return nil
}

174
tlparser/parser.go 100644
View File

@ -0,0 +1,174 @@
package tlparser
import (
"bufio"
"io"
"strings"
)
func Parse(reader io.Reader) (*Schema, error) {
schema := &Schema{
Types: []*Type{},
Classes: []*Class{},
Functions: []*Function{},
}
scanner := bufio.NewScanner(reader)
hitFunctions := false
for scanner.Scan() {
line := scanner.Text()
switch {
case strings.HasPrefix(line, "//@description"):
if hitFunctions {
schema.Functions = append(schema.Functions, parseFunction(line, scanner))
} else {
schema.Types = append(schema.Types, parseType(line, scanner))
}
case strings.HasPrefix(line, "//@class"):
schema.Classes = append(schema.Classes, parseClass(line, scanner))
case strings.Contains(line, "---functions---"):
hitFunctions = true
case line == "":
default:
bodyFields := strings.Fields(line)
name := bodyFields[0]
class := strings.TrimRight(bodyFields[len(bodyFields)-1], ";")
if hitFunctions {
schema.Functions = append(schema.Functions, &Function{
Name: name,
Description: "",
Class: class,
Properties: []*Property{},
IsSynchronous: false,
Type: FUNCTION_TYPE_UNKNOWN,
})
} else {
if name == "vector" {
name = "vector<t>"
class = "Vector<T>"
}
schema.Types = append(schema.Types, &Type{
Name: name,
Description: "",
Class: class,
Properties: []*Property{},
})
}
}
}
return schema, nil
}
func parseType(firstLine string, scanner *bufio.Scanner) *Type {
name, description, class, properties, _ := parseEntity(firstLine, scanner)
return &Type{
Name: name,
Description: description,
Class: class,
Properties: properties,
}
}
func parseFunction(firstLine string, scanner *bufio.Scanner) *Function {
name, description, class, properties, isSynchronous := parseEntity(firstLine, scanner)
return &Function{
Name: name,
Description: description,
Class: class,
Properties: properties,
IsSynchronous: isSynchronous,
Type: FUNCTION_TYPE_UNKNOWN,
}
}
func parseClass(firstLine string, scanner *bufio.Scanner) *Class {
class := &Class{
Name: "",
Description: "",
}
classLineParts := strings.Split(firstLine, "@")
_, class.Name = parseProperty(classLineParts[1])
_, class.Description = parseProperty(classLineParts[2])
return class
}
func parseEntity(firstLine string, scanner *bufio.Scanner) (string, string, string, []*Property, bool) {
name := ""
description := ""
class := ""
properties := []*Property{}
propertiesLine := strings.TrimLeft(firstLine, "//")
Loop:
for scanner.Scan() {
line := scanner.Text()
switch {
case strings.HasPrefix(line, "//@"):
propertiesLine += " " + strings.TrimLeft(line, "//")
case strings.HasPrefix(line, "//-"):
propertiesLine += " " + strings.TrimLeft(line, "//-")
default:
bodyFields := strings.Fields(line)
name = bodyFields[0]
for _, rawProperty := range bodyFields[1 : len(bodyFields)-2] {
propertyParts := strings.Split(rawProperty, ":")
property := &Property{
Name: propertyParts[0],
Type: propertyParts[1],
}
properties = append(properties, property)
}
class = strings.TrimRight(bodyFields[len(bodyFields)-1], ";")
break Loop
}
}
rawProperties := strings.Split(propertiesLine, "@")
for _, rawProperty := range rawProperties[1:] {
name, value := parseProperty(rawProperty)
switch {
case name == "description":
description = value
default:
name = strings.TrimPrefix(name, "param_")
property := getProperty(properties, name)
property.Description = value
}
}
return name, description, class, properties, strings.Contains(description, "Can be called synchronously")
}
func parseProperty(str string) (string, string) {
strParts := strings.Fields(str)
return strParts[0], strings.Join(strParts[1:], " ")
}
func getProperty(properties []*Property, name string) *Property {
for _, property := range properties {
if property.Name == name {
return property
}
}
return nil
}

43
tlparser/type.go 100644
View File

@ -0,0 +1,43 @@
package tlparser
type Schema struct {
Types []*Type `json:"types"`
Classes []*Class `json:"classes"`
Functions []*Function `json:"functions"`
}
type Type struct {
Name string `json:"name"`
Description string `json:"description"`
Class string `json:"class"`
Properties []*Property `json:"properties"`
}
type Class struct {
Name string `json:"name"`
Description string `json:"description"`
}
type FunctionType int
const (
FUNCTION_TYPE_UNKNOWN FunctionType = iota
FUNCTION_TYPE_COMMON
FUNCTION_TYPE_USER
FUNCTION_TYPE_BOT
)
type Function struct {
Name string `json:"name"`
Description string `json:"description"`
Class string `json:"class"`
Properties []*Property `json:"properties"`
IsSynchronous bool `json:"is_synchronous"`
Type FunctionType `json:"type"`
}
type Property struct {
Name string `json:"name"`
Type string `json:"type"`
Description string `json:"description"`
}