From 79cee88f86b9c267417b6a84b0dfd4718658bf59 Mon Sep 17 00:00:00 2001 From: Amarnathcjd Date: Sun, 9 Oct 2022 15:49:08 +0530 Subject: [PATCH] Push V1.0.0 --- errors.go | 4 +- examples/auth/userauth.go | 51 +- examples/bot/bot.go | 108 --- examples/bot/echobot.go | 45 ++ examples/getchats/getchats.go | 40 ++ examples/inline/inlinebot.go | 38 ++ handshake.go | 6 +- internal/utils/logging.go | 69 ++ mtproto.go | 42 +- network.go | 3 - telegram/auth.go | 3 + telegram/buttons.go | 32 + telegram/callbackquery.go | 13 +- telegram/channels.go | 494 ++++++++++++++ telegram/client.go | 19 +- telegram/const.go | 7 +- telegram/formatting.go | 18 +- telegram/generate.go | 7 - telegram/helpers.go | 11 +- telegram/inlinequery.go | 12 +- telegram/media.go | 475 +++++++++++++ telegram/messages.go | 831 +++++++++++++++++++++++ telegram/methods.go | 1183 --------------------------------- telegram/newmessage.go | 12 +- telegram/types.go | 242 ------- telegram/updates.go | 19 +- telegram/uploader.go | 432 ------------ telegram/users.go | 106 +++ telegram/utils.go | 66 +- 29 files changed, 2265 insertions(+), 2123 deletions(-) delete mode 100644 examples/bot/bot.go create mode 100644 examples/bot/echobot.go create mode 100644 examples/getchats/getchats.go create mode 100644 examples/inline/inlinebot.go create mode 100644 internal/utils/logging.go create mode 100644 telegram/channels.go delete mode 100644 telegram/generate.go create mode 100644 telegram/media.go create mode 100644 telegram/messages.go delete mode 100644 telegram/methods.go delete mode 100644 telegram/uploader.go create mode 100644 telegram/users.go diff --git a/errors.go b/errors.go index e834b6ab..691a8a4f 100644 --- a/errors.go +++ b/errors.go @@ -422,10 +422,10 @@ var errorMessages = map[string]string{ "SCHEDULE_DATE_TOO_LATE": "The date you tried to schedule is too far in the future (last known limit of 1 year and a few hours)", "SCHEDULE_STATUS_PRIVATE": "You cannot schedule a message until the person comes online if their privacy does not show this information", "SCHEDULE_TOO_MUCH": "You cannot schedule more messages in this chat (last known limit of 100 per chat)", - "SCORE_INVALID": "", + "SCORE_INVALID": "The given game score is invalid", "SEARCH_QUERY_EMPTY": "The search query is empty", "SECONDS_INVALID": "Slow mode only supports certain values (e.g. 0, 10s, 30s, 1m, 5m, 15m and 1h)", - "SEND_AS_PEER_INVALID": "", + "SEND_AS_PEER_INVALID": "The sendAs Peer is Invalid or cannot be used as Bot", "SEND_CODE_UNAVAILABLE": "", "SEND_MESSAGE_MEDIA_INVALID": "The message media was invalid or not specified", "SEND_MESSAGE_TYPE_INVALID": "The message type is invalid", diff --git a/examples/auth/userauth.go b/examples/auth/userauth.go index 7e849f84..1d1ae35f 100644 --- a/examples/auth/userauth.go +++ b/examples/auth/userauth.go @@ -7,47 +7,32 @@ import ( ) const ( - appID = 6 - appHash = "" + appID = 6 + appHash = "YOUR_APP_HASH" + phoneNum = "YOUR_PHONE_NUMBER" ) func main() { - client, err := telegram.TelegramClient(telegram.ClientConfig{ - AppID: appID, - AppHash: appHash, - StringSession: "", // (if this value is specified, client.Login is not Necessary.) + // Create a new client + client, _ := telegram.TelegramClient(telegram.ClientConfig{ + AppID: appID, + AppHash: appHash, + LogLevel: telegram.LogInfo, + // StringSession: "", // Uncomment this line to use string session }) - if err != nil { + // Authenticate the client using the bot token + // This will send a code to the phone number if it is not already authenticated + if _, err := client.Login(phoneNum); err != nil { panic(err) } - // Login with phone number, if you have a string session, you can skip this step. - // Code AuthFlow implemented - phoneNumber := "+1234567890" - if authed, err := client.Login(phoneNumber); !authed { - panic(err) - } - - stringSession := client.ExportSession() - fmt.Println("String Session: ", stringSession) - - me, _ := client.GetMe() - fmt.Printf("Logged in as %s\n", me.Username) - - m, err := client.GetMessages("durov", &telegram.SearchOption{Limit: 1}) + // Do something with the client + // ... + me, err := client.GetMe() if err != nil { - fmt.Println(err) - } else { - fmt.Println(m[0].Marshal()) + panic(err) } - - // Add handlers - client.AddMessageHandler(".start", Start) - client.Idle() // Blocks until client.Stop() is called -} - -func Start(m *telegram.NewMessage) error { - _, err := m.Edit("Hello World") - return err + client.SendMessage("me", fmt.Sprintf("Hello, %s!", me.FirstName)) + fmt.Println("Logged in as", me.Username) } diff --git a/examples/bot/bot.go b/examples/bot/bot.go deleted file mode 100644 index df462814..00000000 --- a/examples/bot/bot.go +++ /dev/null @@ -1,108 +0,0 @@ -package examples - -import ( - "fmt" - "time" - - "github.com/amarnathcjd/gogram/telegram" -) - -const ( - appID = 6 - appHash = "" - botToken = "" -) - -func main() { - client, err := telegram.TelegramClient(telegram.ClientConfig{ - AppID: appID, - AppHash: appHash, - StringSession: "", // (if this value is specified, client.Login is not Necessary.) - }) - - if err != nil { - panic(err) - } - - // Login with bot token, if you have a string session, you can skip this step. - err = client.LoginBot(botToken) - if err != nil { - panic(err) - } - - stringSession := client.ExportSession() - fmt.Println("String Session: ", stringSession) - - me, _ := client.GetMe() - fmt.Printf("Logged in as %s\n", me.Username) - - // Add handlers - client.AddMessageHandler("/start", Start) - client.AddMessageHandler("/download", DownloadFile) - client.AddMessageHandler("/upload", UploadFile) - client.AddInlineHandler("test", InlineQuery) - - client.Idle() // Blocks until client.Stop() is called -} - -func Start(m *telegram.NewMessage) error { - _, err := m.Reply("Hello World") - return err -} - -func InlineQuery(m *telegram.InlineQuery) error { - var b = m.Builder() - b.Article( - "Test Article Title", - "Test Article Description", - "Test Article Text", - ) - _, err := m.Answer(b.Results()) - return err -} - -func DownloadFile(m *telegram.NewMessage) error { - if !m.IsMedia() { - m.Reply("Not Media!") - } - var p = telegram.Progress{} - e, _ := m.Reply("Downloading...") - go func() { - for range time.NewTicker(time.Second * 3).C { - if p.Percentage() == 100 { - e.Edit(fmt.Sprintf("Downloaded... %v%%", p.Percentage())) - break - } - e.Edit(fmt.Sprintf("Download...\nProgress %v", p.Percentage())) - } - }() - _, err := m.Download(&telegram.DownloadOptions{Progress: &p}) - return err -} - -func UploadFile(m *telegram.NewMessage) error { - if m.Args() == "" { - m.Reply("Please specify a file path!") - } - message, _ := m.Reply("Uploading...") - defer message.Delete() - startTime := time.Now() - p := telegram.Progress{} - p.Init() - go func() { - for range time.NewTicker(time.Second * 3).C { - if p.Percentage() == 100 { - message.Edit(fmt.Sprintf("Uploaded... %v%%", p.Percentage())) - break - } - message.Edit(fmt.Sprintf("Uploading...\nProgress %v", p.Percentage())) - } - }() - media, err := m.Client.UploadFile(m.Args(), &telegram.UploadOptions{Progress: &p, Threaded: true, Threads: 10}) - if err != nil { - return err - } - message.Edit(fmt.Sprintf("Uploaded in %v", time.Since(startTime))) - _, e := m.RespondMedia(media) - return e -} diff --git a/examples/bot/echobot.go b/examples/bot/echobot.go new file mode 100644 index 00000000..fa648e09 --- /dev/null +++ b/examples/bot/echobot.go @@ -0,0 +1,45 @@ +package examples + +import ( + "fmt" + + "github.com/amarnathcjd/gogram/telegram" +) + +const ( + appID = 6 + appHash = "YOUR_APP_HASH" + botToken = "YOUR_BOT_TOKEN" +) + +func main() { + // Create a new client + client, _ := telegram.TelegramClient(telegram.ClientConfig{ + AppID: appID, + AppHash: appHash, + LogLevel: telegram.LogInfo, + }) + + // Authenticate the client using the bot token + if err := client.LoginBot(botToken); err != nil { + panic(err) + } + + // Add a message handler + client.AddMessageHandler(telegram.OnNewMessage, func(message *telegram.NewMessage) error { + var ( + err error + ) + // Print the message + fmt.Println(message.Marshal()) + + // Send a message + if message.IsPrivate() { + _, err = message.Respond(message) + } + return err + }) + + // Start polling + client.Idle() +} diff --git a/examples/getchats/getchats.go b/examples/getchats/getchats.go new file mode 100644 index 00000000..352c30aa --- /dev/null +++ b/examples/getchats/getchats.go @@ -0,0 +1,40 @@ +package examples + +import ( + "fmt" + + "github.com/amarnathcjd/gogram/telegram" +) + +const ( + appID = 6 + appHash = "YOUR_APP_HASH" + phoneNum = "YOUR_PHONE_NUMBER" +) + +func main() { + // Create a new client + client, _ := telegram.TelegramClient(telegram.ClientConfig{ + AppID: appID, + AppHash: appHash, + LogLevel: telegram.LogInfo, + // StringSession: "", // Uncomment this line to use string session + }) + + // Authenticate the client using the bot token + // This will send a code to the phone number if it is not already authenticated + if _, err := client.Login(phoneNum); err != nil { + panic(err) + } + + dialogs, err := client.GetDialogs() + if err != nil { + panic(err) + } + for _, dialog := range dialogs { + switch d := dialog.(type) { + case *telegram.DialogObj: + fmt.Println(d.TopMessage) + } + } +} diff --git a/examples/inline/inlinebot.go b/examples/inline/inlinebot.go new file mode 100644 index 00000000..7e18f203 --- /dev/null +++ b/examples/inline/inlinebot.go @@ -0,0 +1,38 @@ +package examples + +import ( + "github.com/amarnathcjd/gogram/telegram" +) + +const ( + appID = 6 + appHash = "YOUR_APP_HASH" + botToken = "YOUR_BOT_TOKEN" +) + +func main() { + // Create a new client + client, _ := telegram.TelegramClient(telegram.ClientConfig{ + AppID: appID, + AppHash: appHash, + LogLevel: telegram.LogInfo, + }) + + // Authenticate the client using the bot token + if err := client.LoginBot(botToken); err != nil { + panic(err) + } + + // Add a inline query handler + client.AddInlineHandler(telegram.OnInlineQuery, HelloWorld) + + // Start polling + client.Idle() +} + +func HelloWorld(i *telegram.InlineQuery) error { + builder := i.Builder() + builder.Article("Hello World", "Hello World", "This is a test article") + _, err := i.Answer(builder.Results()) + return err +} diff --git a/handshake.go b/handshake.go index 2388d9a4..d14537d7 100644 --- a/handshake.go +++ b/handshake.go @@ -56,7 +56,7 @@ func (m *MTProto) makeAuthKey() error { NewNonce: nonceSecond, }) if err != nil { - log.Printf("TgCrypto - makeAuthKey: %s", err) + m.Logger.Warn(fmt.Sprintf("TgCrypto - makeAuthKey: %s", err)) return err } @@ -82,7 +82,7 @@ func (m *MTProto) makeAuthKey() error { return fmt.Errorf("reqDHParams: server nonce mismatch") } - // check of hash, trandom bytes trail removing occurs in this func already + // check of hash, random bytes trail removing occurs in this func already decodedMessage := ige.DecryptMessageWithTempKeys(dhParams.EncryptedAnswer, nonceSecond.Int, nonceServer.Int) data, err := tl.DecodeUnknownObject(decodedMessage) if err != nil { @@ -162,7 +162,7 @@ func (m *MTProto) makeAuthKey() error { if !m.memorySession { err = m.SaveSession() if err != nil { - log.Printf("TgCrypto - savesession: %s", err) + m.Logger.Error(fmt.Sprintf("TgCrypto - makeAuthKey: %s", err)) } } return err diff --git a/internal/utils/logging.go b/internal/utils/logging.go new file mode 100644 index 00000000..958acf2e --- /dev/null +++ b/internal/utils/logging.go @@ -0,0 +1,69 @@ +package utils + +import "log" + +const ( + // DebugLevel is the lowest level of logging + DebugLevel = iota + // InfoLevel is the second lowest level of logging + InfoLevel = iota + // WarnLevel is the third highest level of logging + WarnLevel = iota + // ErrorLevel is the highest level of logging + ErrorLevel = iota +) + +// Logger is the logging struct. +type Logger struct { + Level int + Prefix string +} + +// SetLevelString sets the level string +func (l *Logger) SetLevel(level string) *Logger { + switch level { + case "debug": + l.Level = DebugLevel + case "info": + l.Level = InfoLevel + case "warn": + l.Level = WarnLevel + case "error": + l.Level = ErrorLevel + default: + l.Level = InfoLevel + } + return l +} + +// Log logs a message at the given level. +func (l *Logger) Error(msg interface{}) { + if l.Level <= ErrorLevel { + log.Printf("%s - ERROR - %s", l.Prefix, msg) + } +} + +func (l *Logger) Warn(msg interface{}) { + if l.Level <= WarnLevel { + log.Printf("%s - WARN - %s", l.Prefix, msg) + } +} + +func (l *Logger) Info(msg interface{}) { + if l.Level <= InfoLevel { + log.Printf("%s - INFO - %s", l.Prefix, msg) + } +} + +func (l *Logger) Debug(msg interface{}) { + if l.Level <= DebugLevel { + log.Printf("%s - DEBUG - %s", l.Prefix, msg) + } +} + +// NewLogger returns a new Logger instance. +func NewLogger(prefix string) *Logger { + return &Logger{ + Prefix: prefix, + } +} diff --git a/mtproto.go b/mtproto.go index b66247cd..2395136a 100644 --- a/mtproto.go +++ b/mtproto.go @@ -7,7 +7,6 @@ import ( "crypto/rsa" "fmt" "io" - "log" "os" "path/filepath" "reflect" @@ -31,7 +30,6 @@ var wd, _ = os.Getwd() type MTProto struct { Addr string - AppID int32 transport transport.Transport stopRoutines context.CancelFunc routineswg sync.WaitGroup @@ -60,7 +58,7 @@ type MTProto struct { serviceChannel chan tl.Object serviceModeActivated bool - Logger *log.Logger + Logger *utils.Logger serverRequestHandlers []customHandlerFunc } @@ -76,7 +74,7 @@ type Config struct { ServerHost string PublicKey *rsa.PublicKey DataCenter int - AppID int32 + LogLevel string } func NewMTProto(c Config) (*MTProto, error) { @@ -112,8 +110,7 @@ func NewMTProto(c Config) (*MTProto, error) { responseChannels: utils.NewSyncIntObjectChan(), expectedTypes: utils.NewSyncIntReflectTypes(), serverRequestHandlers: make([]customHandlerFunc, 0), - Logger: log.New(os.Stderr, "MTProto - ", log.LstdFlags), - AppID: c.AppID, + Logger: utils.NewLogger("MTProto").SetLevel(c.LogLevel), memorySession: c.MemorySession, } if c.StringSession != "" { @@ -163,7 +160,6 @@ func (m *MTProto) ReconnectToNewDC(dc int) (*MTProto, error) { newAddr := utils.DcList[dc] m.sessionStorage.Delete() cfg := Config{ - AppID: m.AppID, DataCenter: dc, PublicKey: m.PublicKey, ServerHost: newAddr, @@ -173,7 +169,7 @@ func (m *MTProto) ReconnectToNewDC(dc int) (*MTProto, error) { sender, _ := NewMTProto(cfg) sender.serverRequestHandlers = m.serverRequestHandlers m.stopRoutines() - m.Logger.Println("User Migrated to DC", dc) + m.Logger.Info(fmt.Sprintf("User Migrated to new DC: %d", dc)) err := sender.CreateConnection(true) if err != nil { return nil, fmt.Errorf("creating connection: %w", err) @@ -184,7 +180,6 @@ func (m *MTProto) ReconnectToNewDC(dc int) (*MTProto, error) { func (m *MTProto) ExportNewSender(dcID int, mem bool) (*MTProto, error) { newAddr := utils.DcList[dcID] cfg := Config{ - AppID: m.AppID, DataCenter: dcID, PublicKey: m.PublicKey, ServerHost: newAddr, @@ -195,7 +190,7 @@ func (m *MTProto) ExportNewSender(dcID int, mem bool) (*MTProto, error) { cfg.SessionStorage = m.sessionStorage } sender, _ := NewMTProto(cfg) - m.Logger.Println("Exporting new sender for DC", dcID) + m.Logger.Info(fmt.Sprintf("Exporting new sender for DC %d", dcID)) err := sender.CreateConnection(true) if err != nil { return nil, fmt.Errorf("creating connection: %w", err) @@ -217,14 +212,14 @@ func (m *MTProto) CreateConnection(withLog bool) error { ctx, cancelfunc := context.WithCancel(context.Background()) m.stopRoutines = cancelfunc if withLog { - m.Logger.Printf("Connecting to %s/TcpFull...", m.Addr) + m.Logger.Info(fmt.Sprintf("Connecting to %s/TcpFull...", m.Addr)) } err := m.connect(ctx) if err != nil { return err } if withLog { - m.Logger.Printf("Connection to %s/TcpFull complete!", m.Addr) + m.Logger.Info(fmt.Sprintf("Connection to %s/TcpFull complete!", m.Addr)) } m.startReadingResponses(ctx) @@ -270,7 +265,7 @@ func (m *MTProto) makeRequest(data tl.Object, expectedTypes ...reflect.Type) (an case *objects.RpcError: realErr := RpcErrorToNative(r).(*ErrResponseCode) if strings.Contains(realErr.Message, "FLOOD_WAIT_") { - m.Logger.Printf("Flood wait detected on %s, retrying in %d seconds", strings.ReplaceAll(reflect.TypeOf(data).Elem().Name(), "Params", ""), realErr.AdditionalInfo.(int)) + m.Logger.Info(fmt.Sprintf("Flood wait detected on %s, retrying in %d seconds", strings.ReplaceAll(reflect.TypeOf(data).Elem().Name(), "Params", ""), realErr.AdditionalInfo.(int))) time.Sleep(time.Duration(realErr.AdditionalInfo.(int)) * time.Second) return m.makeRequest(data, expectedTypes...) } @@ -300,7 +295,7 @@ func (m *MTProto) Disconnect() error { func (m *MTProto) Terminate() error { m.stopRoutines() m.responseChannels.Close() - m.Logger.Printf("Disconnecting Borrowed Sender from %s/TcpFull...", m.Addr) + m.Logger.Info(fmt.Sprintf("Disconnecting Borrowed Sender from %s/TcpFull...", m.Addr)) return nil } @@ -310,12 +305,12 @@ func (m *MTProto) Reconnect(WithLogs bool) error { return errors.Wrap(err, "disconnecting") } if WithLogs { - m.Logger.Printf("Reconnecting to %s/TcpFull...", m.Addr) + m.Logger.Info(fmt.Sprintf("Reconnecting to %s/TcpFull...", m.Addr)) } err = m.CreateConnection(WithLogs) if err == nil && WithLogs { - m.Logger.Printf("Connected to %s/TcpFull complete!", m.Addr) + m.Logger.Info(fmt.Sprintf("Connected to %s/TcpFull complete!", m.Addr)) } m.InvokeRequestWithoutUpdate(&utils.PingParams{ PingID: 123456789, @@ -345,7 +340,7 @@ func (m *MTProto) startPinging(ctx context.Context) { case <-ticker.C: _, err := m.ping(0xCADACADA) if err != nil { - m.Logger.Printf("ping unsuccessfull: %v", err) + m.Logger.Info(fmt.Sprintf("ping unsuccessfull: %v", err)) } } } @@ -369,7 +364,7 @@ func (m *MTProto) startReadingResponses(ctx context.Context) { case io.EOF: err = m.Reconnect(false) if err != nil { - m.Logger.Println("reconnecting error:", err) + m.Logger.Error(errors.Wrap(err, "reconnecting")) } return @@ -377,12 +372,11 @@ func (m *MTProto) startReadingResponses(ctx context.Context) { if strings.Contains(err.Error(), "required to reconnect!") { err = m.Reconnect(false) if err != nil { - - m.Logger.Println("reconnecting error:", err) + m.Logger.Error(errors.Wrap(err, "reconnecting error")) } return } else { - m.Logger.Println("reading error:", err) + m.Logger.Error(errors.Wrap(err, "reading message")) } } } @@ -470,7 +464,7 @@ messageTypeSwitching: if !m.memorySession { err := m.SaveSession() if err != nil { - m.Logger.Println(errors.Wrap(err, "saving session")) + m.Logger.Error(errors.Wrap(err, "saving session")) } } @@ -506,7 +500,7 @@ messageTypeSwitching: } } if !processed { - m.Logger.Println(errors.New("got nonsystem message from server: " + reflect.TypeOf(message).String())) + m.Logger.Error(errors.New("got nonsystem message from server: " + reflect.TypeOf(message).String())) } } @@ -527,7 +521,7 @@ func (m *MTProto) SwitchDC(dc int) error { } m.Addr = newIP - m.Logger.Printf("Reconnecting to new data center %v", dc) + m.Logger.Info(fmt.Sprintf("Reconnecting to new data center %v", dc)) m.encrypted = false err := m.Reconnect(true) if err != nil { diff --git a/network.go b/network.go index 612221e7..2eb3e0de 100644 --- a/network.go +++ b/network.go @@ -71,12 +71,9 @@ func (m *MTProto) sendPacket(request tl.Object, expectedTypes ...reflect.Type) ( func (m *MTProto) writeRPCResponse(msgID int, data tl.Object) error { v, ok := m.responseChannels.Get(msgID) if !ok { - log.Printf("Network - no response channel for message %d", msgID) return fmt.Errorf("no response channel for message %d", msgID) } - v <- data - m.responseChannels.Delete(msgID) m.expectedTypes.Delete(msgID) return nil diff --git a/telegram/auth.go b/telegram/auth.go index 8d02fd9a..9b083cbf 100644 --- a/telegram/auth.go +++ b/telegram/auth.go @@ -136,6 +136,9 @@ func (c *Client) Login(phoneNumber string, options ...*LoginOptions) (bool, erro fmt.Println("Invalid response, try again") } } + } else { + return false, nil + // TODO: implement } AuthResultSwitch: switch auth := Auth.(type) { diff --git a/telegram/buttons.go b/telegram/buttons.go index 38f313e5..badaa08e 100644 --- a/telegram/buttons.go +++ b/telegram/buttons.go @@ -61,3 +61,35 @@ func (b Button) Keyboard(Rows ...*KeyboardButtonRow) *ReplyInlineMarkup { func (b Button) Clear() *ReplyKeyboardHide { return &ReplyKeyboardHide{} } + +type ButtonID struct { + Data []byte + Text string +} + +// In Beta +func (m *NewMessage) Click(o ...*ButtonID) (*MessagesBotCallbackAnswer, error) { + if m.ReplyMarkup() != nil { + switch mark := (*m.ReplyMarkup()).(type) { + case *ReplyInlineMarkup: + for _, row := range mark.Rows { + for _, button := range row.Buttons { + switch b := button.(type) { + case *KeyboardButtonCallback: + for _, id := range o { + if string(id.Data) == string(b.Data) || id.Text == b.Text { + return m.Client.MessagesGetBotCallbackAnswer(&MessagesGetBotCallbackAnswerParams{ + Peer: m.Peer, + MsgID: m.ID, + Data: b.Data, + Game: false, + }) + } + } + } + } + } + } + } + return nil, nil +} diff --git a/telegram/callbackquery.go b/telegram/callbackquery.go index 2e88b75c..bc107f2c 100644 --- a/telegram/callbackquery.go +++ b/telegram/callbackquery.go @@ -123,8 +123,8 @@ func (b *CallbackQuery) Edit(Text interface{}, options ...*SendOptions) (*NewMes return b.Client.EditMessage(b.Peer, b.MessageID, Text, &opts) } -func (b *CallbackQuery) Delete() error { - return b.Client.DeleteMessage(b.Peer, b.MessageID) +func (b *CallbackQuery) Delete() (*MessagesAffectedMessages, error) { + return b.Client.DeleteMessages(b.Peer, []int32{b.MessageID}) } func (b *CallbackQuery) Reply(Text interface{}, options ...*SendOptions) (*NewMessage, error) { @@ -166,7 +166,14 @@ func (b *CallbackQuery) ForwardTo(ChatID int64, options ...*ForwardOptions) (*Ne if len(options) > 0 { opts = *options[0] } - return b.Client.ForwardMessage(b.Peer, ChatID, []int32{b.MessageID}, &opts) + m, err := b.Client.Forward(b.Peer, ChatID, []int32{b.MessageID}, &opts) + if err != nil { + return nil, err + } + if len(m) == 0 { + return nil, fmt.Errorf("message not found") + } + return &m[0], nil } func (b *CallbackQuery) Marshal() string { diff --git a/telegram/channels.go b/telegram/channels.go new file mode 100644 index 00000000..9888c673 --- /dev/null +++ b/telegram/channels.go @@ -0,0 +1,494 @@ +package telegram + +import "github.com/pkg/errors" + +// GetChatPhotos returns the profile photos of a chat +// Params: +// - chatID: The ID of the chat +// - limit: The maximum number of photos to be returned +func (c *Client) GetChatPhotos(chatID interface{}, limit ...int32) ([]Photo, error) { + if limit == nil { + limit = []int32{1} + } + messages, err := c.GetMessages(chatID, &SearchOption{Limit: limit[0], + Filter: &InputMessagesFilterChatPhotos{}}) + if err != nil { + return nil, err + } + var photos []Photo + for _, message := range messages { + if message.Action != nil { + switch action := message.Action.(type) { + case *MessageActionChatEditPhoto: + photos = append(photos, action.Photo) + case *MessageActionChatDeletePhoto: + } + } + } + return photos, nil +} + +// GetChatPhoto returns the current chat photo +// Params: +// - chatID: chat id +func (c *Client) GetChatPhoto(chatID interface{}) (Photo, error) { + photos, err := c.GetChatPhotos(chatID) + if err != nil { + return &PhotoObj{}, err + } + if len(photos) > 0 { + return photos[0], nil + } + return &PhotoObj{}, nil // GetFullChannel TODO +} + +// JoinChannel joins a channel or chat by its username or id +// Params: +// - Channel: the username or id of the channel or chat +func (c *Client) JoinChannel(Channel interface{}) error { + switch p := Channel.(type) { + case string: + if TG_JOIN_RE.MatchString(p) { + _, err := c.MessagesImportChatInvite(TG_JOIN_RE.FindStringSubmatch(p)[2]) + if err != nil { + return err + } + } + default: + channel, err := c.GetSendablePeer(Channel) + if err != nil { + return err + } + if chat, ok := channel.(*InputPeerChannel); ok { + _, err = c.ChannelsJoinChannel(&InputChannelObj{ChannelID: chat.ChannelID, AccessHash: chat.AccessHash}) + if err != nil { + return err + } + } else if chat, ok := channel.(*InputPeerChat); ok { + _, err = c.MessagesAddChatUser(chat.ChatID, &InputUserEmpty{}, 0) + if err != nil { + return err + } + } else { + return errors.New("peer is not a channel or chat") + } + } + return nil +} + +// LeaveChannel leaves a channel or chat +// Params: +// - Channel: Channel or chat to leave +// - Revoke: If true, the channel will be deleted +func (c *Client) LeaveChannel(Channel interface{}, Revoke ...bool) error { + revokeChat := getVariadic(Revoke, false).(bool) + channel, err := c.GetSendablePeer(Channel) + if err != nil { + return err + } + if chat, ok := channel.(*InputPeerChannel); ok { + _, err = c.ChannelsLeaveChannel(&InputChannelObj{ChannelID: chat.ChannelID, AccessHash: chat.AccessHash}) + if err != nil { + return err + } + } else if chat, ok := channel.(*InputPeerChat); ok { + _, err = c.MessagesDeleteChatUser(revokeChat, chat.ChatID, &InputUserEmpty{}) + if err != nil { + return err + } + } else { + return errors.New("peer is not a channel or chat") + } + return nil +} + +const ( + Admin = "admin" + Creator = "creator" + Member = "member" + Restricted = "restricted" + Left = "left" + Kicked = "kicked" +) + +type Participant struct { + User *UserObj `json:"user,omitempty"` + Participant ChannelParticipant `json:"participant,omitempty"` + Status string `json:"status,omitempty"` + Rights *ChatAdminRights `json:"rights,omitempty"` + Rank string `json:"rank,omitempty"` +} + +// GetChatMember returns the members of a chat +// Params: +// - chatID: The ID of the chat +// - userID: The ID of the user +func (c *Client) GetChatMember(chatID interface{}, userID interface{}) (*Participant, error) { + channel, err := c.GetSendablePeer(chatID) + if err != nil { + return nil, err + } + user, err := c.GetSendablePeer(userID) + if err != nil { + return nil, err + } + chat, ok := channel.(*InputPeerChannel) + if !ok { + return nil, errors.New("peer is not a channel") + } + participant, err := c.ChannelsGetParticipant(&InputChannelObj{ChannelID: chat.ChannelID, AccessHash: chat.AccessHash}, user) + if err != nil { + return nil, err + } + c.Cache.UpdatePeersToCache(participant.Users, participant.Chats) + var ( + status string = Member + rights *ChatAdminRights = &ChatAdminRights{} + rank string = "" + UserID int64 = 0 + ) + switch p := participant.Participant.(type) { + case *ChannelParticipantCreator: + status = Creator + rights = p.AdminRights + rank = p.Rank + UserID = p.UserID + case *ChannelParticipantAdmin: + status = Admin + rights = p.AdminRights + rank = p.Rank + UserID = p.UserID + case *ChannelParticipantObj: + status = Member + case *ChannelParticipantSelf: + status = Member + UserID = p.UserID + case *ChannelParticipantBanned: + status = Restricted + UserID = c.GetPeerID(p.Peer) + case *ChannelParticipantLeft: + status = Left + UserID = c.GetPeerID(p.Peer) + } + partUser, err := c.Cache.GetUser(UserID) + if err != nil { + return nil, err + } + return &Participant{ + User: partUser, + Participant: participant.Participant, + Status: status, + Rights: rights, + Rank: rank, + }, nil +} + +type ParticipantOptions struct { + Query string `json:"query,omitempty"` + Filter ChannelParticipantsFilter `json:"filter,omitempty"` + Offset int32 `json:"offset,omitempty"` + Limit int32 `json:"limit,omitempty"` +} + +// GetChatMembers returns the members of a chat +// Params: +// - chatID: The ID of the chat +// - filter: The filter to use +// - offset: The offset to use +// - limit: The limit to use +func (c *Client) GetChatMembers(chatID interface{}, Opts ...*ParticipantOptions) ([]*Participant, int32, error) { + channel, err := c.GetSendablePeer(chatID) + if err != nil { + return nil, 0, err + } + chat, ok := channel.(*InputPeerChannel) + if !ok { + return nil, 0, errors.New("peer is not a channel") + } + opts := getVariadic(Opts, &ParticipantOptions{Filter: &ChannelParticipantsSearch{}, Limit: 1}).(*ParticipantOptions) + if opts.Query != "" { + opts.Filter = &ChannelParticipantsSearch{Q: opts.Query} + } + participants, err := c.ChannelsGetParticipants(&InputChannelObj{ChannelID: chat.ChannelID, AccessHash: chat.AccessHash}, opts.Filter, opts.Offset, opts.Limit, 0) + if err != nil { + return nil, 0, err + } + cParts, ok := participants.(*ChannelsChannelParticipantsObj) + if !ok { + return nil, 0, errors.New("could not get participants") + } + c.Cache.UpdatePeersToCache(cParts.Users, cParts.Chats) + var ( + status string = Member + rights *ChatAdminRights = &ChatAdminRights{} + rank string = "" + UserID int64 = 0 + ) + participantsList := make([]*Participant, 0) + for _, p := range cParts.Participants { + switch p := p.(type) { + case *ChannelParticipantCreator: + status = Creator + rights = p.AdminRights + rank = p.Rank + UserID = p.UserID + case *ChannelParticipantAdmin: + status = Admin + rights = p.AdminRights + rank = p.Rank + UserID = p.UserID + case *ChannelParticipantObj: + status = Member + case *ChannelParticipantSelf: + status = Member + UserID = p.UserID + case *ChannelParticipantBanned: + status = Restricted + UserID = c.GetPeerID(p.Peer) + case *ChannelParticipantLeft: + status = Left + UserID = c.GetPeerID(p.Peer) + } + partUser, err := c.Cache.GetUser(UserID) + if err != nil { + return nil, 0, err + } + participantsList = append(participantsList, &Participant{ + User: partUser, + Participant: p, + Status: status, + Rights: rights, + Rank: rank, + }) + } + return participantsList, cParts.Count, nil +} + +type AdminOptions struct { + IsAdmin bool `json:"is_admin,omitempty"` + Rights *ChatAdminRights `json:"rights,omitempty"` + Rank string `json:"rank,omitempty"` +} + +// Edit Admin rights of a user in a chat, +// returns true if successfull +func (c *Client) EditAdmin(PeerID interface{}, UserID interface{}, Opts ...*AdminOptions) (bool, error) { + opts := getVariadic(Opts, &AdminOptions{IsAdmin: true, Rights: &ChatAdminRights{}, Rank: "Admin"}).(*AdminOptions) + peer, err := c.GetSendablePeer(PeerID) + if err != nil { + return false, err + } + u, err := c.GetSendablePeer(UserID) + if err != nil { + return false, err + } + user, ok := u.(*InputPeerUser) + if !ok { + return false, errors.New("peer is not a user") + } + switch p := peer.(type) { + case *InputPeerChannel: + if opts.IsAdmin { + _, err := c.ChannelsEditAdmin(&InputChannelObj{ChannelID: p.ChannelID, AccessHash: p.AccessHash}, &InputUserObj{UserID: user.UserID, AccessHash: user.AccessHash}, opts.Rights, opts.Rank) + if err != nil { + return false, err + } + } else { + _, err := c.ChannelsEditAdmin(&InputChannelObj{ChannelID: p.ChannelID, AccessHash: p.AccessHash}, &InputUserObj{UserID: user.UserID, AccessHash: user.AccessHash}, &ChatAdminRights{}, "") + if err != nil { + return false, err + } + } + case *InputPeerChat: + _, err := c.MessagesEditChatAdmin(p.ChatID, &InputUserObj{UserID: user.UserID, AccessHash: user.AccessHash}, opts.IsAdmin) + if err != nil { + return false, err + } + default: + return false, errors.New("peer is not a chat or channel") + } + return true, nil +} + +type BannedOptions struct { + Ban bool `json:"ban,omitempty"` + Unban bool `json:"unban,omitempty"` + Mute bool `json:"mute,omitempty"` + Unmute bool `json:"unmute,omitempty"` + Rights *ChatBannedRights `json:"rights,omitempty"` +} + +// Edit Restricted rights of a user in a chat, +// returns true if successfull +func (c *Client) EditBanned(PeerID interface{}, UserID interface{}, opts ...*BannedOptions) (bool, error) { + o := getVariadic(opts, &BannedOptions{Ban: true, Rights: &ChatBannedRights{}}).(*BannedOptions) + peer, err := c.GetSendablePeer(PeerID) + if err != nil { + return false, err + } + u, err := c.GetSendablePeer(UserID) + if err != nil { + return false, err + } + switch p := peer.(type) { + case *InputPeerChannel: + if o.Ban { + o.Rights.ViewMessages = true + } + if o.Unban { + o.Rights.ViewMessages = false + } + if o.Mute { + o.Rights.SendMessages = true + } + if o.Unmute { + o.Rights.SendMessages = false + } + _, err := c.ChannelsEditBanned(&InputChannelObj{ChannelID: p.ChannelID, AccessHash: p.AccessHash}, u, o.Rights) + if err != nil { + return false, err + } + case *InputPeerChat: + // TODO: Implement + return false, errors.New("not implemented") + default: + return false, errors.New("peer is not a chat or channel") + } + return true, nil +} + +func (c *Client) KickParticipant(PeerID interface{}, UserID interface{}) (bool, error) { + peer, err := c.GetSendablePeer(PeerID) + if err != nil { + return false, err + } + u, err := c.GetSendablePeer(UserID) + if err != nil { + return false, err + } + switch p := peer.(type) { + case *InputPeerChannel: + _, err := c.EditBanned(p, u, &BannedOptions{Ban: true}) + if err != nil { + return false, err + } + _, err = c.EditBanned(p, u, &BannedOptions{Unban: true}) + if err != nil { + return false, err + } + case *InputPeerChat: + user, ok := u.(*InputPeerUser) + if !ok { + return false, errors.New("peer is not a user") + } + _, err := c.MessagesDeleteChatUser(false, c.GetPeerID(p), &InputUserObj{UserID: user.UserID, AccessHash: user.AccessHash}) + if err != nil { + return false, err + } + default: + return false, errors.New("peer is not a chat or channel") + } + return true, nil +} + +type TitleOptions struct { + LastName string `json:"last_name,omitempty"` + About string `json:"about,omitempty"` +} + +// Edit the title of a chat, channel or self, +// returns true if successfull +func (c *Client) EditTitle(PeerID interface{}, Title string, Opts ...*TitleOptions) (bool, error) { + opts := getVariadic(Opts, &TitleOptions{}).(*TitleOptions) + peer, err := c.GetSendablePeer(PeerID) + if err != nil { + return false, err + } + switch p := peer.(type) { + case *InputPeerChannel: + _, err := c.ChannelsEditTitle(&InputChannelObj{ChannelID: p.ChannelID, AccessHash: p.AccessHash}, Title) + if err != nil { + return false, err + } + case *InputPeerChat: + _, err := c.MessagesEditChatTitle(p.ChatID, Title) + if err != nil { + return false, err + } + case *InputPeerSelf: + _, err := c.AccountUpdateProfile(opts.LastName, Title, opts.About) + if err != nil { + return false, err + } + default: + return false, errors.New("peer is not a chat or channel or self") + } + return true, nil +} + +// GetStats returns the stats of the channel or message +// Params: +// - channelID: the channel ID +// - messageID: the message ID +func (c *Client) GetStats(channelID interface{}, messageID ...interface{}) (*StatsBroadcastStats, *StatsMessageStats, error) { + peerID, err := c.GetSendablePeer(channelID) + if err != nil { + return nil, nil, err + } + channelPeer, ok := peerID.(*InputPeerChannel) + if !ok { + return nil, nil, errors.New("could not convert peer to channel") + } + var MessageID int32 = getVariadic(messageID, 0).(int32) + if MessageID > 0 { + resp, err := c.StatsGetMessageStats(true, &InputChannelObj{ + ChannelID: channelPeer.ChannelID, + AccessHash: channelPeer.AccessHash, + }, MessageID) + if err != nil { + return nil, nil, err + } + return nil, resp, nil + } + resp, err := c.StatsGetBroadcastStats(true, &InputChannelObj{ + ChannelID: channelPeer.ChannelID, + AccessHash: channelPeer.AccessHash, + }) + if err != nil { + return nil, nil, err + } + return resp, nil, nil +} + +type InviteLinkOptions struct { + LegacyRevokePermanent bool `json:"legacy_revoke_permanent,omitempty"` + Expire int32 `json:"expire,omitempty"` + Limit int32 `json:"limit,omitempty"` + Title string `json:"title,omitempty"` + RequestNeeded bool `json:"request_needed,omitempty"` +} + +// GetChatInviteLink returns the invite link of a chat +// Params: +// - peerID : The ID of the chat +// - LegacyRevoke : If true, the link will be revoked +// - Expire: The time in seconds after which the link will expire +// - Limit: The maximum number of users that can join the chat using the link +// - Title: The title of the link +// - RequestNeeded: If true, join requests will be needed to join the chat +func (c *Client) GetChatInviteLink(peerID interface{}, LinkOpts ...*InviteLinkOptions) (ExportedChatInvite, error) { + LinkOptions := getVariadic(LinkOpts, &InviteLinkOptions{}).(*InviteLinkOptions) + peer, err := c.GetSendablePeer(peerID) + if err != nil { + return nil, err + } + link, err := c.MessagesExportChatInvite(&MessagesExportChatInviteParams{ + Peer: peer, + LegacyRevokePermanent: LinkOptions.LegacyRevokePermanent, + RequestNeeded: LinkOptions.RequestNeeded, + UsageLimit: LinkOptions.Limit, + Title: LinkOptions.Title, + ExpireDate: LinkOptions.Expire, + }) + return link, err +} diff --git a/telegram/client.go b/telegram/client.go index a860e1b1..ae4164e1 100644 --- a/telegram/client.go +++ b/telegram/client.go @@ -3,9 +3,7 @@ package telegram import ( - "log" "net" - "os" "path/filepath" "reflect" "runtime" @@ -18,6 +16,7 @@ import ( "github.com/amarnathcjd/gogram/internal/keys" "github.com/amarnathcjd/gogram/internal/session" + "github.com/amarnathcjd/gogram/internal/utils" ) type ( @@ -30,8 +29,7 @@ type ( ParseMode string AppID int32 ApiHash string - // Custom logger for client - L Log + Log *utils.Logger } ClientConfig struct { @@ -44,7 +42,7 @@ type ( AppHash string ParseMode string DataCenter int - AllowUpdates bool + LogLevel string } ) @@ -78,8 +76,8 @@ func TelegramClient(c ClientConfig) (*Client, error) { ServerHost: GetHostIp(dcID), PublicKey: publicKeys[0], DataCenter: dcID, - AppID: int32(c.AppID), StringSession: c.StringSession, + LogLevel: getStr(c.LogLevel, LogInfo), }) if err != nil { return nil, errors.Wrap(err, "MTProto client") @@ -95,9 +93,7 @@ func TelegramClient(c ClientConfig) (*Client, error) { config: &c, Cache: cache, ParseMode: getStr(c.ParseMode, "HTML"), - L: Log{ - Logger: log.New(os.Stdout, "", log.LstdFlags), - }, + Log: utils.NewLogger("Telegram").SetLevel("debug"), } resp, err := client.InvokeWithLayer(ApiVersion, &InitConnectionParams{ @@ -131,10 +127,7 @@ func TelegramClient(c ClientConfig) (*Client, error) { client.stop = stop client.AppID = int32(c.AppID) client.ApiHash = c.AppHash - c.AllowUpdates = true - if c.AllowUpdates { - client.AddCustomServerRequestHandler(HandleUpdate) - } + client.AddCustomServerRequestHandler(HandleUpdate) return client, nil } diff --git a/telegram/const.go b/telegram/const.go index 1f814447..3a25684a 100644 --- a/telegram/const.go +++ b/telegram/const.go @@ -4,10 +4,15 @@ import "regexp" const ( ApiVersion = 146 - Version = "v0.2.9-beta" + Version = "v1.0.0" DefaultDC = 4 + LogDebug = "debug" + LogInfo = "info" + LogWarn = "warn" + LogError = "error" + MarkDown string = "Markdown" HTML string = "HTML" MarkDownV2 string = "MarkdownV2" diff --git a/telegram/formatting.go b/telegram/formatting.go index 98a6574e..d1d8a23c 100644 --- a/telegram/formatting.go +++ b/telegram/formatting.go @@ -9,15 +9,25 @@ import ( "github.com/PuerkitoBio/goquery" ) +func parseEntities(text string, parseMode string) (entities []MessageEntity, newText string) { + switch parseMode { + case HTML: + return parseHTML(text) + case MarkDown: + return parseMarkdown(text) + } + return []MessageEntity{}, text +} + func (c *Client) FormatMessage(message string, mode string) ([]MessageEntity, string) { if mode == HTML { - return c.ParseHtml(message) + return parseHTML(message) } else { - return ParseMarkDown(message) + return parseMarkdown(message) } } -func (c *Client) ParseHtml(t string) ([]MessageEntity, string) { +func parseHTML(t string) ([]MessageEntity, string) { var entities []MessageEntity doc, err := goquery.NewDocumentFromReader(strings.NewReader(strings.TrimSpace(t))) if err != nil { @@ -208,7 +218,7 @@ func MarkdownToHTML(text string) string { } // In Beta -func ParseMarkDown(message string) (entities []MessageEntity, finalText string) { +func parseMarkdown(message string) (entities []MessageEntity, finalText string) { // regex of md md := map[string]*regexp.Regexp{ "bold": regexp.MustCompile(`\*([^\*]+)\*`), diff --git a/telegram/generate.go b/telegram/generate.go deleted file mode 100644 index f7c25a33..00000000 --- a/telegram/generate.go +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (c) 2022 RoseLoverX - -// this file is used only for go generate tool. don't touch it! - -package telegram - -//go:generate go run ../internal/cmd/tlgen ../schemes/api_latest.tl . diff --git a/telegram/helpers.go b/telegram/helpers.go index 57beeda0..ea2865d8 100644 --- a/telegram/helpers.go +++ b/telegram/helpers.go @@ -61,7 +61,7 @@ func GenRandInt() int64 { return int64(rand.Int31()) } -func (c *Client) getMultiMedia(m interface{}, attrs *CustomAttrs) ([]*InputSingleMedia, error) { +func (c *Client) getMultiMedia(m interface{}, attrs *MediaMetadata) ([]*InputSingleMedia, error) { var media []*InputSingleMedia var inputMedia []InputMedia switch m := m.(type) { @@ -317,7 +317,7 @@ func (c *Client) GetPeerID(Peer interface{}) int64 { } } -func (c *Client) getSendableMedia(mediaFile interface{}, attr *CustomAttrs) (InputMedia, error) { +func (c *Client) getSendableMedia(mediaFile interface{}, attr *MediaMetadata) (InputMedia, error) { mediaTypeSwitch: switch media := mediaFile.(type) { case string: @@ -339,6 +339,7 @@ mediaTypeSwitch: } case InputMedia: return media, nil + // case Photo: case MessageMedia: switch media := media.(type) { case *MessageMediaPhoto: @@ -591,9 +592,9 @@ func packMessage(c *Client, message Message) *NewMessage { FileID := PackBotFileID(m.Media()) m.File = &CustomFile{ FileID: FileID, - Name: GetFileName(m.Media()), - Size: GetFileSize(m.Media()), - Ext: GetFileExt(m.Media()), + Name: getFileName(m.Media()), + Size: getFileSize(m.Media()), + Ext: getFileExt(m.Media()), } } return m diff --git a/telegram/inlinequery.go b/telegram/inlinequery.go index 880a3eab..c8af5957 100644 --- a/telegram/inlinequery.go +++ b/telegram/inlinequery.go @@ -92,7 +92,7 @@ func (b *InlineBuilder) Photo(photo interface{}, options ...*ArticleOptions) Inp } else { opts = ArticleOptions{} } - Photo, _ := b.Client.getSendableMedia(photo, &CustomAttrs{}) + Photo, _ := b.Client.getSendableMedia(photo, &MediaMetadata{}) var Image InputPhoto PhotoTypeSwitch: switch p := Photo.(type) { @@ -103,10 +103,10 @@ PhotoTypeSwitch: if err != nil { Image = &InputPhotoEmpty{} } - Photo, _ = b.Client.getSendableMedia(media, &CustomAttrs{}) + Photo, _ = b.Client.getSendableMedia(media, &MediaMetadata{}) goto PhotoTypeSwitch default: - b.Client.Logger.Println("InlineBuilder.Photo: Photo is not a InputMediaPhoto") + b.Client.Logger.Warn("InlineBuilder.Photo: Photo is not a InputMediaPhoto") Image = &InputPhotoEmpty{} } e, text := b.Client.FormatMessage(opts.Caption, getValue(opts.ParseMode, b.Client.ParseMode).(string)) @@ -148,7 +148,7 @@ func (b *InlineBuilder) Document(document interface{}, options ...*ArticleOption } else { opts = ArticleOptions{} } - Document, _ := b.Client.getSendableMedia(document, &CustomAttrs{}) + Document, _ := b.Client.getSendableMedia(document, &MediaMetadata{}) var Doc InputDocument DocTypeSwitch: switch p := Document.(type) { @@ -159,10 +159,10 @@ DocTypeSwitch: if err != nil { Doc = &InputDocumentEmpty{} } - Document, _ = b.Client.getSendableMedia(media, &CustomAttrs{}) + Document, _ = b.Client.getSendableMedia(media, &MediaMetadata{}) goto DocTypeSwitch default: - b.Client.Logger.Println("InlineBuilder.Document: Document is not a InputMediaDocument") + b.Client.Logger.Warn("InlineBuilder.Document: Document is not a InputMediaDocument") Doc = &InputDocumentEmpty{} } e, text := b.Client.FormatMessage(opts.Caption, getValue(opts.ParseMode, b.Client.ParseMode).(string)) diff --git a/telegram/media.go b/telegram/media.go new file mode 100644 index 00000000..b45f7beb --- /dev/null +++ b/telegram/media.go @@ -0,0 +1,475 @@ +package telegram + +import ( + "bytes" + "crypto/md5" + "fmt" + "hash" + "io" + "io/fs" + "log" + "math/rand" + "os" + "path/filepath" + "sync" + + "github.com/pkg/errors" +) + +const ( + DEFAULT_WORKERS = 5 + DEFAULT_PARTS = 512 * 1024 +) + +type UploadOptions struct { + // Worker count for upload file. + Threads int `json:"threads,omitempty"` + // Chunk size for upload file. + ChunkSize int32 `json:"chunk_size,omitempty"` + // File name for upload file. + FileName string `json:"file_name,omitempty"` +} + +// UploadFile upload file to telegram. +// file can be string, []byte, io.Reader, fs.File +func (c *Client) UploadFile(file interface{}, Opts ...*UploadOptions) (InputFile, error) { + opts := getVariadic(Opts, &UploadOptions{}).(*UploadOptions) + u := &Uploader{ + Source: file, + Client: c, + ChunkSize: opts.ChunkSize, + Worker: opts.Threads, + } + u.Meta.Name = opts.FileName + return u.Upload() +} + +type ( + Uploader struct { + *Client + Parts int32 + ChunkSize int32 + Worker int + Source interface{} + Workers []*Client + wg *sync.WaitGroup + FileID int64 + Meta struct { + Big bool + Hash hash.Hash + Name string + Size int64 + } + } +) + +func (u *Uploader) Upload() (InputFile, error) { + u.Init() + u.Start() + return u.saveFile() +} + +func (u *Uploader) Init() error { + switch s := u.Source.(type) { + case string: + if u.Meta.Size == 0 { + fi, err := os.Stat(s) + if err != nil { + return err + } + u.Meta.Size = fi.Size() + u.Meta.Name = fi.Name() + } + case []byte: + u.Meta.Size = int64(len(s)) + case fs.File: + fi, err := s.Stat() + if err != nil { + return err + } + u.Meta.Size = fi.Size() + u.Meta.Name = fi.Name() + case io.Reader: + buff := bytes.NewBuffer([]byte{}) + u.Meta.Size, _ = io.Copy(buff, s) + u.Source = bytes.NewReader(buff.Bytes()) + } + if u.Parts == 0 { + u.Parts = int32(u.Meta.Size / DEFAULT_PARTS) + if u.Parts == 0 { + u.Parts = 1 + } + } + if u.ChunkSize == 0 { + u.ChunkSize = DEFAULT_PARTS + } + if int64(u.ChunkSize) > u.Meta.Size { + u.ChunkSize = int32(u.Meta.Size) + } + if u.Worker == 0 { + u.Worker = DEFAULT_WORKERS + } + // < 10MB + if u.Meta.Size < 10*1024*1024 { + u.Meta.Big = false + u.Meta.Hash = md5.New() + } else { + u.Meta.Big = true + } + u.FileID = GenerateRandomLong() + u.wg = &sync.WaitGroup{} + return nil +} + +func (u *Uploader) allocateWorkers() { + if u.Worker == 1 { + u.Workers = []*Client{u.Client} + return + } + for i := 0; i < u.Worker; i++ { + w, err := u.Client.ExportSender(u.Client.GetDC()) + if err != nil { + panic(err) + } + u.Workers = append(u.Workers, w) + } +} + +func (u *Uploader) saveFile() (InputFile, error) { + if u.Meta.Big { + return &InputFileBig{u.FileID, u.Parts, u.Meta.Name}, nil + } else { + return &InputFileObj{u.FileID, u.Parts, u.Meta.Name, string(u.Meta.Hash.Sum(nil))}, nil + } +} + +func (u *Uploader) DividePartsToWorkers() [][]int32 { + var ( + parts = u.Parts + worker = u.Worker + ) + if parts < int32(worker) { + worker = int(parts) + } + var ( + perWorker = parts / int32(worker) + remainder = parts % int32(worker) + ) + var ( + start = int32(0) + end = int32(0) + ) + var ( + partsToWorkers = make([][]int32, worker) + ) + for i := 0; i < worker; i++ { + end = start + perWorker + if remainder > 0 { + end++ + remainder-- + } + partsToWorkers[i] = []int32{start, end} + start = end + } + u.Worker = worker + u.Parts = parts + u.allocateWorkers() + return partsToWorkers +} + +func (u *Uploader) Start() error { + var ( + parts = u.DividePartsToWorkers() + ) + for i, w := range u.Workers { + u.wg.Add(1) + go u.uploadParts(w, parts[i]) + } + u.wg.Wait() + return nil +} + +func (u *Uploader) readPart(part int32) ([]byte, error) { + var ( + err error + ) + switch s := u.Source.(type) { + case string: + f, err := os.Open(s) + if err != nil { + return nil, err + } + defer f.Close() + _, err = f.Seek(int64(part*u.ChunkSize), 0) + if err != nil { + return nil, err + } + buf := make([]byte, u.ChunkSize) + _, err = f.Read(buf) + if err != nil { + return nil, err + } + return buf, nil + case []byte: + return s[part*u.ChunkSize : (part+1)*u.ChunkSize], nil + case fs.File: + fs, err := s.Stat() + if err != nil { + return nil, err + } + f, err := os.Open(fs.Name()) + if err != nil { + return nil, err + } + defer f.Close() + _, err = f.Seek(int64(part*u.ChunkSize), 0) + if err != nil { + return nil, err + } + buf := make([]byte, u.ChunkSize) + _, err = f.Read(buf) + if err != nil { + return nil, err + } + return buf, nil + case *bytes.Reader: + // coverted io.Reader to bytes.Reader + buf := make([]byte, u.ChunkSize) + _, err = s.ReadAt(buf, int64(part*u.ChunkSize)) + if err != nil { + return nil, err + } + return buf, nil + default: + return nil, errors.New("unknown source type") + } +} + +func (u *Uploader) uploadParts(w *Client, parts []int32) { + defer u.wg.Done() + for i := parts[0]; i < parts[1]; i++ { + buf, err := u.readPart(i) + if err != nil { + log.Println(err) + continue + } + if u.Meta.Big { + _, err = w.UploadSaveBigFilePart(u.FileID, i, u.Parts, buf) + } else { + u.Meta.Hash.Write(buf) + _, err = w.UploadSaveFilePart(u.FileID, i, buf) + } + w.Logger.Debug(fmt.Sprintf("uploaded part %d of %d", i, u.Parts)) + if err != nil { + panic(err) + } + } +} + +type DownloadOptions struct { + // Download path to save file + FileName string `json:"file_name,omitempty"` + // Datacenter ID of file + DcID int32 `json:"dc_id,omitempty"` + // Size of file + Size int32 `json:"size,omitempty"` + // Worker count to download file + Threads int `json:"threads,omitempty"` + // Chunk size to download file + ChunkSize int32 `json:"chunk_size,omitempty"` +} + +func (c *Client) DownloadMedia(file interface{}, Opts ...*DownloadOptions) (string, error) { + opts := getVariadic(Opts, &DownloadOptions{}).(*DownloadOptions) + location, dc, size, fileName, err := getFileLocation(file) + if err != nil { + return "", err + } + dc = getValue(dc, opts.DcID).(int32) + dc = getValue(dc, c.GetDC()).(int32) + size = getValue(size, int64(opts.Size)).(int64) + fileName = getValue(fileName, opts.FileName).(string) + d := &Downloader{ + Client: c, + Source: location, + FileName: fileName, + DcID: dc, + Size: int32(size), + Worker: opts.Threads, + ChunkSize: getValue(opts.ChunkSize, DEFAULT_PARTS).(int32), + } + return d.Download() +} + +type ( + Downloader struct { + *Client + Parts int32 + ChunkSize int32 + Worker int + Source InputFileLocation + Size int32 + DcID int32 + Workers []*Client + FileName string + wg *sync.WaitGroup + } +) + +func (d *Downloader) Download() (string, error) { + d.Init() + return d.Start() +} + +func (d *Downloader) Init() { + if d.Parts == 0 { + d.Parts = int32(d.Size / DEFAULT_PARTS) + if d.Parts == 0 { + d.Parts = 1 + } + } + if d.ChunkSize == 0 { + d.ChunkSize = DEFAULT_PARTS + } + + if d.Worker == 0 { + d.Worker = DEFAULT_WORKERS + } + if d.Worker > int(d.Parts) { + d.Worker = int(d.Parts) + } + d.wg = &sync.WaitGroup{} + if d.FileName == "" { + d.FileName = GenerateRandomString(10) + } + d.createFile() + d.allocateWorkers() +} + +func (d *Downloader) createFile() (*os.File, error) { + if pathIsDir(d.FileName) { + d.FileName = filepath.Join(d.FileName, GenerateRandomString(10)) + os.MkdirAll(filepath.Dir(d.FileName), 0755) + } + return os.Create(d.FileName) +} + +func (d *Downloader) onError() { + os.Remove(d.FileName) +} + +func (d *Downloader) allocateWorkers() { + if d.Worker == 1 { + d.Workers = []*Client{d.Client} + return + } + for i := 0; i < d.Worker; i++ { + w, err := d.Client.ExportSender(int(d.DcID)) + if err != nil { + panic(err) + } + d.Workers = append(d.Workers, w) + } +} + +func (d *Downloader) DividePartsToWorkers() [][]int32 { + var ( + parts = d.Parts + worker = d.Worker + ) + if parts < int32(worker) { + worker = int(parts) + } + var ( + perWorker = parts / int32(worker) + remainder = parts % int32(worker) + ) + var ( + start = int32(0) + end = int32(0) + ) + var ( + partsToWorkers = make([][]int32, worker) + ) + for i := 0; i < worker; i++ { + end = start + perWorker + if remainder > 0 { + end++ + remainder-- + } + partsToWorkers[i] = []int32{start, end} + start = end + } + d.Worker = worker + d.Parts = parts + return partsToWorkers +} + +func (d *Downloader) Start() (string, error) { + var ( + parts = d.DividePartsToWorkers() + ) + for i, w := range d.Workers { + d.wg.Add(1) + go d.downloadParts(w, parts[i]) + } + d.wg.Wait() + return d.FileName, nil +} + +func (d *Downloader) writeAt(buf []byte, offset int64) error { + f, err := os.OpenFile(d.FileName, os.O_WRONLY, 0644) + if err != nil { + return err + } + defer f.Close() + _, err = f.WriteAt(buf, offset) + if err != nil { + return err + } + return nil +} + +func (d *Downloader) calcOffset(part int32) int64 { + return int64(part * d.ChunkSize) +} + +func (d *Downloader) downloadParts(w *Client, parts []int32) { + defer d.wg.Done() + for i := parts[0]; i < parts[1]; i++ { + buf, err := w.UploadGetFile(&UploadGetFileParams{ + Location: d.Source, + Offset: d.calcOffset(i), + Limit: d.ChunkSize, + CdnSupported: false, + }) + w.Logger.Debug(fmt.Sprintf("downloaded part %d of %d", i, d.Parts)) + var buffer []byte + switch v := buf.(type) { + case *UploadFileObj: + buffer = v.Bytes + case *UploadFileCdnRedirect: + return // TODO + } + if err != nil { + panic(err) + } + err = d.writeAt(buffer, d.calcOffset(i)) + if err != nil { + panic(err) + } + } +} + +func GenerateRandomString(n int) string { + var letter = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + b := make([]rune, n) + for i := range b { + b[i] = letter[rand.Intn(len(letter))] + } + return string(b) +} + +// TODO CUSTOM LOGGING +// READY for V1.0 +// TODO: add more methods diff --git a/telegram/messages.go b/telegram/messages.go new file mode 100644 index 00000000..28034686 --- /dev/null +++ b/telegram/messages.go @@ -0,0 +1,831 @@ +package telegram + +import ( + "fmt" + "math/rand" + "reflect" + + "github.com/pkg/errors" +) + +type SendOptions struct { + ReplyID int32 `json:"reply_id,omitempty"` + Caption interface{} `json:"caption,omitempty"` + ParseMode string `json:"parse_mode,omitempty"` + Silent bool `json:"silent,omitempty"` + LinkPreview bool `json:"link_preview,omitempty"` + ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"` + ClearDraft bool `json:"clear_draft,omitempty"` + NoForwards bool `json:"no_forwards,omitempty"` + ScheduleDate int32 `json:"schedule_date,omitempty"` + SendAs interface{} `json:"send_as,omitempty"` + Thumb InputFile `json:"thumb,omitempty"` + TTL int32 `json:"ttl,omitempty"` + ForceDocument bool `json:"force_document,omitempty"` + FileName string `json:"file_name,omitempty"` + Attributes []DocumentAttribute `json:"attributes,omitempty"` + Media interface{} +} + +// SendMessage sends a message. +// This method is a wrapper for messages.sendMessage. +// Params: +// - peerID: ID of the peer to send the message to. +// - Message: Text of the message to be sent. +// - Opts: Optional parameters. +func (c *Client) SendMessage(peerID interface{}, message interface{}, opts ...*SendOptions) (*NewMessage, error) { + opt := getVariadic(opts, &SendOptions{}).(*SendOptions) + opt.ParseMode = getStr(opt.ParseMode, c.ParseMode) + var ( + entities []MessageEntity + textMessage string + rawText string + media interface{} + ) + switch message := message.(type) { + case string: + entities, textMessage = parseEntities(message, opt.ParseMode) + rawText = message + case *MessageMedia, *InputMedia, *InputFile: + media = message + case *NewMessage: + entities = message.Message.Entities + textMessage = message.MessageText() + rawText = message.MessageText() + media = message.Media() + default: + return nil, fmt.Errorf("invalid message type: %s", reflect.TypeOf(message)) + } + media = getValue(media, opt.Media) + if media != nil { + opt.Caption = getValue(opt.Caption, rawText) + return c.SendMedia(peerID, media, convertOption(opt)) + } + senderPeer, err := c.GetSendablePeer(peerID) + if err != nil { + return nil, err + } + var sendAs InputPeer + if opt.SendAs != nil { + sendAs, err = c.GetSendablePeer(opt.SendAs) + if err != nil { + return nil, err + } + } + return c.sendMessage(senderPeer, textMessage, entities, sendAs, opt) +} + +func (c *Client) sendMessage(Peer InputPeer, Message string, entities []MessageEntity, sendAs InputPeer, opt *SendOptions) (*NewMessage, error) { + updateResp, err := c.MessagesSendMessage(&MessagesSendMessageParams{ + NoWebpage: !opt.LinkPreview, + Silent: opt.Silent, + Background: false, + ClearDraft: opt.ClearDraft, + Noforwards: opt.NoForwards, + UpdateStickersetsOrder: false, + Peer: Peer, + ReplyToMsgID: opt.ReplyID, + Message: Message, + RandomID: GenRandInt(), + ReplyMarkup: opt.ReplyMarkup, + Entities: entities, + ScheduleDate: opt.ScheduleDate, + SendAs: sendAs, + }) + if err != nil { + return nil, err + } + if updateResp != nil { + return packMessage(c, processUpdate(updateResp)), nil + } + return nil, errors.New("no response") +} + +// EditMessage edits a message. +// This method is a wrapper for messages.editMessage. +func (c *Client) EditMessage(peerID interface{}, id int32, message interface{}, opts ...*SendOptions) (*NewMessage, error) { + opt := getVariadic(opts, &SendOptions{}).(*SendOptions) + opt.ParseMode = getStr(opt.ParseMode, c.ParseMode) + var ( + entities []MessageEntity + textMessage string + media interface{} + ) + switch message := message.(type) { + case string: + entities, textMessage = parseEntities(message, opt.ParseMode) + case *MessageMedia, *InputMedia, *InputFile: + media = message + case *NewMessage: + entities = message.Message.Entities + textMessage = message.MessageText() + media = message.Media() + default: + return nil, fmt.Errorf("invalid message type: %s", reflect.TypeOf(message)) + } + media = getValue(media, opt.Media) + switch p := peerID.(type) { + case *InputBotInlineMessageID: + return c.editBotInlineMessage(*p, textMessage, entities, media, opt) + } + senderPeer, err := c.GetSendablePeer(peerID) + if err != nil { + return nil, err + } + return c.editMessage(senderPeer, id, textMessage, entities, media, opt) +} + +func (c *Client) editMessage(Peer InputPeer, id int32, Message string, entities []MessageEntity, Media interface{}, options *SendOptions) (*NewMessage, error) { + var ( + media InputMedia + err error + ) + if Media != nil { + media, err = c.getSendableMedia(Media, &MediaMetadata{ + Attributes: options.Attributes, + TTL: options.TTL, + ForceDocument: options.ForceDocument, + Thumb: options.Thumb, + FileName: options.FileName, + }) + if err != nil { + return nil, err + } + } + updateResp, err := c.MessagesEditMessage(&MessagesEditMessageParams{ + Peer: Peer, + ID: id, + Message: Message, + NoWebpage: !options.LinkPreview, + ReplyMarkup: options.ReplyMarkup, + Entities: entities, + Media: media, + ScheduleDate: options.ScheduleDate, + }) + if err != nil { + return nil, err + } + if updateResp != nil { + return packMessage(c, processUpdate(updateResp)), nil + } + return nil, errors.New("no response") +} + +func (c *Client) editBotInlineMessage(ID InputBotInlineMessageID, Message string, entities []MessageEntity, Media interface{}, options *SendOptions) (*NewMessage, error) { + var ( + media InputMedia + err error + ) + if Media != nil { + media, err = c.getSendableMedia(Media, &MediaMetadata{ + Attributes: options.Attributes, + TTL: options.TTL, + ForceDocument: options.ForceDocument, + Thumb: options.Thumb, + FileName: options.FileName, + }) + if err != nil { + return nil, err + } + } + editRequest := &MessagesEditInlineBotMessageParams{ + ID: ID, + Message: Message, + NoWebpage: !options.LinkPreview, + ReplyMarkup: options.ReplyMarkup, + Entities: entities, + Media: media, + } + var ( + editTrue bool + dcID int32 + ) + switch id := ID.(type) { + case *InputBotInlineMessageID64: + dcID = id.DcID + case *InputBotInlineMessageIDObj: + dcID = id.DcID + } + if dcID != int32(c.GetDC()) { + newSender, _ := c.ExportSender(int(dcID)) + editTrue, err = newSender.MessagesEditInlineBotMessage(editRequest) + newSender.Terminate() + } else { + editTrue, err = c.MessagesEditInlineBotMessage(editRequest) + } + if err != nil { + return nil, err + } + if editTrue { + return &NewMessage{ID: 0}, nil + } + return nil, errors.New("request failed") +} + +type MediaOptions struct { + Caption interface{} `json:"caption,omitempty"` + ParseMode string `json:"parse_mode,omitempty"` + Silent bool `json:"silent,omitempty"` + LinkPreview bool `json:"link_preview,omitempty"` + ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"` + ClearDraft bool `json:"clear_draft,omitempty"` + NoForwards bool `json:"no_forwards,omitempty"` + Thumb InputFile `json:"thumb,omitempty"` + NoSoundVideo bool `json:"no_sound_video,omitempty"` + ForceDocument bool `json:"force_document,omitempty"` + ReplyID int32 `json:"reply_id,omitempty"` + FileName string `json:"file_name,omitempty"` + TTL int32 `json:"ttl,omitempty"` + Attributes []DocumentAttribute `json:"attributes,omitempty"` + ScheduleDate int32 `json:"schedule_date,omitempty"` + SendAs interface{} `json:"send_as,omitempty"` +} + +type MediaMetadata struct { + FileName string `json:"file_name,omitempty"` + Thumb InputFile `json:"thumb,omitempty"` + Attributes []DocumentAttribute `json:"attributes,omitempty"` + ForceDocument bool `json:"force_document,omitempty"` + TTL int32 `json:"ttl,omitempty"` +} + +// SendMedia sends a media message. +// This method is a wrapper for messages.sendMedia. +// Params: +// - peerID: ID of the peer to send the message to. +// - Media: Media to send. +// - Opts: Optional parameters. +func (c *Client) SendMedia(peerID interface{}, Media interface{}, opts ...*MediaOptions) (*NewMessage, error) { + opt := getVariadic(opts, &MediaOptions{}).(*MediaOptions) + opt.ParseMode = getStr(opt.ParseMode, c.ParseMode) + var ( + entities []MessageEntity + textMessage string + ) + sendMedia, err := c.getSendableMedia(Media, &MediaMetadata{FileName: opt.FileName, Thumb: opt.Thumb, ForceDocument: opt.ForceDocument, Attributes: opt.Attributes, TTL: opt.TTL}) + if err != nil { + return nil, err + } + switch cap := opt.Caption.(type) { + case string: + entities, textMessage = parseEntities(cap, opt.ParseMode) + case *NewMessage: + entities = cap.Message.Entities + textMessage = cap.MessageText() + } + senderPeer, err := c.GetSendablePeer(peerID) + if err != nil { + return nil, err + } + var sendAs InputPeer + if opt.SendAs != nil { + sendAs, err = c.GetSendablePeer(opt.SendAs) + if err != nil { + return nil, err + } + } + return c.sendMedia(senderPeer, sendMedia, textMessage, entities, sendAs, opt) +} + +func (c *Client) sendMedia(Peer InputPeer, Media InputMedia, Caption string, entities []MessageEntity, sendAs InputPeer, opt *MediaOptions) (*NewMessage, error) { + updateResp, err := c.MessagesSendMedia(&MessagesSendMediaParams{ + Silent: opt.Silent, + Background: false, + ClearDraft: opt.ClearDraft, + Noforwards: opt.NoForwards, + UpdateStickersetsOrder: false, + Peer: Peer, + ReplyToMsgID: opt.ReplyID, + Media: Media, + RandomID: GenRandInt(), + ReplyMarkup: opt.ReplyMarkup, + Message: Caption, + Entities: entities, + ScheduleDate: opt.ScheduleDate, + SendAs: sendAs, + }) + if err != nil { + return nil, err + } + if updateResp != nil { + return packMessage(c, processUpdate(updateResp)), nil + } + return nil, errors.New("no response") +} + +// SendAlbum sends a media album. +// This method is a wrapper for messages.sendMultiMedia. +// Params: +// - peerID: ID of the peer to send the message to. +// - Album: List of media to send. +// - Opts: Optional parameters. +func (c *Client) SendAlbum(peerID interface{}, Album interface{}, opts ...*MediaOptions) ([]*NewMessage, error) { + opt := getVariadic(opts, &MediaOptions{}).(*MediaOptions) + opt.ParseMode = getStr(opt.ParseMode, c.ParseMode) + var ( + entities []MessageEntity + textMessage string + ) + InputAlbum, multiErr := c.getMultiMedia(Album, &MediaMetadata{FileName: opt.FileName, Thumb: opt.Thumb, ForceDocument: opt.ForceDocument, Attributes: opt.Attributes, TTL: opt.TTL}) + if multiErr != nil { + return nil, multiErr + } + + switch cap := opt.Caption.(type) { + case string: + entities, textMessage = parseEntities(cap, opt.ParseMode) + case *NewMessage: + entities = cap.Message.Entities + textMessage = cap.MessageText() + } + InputAlbum[len(InputAlbum)-1].Message = textMessage + InputAlbum[len(InputAlbum)-1].Entities = entities + senderPeer, err := c.GetSendablePeer(peerID) + if err != nil { + return nil, err + } + var sendAs InputPeer + if opt.SendAs != nil { + sendAs, err = c.GetSendablePeer(opt.SendAs) + if err != nil { + return nil, err + } + } + return c.sendAlbum(senderPeer, InputAlbum, textMessage, entities, sendAs, opt) +} + +func (c *Client) sendAlbum(Peer InputPeer, Album []*InputSingleMedia, Caption string, entities []MessageEntity, sendAs InputPeer, opt *MediaOptions) ([]*NewMessage, error) { + updateResp, err := c.MessagesSendMultiMedia(&MessagesSendMultiMediaParams{ + Silent: opt.Silent, + Background: false, + ClearDraft: opt.ClearDraft, + Noforwards: opt.NoForwards, + UpdateStickersetsOrder: false, + Peer: Peer, + ReplyToMsgID: opt.ReplyID, + ScheduleDate: opt.ScheduleDate, + SendAs: sendAs, + MultiMedia: Album, + }) + if err != nil { + return nil, err + } + var m []*NewMessage + if updateResp != nil { + updates := processUpdates(updateResp) + for _, update := range updates { + m = append(m, packMessage(c, update)) + } + } + return m, errors.New("no response") +} + +// SendReaction sends a reaction to a message. +// This method is a wrapper for messages.sendReaction +// Params: +// - peerID: ID of the peer to send the message to. +// - msgID: ID of the message to react to. +// - reaction: Reaction to send. +// - big: Whether to use big emoji. +func (c *Client) SendReaction(peerID interface{}, msgID int32, reaction interface{}, big ...bool) error { + b := getVariadic(big, false).(bool) + peer, err := c.GetSendablePeer(peerID) + if err != nil { + return err + } + var r []Reaction + switch reaction := reaction.(type) { + case string: + if reaction == "" { + r = append(r, &ReactionEmpty{}) + } + r = append(r, &ReactionEmoji{reaction}) + case []string: + for _, v := range reaction { + if v == "" { + r = append(r, &ReactionEmpty{}) + } + r = append(r, &ReactionEmoji{v}) + } + case ReactionCustomEmoji: + r = append(r, &reaction) + case []ReactionCustomEmoji: + for _, v := range reaction { + r = append(r, &v) + } + } + _, err = c.MessagesSendReaction(&MessagesSendReactionParams{ + Peer: peer, + Big: b, + AddToRecent: true, + MsgID: msgID, + Reaction: r, + }) + return err +} + +// SendDice sends a special dice message. +// This method calls messages.sendMedia with a dice media. +func (c *Client) SendDice(peerID interface{}, emoji string) (*NewMessage, error) { + return c.SendMedia(peerID, &InputMediaDice{Emoticon: emoji}) +} + +// SendAction sends a chat action. +// This method is a wrapper for messages.setTyping. +func (c *Client) SendAction(PeerID interface{}, Action interface{}, topMsgID ...int32) (*ActionResult, error) { + peerChat, err := c.GetSendablePeer(PeerID) + if err != nil { + return nil, err + } + TopMsgID := getVariadic(topMsgID, 0).(int32) + switch a := Action.(type) { + case string: + if action, ok := Actions[a]; ok { + _, err = c.MessagesSetTyping(peerChat, TopMsgID, action) + } else { + return nil, errors.New("unknown action") + } + case *SendMessageAction: + _, err = c.MessagesSetTyping(peerChat, TopMsgID, *a) + default: + return nil, errors.New("unknown action type") + } + return &ActionResult{Peer: peerChat, Client: c}, err +} + +// SendReadAck sends a read acknowledgement. +// This method is a wrapper for messages.readHistory. +func (c *Client) SendReadAck(PeerID interface{}, MaxID ...int32) (*MessagesAffectedMessages, error) { + peerChat, err := c.GetSendablePeer(PeerID) + if err != nil { + return nil, err + } + maxID := getVariadic(MaxID, 0).(int32) + return c.MessagesReadHistory(peerChat, maxID) +} + +// SendPoll sends a poll. TODO + +type ForwardOptions struct { + HideCaption bool `json:"hide_caption,omitempty"` + HideAuthor bool `json:"hide_author,omitempty"` + Silent bool `json:"silent,omitempty"` + Protected bool `json:"protected,omitempty"` + Background bool `json:"background,omitempty"` + WithMyScore bool `json:"with_my_score,omitempty"` + SendAs interface{} `json:"send_as,omitempty"` + ScheduleDate int32 `json:"schedule_date,omitempty"` +} + +// Forward forwards a message. +// This method is a wrapper for messages.forwardMessages. +func (c *Client) Forward(peerID interface{}, fromPeerID interface{}, msgIDs []int32, opts ...*ForwardOptions) ([]NewMessage, error) { + opt := getVariadic(opts, &ForwardOptions{}).(*ForwardOptions) + toPeer, err := c.GetSendablePeer(peerID) + if err != nil { + return nil, err + } + fromPeer, err := c.GetSendablePeer(fromPeerID) + if err != nil { + return nil, err + } + randomIDs := make([]int64, len(msgIDs)) + for i := range randomIDs { + randomIDs[i] = rand.Int63() + } + var sendAs InputPeer + if opt.SendAs != nil { + sendAs, err = c.GetSendablePeer(opt.SendAs) + if err != nil { + return nil, err + } + } + updateResp, err := c.MessagesForwardMessages(&MessagesForwardMessagesParams{ + ToPeer: toPeer, + FromPeer: fromPeer, + ID: msgIDs, + RandomID: randomIDs, + Silent: opt.Silent, + Background: false, + Noforwards: opt.Protected, + ScheduleDate: opt.ScheduleDate, + DropAuthor: opt.HideAuthor, + DropMediaCaptions: opt.HideCaption, + SendAs: sendAs, + }) + if err != nil { + return nil, err + } + var m []NewMessage + if updateResp != nil { + updates := processUpdates(updateResp) + for _, update := range updates { + m = append(m, *packMessage(c, update)) + } + } + return m, nil +} + +// DeleteMessages deletes messages. +// This method is a wrapper for messages.deleteMessages. +func (c *Client) DeleteMessages(peerID interface{}, msgIDs []int32, Revoke ...bool) (*MessagesAffectedMessages, error) { + revoke := getVariadic(Revoke, false).(bool) + peer, err := c.GetSendablePeer(peerID) + if err != nil { + return nil, err + } + switch peer := peer.(type) { + case *InputPeerChannel: + return c.ChannelsDeleteMessages(&InputChannelObj{ + ChannelID: peer.ChannelID, + AccessHash: peer.AccessHash, + }, msgIDs) + case *InputPeerChat, *InputPeerUser: + return c.MessagesDeleteMessages(revoke, msgIDs) + default: + return nil, errors.New("invalid peer type") + } +} + +// GetCustomEmoji gets the document of a custom emoji +// Params: +// - docIDs: the document id of the emoji +func (c *Client) GetCustomEmoji(docIDs ...int64) ([]Document, error) { + var em []int64 + em = append(em, docIDs...) + emojis, err := c.MessagesGetCustomEmojiDocuments(em) + if err != nil { + return nil, err + } + return emojis, nil +} + +type SearchOption struct { + IDs []int32 `json:"ids,omitempty"` + Query string `json:"query,omitempty"` + Offset int32 `json:"offset,omitempty"` + Limit int32 `json:"limit,omitempty"` + Filter MessagesFilter `json:"filter,omitempty"` + TopMsgID int32 `json:"top_msg_id,omitempty"` + MaxID int32 `json:"max_id,omitempty"` + MinID int32 `json:"min_id,omitempty"` + MaxDate int32 `json:"max_date,omitempty"` + MinDate int32 `json:"min_date,omitempty"` +} + +func (c *Client) GetMessages(PeerID interface{}, Opts ...*SearchOption) ([]NewMessage, error) { + opt := getVariadic(Opts, &SearchOption{}).(*SearchOption) + peer, err := c.GetSendablePeer(PeerID) + if err != nil { + return nil, err + } + var ( + m []Message + messages []NewMessage + inputIDs []InputMessage + result MessagesMessages + ) + for _, id := range opt.IDs { + inputIDs = append(inputIDs, &InputMessageID{ID: id}) + } + if len(inputIDs) == 0 && opt.Query == "" && opt.Limit == 0 { + opt.Limit = 1 + } + if len(opt.IDs) > 0 { + switch peer := peer.(type) { + case *InputPeerChannel: + result, err = c.ChannelsGetMessages(&InputChannelObj{ChannelID: peer.ChannelID, AccessHash: peer.AccessHash}, inputIDs) + case *InputPeerChat, *InputPeerUser: + result, err = c.MessagesGetMessages(inputIDs) + default: + return nil, errors.New("invalid peer type") + } + if err != nil { + return nil, err + } + switch result := result.(type) { + case *MessagesChannelMessages: + m = append(m, result.Messages...) + case *MessagesMessagesObj: + m = append(m, result.Messages...) + } + } else { + result, err = c.MessagesSearch(&MessagesSearchParams{ + Peer: peer, + Q: opt.Query, + OffsetID: opt.Offset, + AddOffset: opt.Limit, + Filter: opt.Filter, + MinDate: opt.MinDate, + MaxDate: opt.MaxDate, + MinID: opt.MinID, + MaxID: opt.MaxID, + Limit: opt.Limit, + TopMsgID: opt.TopMsgID, + }) + if err != nil { + return nil, err + } + switch result := result.(type) { + case *MessagesChannelMessages: + m = append(m, result.Messages...) + case *MessagesMessagesObj: + m = append(m, result.Messages...) + } + } + for _, msg := range m { + messages = append(messages, *packMessage(c, msg)) + } + return messages, nil +} + +type PinOptions struct { + Unpin bool `json:"unpin,omitempty"` + PmOneside bool `json:"pm_oneside,omitempty"` + Silent bool `json:"silent,omitempty"` +} + +// Pin pins a message. +// This method is a wrapper for messages.pinMessage. +func (c *Client) PinMessage(PeerID interface{}, MsgID int32, Opts ...*PinOptions) (Updates, error) { + opts := getVariadic(Opts, &PinOptions{}).(*PinOptions) + peer, err := c.GetSendablePeer(PeerID) + if err != nil { + return nil, err + } + resp, err := c.MessagesUpdatePinnedMessage(&MessagesUpdatePinnedMessageParams{ + Peer: peer, + ID: MsgID, + Unpin: opts.Unpin, + PmOneside: opts.PmOneside, + Silent: opts.Silent, + }) + if err != nil { + return nil, err + } + return resp, nil +} + +// UnpinMessage unpins a message. +func (c *Client) UnpinMessage(PeerID interface{}, MsgID int32, Opts ...*PinOptions) (Updates, error) { + opts := getVariadic(Opts, &PinOptions{}).(*PinOptions) + opts.Unpin = true + return c.PinMessage(PeerID, MsgID, opts) +} + +// Gets the current pinned message in a chat +func (c *Client) GetPinnedMessage(PeerID interface{}) (*NewMessage, error) { + resp, err := c.GetMessages(PeerID, &SearchOption{ + Filter: &InputMessagesFilterPinned{}, + Limit: 1, + }) + if err != nil { + return nil, err + } + if len(resp) == 0 { + return nil, errors.New("no pinned message") + } + return &resp[0], nil +} + +type InlineSendOptions struct { + Gallery bool `json:"gallery,omitempty"` + NextOffset string `json:"next_offset,omitempty"` + CacheTime int32 `json:"cache_time,omitempty"` + Private bool `json:"private,omitempty"` + SwitchPm string `json:"switch_pm,omitempty"` + SwitchPmText string `json:"switch_pm_text,omitempty"` +} + +func (c *Client) AnswerInlineQuery(QueryID int64, Results []InputBotInlineResult, Options ...*InlineSendOptions) (bool, error) { + options := getVariadic(Options, &InlineSendOptions{}).(*InlineSendOptions) + options.CacheTime = getValue(options.CacheTime, 60).(int32) + request := &MessagesSetInlineBotResultsParams{ + Gallery: options.Gallery, + Private: options.Private, + QueryID: QueryID, + Results: Results, + CacheTime: options.CacheTime, + NextOffset: options.NextOffset, + } + if options.SwitchPm != "" { + request.SwitchPm = &InlineBotSwitchPm{ + Text: options.SwitchPm, + StartParam: getValue(options.SwitchPmText, "start").(string), + } + } + resp, err := c.MessagesSetInlineBotResults(request) + if err != nil { + return false, err + } + return resp, nil +} + +type CallbackOptions struct { + Alert bool `json:"alert,omitempty"` + CacheTime int32 `json:"cache_time,omitempty"` + URL string `json:"url,omitempty"` +} + +func (c *Client) AnswerCallbackQuery(QueryID int64, Text string, Opts ...*CallbackOptions) (bool, error) { + options := getVariadic(Opts, &CallbackOptions{}).(*CallbackOptions) + request := &MessagesSetBotCallbackAnswerParams{ + QueryID: QueryID, + Message: Text, + Alert: options.Alert, + } + if options.URL != "" { + request.URL = options.URL + } + if options.CacheTime != 0 { + request.CacheTime = options.CacheTime + } + resp, err := c.MessagesSetBotCallbackAnswer(request) + if err != nil { + return false, err + } + return resp, nil +} + +type InlineOptions struct { + Dialog interface{} + Offset int32 + Query string + GeoPoint InputGeoPoint +} + +// InlineQuery performs an inline query and returns the results. +// Params: +// - peerID: The ID of the Inline Bot. +// - Query: The query to send. +// - Offset: The offset to send. +// - Dialog: The chat or channel to send the query to. +// - GeoPoint: The location to send. +func (c *Client) InlineQuery(peerID interface{}, Options ...*InlineOptions) (*MessagesBotResults, error) { + options := getVariadic(Options, &InlineOptions{}).(*InlineOptions) + peer, err := c.GetSendablePeer(peerID) + if err != nil { + return nil, err + } + var dialog InputPeer = &InputPeerEmpty{} + if options.Dialog != nil { + dialog, err = c.GetSendablePeer(options.Dialog) + if err != nil { + return nil, err + } + } + bot, ok := peer.(*InputPeerUser) + if !ok { + return nil, errors.New("peer is not a bot") + } + resp, err := c.MessagesGetInlineBotResults(&MessagesGetInlineBotResultsParams{ + Bot: &InputUserObj{UserID: bot.UserID, AccessHash: bot.AccessHash}, + Peer: dialog, + Query: options.Query, + Offset: fmt.Sprintf("%d", options.Offset), + GeoPoint: options.GeoPoint, + }) + if err != nil { + return nil, err + } + return resp, nil +} + +// Internal functions + +func convertOption(s *SendOptions) *MediaOptions { + return &MediaOptions{ + ReplyID: s.ReplyID, + Caption: s.Caption, + ParseMode: s.ParseMode, + Silent: s.Silent, + LinkPreview: s.LinkPreview, + ReplyMarkup: s.ReplyMarkup, + ClearDraft: s.ClearDraft, + NoForwards: s.NoForwards, + ScheduleDate: s.ScheduleDate, + SendAs: s.SendAs, + Thumb: s.Thumb, + TTL: s.TTL, + ForceDocument: s.ForceDocument, + FileName: s.FileName, + Attributes: s.Attributes, + } +} + +func getVariadic(v interface{}, def interface{}) interface{} { + if v == nil { + return def + } + rv := reflect.ValueOf(v) + if rv.Kind() == reflect.Ptr { + rv = rv.Elem() + } + if rv.Kind() != reflect.Slice { + return v + } + if rv.Len() == 0 { + return def + } + return rv.Index(0).Interface() +} diff --git a/telegram/methods.go b/telegram/methods.go deleted file mode 100644 index e7ab2924..00000000 --- a/telegram/methods.go +++ /dev/null @@ -1,1183 +0,0 @@ -package telegram - -import ( - "fmt" - "reflect" - - "github.com/pkg/errors" -) - -func (c *Client) GetMe() (*UserObj, error) { - resp, err := c.UsersGetFullUser(&InputUserSelf{}) - if err != nil { - return nil, errors.Wrap(err, "getting user") - } - user, ok := resp.Users[0].(*UserObj) - if !ok { - return nil, errors.New("got wrong response: " + reflect.TypeOf(resp).String()) - } - return user, nil -} - -func (c *Client) SendMessage(peerID interface{}, TextObj interface{}, Opts ...*SendOptions) (*NewMessage, error) { - var options SendOptions - if len(Opts) > 0 { - options = *Opts[0] - } - if options.ParseMode == "" { - options.ParseMode = c.ParseMode - } - var e []MessageEntity - var Text string - switch TextObj := TextObj.(type) { - case string: - e, Text = c.FormatMessage(TextObj, options.ParseMode) - case MessageMedia, InputMedia: - return c.SendMedia(peerID, TextObj, &MediaOptions{ - Caption: options.Caption, - ParseMode: options.ParseMode, - LinkPreview: options.LinkPreview, - ReplyID: options.ReplyID, - ReplyMarkup: options.ReplyMarkup, - NoForwards: options.NoForwards, - Silent: options.Silent, - ClearDraft: options.ClearDraft, - }) - case *NewMessage: - if TextObj.Media() != nil { - return c.SendMedia(peerID, TextObj.Media(), &MediaOptions{ - Caption: getValue(options.Caption, TextObj.Text()).(string), - ParseMode: options.ParseMode, - LinkPreview: options.LinkPreview, - ReplyID: options.ReplyID, - ReplyMarkup: *getValue(&options.ReplyMarkup, TextObj.ReplyMarkup).(*ReplyMarkup), - NoForwards: options.NoForwards, - Silent: options.Silent, - ClearDraft: options.ClearDraft, - }) - } - Text = TextObj.Text() - e, Text = c.FormatMessage(Text, options.ParseMode) - options.ReplyMarkup = *getValue(&options.ReplyMarkup, TextObj.ReplyMarkup).(*ReplyMarkup) - } - PeerToSend, err := c.GetSendablePeer(peerID) - if err != nil { - return nil, err - } - Update, err := c.MessagesSendMessage(&MessagesSendMessageParams{ - Peer: PeerToSend, - Message: Text, - RandomID: GenRandInt(), - ReplyToMsgID: options.ReplyID, - Entities: e, - ReplyMarkup: options.ReplyMarkup, - NoWebpage: options.LinkPreview, - Silent: options.Silent, - ClearDraft: options.ClearDraft, - }) - if err != nil { - return nil, err - } - return packMessage(c, processUpdate(Update)), err -} - -func (c *Client) EditMessage(peerID interface{}, MsgID int32, TextObj interface{}, Opts ...*SendOptions) (*NewMessage, error) { - var options SendOptions - if len(Opts) > 0 { - options = *Opts[0] - } - if options.ParseMode == "" { - options.ParseMode = c.ParseMode - } - var err error - var e []MessageEntity - var Text string - var Media InputMedia - if options.Media != nil { - m, err := c.getSendableMedia(options.Media, &CustomAttrs{ - Attributes: options.Attributes, - TTL: options.TTL, - ForceDocument: options.ForceDocument, - Thumb: options.Thumb, - FileName: options.FileName, - }) - if err == nil { - Media = m - } else { - Media = &InputMediaEmpty{} - } - } - switch TextObj := TextObj.(type) { - case string: - e, Text = c.FormatMessage(TextObj, options.ParseMode) - } - switch peerID := peerID.(type) { - case InputBotInlineMessageID: - sender := c - switch p := peerID.(type) { - case *InputBotInlineMessageIDObj: - if int(p.DcID) != sender.GetDC() { - sender, err = sender.ExportSender(int(p.DcID)) - if err != nil { - return nil, err - } - defer sender.Terminate() - } - case *InputBotInlineMessageID64: - if int(p.DcID) != sender.GetDC() { - sender, err = sender.ExportSender(int(p.DcID)) - if err != nil { - return nil, err - } - defer sender.Terminate() - } - } - _, err := sender.MessagesEditInlineBotMessage(&MessagesEditInlineBotMessageParams{ - NoWebpage: !options.LinkPreview, - ID: peerID, - Message: Text, - Media: Media, - ReplyMarkup: options.ReplyMarkup, - Entities: e, - }) - if err != nil { - return nil, err - } - return &NewMessage{ID: 0}, nil - case nil: - return nil, errors.New("peerID cant be nil") - default: - PeerToSend, err := c.GetSendablePeer(peerID) - if err != nil { - return nil, err - } - Update, err := c.MessagesEditMessage(&MessagesEditMessageParams{ - Peer: PeerToSend, - Message: Text, - ID: MsgID, - Entities: e, - NoWebpage: options.LinkPreview, - Media: Media, - ReplyMarkup: options.ReplyMarkup, - ScheduleDate: options.ScheduleDate, - }) - if err != nil { - return nil, err - } - return packMessage(c, processUpdate(Update)), err - } -} - -func (c *Client) DeleteMessage(peerID interface{}, MsgIDs ...int32) error { - PeerToSend, err := c.GetSendablePeer(peerID) - if err != nil { - return err - } - PeerChannel, ok := PeerToSend.(*InputPeerChannel) - if !ok { - _, err = c.MessagesDeleteMessages(true, MsgIDs) - return err - } - _, err = c.ChannelsDeleteMessages(&InputChannelObj{ChannelID: PeerChannel.ChannelID, AccessHash: PeerChannel.AccessHash}, MsgIDs) - return err -} - -func (c *Client) ForwardMessage(fromID interface{}, toID interface{}, MsgIDs []int32, Opts ...*ForwardOptions) (*NewMessage, error) { - var options ForwardOptions - FromPeer, err := c.GetSendablePeer(fromID) - if err != nil { - return nil, err - } - ToPeer, err := c.GetSendablePeer(toID) - if err != nil { - return nil, err - } - Update, err := c.MessagesForwardMessages(&MessagesForwardMessagesParams{ - FromPeer: FromPeer, - ToPeer: ToPeer, - ID: MsgIDs, - RandomID: []int64{GenRandInt()}, - DropMediaCaptions: options.HideCaption, - DropAuthor: options.HideAuthor, - Noforwards: options.Protected, - Silent: options.Silent, - }) - if err != nil { - return nil, err - } - return packMessage(c, processUpdate(Update)), nil -} - -func (c *Client) SendDice(peerID interface{}, Emoji string) (*NewMessage, error) { - media := &InputMediaDice{ - Emoticon: Emoji, - } - return c.SendMedia(peerID, media) -} - -func (c *Client) SendMedia(peerID interface{}, Media interface{}, Opts ...*MediaOptions) (*NewMessage, error) { - var options MediaOptions - if len(Opts) > 0 { - options = *Opts[0] - } - if options.ParseMode == "" { - options.ParseMode = c.ParseMode - } - var Caption string - var e []MessageEntity - switch Capt := options.Caption.(type) { - case string: - e, Caption = c.FormatMessage(Capt, options.ParseMode) - } - PeerToSend, err := c.GetSendablePeer(peerID) - if err != nil { - return nil, err - } - MediaFile, err := c.getSendableMedia(Media, &CustomAttrs{ - FileName: options.FileName, - Thumb: options.Thumb, - ForceDocument: options.ForceDocument, - Attributes: options.Attributes, - TTL: options.TTL, - }) - if err != nil { - return nil, err - } - Update, err := c.MessagesSendMedia(&MessagesSendMediaParams{ - Peer: PeerToSend, - Media: MediaFile, - Message: Caption, - RandomID: GenRandInt(), - ReplyToMsgID: options.ReplyID, - Entities: e, - ReplyMarkup: options.ReplyMarkup, - Silent: options.Silent, - ClearDraft: options.ClearDraft, - Noforwards: options.NoForwards, - }) - if err != nil { - return nil, err - } - return packMessage(c, processUpdate(Update)), err -} - -func (c *Client) SendAlbum(peerID interface{}, Media interface{}, Opts ...*MediaOptions) ([]*NewMessage, error) { - var options MediaOptions - if len(Opts) > 0 { - options = *Opts[0] - } - if options.ParseMode == "" { - options.ParseMode = c.ParseMode - } - var Caption string - var e []MessageEntity - switch Capt := options.Caption.(type) { - case string: - e, Caption = c.FormatMessage(Capt, options.ParseMode) - } - PeerToSend, err := c.GetSendablePeer(peerID) - if err != nil { - return nil, err - } - MediaFiles, multiErr := c.getMultiMedia(Media, &CustomAttrs{ - FileName: options.FileName, - Thumb: options.Thumb, - ForceDocument: options.ForceDocument, - Attributes: options.Attributes, - TTL: options.TTL, - }) - if err != nil { - return nil, multiErr - } - MediaFiles[len(MediaFiles)-1].Message = Caption - MediaFiles[len(MediaFiles)-1].Entities = e - Update, err := c.MessagesSendMultiMedia(&MessagesSendMultiMediaParams{ - Peer: PeerToSend, - Silent: options.Silent, - ClearDraft: options.ClearDraft, - Noforwards: options.NoForwards, - ReplyToMsgID: options.ReplyID, - MultiMedia: MediaFiles, - ScheduleDate: options.ScheduleDate, - SendAs: options.SendAs, - }) - if err != nil { - return nil, err - } - var m []*NewMessage - updates := processUpdates(Update) - for _, update := range updates { - m = append(m, packMessage(c, update)) - } - return m, nil -} - -func (c *Client) SendReaction(peerID interface{}, MsgID int32, reactionEmoji interface{}, Big ...bool) error { - var big bool - if len(Big) > 0 { - big = Big[0] - } - PeerToSend, err := c.GetSendablePeer(peerID) - if err != nil { - return err - } - var r []Reaction - switch reaction := reactionEmoji.(type) { - case string: - if reaction == "" { - r = append(r, &ReactionEmpty{}) - } - r = append(r, &ReactionEmoji{reaction}) - case []string: - for _, v := range reaction { - if v == "" { - r = append(r, &ReactionEmpty{}) - } - r = append(r, &ReactionEmoji{v}) - } - case ReactionCustomEmoji: - r = append(r, &reaction) - case []ReactionCustomEmoji: - for _, v := range reaction { - r = append(r, &v) - } - } - _, err = c.MessagesSendReaction(&MessagesSendReactionParams{ - Peer: PeerToSend, - Big: big, - AddToRecent: true, - MsgID: MsgID, - Reaction: r, - }) - return err -} - -func (c *Client) GetParticipants(PeerID interface{}, opts ...*ParticipantOptions) ([]Participant, int32, error) { - var options = &ParticipantOptions{} - if len(opts) > 0 { - options = opts[0] - } else { - options = &ParticipantOptions{ - Filter: &ChannelParticipantsSearch{}, - Offset: 0, - Limit: 1, - Query: "", - } - } - if options.Query != "" { - options.Filter = &ChannelParticipantsSearch{ - Q: options.Query, - } - } - PeerToSend, err := c.GetSendablePeer(PeerID) - if err != nil { - return nil, 0, err - } - Channel, ok := PeerToSend.(*InputPeerChannel) - if !ok { - return nil, 0, errors.New("peer is not a channel") - } - p, err := c.ChannelsGetParticipants( - &InputChannelObj{ChannelID: Channel.ChannelID, AccessHash: Channel.AccessHash}, - options.Filter, - options.Offset, - options.Limit, - 0, - ) - if err != nil { - return nil, 0, err - } - ParticipantsResponse := p.(*ChannelsChannelParticipantsObj) - c.Cache.UpdatePeersToCache(ParticipantsResponse.Users, ParticipantsResponse.Chats) - var Participants []Participant - for _, u := range ParticipantsResponse.Participants { - var p = &Participant{} - p.Participant = u - switch u := u.(type) { - case *ChannelParticipantObj: - p.User, _ = c.Cache.GetUser(u.UserID) - p.Rights = &ChatAdminRights{} - case *ChannelParticipantLeft: - peerID, _ := c.GetSendablePeer(u.Peer) - if u, ok := peerID.(*InputPeerUser); ok { - p.User, _ = c.Cache.GetUser(u.UserID) - } - p.Left = true - p.Rights = &ChatAdminRights{} - case *ChannelParticipantBanned: - peerID, _ := c.GetSendablePeer(u.Peer) - if u, ok := peerID.(*InputPeerUser); ok { - p.User, _ = c.Cache.GetUser(u.UserID) - } - p.Left = true - p.Banned = true - p.Rights = &ChatAdminRights{} - case *ChannelParticipantAdmin: - p.User, _ = c.Cache.GetUser(u.UserID) - p.Admin = true - p.Rights = u.AdminRights - case *ChannelParticipantCreator: - p.User, _ = c.Cache.GetUser(u.UserID) - p.Creator = true - p.Admin = true - p.Rights = u.AdminRights - default: - fmt.Println("unknown participant type", reflect.TypeOf(u).String()) - } - Participants = append(Participants, *p) - - } - return Participants, ParticipantsResponse.Count, nil -} - -func (c *Client) GetChatMember(PeerID interface{}, UserID interface{}) (Participant, error) { - PeerToSend, err := c.GetSendablePeer(PeerID) - if err != nil { - return Participant{}, err - } - Channel, ok := PeerToSend.(*InputPeerChannel) - if !ok { - return Participant{}, errors.New("peer is not a channel") - } - PeerPart, err := c.GetSendablePeer(UserID) - if err != nil { - return Participant{}, err - } - ParticipantResponse, err := c.ChannelsGetParticipant( - &InputChannelObj{ChannelID: Channel.ChannelID, AccessHash: Channel.AccessHash}, - PeerPart, - ) - if err != nil { - return Participant{}, err - } - c.Cache.UpdatePeersToCache(ParticipantResponse.Users, ParticipantResponse.Chats) - var Participant = &Participant{} - Participant.Participant = ParticipantResponse.Participant - switch P := ParticipantResponse.Participant.(type) { - case *ChannelParticipantAdmin: - Participant.Admin = true - Participant.Rights = P.AdminRights - case *ChannelParticipantCreator: - Participant.Creator = true - Participant.Rights = P.AdminRights - case *ChannelParticipantLeft: - Participant.Left = true - Participant.Rights = &ChatAdminRights{} - case *ChannelParticipantBanned: - Participant.Banned = true - Participant.Rights = &ChatAdminRights{} - case *ChannelParticipantObj: - Participant.Rights = &ChatAdminRights{} - } - return *Participant, nil -} - -func (c *Client) SendAction(PeerID interface{}, Action interface{}) (*ActionResult, error) { - PeerToSend, err := c.GetSendablePeer(PeerID) - if err != nil { - return nil, err - } - switch a := Action.(type) { - case string: - if action, ok := Actions[a]; ok { - _, err = c.MessagesSetTyping(PeerToSend, 0, action) - } else { - return nil, errors.New("unknown action") - } - case *SendMessageAction: - _, err = c.MessagesSetTyping(PeerToSend, 0, *a) - default: - return nil, errors.New("unknown action type") - } - return &ActionResult{PeerToSend, c}, err -} - -func (c *Client) SendReadAcknowledge(PeerID interface{}, MaxID ...int32) (*MessagesAffectedMessages, error) { - PeerToSend, err := c.GetSendablePeer(PeerID) - if err != nil { - return nil, err - } - return c.MessagesReadHistory(PeerToSend, MaxID[0]) -} - -func (c *Client) AnswerInlineQuery(QueryID int64, Results []InputBotInlineResult, Options ...*InlineSendOptions) (bool, error) { - var options *InlineSendOptions - if len(Options) > 0 { - options = Options[0] - } else { - options = &InlineSendOptions{} - } - options.CacheTime = getValue(options.CacheTime, 60).(int32) - request := &MessagesSetInlineBotResultsParams{ - Gallery: options.Gallery, - Private: options.Private, - QueryID: QueryID, - Results: Results, - CacheTime: options.CacheTime, - NextOffset: options.NextOffset, - } - if options.SwitchPm != "" { - request.SwitchPm = &InlineBotSwitchPm{ - Text: options.SwitchPm, - StartParam: getValue(options.SwitchPmText, "start").(string), - } - } - resp, err := c.MessagesSetInlineBotResults(request) - if err != nil { - return false, err - } - return resp, nil -} - -func (c *Client) AnswerCallbackQuery(QueryID int64, Text string, Opts ...*CallbackOptions) (bool, error) { - var options *CallbackOptions - if len(Opts) > 0 { - options = Opts[0] - } else { - options = &CallbackOptions{} - } - request := &MessagesSetBotCallbackAnswerParams{ - QueryID: QueryID, - Message: Text, - Alert: options.Alert, - } - if options.URL != "" { - request.URL = options.URL - } - if options.CacheTime != 0 { - request.CacheTime = options.CacheTime - } - resp, err := c.MessagesSetBotCallbackAnswer(request) - if err != nil { - return false, err - } - return resp, nil -} - -// Edit Admin rights of a user in a chat, -// returns true if successfull -func (c *Client) EditAdmin(PeerID interface{}, UserID interface{}, opts ...*AdminOptions) (bool, error) { - var ( - IsAdmin bool - Rank string - AdminRights *ChatAdminRights - err error - ) - if len(opts) > 0 { - IsAdmin = opts[0].IsAdmin - AdminRights = opts[0].Rights - Rank = opts[0].Rank - } else { - IsAdmin = true - AdminRights = &ChatAdminRights{} - } - PeerToSend, err := c.GetSendablePeer(PeerID) - if err != nil { - return false, err - } - PeerPart, err := c.GetSendablePeer(UserID) - if err != nil { - return false, err - } - if user, ok := PeerPart.(*InputPeerUser); !ok { - return false, errors.New("peer is not a user") - } else { - switch p := PeerToSend.(type) { - case *InputPeerChannel: - _, err = c.ChannelsEditAdmin( - &InputChannelObj{ChannelID: p.ChannelID, AccessHash: p.AccessHash}, - &InputUserObj{UserID: user.UserID, AccessHash: user.AccessHash}, - AdminRights, - Rank, - ) - case *InputPeerChat: - _, err = c.MessagesEditChatAdmin(p.ChatID, &InputUserObj{UserID: user.UserID, AccessHash: user.AccessHash}, IsAdmin) - default: - return false, errors.New("peer is not a chat or channel") - } - } - if err != nil { - return false, err - } - return true, nil -} - -func (c *Client) EditBanned(PeerID interface{}, UserID interface{}, opts ...*BannedOptions) (bool, error) { - var ( - BannedOptions *BannedOptions - err error - ) - if len(opts) > 0 { - BannedOptions = opts[0] - } - if BannedOptions.Rights == nil { - BannedOptions.Rights = &ChatBannedRights{} - } - if BannedOptions.Ban { - BannedOptions.Rights.ViewMessages = true - } else if BannedOptions.Unban { - BannedOptions.Rights.ViewMessages = false - } else if BannedOptions.Mute { - BannedOptions.Rights.SendMessages = true - } else if BannedOptions.Unmute { - BannedOptions.Rights.SendMessages = false - } - PeerToSend, err := c.GetSendablePeer(PeerID) - if err != nil { - return false, err - } - PeerPart, err := c.GetSendablePeer(UserID) - if err != nil { - return false, err - } - switch p := PeerToSend.(type) { - case *InputPeerChannel: - _, err = c.ChannelsEditBanned( - &InputChannelObj{ChannelID: p.ChannelID, AccessHash: p.AccessHash}, - PeerPart, - BannedOptions.Rights, - ) - case *InputPeerChat: - return false, errors.New("method not found") - default: - return false, errors.New("peer is not a chat or channel") - } - - if err != nil { - return false, err - } - return true, nil -} - -func (c *Client) KickParticipant(PeerID interface{}, UserID interface{}) (bool, error) { - PeerToSend, err := c.GetSendablePeer(PeerID) - if err != nil { - return false, err - } - PeerPart, err := c.GetSendablePeer(UserID) - if err != nil { - return false, err - } - switch p := PeerToSend.(type) { - case *InputPeerChannel: - _, err := c.EditBanned(p, PeerPart, &BannedOptions{Ban: true}) - if err != nil { - return false, err - } - return c.EditBanned(p, PeerPart, &BannedOptions{Unban: true}) - case *InputPeerChat: - u, ok := PeerPart.(*InputPeerUser) - if !ok { - return false, errors.New("peer is not a user") - } - _, err = c.MessagesDeleteChatUser(true, p.ChatID, &InputUserObj{UserID: u.UserID, AccessHash: u.AccessHash}) - default: - return false, errors.New("peer is not a chat or channel") - } - if err != nil { - return false, err - } - return true, nil -} - -func (c *Client) EditTitle(PeerID interface{}, Title string, Opts ...*TitleOptions) (bool, error) { - options := &TitleOptions{} - if len(Opts) > 0 { - options = Opts[0] - } - PeerToSend, err := c.GetSendablePeer(PeerID) - if err != nil { - return false, err - } - switch p := PeerToSend.(type) { - case *InputPeerChannel: - _, err = c.ChannelsEditTitle(&InputChannelObj{ChannelID: p.ChannelID, AccessHash: p.AccessHash}, Title) - case *InputPeerChat: - _, err = c.MessagesEditChatTitle(p.ChatID, Title) - case *InputPeerSelf: - _, err = c.AccountUpdateProfile(Title, options.LastName, options.About) - if err != nil { - return false, err - } - default: - return false, errors.New("peer is not a chat or channel or self") - } - if err != nil { - return false, err - } - return true, nil -} - -// GetMessages returns a slice of messages from a chat, -// if IDs are not specifed - MessagesSearch is used. -func (c *Client) GetMessages(PeerID interface{}, Options ...*SearchOption) ([]NewMessage, error) { - var ( - Opts = &SearchOption{} - err error - ) - if len(Options) > 0 { - Opts = Options[0] - } - PeerToSend, err := c.GetSendablePeer(PeerID) - if err != nil { - return nil, err - } - var Messages []NewMessage - var MessagesSlice []Message - var MsgIDs []InputMessage - for _, ID := range Opts.IDs { - MsgIDs = append(MsgIDs, &InputMessageID{ID: ID}) - } - if len(MsgIDs) == 0 && (Opts.Query == "" && Opts.Limit == 0) { - Opts.Limit = 1 - } - if Opts.Filter == nil { - Opts.Filter = &InputMessagesFilterEmpty{} - } - switch p := PeerToSend.(type) { - case *InputPeerChannel: - if len(MsgIDs) > 0 { - resp, err := c.ChannelsGetMessages(&InputChannelObj{ChannelID: p.ChannelID, AccessHash: p.AccessHash}, MsgIDs) - if err != nil { - return nil, err - } - Messages, ok := resp.(*MessagesChannelMessages) - if !ok { - return nil, errors.New("could not convert messages: " + reflect.TypeOf(resp).String()) - } - MessagesSlice = Messages.Messages - } else { - resp, err := c.MessagesSearch(&MessagesSearchParams{ - Peer: &InputPeerChannel{ - ChannelID: p.ChannelID, - AccessHash: p.AccessHash, - }, - Q: Opts.Query, - OffsetID: Opts.Offset, - Limit: Opts.Limit, - Filter: Opts.Filter, - MaxDate: Opts.MaxDate, - MinDate: Opts.MinDate, - MaxID: Opts.MaxID, - MinID: Opts.MinID, - TopMsgID: Opts.TopMsgID, - }) - if err != nil { - return nil, err - } - Messages, ok := resp.(*MessagesChannelMessages) - if !ok { - return nil, errors.New("could not convert messages: " + reflect.TypeOf(resp).String()) - } - MessagesSlice = Messages.Messages - } - case *InputPeerChat: - if len(MsgIDs) > 0 { - resp, err := c.MessagesGetMessages(MsgIDs) - if err != nil { - return nil, err - } - Messages, ok := resp.(*MessagesMessagesObj) - if !ok { - return nil, errors.New("could not convert messages: " + reflect.TypeOf(resp).String()) - } - MessagesSlice = Messages.Messages - } else { - resp, err := c.MessagesSearch(&MessagesSearchParams{ - Peer: &InputPeerChat{ - ChatID: p.ChatID, - }, - Q: Opts.Query, - OffsetID: Opts.Offset, - Limit: Opts.Limit, - Filter: Opts.Filter, - MaxDate: Opts.MaxDate, - MinDate: Opts.MinDate, - MaxID: Opts.MaxID, - MinID: Opts.MinID, - TopMsgID: Opts.TopMsgID, - }) - if err != nil { - return nil, err - } - Messages, ok := resp.(*MessagesChannelMessages) - if !ok { - return nil, errors.New("could not convert messages: " + reflect.TypeOf(resp).String()) - } - MessagesSlice = Messages.Messages - } - case *InputPeerUser: - if len(MsgIDs) > 0 { - resp, err := c.MessagesGetMessages(MsgIDs) - if err != nil { - return nil, err - } - Messages, ok := resp.(*MessagesMessagesObj) - if !ok { - return nil, errors.New("could not convert messages: " + reflect.TypeOf(resp).String()) - } - MessagesSlice = Messages.Messages - } else { - resp, err := c.MessagesSearch(&MessagesSearchParams{ - Peer: &InputPeerUser{ - UserID: p.UserID, - AccessHash: p.AccessHash, - }, - Q: Opts.Query, - OffsetID: Opts.Offset, - Limit: Opts.Limit, - Filter: Opts.Filter, - MaxDate: Opts.MaxDate, - MinDate: Opts.MinDate, - MaxID: Opts.MaxID, - MinID: Opts.MinID, - TopMsgID: Opts.TopMsgID, - }) - if err != nil { - return nil, err - } - Messages, ok := resp.(*MessagesMessagesObj) - if !ok { - return nil, errors.New("could not convert messages: " + reflect.TypeOf(resp).String()) - } - MessagesSlice = Messages.Messages - } - } - for _, Message := range MessagesSlice { - Messages = append(Messages, *packMessage(c, Message)) - } - return Messages, nil -} - -func (c *Client) GetDialogs(Opts ...*DialogOptions) ([]Dialog, error) { - Options := &DialogOptions{} - if len(Opts) > 0 { - Options = Opts[0] - } - if Options.Limit > 1000 { - Options.Limit = 1000 - } else if Options.Limit < 1 { - Options.Limit = 1 - } - resp, err := c.MessagesGetDialogs(&MessagesGetDialogsParams{ - OffsetDate: Options.OffsetDate, - OffsetID: Options.OffsetID, - OffsetPeer: Options.OffsetPeer, - Limit: Options.Limit, - FolderID: Options.FolderID, - ExcludePinned: Options.ExcludePinned, - }) - if err != nil { - return nil, err - } - switch p := resp.(type) { - case *MessagesDialogsObj: - go func() { c.Cache.UpdatePeersToCache(p.Users, p.Chats) }() - return p.Dialogs, nil - case *MessagesDialogsSlice: - go func() { c.Cache.UpdatePeersToCache(p.Users, p.Chats) }() - return p.Dialogs, nil - default: - return nil, errors.New("could not convert dialogs: " + reflect.TypeOf(resp).String()) - } -} - -// GetStats returns the stats of the channel or message -// Params: -// - channelID: the channel ID -// - messageID: the message ID -func (c *Client) GetStats(channelID interface{}, messageID ...interface{}) (*StatsBroadcastStats, *StatsMessageStats, error) { - peerID, err := c.GetSendablePeer(channelID) - if err != nil { - return nil, nil, err - } - channelPeer, ok := peerID.(*InputPeerChannel) - if !ok { - return nil, nil, errors.New("could not convert peer to channel") - } - if len(messageID) > 0 { - msgID := messageID[0].(int32) - resp, err := c.StatsGetMessageStats(true, &InputChannelObj{ - ChannelID: channelPeer.ChannelID, - AccessHash: channelPeer.AccessHash, - }, msgID) - if err != nil { - return nil, nil, err - } - return nil, resp, nil - } - resp, err := c.StatsGetBroadcastStats(true, &InputChannelObj{ - ChannelID: channelPeer.ChannelID, - AccessHash: channelPeer.AccessHash, - }) - if err != nil { - return nil, nil, err - } - return resp, nil, nil -} - -func (c *Client) PinMessage(PeerID interface{}, MsgID int32, Opts ...*PinOptions) (Updates, error) { - PeerToSend, err := c.GetSendablePeer(PeerID) - if err != nil { - return nil, err - } - Options := &PinOptions{} - if len(Opts) > 0 { - Options = Opts[0] - } - resp, err := c.MessagesUpdatePinnedMessage(&MessagesUpdatePinnedMessageParams{ - Peer: PeerToSend, - ID: MsgID, - Unpin: Options.Unpin, - PmOneside: Options.PmOneside, - Silent: Options.Silent, - }) - if err != nil { - return nil, err - } - return resp, nil -} - -func (c *Client) UnPinAll(PeerID interface{}) error { - PeerToSend, err := c.GetSendablePeer(PeerID) - if err != nil { - return err - } - _, err = c.MessagesUnpinAllMessages(PeerToSend) - if err != nil { - return err - } - return nil -} - -// GetProfilePhotos returns the profile photos of a user -// Params: -// - userID: The user ID -// - Offset: The offset to start from -// - Limit: The number of photos to return -// - MaxID: The maximum ID of the photo to return -func (c *Client) GetProfilePhotos(userID interface{}, Opts ...*PhotosOptions) ([]Photo, error) { - PeerToSend, err := c.GetSendablePeer(userID) - if err != nil { - return nil, err - } - Options := &PhotosOptions{} - if len(Opts) > 0 { - Options = Opts[0] - } - if Options.Limit > 80 { - Options.Limit = 80 - } else if Options.Limit < 1 { - Options.Limit = 1 - } - User, ok := PeerToSend.(*InputPeerUser) - if !ok { - return nil, errors.New("peer is not a user") - } - resp, err := c.PhotosGetUserPhotos( - &InputUserObj{UserID: User.UserID, AccessHash: User.AccessHash}, - Options.Offset, - Options.MaxID, - Options.Limit, - ) - if err != nil { - return nil, err - } - switch p := resp.(type) { - case *PhotosPhotosObj: - c.Cache.UpdatePeersToCache(p.Users, []Chat{}) - return p.Photos, nil - case *PhotosPhotosSlice: - c.Cache.UpdatePeersToCache(p.Users, []Chat{}) - return p.Photos, nil - default: - return nil, errors.New("could not convert photos: " + reflect.TypeOf(resp).String()) - } -} - -// GetChatPhotos returns the profile photos of a chat -// Params: -// - chatID: The ID of the chat -// - limit: The maximum number of photos to be returned -func (c *Client) GetChatPhotos(chatID interface{}, limit ...int32) ([]Photo, error) { - if limit == nil { - limit = []int32{1} - } - messages, err := c.GetMessages(chatID, &SearchOption{Limit: limit[0], - Filter: &InputMessagesFilterChatPhotos{}}) - if err != nil { - return nil, err - } - var photos []Photo - for _, message := range messages { - if message.IsMedia() { - switch p := message.Media().(type) { - case *MessageMediaPhoto: - photos = append(photos, p.Photo) - } - } - } - return photos, nil -} - -// GetChatPhoto returns the chat photo -// Params: -// - chatID: chat id -func (c *Client) GetChatPhoto(chatID interface{}) (Photo, error) { - photos, err := c.GetChatPhotos(chatID) - if err != nil { - return &PhotoObj{}, err - } - if len(photos) > 0 { - return photos[0], nil - } - return &PhotoObj{}, nil -} - -// InlineQuery performs an inline query and returns the results. -// Params: -// - peerID: The ID of the Inline Bot. -// - Query: The query to send. -// - Offset: The offset to send. -// - Dialog: The chat or channel to send the query to. -// - GeoPoint: The location to send. -func (c *Client) InlineQuery(peerID interface{}, Options ...*InlineOptions) (*MessagesBotResults, error) { - Opts := &InlineOptions{} - if len(Options) > 0 { - Opts = Options[0] - } - PeerBot, err := c.GetSendablePeer(peerID) - var Peer InputPeer - if Opts.Dialog != nil { - Peer, err = c.GetSendablePeer(Opts.Dialog) - - } else { - Peer = &InputPeerEmpty{} - } - if err != nil { - return nil, err - } - var m *MessagesBotResults - if u, ok := PeerBot.(*InputPeerUser); ok { - m, err = c.MessagesGetInlineBotResults(&MessagesGetInlineBotResultsParams{ - Bot: &InputUserObj{UserID: u.UserID, AccessHash: u.AccessHash}, - Peer: Peer, - Query: Opts.Query, - Offset: fmt.Sprint(Opts.Offset), - GeoPoint: Opts.GeoPoint, - }) - if err != nil { - return nil, err - } - } else { - return nil, errors.New("peer is not a bot") - } - if err != nil { - return nil, err - } - return m, nil -} - -// JoinChannel joins a channel or chat by its username or id -// Params: -// - Channel: the username or id of the channel or chat -func (c *Client) JoinChannel(Channel interface{}) error { - switch p := Channel.(type) { - case string: - if TG_JOIN_RE.MatchString(p) { - _, err := c.MessagesImportChatInvite(TG_JOIN_RE.FindStringSubmatch(p)[2]) - if err != nil { - return err - } - } - default: - Channel, err := c.GetSendablePeer(Channel) - if err != nil { - return err - } - if channel, ok := Channel.(*InputPeerChannel); ok { - _, err = c.ChannelsJoinChannel(&InputChannelObj{ChannelID: channel.ChannelID, AccessHash: channel.AccessHash}) - if err != nil { - return err - } - } else if channel, ok := Channel.(*InputPeerChat); ok { - _, err = c.MessagesAddChatUser(channel.ChatID, &InputUserEmpty{}, 0) - if err != nil { - return err - } - } else { - return errors.New("peer is not a channel or chat") - } - } - return nil -} - -// LeaveChannel leaves a channel or chat -// Params: -// - Channel: Channel or chat to leave -// - Revoke: If true, the channel will be deleted -func (c *Client) LeaveChannel(Channel interface{}, Revoke ...bool) error { - revokeChat := false - if len(Revoke) > 0 { - revokeChat = Revoke[0] - } - Channel, err := c.GetSendablePeer(Channel) - if err != nil { - return err - } - if channel, ok := Channel.(*InputPeerChannel); ok { - _, err = c.ChannelsLeaveChannel(&InputChannelObj{ChannelID: channel.ChannelID, AccessHash: channel.AccessHash}) - if err != nil { - return err - } - } else if channel, ok := Channel.(*InputPeerChat); ok { - _, err = c.MessagesDeleteChatUser(revokeChat, channel.ChatID, &InputUserEmpty{}) - if err != nil { - return err - } - } else { - return errors.New("peer is not a channel or chat") - } - return nil -} - -// GetChatInviteLink returns the invite link of a chat -// Params: -// - peerID : The ID of the chat -// - LegacyRevoke : If true, the link will be revoked -// - Expire: The time in seconds after which the link will expire -// - Limit: The maximum number of users that can join the chat using the link -// - Title: The title of the link -// - RequestNeeded: If true, join requests will be needed to join the chat -func (c *Client) GetChatInviteLink(peerID interface{}, LinkOpts ...*InviteLinkOptions) (ExportedChatInvite, error) { - LinkOptions := &InviteLinkOptions{} - if len(LinkOpts) > 0 { - LinkOptions = LinkOpts[0] - } - peer, err := c.GetSendablePeer(peerID) - if err != nil { - return nil, err - } - link, err := c.MessagesExportChatInvite(&MessagesExportChatInviteParams{ - Peer: peer, - LegacyRevokePermanent: LinkOptions.LegacyRevokePermanent, - RequestNeeded: LinkOptions.RequestNeeded, - UsageLimit: LinkOptions.Limit, - Title: LinkOptions.Title, - ExpireDate: LinkOptions.Expire, - }) - return link, err -} - -// GetCustomEmoji gets the document of a custom emoji -// Params: -// - docIDs: the document id of the emoji -func (c *Client) GetCustomEmoji(docIDs ...int64) ([]Document, error) { - var em []int64 - em = append(em, docIDs...) - emojis, err := c.MessagesGetCustomEmojiDocuments(em) - if err != nil { - return nil, err - } - return emojis, nil -} diff --git a/telegram/newmessage.go b/telegram/newmessage.go index 711021c9..25205994 100644 --- a/telegram/newmessage.go +++ b/telegram/newmessage.go @@ -486,8 +486,8 @@ func (m *NewMessage) RespondMedia(Media interface{}, Opts ...MediaOptions) (*New } // Delete deletes the message -func (m *NewMessage) Delete() error { - return m.Client.DeleteMessage(m.ChatID(), m.ID) +func (m *NewMessage) Delete() (*MessagesAffectedMessages, error) { + return m.Client.DeleteMessages(m.ChatID(), []int32{m.ID}) } // React to a message @@ -499,12 +499,12 @@ func (m *NewMessage) React(Reaction ...string) error { // Forward forwards the message to a chat func (m *NewMessage) ForwardTo(PeerID interface{}, Opts ...*ForwardOptions) (*NewMessage, error) { - resp, err := m.Client.ForwardMessage(m.ChatID(), PeerID, []int32{m.ID}, Opts...) - if resp == nil { + resps, err := m.Client.Forward(m.ChatID(), PeerID, []int32{m.ID}, Opts...) + if resps == nil { return nil, err } - resp.Message.PeerID = m.Message.PeerID - return resp, err + resps[0].Message.PeerID = m.Message.PeerID + return &resps[0], err } // Download Media to Disk, diff --git a/telegram/types.go b/telegram/types.go index 5c81098b..74a8b220 100644 --- a/telegram/types.go +++ b/telegram/types.go @@ -70,22 +70,6 @@ type ( rwlock sync.RWMutex } - DialogOptions struct { - OffsetID int32 `json:"offset_id,omitempty"` - OffsetDate int32 `json:"offset_date,omitempty"` - OffsetPeer InputPeer `json:"offset_peer,omitempty"` - Limit int32 `json:"limit,omitempty"` - ExcludePinned bool `json:"exclude_pinned,omitempty"` - FolderID int32 `json:"folder_id,omitempty"` - } - - InlineOptions struct { - Dialog interface{} - Offset int32 - Query string - GeoPoint InputGeoPoint - } - LoginOptions struct { Password string `json:"password,omitempty"` Code string `json:"code,omitempty"` @@ -95,138 +79,11 @@ type ( LastName string `json:"last_name,omitempty"` } - DownloadOptions struct { - Progress *Progress `json:"progress,omitempty"` - FileName string `json:"file_name,omitempty"` - DcID int32 `json:"dc_id,omitempty"` - Size int32 `json:"size,omitempty"` - Threaded bool `json:"threaded,omitempty"` - Threads int `json:"threads,omitempty"` - } - - UploadOptions struct { - Progress *Progress `json:"progress,omitempty"` - DcID int32 `json:"dc_id,omitempty"` - Threaded bool `json:"threaded,omitempty"` - Threads int `json:"threads,omitempty"` - } - - SendOptions struct { - ReplyID int32 `json:"reply_id,omitempty"` - Caption interface{} `json:"caption,omitempty"` - ParseMode string `json:"parse_mode,omitempty"` - Silent bool `json:"silent,omitempty"` - LinkPreview bool `json:"link_preview,omitempty"` - ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"` - ClearDraft bool `json:"clear_draft,omitempty"` - NoForwards bool `json:"no_forwards,omitempty"` - ScheduleDate int32 `json:"schedule_date,omitempty"` - SendAs InputPeer `json:"send_as,omitempty"` - Thumb InputFile `json:"thumb,omitempty"` - TTL int32 `json:"ttl,omitempty"` - ForceDocument bool `json:"force_document,omitempty"` - FileName string `json:"file_name,omitempty"` - Attributes []DocumentAttribute `json:"attributes,omitempty"` - Media interface{} - } - - MediaOptions struct { - Caption interface{} `json:"caption,omitempty"` - ParseMode string `json:"parse_mode,omitempty"` - Silent bool `json:"silent,omitempty"` - LinkPreview bool `json:"link_preview,omitempty"` - ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"` - ClearDraft bool `json:"clear_draft,omitempty"` - NoForwards bool `json:"no_forwards,omitempty"` - Thumb InputFile `json:"thumb,omitempty"` - NoSoundVideo bool `json:"no_sound_video,omitempty"` - ForceDocument bool `json:"force_document,omitempty"` - ReplyID int32 `json:"reply_id,omitempty"` - FileName string `json:"file_name,omitempty"` - TTL int32 `json:"ttl,omitempty"` - Attributes []DocumentAttribute `json:"attributes,omitempty"` - ScheduleDate int32 `json:"schedule_date,omitempty"` - SendAs InputPeer `json:"send_as,omitempty"` - } - - CustomAttrs struct { - FileName string `json:"file_name,omitempty"` - Thumb InputFile `json:"thumb,omitempty"` - Attributes []DocumentAttribute `json:"attributes,omitempty"` - ForceDocument bool `json:"force_document,omitempty"` - TTL int32 `json:"ttl,omitempty"` - } - - ForwardOptions struct { - HideCaption bool `json:"hide_caption,omitempty"` - HideAuthor bool `json:"hide_author,omitempty"` - Silent bool `json:"silent,omitempty"` - Protected bool `json:"protected,omitempty"` - } - - Participant struct { - User *UserObj `json:"user,omitempty"` - Admin bool `json:"admin,omitempty"` - Banned bool `json:"banned,omitempty"` - Creator bool `json:"creator,omitempty"` - Left bool `json:"left,omitempty"` - Participant ChannelParticipant `json:"participant,omitempty"` - Rights *ChatAdminRights `json:"rights,omitempty"` - } - - ParticipantOptions struct { - Query string `json:"query,omitempty"` - Filter ChannelParticipantsFilter `json:"filter,omitempty"` - Offset int32 `json:"offset,omitempty"` - Limit int32 `json:"limit,omitempty"` - } - - AdminOptions struct { - IsAdmin bool `json:"is_admin,omitempty"` - Rights *ChatAdminRights `json:"rights,omitempty"` - Rank string `json:"rank,omitempty"` - } - - PhotosOptions struct { - MaxID int64 `json:"max_id,omitempty"` - Offset int32 `json:"offset,omitempty"` - Limit int32 `json:"limit,omitempty"` - } - - PinOptions struct { - Unpin bool `json:"unpin,omitempty"` - PmOneside bool `json:"pm_oneside,omitempty"` - Silent bool `json:"silent,omitempty"` - } - - BannedOptions struct { - Ban bool `json:"ban,omitempty"` - Unban bool `json:"unban,omitempty"` - Mute bool `json:"mute,omitempty"` - Unmute bool `json:"unmute,omitempty"` - Rights *ChatBannedRights `json:"rights,omitempty"` - } - ActionResult struct { Peer InputPeer `json:"peer,omitempty"` Client *Client `json:"client,omitempty"` } - InlineSendOptions struct { - Gallery bool `json:"gallery,omitempty"` - NextOffset string `json:"next_offset,omitempty"` - CacheTime int32 `json:"cache_time,omitempty"` - Private bool `json:"private,omitempty"` - SwitchPm string `json:"switch_pm,omitempty"` - SwitchPmText string `json:"switch_pm_text,omitempty"` - } - - CallbackOptions struct { - Alert bool `json:"alert,omitempty"` - CacheTime int32 `json:"cache_time,omitempty"` - URL string `json:"url,omitempty"` - } - ArticleOptions struct { ID string `json:"id,omitempty"` ExcludeMedia bool `json:"exclude_media,omitempty"` @@ -243,32 +100,6 @@ type ( Invoice *InputBotInlineMessageMediaInvoice `json:"invoice,omitempty"` } - SearchOption struct { - IDs []int32 `json:"ids,omitempty"` - Query string `json:"query,omitempty"` - Offset int32 `json:"offset,omitempty"` - Limit int32 `json:"limit,omitempty"` - Filter MessagesFilter `json:"filter,omitempty"` - TopMsgID int32 `json:"top_msg_id,omitempty"` - MaxID int32 `json:"max_id,omitempty"` - MinID int32 `json:"min_id,omitempty"` - MaxDate int32 `json:"max_date,omitempty"` - MinDate int32 `json:"min_date,omitempty"` - } - - InviteLinkOptions struct { - LegacyRevokePermanent bool `json:"legacy_revoke_permanent,omitempty"` - Expire int32 `json:"expire,omitempty"` - Limit int32 `json:"limit,omitempty"` - Title string `json:"title,omitempty"` - RequestNeeded bool `json:"request_needed,omitempty"` - } - - TitleOptions struct { - LastName string `json:"last_name,omitempty"` - About string `json:"about,omitempty"` - } - PasswordOptions struct { Hint string `json:"hint,omitempty"` Email string `json:"email,omitempty"` @@ -345,79 +176,6 @@ func (a *ActionResult) Cancel() bool { return b } -func (p Participant) IsCreator() bool { - return p.Creator -} - -func (p Participant) IsAdmin() bool { - return p.Admin -} - -func (p Participant) IsBanned() bool { - return p.Banned -} - -func (p Participant) IsLeft() bool { - return p.Left -} - -func (p Participant) GetUser() *UserObj { - return p.User -} - -func (p Participant) GetParticipant() ChannelParticipant { - return p.Participant -} - -func (r ChatAdminRights) CanChangeInfo() bool { - return r.ChangeInfo -} - -func (r ChatAdminRights) CanPostMessages() bool { - return r.PostMessages -} - -func (r ChatAdminRights) CanEditMessages() bool { - return r.EditMessages -} - -func (r ChatAdminRights) CanDeleteMessages() bool { - return r.DeleteMessages -} - -func (r ChatAdminRights) CanBanUsers() bool { - return r.BanUsers -} - -func (r ChatAdminRights) CanInviteUsers() bool { - return r.InviteUsers -} - -func (r ChatAdminRights) CanPinMessages() bool { - return r.PinMessages -} - -func (r ChatAdminRights) CanPromoteMembers() bool { - return r.AddAdmins -} - -func (r ChatAdminRights) IsAnonymous() bool { - return r.Anonymous -} - -func (r ChatAdminRights) CanManageCall() bool { - return r.ManageCall -} - -func (p Participant) GetRank() string { - if pp, ok := p.Participant.(*ChannelParticipantCreator); ok { - return pp.Rank - } else if pp, ok := p.Participant.(*ChannelParticipantAdmin); ok { - return pp.Rank - } - return "" -} - // Custom logger func (l *Log) Error(err error) { diff --git a/telegram/updates.go b/telegram/updates.go index a76972f8..144c7751 100644 --- a/telegram/updates.go +++ b/telegram/updates.go @@ -1,3 +1,5 @@ +// Copyright (c) 2022, amarnathcjd + package telegram import ( @@ -24,7 +26,7 @@ func HandleMessageUpdateWithDiffrence(update Message, Pts int32, Limit int32) { for _, handle := range MessageHandles { if handle.IsMatch(m.Message) { if err != nil { - handle.Client.Logger.Println(err) + handle.Client.Logger.Error(err) return } else if msg == nil { return @@ -36,6 +38,7 @@ func HandleMessageUpdateWithDiffrence(update Message, Pts int32, Limit int32) { } func (c *Client) getDiffrence(Pts int32, Limit int32) (Message, error) { + c.Logger.Debug(fmt.Sprintf("Getting diffrence for %d", Pts)) updates, err := c.UpdatesGetDifference(Pts-1, Limit, int32(time.Now().Unix()), 0) if err != nil { return nil, err @@ -62,11 +65,11 @@ func handleMessage(message Message, h MessageHandle) error { m := packMessage(h.Client, message) if h.Filters != nil && h.Filter(m) { if err := h.Handler(m); err != nil { - h.Client.L.Error(err) + h.Client.Log.Error(err) } } else if h.Filters == nil { if err := h.Handler(m); err != nil { - h.Client.L.Error(err) + h.Client.Log.Error(err) } } return nil @@ -83,7 +86,7 @@ func HandleMessageUpdate(update Message) { case *MessageService: for _, handle := range ActionHandles { if err := handle.Handler(packMessage(handle.Client, msg)); err != nil { - handle.Client.L.Error(err) + handle.Client.Log.Error(err) } } } @@ -95,7 +98,7 @@ func HandleEditUpdate(update Message) { for _, handle := range MessageEdit { if handle.IsMatch(msg.Message) { if err := handle.Handler(packMessage(handle.Client, msg)); err != nil { - handle.Client.L.Error(err) + handle.Client.Log.Error(err) } } } @@ -106,7 +109,7 @@ func HandleInlineUpdate(update *UpdateBotInlineQuery) { for _, handle := range InlineHandles { if handle.IsMatch(update.Query) { if err := handle.Handler(packInlineQuery(handle.Client, update)); err != nil { - handle.Client.L.Error(err) + handle.Client.Log.Error(err) } } } @@ -116,7 +119,7 @@ func HandleCallbackUpdate(update *UpdateBotCallbackQuery) { for _, handle := range CallbackHandles { if handle.IsMatch(update.Data) { if err := handle.Handler(packCallbackQuery(handle.Client, update)); err != nil { - handle.Client.L.Error(err) + handle.Client.Log.Error(err) } } } @@ -126,7 +129,7 @@ func HandleRawUpdate(update Update) { for _, handle := range RawHandles { if reflect.TypeOf(handle.updateType) == reflect.TypeOf(update) { if err := handle.Handler(update); err != nil { - handle.Client.L.Error(err) + handle.Client.Log.Error(err) } } } diff --git a/telegram/uploader.go b/telegram/uploader.go deleted file mode 100644 index 2b1a7a63..00000000 --- a/telegram/uploader.go +++ /dev/null @@ -1,432 +0,0 @@ -// Copyright (c) 2022, amarnathcjd - -package telegram - -import ( - "bufio" - "bytes" - "crypto/md5" - "fmt" - "log" - "math" - "os" - "path/filepath" - "regexp" - "strings" - "sync" - - "github.com/pkg/errors" -) - -// UploadFile is a function that uploads a file to telegram as byte chunks -// and returns the InputFile object -// File can be Path to file, or a byte array -func (c *Client) UploadFile(file interface{}, Opts ...*UploadOptions) (InputFile, error) { - var ( - opts *UploadOptions - fileName string - fileSize int64 - totalParts int32 - Index int32 - bigFile bool - chunkSize int - reader *bufio.Reader - fileID = GenerateRandomLong() - hasher = md5.New() - fileBytes *os.File - Prog *Progress - ) - if len(Opts) > 0 { - opts = Opts[0] - Prog = opts.Progress - if Prog == nil { - Prog = &Progress{} - } - } else { - opts = &UploadOptions{} - Prog = &Progress{} - } - switch f := file.(type) { - case string: - fileSize, fileName = getFileStat(f) - if fileSize == 0 { - return nil, errors.New("file not found") - } - chunkSize = getAppropriatedPartSize(fileSize) - totalParts = int32(math.Ceil(float64(fileSize) / float64(chunkSize))) - bigFile = fileSize > 10*1024*1024 // 10MB - fileBytes, err := os.Open(f) - if err != nil { - return nil, errors.Wrap(err, "opening file") - } - defer fileBytes.Close() - reader = bufio.NewReader(fileBytes) - case []byte: // TODO: Add support for goroutines for byte array - fileSize = int64(len(f)) - chunkSize = getAppropriatedPartSize(fileSize) - totalParts = int32(math.Ceil(float64(fileSize) / float64(chunkSize))) - bigFile = fileSize > 10*1024*1024 - reader = bufio.NewReaderSize(bytes.NewReader(f), chunkSize) - case InputFile: - return f, nil // already an Uploaded file - default: - return nil, errors.New("invalid file type") - } - Prog.Total = int64(totalParts) - log.Println("Client - INFO - Uploading file", fileName, "with", totalParts, "parts of", chunkSize, "bytes") - if opts.Threaded && bigFile { - return c.uploadBigMultiThread(fileName, int(fileSize), fileID, fileBytes, int32(chunkSize), totalParts, Prog, opts.Threads) - } - buffer := make([]byte, chunkSize) - for { - n, err := reader.Read(buffer) - if err != nil { - break - } - if bigFile { - _, err = c.UploadSaveBigFilePart(fileID, Index, totalParts, buffer[:n]) - } else { - hasher.Write(buffer[:n]) - _, err = c.UploadSaveFilePart(fileID, Index, buffer[:n]) - } - if err != nil { - return nil, errors.Wrap(err, "uploading file") - } - Index++ - } - if bigFile { - return &InputFileBig{ID: fileID, Name: fileName, Parts: totalParts}, nil - } - return &InputFileObj{ID: fileID, Parts: Index, Name: fileName, Md5Checksum: fmt.Sprintf("%x", hasher.Sum(nil))}, nil -} - -// DownloadMedia is a function that downloads a media file from telegram, -// and returns the file path, -// FileDL can be MessageMedia, FileLocation, NewMessage -func (c *Client) DownloadMedia(FileDL interface{}, DLOptions ...*DownloadOptions) (string, error) { - var ( - fileLocation InputFileLocation - fileSize int64 - DcID int32 - Opts *DownloadOptions - Prog *Progress - ) - if len(DLOptions) > 0 { - Opts = DLOptions[0] - } else { - Opts = &DownloadOptions{} - } - switch f := FileDL.(type) { - case *MessageMediaContact: - file, err := os.Create(getValue(Opts.FileName, f.PhoneNumber+".contact").(string)) - if err != nil { - return "", errors.Wrap(err, "creating file") - } - defer file.Close() - vcard_4 := fmt.Sprintf("BEGIN:VCARD\nVERSION:4.0\nN:%s;%s;;;\nFN:%s %s\nTEL;TYPE=CELL:%s\nEND:VCARD", f.LastName, f.FirstName, f.FirstName, f.LastName, f.PhoneNumber) - _, err = file.WriteString(vcard_4) - if err != nil { - return "", errors.Wrap(err, "writing to file") - } - return file.Name(), nil - default: - fileLocation, DcID, fileSize = GetInputFileLocation(FileDL) - } - if fileLocation == nil { - return "", errors.New("could not get file location: " + fmt.Sprintf("%T", FileDL)) - } - if fileSize == 0 { - fileSize = 622 - } - if DcID == 0 { - DcID = Opts.DcID - } - chunkSize := getAppropriatedPartSize(fileSize) - totalParts := int32(math.Ceil(float64(fileSize) / float64(chunkSize))) - if Opts.Progress != nil { - Prog = Opts.Progress - } - Prog.Total = int64(totalParts) - var ( - fileName string - file *os.File - err error - ) - if Opts.FileName != "" { - fileName = Opts.FileName - } else { - fileName = getValue(GetFileName(FileDL), "download").(string) - } - if isPathDirectoryLike(fileName) { - fileName = filepath.Join(fileName, GetFileName(FileDL)) - os.MkdirAll(fileName, os.ModePerm) - } - file, err = os.Create(fileName) - if err != nil { - return "", errors.Wrap(err, "creating file") - } - defer file.Close() - - log.Println("Client - INFO - Downloading file", fileName, "with", totalParts, "parts of", chunkSize, "bytes") - if Opts.Threaded { - return fileName, c.downloadBigMultiThread(file, fileLocation, totalParts, int32(chunkSize), DcID, Prog, Opts.Threads) - } - if DcID != int32(c.GetDC()) { - c.Logger.Println("Client - INFO - File Lives on DC", DcID, ", Exporting Sender...") - s, _ := c.ExportSender(int(DcID)) - defer s.Terminate() - _, err = getFile(s, fileLocation, file, int32(chunkSize), totalParts, Prog) - if err != nil { - return "", errors.Wrap(err, "downloading file") - } - } else { - _, err = getFile(c, fileLocation, file, int32(chunkSize), totalParts, Prog) - if err != nil { - return "", errors.Wrap(err, "downloading file") - } - } - return fileName, nil -} - -func (c *Client) DownloadProfilePhoto(PeerID interface{}, Pfp interface{}, DLOptions ...*DownloadOptions) (string, error) { - var ( - Opts *DownloadOptions - Prog *Progress - ) - if len(DLOptions) > 0 { - Opts = DLOptions[0] - } else { - Opts = &DownloadOptions{} - } - if Opts.FileName == "" { - Opts.FileName = "profile_photo.jpg" - } - location, dcID, fileSize, err := c.getPeerPhotoLocation(PeerID, Pfp) - Opts.Size = fileSize - if err != nil { - return "", errors.Wrap(err, "getting photo location") - } - if location == nil { - return "", errors.New("could not get photo location") - } - return c.DownloadMedia(location, &DownloadOptions{DcID: dcID, Progress: Prog, FileName: Opts.FileName}) -} - -func (c *Client) uploadBigMultiThread(fileName string, fileSize int, fileID int64, fileBytes *os.File, chunkSize int32, totalParts int32, prog *Progress, threadCount int) (*InputFileBig, error) { - if threadCount <= 0 { - threadCount = 20 - } - if totalParts < int32(threadCount) { - threadCount = int(totalParts) - } - partsAllocation := MultiThreadAllocation(int32(chunkSize), totalParts, threadCount) - senders := make([]*Client, threadCount) - wg := sync.WaitGroup{} - for i := 0; i < threadCount; i++ { - wg.Add(1) - go func(ix int) { - defer wg.Done() - senders[ix], _ = c.ExportSender(c.GetDC()) - }(i) - } - wg.Wait() - log.Println("Client - INFO - Uploading file", fileName, "with", totalParts, "parts of", chunkSize, "bytes") - for i := 0; i < threadCount; i++ { - wg.Add(1) - go func(i int) { - defer wg.Done() - for j := partsAllocation[i][0]; j < partsAllocation[i][1]; j++ { - buffer := make([]byte, chunkSize) - _, _ = fileBytes.ReadAt(buffer, int64(j*int32(chunkSize))) - _, err := senders[i].UploadSaveBigFilePart(fileID, j, totalParts, buffer) - if prog != nil { - prog.Add(1) - } - if err != nil { - log.Println("Error in uploading", err) - } - } - }(i) - } - wg.Wait() - for i := 0; i < threadCount; i++ { - senders[i].Terminate() - } - return &InputFileBig{ - ID: fileID, - Name: fileName, - Parts: totalParts, - }, nil -} - -func (c *Client) downloadBigMultiThread(file *os.File, fileLocation InputFileLocation, totalParts int32, chunkSize int32, dcID int32, prog *Progress, ThreadCount int) error { - if ThreadCount <= 0 { - ThreadCount = 20 - } - if totalParts < int32(ThreadCount) { - ThreadCount = int(totalParts) - } - partsAllocation := MultiThreadAllocation(int32(chunkSize), totalParts, ThreadCount) - senders := make([]*Client, ThreadCount) - wg := sync.WaitGroup{} - for i := 0; i < ThreadCount; i++ { - wg.Add(1) - go func(ix int) { - defer wg.Done() - senders[ix], _ = c.ExportSender(int(dcID)) - }(i) - } - wg.Wait() - for i := 0; i < ThreadCount; i++ { - wg.Add(1) - go func(i int) { - defer wg.Done() - for j := partsAllocation[i][0]; j < partsAllocation[i][1]; j++ { - b, err := senders[i].getFilePart(fileLocation, chunkSize, j) - if err != nil { - log.Println("Error in downloading", err) - } - prog.Add(1) - _, _ = file.WriteAt(b, int64(j*int32(chunkSize))) - } - }(i) - } - wg.Wait() - for i := 0; i < ThreadCount; i++ { - senders[i].Terminate() - } - return nil -} - -func (c *Client) getFilePart(l InputFileLocation, chunkSize int32, Offset int32) ([]byte, error) { - var ( - filePart []byte - err error - ) - request, err := c.UploadGetFile(&UploadGetFileParams{ - Precise: false, - CdnSupported: false, - Location: l, - Offset: int64(Offset * chunkSize), - Limit: chunkSize, - }) - if err != nil { - return nil, errors.Wrap(err, "getting file part") - } - switch r := request.(type) { - case *UploadFileCdnRedirect: - // filePart, err = c.getFilePartCdnRedirect(r, chunkSize, Offset) - // if err != nil { - // return nil, errors.Wrap(err, "getting file part") - // } - return nil, errors.New("cdn redirect not supported yet") - case *UploadFileObj: - filePart = r.Bytes - } - return filePart, nil -} - -func getFile(c *Client, location InputFileLocation, f *os.File, chunkSize int32, totalParts int32, progress *Progress) (bool, error) { - for i := int32(0); i < totalParts; i++ { - fileData, err := c.UploadGetFile(&UploadGetFileParams{ - Precise: false, - CdnSupported: false, - Location: location, - Offset: int64(i * int32(chunkSize)), - Limit: chunkSize, - }) - if progress != nil { - progress.Set(int64(i)) - } - - if err != nil { - if strings.Contains(err.Error(), "The file to be accessed is currently stored in DC") { - dcID := regexp.MustCompile(`\d+`).FindString(err.Error()) - return false, errors.New("INVALID_DC_" + dcID) - } - return false, errors.Wrap(err, "downloading file") - } - switch file := fileData.(type) { - case *UploadFileObj: - _, err = f.Write(file.Bytes) - if err != nil { - return false, errors.Wrap(err, "writing file") - } - case *UploadFileCdnRedirect: - return false, errors.New("CDN_REDIRECT: Not implemented yet") - } - } - return true, nil -} - -func MultiThreadAllocation(chunkSize int32, totalParts int32, numGorotines int) map[int][]int32 { - partsForEachGoRoutine := int32(math.Ceil(float64(totalParts) / float64(numGorotines))) - remainingParts := totalParts - partsAllocation := make(map[int][]int32, numGorotines) - for i := 0; i < numGorotines; i++ { - if remainingParts > partsForEachGoRoutine { - partsAllocation[i] = []int32{int32(i) * partsForEachGoRoutine, (int32(i) + 1) * partsForEachGoRoutine} - remainingParts -= partsForEachGoRoutine - } else { - partsAllocation[i] = []int32{int32(i) * partsForEachGoRoutine, totalParts} - } - } - return partsAllocation -} - -func (c *Client) getPeerPhotoLocation(PeerID interface{}, Photo interface{}) (*InputPeerPhotoFileLocation, int32, int32, error) { - var ( - peer InputPeer - err error - ) - peer, err = c.GetSendablePeer(PeerID) - if err != nil { - return nil, 0, 0, errors.Wrap(err, "getting peer") - } - var location *InputPeerPhotoFileLocation - var dcID int32 - var fileSize int32 -PfpTypeSwitch: - switch pfp := Photo.(type) { - case *UserProfilePhotoObj: - location = &InputPeerPhotoFileLocation{ - PhotoID: pfp.PhotoID, - Peer: peer, - Big: true, - } - dcID = pfp.DcID - fileSize = int32(len(pfp.StrippedThumb)) - case *ChatPhotoObj: - location = &InputPeerPhotoFileLocation{ - PhotoID: pfp.PhotoID, - Peer: peer, - Big: true, - } - dcID = pfp.DcID - fileSize = int32(len(pfp.StrippedThumb)) - case *UserObj: - switch pfp.Photo.(type) { - case *UserProfilePhotoObj: - goto PfpTypeSwitch - default: - return nil, 0, 0, errors.New("user has no profile photo") - } - case *ChatObj: - switch pfp.Photo.(type) { - case *ChatPhotoObj: - goto PfpTypeSwitch - default: - return nil, 0, 0, errors.New("chat has no profile photo") - } - case *Channel: - switch pfp.Photo.(type) { - case *ChatPhotoObj: - goto PfpTypeSwitch - default: - return nil, 0, 0, errors.New("channel has no profile photo") - } - default: - return nil, 0, 0, errors.New("invalid profile photo type") - } - return location, dcID, fileSize, nil -} diff --git a/telegram/users.go b/telegram/users.go new file mode 100644 index 00000000..da9952a9 --- /dev/null +++ b/telegram/users.go @@ -0,0 +1,106 @@ +package telegram + +import ( + "reflect" + + "github.com/pkg/errors" +) + +func (c *Client) GetMe() (*UserObj, error) { + resp, err := c.UsersGetFullUser(&InputUserSelf{}) + if err != nil { + return nil, errors.Wrap(err, "getting user") + } + user, ok := resp.Users[0].(*UserObj) + if !ok { + return nil, errors.New("got wrong response: " + reflect.TypeOf(resp).String()) + } + return user, nil +} + +type PhotosOptions struct { + MaxID int64 `json:"max_id,omitempty"` + Offset int32 `json:"offset,omitempty"` + Limit int32 `json:"limit,omitempty"` +} + +// GetProfilePhotos returns the profile photos of a user +// Params: +// - userID: The user ID +// - Offset: The offset to start from +// - Limit: The number of photos to return +// - MaxID: The maximum ID of the photo to return +func (c *Client) GetProfilePhotos(userID interface{}, Opts ...*PhotosOptions) ([]Photo, error) { + Options := getVariadic(Opts, &PhotosOptions{}).(*PhotosOptions) + if Options.Limit > 80 { + Options.Limit = 80 + } else if Options.Limit < 1 { + Options.Limit = 1 + } + peer, err := c.GetSendablePeer(userID) + if err != nil { + return nil, err + } + User, ok := peer.(*InputPeerUser) + if !ok { + return nil, errors.New("peer is not a user") + } + resp, err := c.PhotosGetUserPhotos( + &InputUserObj{UserID: User.UserID, AccessHash: User.AccessHash}, + Options.Offset, + Options.MaxID, + Options.Limit, + ) + if err != nil { + return nil, err + } + switch p := resp.(type) { + case *PhotosPhotosObj: + c.Cache.UpdatePeersToCache(p.Users, []Chat{}) + return p.Photos, nil + case *PhotosPhotosSlice: + c.Cache.UpdatePeersToCache(p.Users, []Chat{}) + return p.Photos, nil + default: + return nil, errors.New("could not convert photos: " + reflect.TypeOf(resp).String()) + } +} + +type DialogOptions struct { + OffsetID int32 `json:"offset_id,omitempty"` + OffsetDate int32 `json:"offset_date,omitempty"` + OffsetPeer InputPeer `json:"offset_peer,omitempty"` + Limit int32 `json:"limit,omitempty"` + ExcludePinned bool `json:"exclude_pinned,omitempty"` + FolderID int32 `json:"folder_id,omitempty"` +} + +func (c *Client) GetDialogs(Opts ...*DialogOptions) ([]Dialog, error) { + Options := getVariadic(Opts, &DialogOptions{}).(*DialogOptions) + if Options.Limit > 1000 { + Options.Limit = 1000 + } else if Options.Limit < 1 { + Options.Limit = 1 + } + resp, err := c.MessagesGetDialogs(&MessagesGetDialogsParams{ + OffsetDate: Options.OffsetDate, + OffsetID: Options.OffsetID, + OffsetPeer: Options.OffsetPeer, + Limit: Options.Limit, + FolderID: Options.FolderID, + ExcludePinned: Options.ExcludePinned, + }) + if err != nil { + return nil, err + } + switch p := resp.(type) { + case *MessagesDialogsObj: + go func() { c.Cache.UpdatePeersToCache(p.Users, p.Chats) }() + return p.Dialogs, nil + case *MessagesDialogsSlice: + go func() { c.Cache.UpdatePeersToCache(p.Users, p.Chats) }() + return p.Dialogs, nil + default: + return nil, errors.New("could not convert dialogs: " + reflect.TypeOf(resp).String()) + } +} diff --git a/telegram/utils.go b/telegram/utils.go index f5c8ea5b..5f2eb6a4 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -113,18 +113,22 @@ func mimeIsPhoto(mime string) bool { return strings.HasPrefix(mime, "image/") && !strings.Contains(mime, "image/webp") } -func GetInputFileLocation(file interface{}) (InputFileLocation, int32, int64) { +func getFileLocation(file interface{}) (InputFileLocation, int32, int64, string, error) { var location interface{} - -MediaMessageSwitch: +mediaMessageSwitch: switch f := file.(type) { + case *Photo, *Document: + location = f case *MessageMediaDocument: location = f.Document case *MessageMediaPhoto: location = f.Photo case *NewMessage: + if !f.IsMedia() { + return nil, 0, 0, "", errors.New("message is not media") + } file = f.Media() - goto MediaMessageSwitch + goto mediaMessageSwitch case *MessageService: if f.Action != nil { switch f := f.Action.(type) { @@ -144,9 +148,9 @@ MediaMessageSwitch: } } case *InputPeerPhotoFileLocation: - return f, 0, 0 + return f, 0, 0, "profile_photo.jpg", nil default: - return nil, 0, 0 + return nil, 0, 0, "", errors.New("unsupported file type") } switch l := location.(type) { case *DocumentObj: @@ -155,22 +159,21 @@ MediaMessageSwitch: AccessHash: l.AccessHash, FileReference: l.FileReference, ThumbSize: "", - }, l.DcID, l.Size + }, l.DcID, l.Size, getFileName(l), nil case *PhotoObj: - size, sizeType := photoSizeByteCount(l.Sizes[len(l.Sizes)-1]) + size, sizeType := getPhotoSize(l.Sizes[len(l.Sizes)-1]) return &InputPhotoFileLocation{ ID: l.ID, AccessHash: l.AccessHash, FileReference: l.FileReference, ThumbSize: sizeType, - }, l.DcID, size - + }, l.DcID, size, getFileName(l), nil default: - return nil, 0, 0 + return nil, 0, 0, "", errors.New("unsupported file type") } } -func photoSizeByteCount(sizes PhotoSize) (int64, string) { +func getPhotoSize(sizes PhotoSize) (int64, string) { switch s := sizes.(type) { case *PhotoSizeObj: return int64(s.Size), s.Type @@ -203,7 +206,7 @@ func getMax(a []int32) int32 { return max } -func isPathDirectoryLike(path string) bool { +func pathIsDir(path string) bool { return strings.HasSuffix(path, "/") || strings.HasSuffix(path, "\\") } @@ -212,7 +215,7 @@ func isPathDirectoryLike(path string) bool { // *MessageMedia // *Document // *Photo -func GetFileName(f interface{}) string { +func getFileName(f interface{}) string { switch f := f.(type) { case *MessageMediaDocument: for _, attr := range f.Document.(*DocumentObj).Attributes { @@ -262,16 +265,20 @@ func GetFileName(f interface{}) string { // Func to get the file size of Media // Accepted types: // *MessageMedia -func GetFileSize(f interface{}) int64 { +func getFileSize(f interface{}) int64 { switch f := f.(type) { case *MessageMediaDocument: return f.Document.(*DocumentObj).Size case *MessageMediaPhoto: - if len(f.Photo.(*PhotoObj).Sizes) == 0 { + if photo, p := f.Photo.(*PhotoObj); p { + if len(photo.Sizes) == 0 { + return 0 + } + s, _ := getPhotoSize(photo.Sizes[len(photo.Sizes)-1]) + return s + } else { return 0 } - s, _ := photoSizeByteCount(f.Photo.(*PhotoObj).Sizes[len(f.Photo.(*PhotoObj).Sizes)-1]) - return s default: return 0 } @@ -282,7 +289,7 @@ func GetFileSize(f interface{}) int64 { // *MessageMedia // *Document // *Photo -func GetFileExt(f interface{}) string { +func getFileExt(f interface{}) string { switch f := f.(type) { case *MessageMediaDocument: doc := f.Document.(*DocumentObj) @@ -333,27 +340,6 @@ func GenerateRandomLong() int64 { return int64(rand.Int31())<<32 | int64(rand.Int31()) } -func getFileStat(filePath string) (int64, string) { - file, err := os.Open(filePath) - if err != nil { - return 0, "" - } - defer file.Close() - fileInfo, _ := file.Stat() - return fileInfo.Size(), fileInfo.Name() -} - -func getAppropriatedPartSize(fileSize int64) int { - if fileSize < 104857600 { - return 128 * 1024 - } else if fileSize < 786432000 { - return 256 * 1024 - } else if fileSize < 2097152000 { - return 256 * 1024 - } - return 0 -} - func getPeerUser(userID int64) *PeerUser { return &PeerUser{ UserID: userID,