initial commit

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

233
client/authorization.go Normal file
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 Normal file
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 Normal file
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 Executable file

File diff suppressed because it is too large Load diff

66
client/listener.go Normal file
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
}

52
client/puller/chat.go Normal file
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
}
}
}

62
client/puller/chats.go Normal file
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
}
}
}

7
client/puller/error.go Normal file
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 Normal file
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"

14
client/tdjson_dynamic.go Normal file
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"

12
client/tdjson_static.go Normal file
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 Normal file
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 Executable file

File diff suppressed because it is too large Load diff

13356
client/unmarshaler.go Executable file

File diff suppressed because it is too large Load diff