diff options
Diffstat (limited to 'telegram')
-rw-r--r-- | telegram/cache/cache.go | 10 | ||||
-rw-r--r-- | telegram/client.go | 22 | ||||
-rw-r--r-- | telegram/commands.go | 328 | ||||
-rw-r--r-- | telegram/connect.go | 142 | ||||
-rw-r--r-- | telegram/handlers.go | 52 | ||||
-rw-r--r-- | telegram/utils.go | 717 | ||||
-rw-r--r-- | telegram/utils_test.go | 155 |
7 files changed, 1109 insertions, 317 deletions
diff --git a/telegram/cache/cache.go b/telegram/cache/cache.go index 3d9608d..6847d3e 100644 --- a/telegram/cache/cache.go +++ b/telegram/cache/cache.go @@ -133,3 +133,13 @@ func (cache *Cache) SetStatus(id int64, show string, status string) { Description: status, } } + +// Destruct splits a cached status into show, description and type +func (status *Status) Destruct() (show, description, typ string) { + show, description = status.XMPP, status.Description + if show == "unavailable" { + typ = show + show = "" + } + return +} diff --git a/telegram/client.go b/telegram/client.go index 49816db..6f6d719 100644 --- a/telegram/client.go +++ b/telegram/client.go @@ -2,6 +2,7 @@ package telegram import ( "github.com/pkg/errors" + "hash/maphash" "path/filepath" "strconv" "sync" @@ -44,7 +45,7 @@ type DelayedStatus struct { type Client struct { client *client.Client authorizer *clientAuthorizer - parameters *client.TdlibParameters + parameters *client.SetTdlibParametersRequest options []client.Option me *client.User @@ -52,6 +53,7 @@ type Client struct { jid string Session *persistence.Session resources map[string]bool + outbox map[string]string content *config.TelegramContentConfig cache *cache.Cache online bool @@ -59,13 +61,22 @@ type Client struct { DelayedStatuses map[int64]*DelayedStatus DelayedStatusesLock sync.Mutex - locks clientLocks + lastMsgHashes map[int64]uint64 + msgHashSeed maphash.Seed + + locks clientLocks + SendMessageLock sync.Mutex } type clientLocks struct { - authorizationReady sync.WaitGroup + authorizationReady sync.Mutex chatMessageLocks map[int64]*sync.Mutex resourcesLock sync.Mutex + outboxLock sync.Mutex + lastMsgHashesLock sync.Mutex + + authorizerReadLock sync.Mutex + authorizerWriteLock sync.Mutex } // NewClient instantiates a Telegram App @@ -92,7 +103,7 @@ func NewClient(conf config.TelegramConfig, jid string, component *xmpp.Component datadir = "./sessions/" // ye olde defaute } - parameters := client.TdlibParameters{ + parameters := client.SetTdlibParametersRequest{ UseTestDc: false, DatabaseDirectory: filepath.Join(datadir, jid), @@ -121,10 +132,13 @@ func NewClient(conf config.TelegramConfig, jid string, component *xmpp.Component jid: jid, Session: session, resources: make(map[string]bool), + outbox: make(map[string]string), content: &conf.Content, cache: cache.NewCache(), options: options, DelayedStatuses: make(map[int64]*DelayedStatus), + lastMsgHashes: make(map[int64]uint64), + msgHashSeed: maphash.MakeSeed(), locks: clientLocks{ chatMessageLocks: make(map[int64]*sync.Mutex), }, diff --git a/telegram/commands.go b/telegram/commands.go index bd9f0f8..87fff72 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -15,11 +15,11 @@ import ( ) const notEnoughArguments string = "Not enough arguments" -const telegramNotInitialized string = "Telegram connection is not initialized yet" +const TelegramNotInitialized string = "Telegram connection is not initialized yet" +const TelegramAuthDone string = "Authorization is done already" const notOnline string = "Not online" -var permissionsAdmin = client.ChatMemberStatusAdministrator{ - CanBeEdited: true, +var permissionsAdmin = client.ChatAdministratorRights{ CanChangeInfo: true, CanPostMessages: true, CanEditMessages: true, @@ -30,20 +30,27 @@ var permissionsAdmin = client.ChatMemberStatusAdministrator{ CanPromoteMembers: false, } var permissionsMember = client.ChatPermissions{ - CanSendMessages: true, - CanSendMediaMessages: true, + CanSendBasicMessages: true, + CanSendAudios: true, + CanSendDocuments: true, + CanSendPhotos: true, + CanSendVideos: true, + CanSendVideoNotes: true, + CanSendVoiceNotes: true, CanSendPolls: true, CanSendOtherMessages: true, CanAddWebPagePreviews: true, CanChangeInfo: true, CanInviteUsers: true, CanPinMessages: true, + CanManageTopics: true, } var permissionsReadonly = client.ChatPermissions{} var transportCommands = map[string]command{ "login": command{"phone", "sign in"}, "logout": command{"", "sign out"}, + "cancelauth": command{"", "quit the signin wizard"}, "code": command{"", "check one-time code"}, "password": command{"", "check 2fa password"}, "setusername": command{"", "update @username"}, @@ -52,6 +59,10 @@ var transportCommands = map[string]command{ "setpassword": command{"[old] [new]", "set or remove password"}, "config": command{"[param] [value]", "view or update configuration options"}, "report": command{"[chat] [comment]", "report a chat by id or @username"}, + "add": command{"@username", "add @username to your chat list"}, + "join": command{"https://t.me/invite_link", "join to chat via invite link or @publicname"}, + "supergroup": command{"title description", "create new supergroup «title» with «description»"}, + "channel": command{"title description", "create new channel «title» with «description»"}, } var chatCommands = map[string]command{ @@ -60,6 +71,7 @@ var chatCommands = map[string]command{ "silent": command{"message", "send a message without sound"}, "schedule": command{"{online | 2006-01-02T15:04:05 | 15:04:05} message", "schedules a message either to timestamp or to whenever the user goes online"}, "forward": command{"message_id target_chat", "forwards a message"}, + "vcard": command{"", "print vCard as text"}, "add": command{"@username", "add @username to your chat list"}, "join": command{"https://t.me/invite_link", "join to chat via invite link or @publicname"}, "group": command{"title", "create groupchat «title» with current user"}, @@ -168,6 +180,10 @@ func rawCmdArguments(cmdline string, start uint8) string { return "" } +func keyValueString(key, value string) string { + return fmt.Sprintf("%s: %s", key, value) +} + func (c *Client) unsubscribe(chatID int64) error { return gateway.SendPresence( c.xmpp, @@ -179,11 +195,17 @@ func (c *Client) unsubscribe(chatID int64) error { func (c *Client) sendMessagesReverse(chatID int64, messages []*client.Message) { for i := len(messages) - 1; i >= 0; i-- { + message := messages[i] + reply, _ := c.getMessageReply(message) + gateway.SendMessage( c.jid, strconv.FormatInt(chatID, 10), - c.formatMessage(0, 0, false, messages[i]), + c.formatMessage(0, 0, false, message), + strconv.FormatInt(message.Id, 10), c.xmpp, + reply, + false, ) } } @@ -215,7 +237,7 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string switch cmd { case "login", "code", "password": if cmd == "login" && c.Session.Login != "" { - return "" + return "Phone number already provided, use /cancelauth to start over" } if len(args) < 1 { @@ -223,36 +245,35 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string } if cmd == "login" { - wasSessionLoginEmpty := c.Session.Login == "" - c.Session.Login = args[0] - - if wasSessionLoginEmpty && c.authorizer == nil { - go func() { - err := c.Connect(resource) - if err != nil { - log.Error(errors.Wrap(err, "TDlib connection failure")) - } - }() - // a quirk for authorizer to become ready. If it's still not, - // nothing bad: the command just needs to be resent again - time.Sleep(1e5) + err := c.TryLogin(resource, args[0]) + if err != nil { + return err.Error() } - } - if c.authorizer == nil { - return telegramNotInitialized - } + c.locks.authorizerWriteLock.Lock() + defer c.locks.authorizerWriteLock.Unlock() - switch cmd { - // sign in - case "login": c.authorizer.PhoneNumber <- args[0] - // check auth code - case "code": - c.authorizer.Code <- args[0] - // check auth password - case "password": - c.authorizer.Password <- args[0] + } else { + c.locks.authorizerWriteLock.Lock() + defer c.locks.authorizerWriteLock.Unlock() + + if c.authorizer == nil { + return TelegramNotInitialized + } + + if c.authorizer.isClosed { + return TelegramAuthDone + } + + switch cmd { + // check auth code + case "code": + c.authorizer.Code <- args[0] + // check auth password + case "password": + c.authorizer.Password <- args[0] + } } // sign out case "logout": @@ -271,6 +292,13 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string } c.Session.Login = "" + // cancel auth + case "cancelauth": + if c.Online() { + return "Not allowed when online, use /logout instead" + } + c.cancelAuth() + return "Cancelled" // set @username case "setusername": if !c.Online() { @@ -290,25 +318,36 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string } // set My Name case "setname": - if !c.Online() { - return notOnline - } - var firstname string var lastname string if len(args) > 0 { firstname = args[0] } + if firstname == "" { + return "The name should contain at least one character" + } if len(args) > 1 { - lastname = args[1] + lastname = rawCmdArguments(cmdline, 1) } - _, err := c.client.SetName(&client.SetNameRequest{ - FirstName: firstname, - LastName: lastname, - }) - if err != nil { - return errors.Wrap(err, "Couldn't set name").Error() + c.locks.authorizerWriteLock.Lock() + if c.authorizer != nil && !c.authorizer.isClosed { + c.authorizer.FirstName <- firstname + c.authorizer.LastName <- lastname + c.locks.authorizerWriteLock.Unlock() + } else { + c.locks.authorizerWriteLock.Unlock() + if !c.Online() { + return notOnline + } + + _, err := c.client.SetName(&client.SetNameRequest{ + FirstName: firstname, + LastName: lastname, + }) + if err != nil { + return errors.Wrap(err, "Couldn't set name").Error() + } } // set About case "setbio": @@ -344,6 +383,10 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string } case "config": if len(args) > 1 { + if gateway.MessageOutgoingPermissionVersion == 0 && args[0] == "carbons" && args[1] == "true" { + return "The server did not allow to enable carbons" + } + value, err := c.Session.Set(args[0], args[1]) if err != nil { return err.Error() @@ -387,6 +430,14 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string } else { return "Reported" } + case "add": + return c.cmdAdd(args) + case "join": + return c.cmdJoin(args) + case "supergroup": + return c.cmdSupergroup(args, cmdline) + case "channel": + return c.cmdChannel(args, cmdline) case "help": return helpString(helpTypeTransport) } @@ -463,14 +514,17 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) return "Last message is empty", true } - content := c.ProcessOutgoingMessage(0, rawCmdArguments(cmdline, 0), "") + content := c.PrepareOutgoingMessageContent(rawCmdArguments(cmdline, 0)) if content != nil { - c.client.EditMessageText(&client.EditMessageTextRequest{ + _, err = c.client.EditMessageText(&client.EditMessageTextRequest{ ChatId: chatID, MessageId: message.Id, InputMessageContent: content, }) + if err != nil { + return "Message editing error", true + } } else { return "Message processing error", true } @@ -480,7 +534,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) return "Not enough arguments", true } - content := c.ProcessOutgoingMessage(0, rawCmdArguments(cmdline, 0), "") + content := c.PrepareOutgoingMessageContent(rawCmdArguments(cmdline, 0)) if content != nil { _, err := c.client.SendMessage(&client.SendMessageRequest{ @@ -559,7 +613,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) } } - content := c.ProcessOutgoingMessage(0, rawCmdArguments(cmdline, 1), "") + content := c.PrepareOutgoingMessageContent(rawCmdArguments(cmdline, 1)) if content != nil { _, err := c.client.SendMessage(&client.SendMessageRequest{ @@ -606,80 +660,33 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) c.ProcessIncomingMessage(targetChatId, message) } } - // add @contact - case "add": - if len(args) < 1 { - return notEnoughArguments, true - } - - chat, err := c.client.SearchPublicChat(&client.SearchPublicChatRequest{ - Username: args[0], - }) + // print vCard + case "vcard": + info, err := c.GetVcardInfo(chatID) if err != nil { return err.Error(), true } - if chat == nil { - return "No error, but chat is nil", true + _, link := c.PermastoreFile(info.Photo, true) + entries := []string{ + keyValueString("Chat title", info.Fn), + keyValueString("Photo", link), + keyValueString("Usernames", c.usernamesToString(info.Nicknames)), + keyValueString("Full name", info.Given+" "+info.Family), + keyValueString("Phone number", info.Tel), } - - c.subscribeToID(chat.Id, chat) + return strings.Join(entries, "\n"), true + // add @contact + case "add": + return c.cmdAdd(args), true // join https://t.me/publichat or @publicchat case "join": - if len(args) < 1 { - return notEnoughArguments, true - } - - if strings.HasPrefix(args[0], "@") { - chat, err := c.client.SearchPublicChat(&client.SearchPublicChatRequest{ - Username: args[0], - }) - if err != nil { - return err.Error(), true - } - if chat == nil { - return "No error, but chat is nil", true - } - _, err = c.client.JoinChat(&client.JoinChatRequest{ - ChatId: chat.Id, - }) - if err != nil { - return err.Error(), true - } - } else { - _, err := c.client.JoinChatByInviteLink(&client.JoinChatByInviteLinkRequest{ - InviteLink: args[0], - }) - if err != nil { - return err.Error(), true - } - } + return c.cmdJoin(args), true // create new supergroup case "supergroup": - if len(args) < 1 { - return notEnoughArguments, true - } - - _, err := c.client.CreateNewSupergroupChat(&client.CreateNewSupergroupChatRequest{ - Title: args[0], - Description: rawCmdArguments(cmdline, 1), - }) - if err != nil { - return err.Error(), true - } + return c.cmdSupergroup(args, cmdline), true // create new channel case "channel": - if len(args) < 1 { - return notEnoughArguments, true - } - - _, err := c.client.CreateNewSupergroupChat(&client.CreateNewSupergroupChatRequest{ - Title: args[0], - Description: rawCmdArguments(cmdline, 1), - IsChannel: true, - }) - if err != nil { - return err.Error(), true - } + return c.cmdChannel(args, cmdline), true // create new secret chat with current user case "secret": _, err := c.client.CreateNewSecretChat(&client.CreateNewSecretChatRequest{ @@ -880,7 +887,10 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) } // clone the permissions - status := permissionsAdmin + status := client.ChatMemberStatusAdministrator{ + CanBeEdited: true, + Rights: &permissionsAdmin, + } if len(args) > 1 { status.CustomTitle = args[1] @@ -930,9 +940,9 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) return "Invalid TTL", true } } - _, err = c.client.SetChatMessageTtl(&client.SetChatMessageTtlRequest{ - ChatId: chatID, - Ttl: int32(ttl), + _, err = c.client.SetChatMessageAutoDeleteTime(&client.SetChatMessageAutoDeleteTimeRequest{ + ChatId: chatID, + MessageAutoDeleteTime: int32(ttl), }) if err != nil { @@ -1076,3 +1086,89 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) return "", true } + +func (c *Client) cmdAdd(args []string) string { + if len(args) < 1 { + return notEnoughArguments + } + + chat, err := c.client.SearchPublicChat(&client.SearchPublicChatRequest{ + Username: args[0], + }) + if err != nil { + return err.Error() + } + if chat == nil { + return "No error, but chat is nil" + } + + c.subscribeToID(chat.Id, chat) + + return "" +} + +func (c *Client) cmdJoin(args []string) string { + if len(args) < 1 { + return notEnoughArguments + } + + if strings.HasPrefix(args[0], "@") { + chat, err := c.client.SearchPublicChat(&client.SearchPublicChatRequest{ + Username: args[0], + }) + if err != nil { + return err.Error() + } + if chat == nil { + return "No error, but chat is nil" + } + _, err = c.client.JoinChat(&client.JoinChatRequest{ + ChatId: chat.Id, + }) + if err != nil { + return err.Error() + } + } else { + _, err := c.client.JoinChatByInviteLink(&client.JoinChatByInviteLinkRequest{ + InviteLink: args[0], + }) + if err != nil { + return err.Error() + } + } + + return "" +} + +func (c *Client) cmdSupergroup(args []string, cmdline string) string { + if len(args) < 1 { + return notEnoughArguments + } + + _, err := c.client.CreateNewSupergroupChat(&client.CreateNewSupergroupChatRequest{ + Title: args[0], + Description: rawCmdArguments(cmdline, 1), + }) + if err != nil { + return err.Error() + } + + return "" +} + +func (c *Client) cmdChannel(args []string, cmdline string) string { + if len(args) < 1 { + return notEnoughArguments + } + + _, err := c.client.CreateNewSupergroupChat(&client.CreateNewSupergroupChatRequest{ + Title: args[0], + Description: rawCmdArguments(cmdline, 1), + IsChannel: true, + }) + if err != nil { + return err.Error() + } + + return "" +} diff --git a/telegram/connect.go b/telegram/connect.go index 37f719e..b1b8b10 100644 --- a/telegram/connect.go +++ b/telegram/connect.go @@ -3,6 +3,7 @@ package telegram import ( "github.com/pkg/errors" "strconv" + "time" "dev.narayana.im/narayana/telegabber/xmpp/gateway" @@ -13,25 +14,25 @@ import ( const chatsLimit int32 = 999 type clientAuthorizer struct { - TdlibParameters chan *client.TdlibParameters + TdlibParameters chan *client.SetTdlibParametersRequest PhoneNumber chan string Code chan string State chan client.AuthorizationState Password chan string + FirstName chan string + LastName chan string + isClosed bool } func (stateHandler *clientAuthorizer) Handle(c *client.Client, state client.AuthorizationState) error { + if stateHandler.isClosed { + return errors.New("Channel is closed") + } stateHandler.State <- state switch state.AuthorizationStateType() { case client.TypeAuthorizationStateWaitTdlibParameters: - _, err := c.SetTdlibParameters(&client.SetTdlibParametersRequest{ - Parameters: <-stateHandler.TdlibParameters, - }) - return err - - case client.TypeAuthorizationStateWaitEncryptionKey: - _, err := c.CheckDatabaseEncryptionKey(&client.CheckDatabaseEncryptionKeyRequest{}) + _, err := c.SetTdlibParameters(<-stateHandler.TdlibParameters) return err case client.TypeAuthorizationStateWaitPhoneNumber: @@ -52,7 +53,11 @@ func (stateHandler *clientAuthorizer) Handle(c *client.Client, state client.Auth return err case client.TypeAuthorizationStateWaitRegistration: - return client.ErrNotSupportedAuthorizationState + _, err := c.RegisterUser(&client.RegisterUserRequest{ + FirstName: <-stateHandler.FirstName, + LastName: <-stateHandler.LastName, + }) + return err case client.TypeAuthorizationStateWaitPassword: _, err := c.CheckAuthenticationPassword(&client.CheckAuthenticationPasswordRequest{ @@ -77,42 +82,54 @@ func (stateHandler *clientAuthorizer) Handle(c *client.Client, state client.Auth } func (stateHandler *clientAuthorizer) Close() { + if stateHandler.isClosed { + return + } + stateHandler.isClosed = true close(stateHandler.TdlibParameters) close(stateHandler.PhoneNumber) close(stateHandler.Code) close(stateHandler.State) close(stateHandler.Password) + close(stateHandler.FirstName) + close(stateHandler.LastName) } // Connect starts TDlib connection func (c *Client) Connect(resource string) error { + log.Warn("Attempting to connect to Telegram network...") + // avoid conflict if another authorization is pending already - c.locks.authorizationReady.Wait() + c.locks.authorizationReady.Lock() if c.Online() { c.roster(resource) + c.locks.authorizationReady.Unlock() return nil } log.Warn("Connecting to Telegram network...") + c.locks.authorizerWriteLock.Lock() c.authorizer = &clientAuthorizer{ - TdlibParameters: make(chan *client.TdlibParameters, 1), + TdlibParameters: make(chan *client.SetTdlibParametersRequest, 1), PhoneNumber: make(chan string, 1), Code: make(chan string, 1), State: make(chan client.AuthorizationState, 10), Password: make(chan string, 1), + FirstName: make(chan string, 1), + LastName: make(chan string, 1), } - c.locks.authorizationReady.Add(1) - go c.interactor() + log.Warn("Interactor launched") c.authorizer.TdlibParameters <- c.parameters + c.locks.authorizerWriteLock.Unlock() tdlibClient, err := client.NewClient(c.authorizer, c.options...) if err != nil { - c.locks.authorizationReady.Done() + c.locks.authorizationReady.Unlock() return errors.Wrap(err, "Couldn't initialize a Telegram client instance") } @@ -130,7 +147,7 @@ func (c *Client) Connect(resource string) error { go c.updateHandler() c.online = true - c.locks.authorizationReady.Done() + c.locks.authorizationReady.Unlock() c.addResource(resource) go func() { @@ -141,14 +158,55 @@ func (c *Client) Connect(resource string) error { log.Errorf("Could not retrieve chats: %v", err) } - gateway.SendPresence(c.xmpp, c.jid, gateway.SPType("subscribe")) - gateway.SendPresence(c.xmpp, c.jid, gateway.SPType("subscribed")) + gateway.SubscribeToTransport(c.xmpp, c.jid) gateway.SendPresence(c.xmpp, c.jid, gateway.SPStatus("Logged in as: "+c.Session.Login)) }() return nil } +func (c *Client) TryLogin(resource string, login string) error { + wasSessionLoginEmpty := c.Session.Login == "" + c.Session.Login = login + + if wasSessionLoginEmpty && c.authorizer == nil { + go func() { + err := c.Connect(resource) + if err != nil { + log.Error(errors.Wrap(err, "TDlib connection failure")) + } + }() + // a quirk for authorizer to become ready. If it's still not, + // nothing bad: just re-login again + time.Sleep(1e5) + } + + c.locks.authorizerWriteLock.Lock() + defer c.locks.authorizerWriteLock.Unlock() + + if c.authorizer == nil { + return errors.New(TelegramNotInitialized) + } + + if c.authorizer.isClosed { + return errors.New(TelegramAuthDone) + } + + return nil +} + +func (c *Client) SetPhoneNumber(login string) error { + c.locks.authorizerWriteLock.Lock() + defer c.locks.authorizerWriteLock.Unlock() + + if c.authorizer == nil || c.authorizer.isClosed { + return errors.New("Authorization not needed") + } + + c.authorizer.PhoneNumber <- login + return nil +} + // Disconnect drops TDlib connection and // returns the flag indicating if disconnecting is permitted func (c *Client) Disconnect(resource string, quit bool) bool { @@ -178,20 +236,23 @@ func (c *Client) Disconnect(resource string, quit bool) bool { ) } - _, err := c.client.Close() - if err != nil { - log.Errorf("Couldn't close the Telegram instance: %v; %#v", err, c) - } - c.forceClose() + c.close() return true } func (c *Client) interactor() { for { + c.locks.authorizerReadLock.Lock() + if c.authorizer == nil { + log.Warn("Authorizer is lost, halting the interactor") + c.locks.authorizerReadLock.Unlock() + return + } state, ok := <-c.authorizer.State if !ok { log.Warn("Interactor is disconnected") + c.locks.authorizerReadLock.Unlock() return } @@ -206,25 +267,56 @@ func (c *Client) interactor() { if c.Session.Login != "" { c.authorizer.PhoneNumber <- c.Session.Login } else { - gateway.SendMessage(c.jid, "", "Please, enter your Telegram login via /login 12345", c.xmpp) + gateway.SendServiceMessage(c.jid, "Please, enter your Telegram login via /login 12345", c.xmpp) } // stage 1: wait for auth code case client.TypeAuthorizationStateWaitCode: log.Warn("Waiting for authorization code...") - gateway.SendMessage(c.jid, "", "Please, enter authorization code via /code 12345", c.xmpp) + gateway.SendServiceMessage(c.jid, "Please, enter authorization code via /code 12345", c.xmpp) + // stage 1b: wait for registration + case client.TypeAuthorizationStateWaitRegistration: + log.Warn("Waiting for full name...") + gateway.SendServiceMessage(c.jid, "This number is not registered yet! Please, enter your name via /setname John Doe", c.xmpp) // stage 2: wait for 2fa case client.TypeAuthorizationStateWaitPassword: log.Warn("Waiting for 2FA password...") - gateway.SendMessage(c.jid, "", "Please, enter 2FA passphrase via /password 12345", c.xmpp) + gateway.SendServiceMessage(c.jid, "Please, enter 2FA passphrase via /password 12345", c.xmpp) } + c.locks.authorizerReadLock.Unlock() } } func (c *Client) forceClose() { + c.locks.authorizerReadLock.Lock() + c.locks.authorizerWriteLock.Lock() + defer c.locks.authorizerReadLock.Unlock() + defer c.locks.authorizerWriteLock.Unlock() + c.online = false c.authorizer = nil } +func (c *Client) close() { + c.locks.authorizerWriteLock.Lock() + if c.authorizer != nil && !c.authorizer.isClosed { + c.authorizer.Close() + } + c.locks.authorizerWriteLock.Unlock() + + if c.client != nil { + _, err := c.client.Close() + if err != nil { + log.Errorf("Couldn't close the Telegram instance: %v; %#v", err, c) + } + } + c.forceClose() +} + +func (c *Client) cancelAuth() { + c.close() + c.Session.Login = "" +} + // Online checks if the updates listener is alive func (c *Client) Online() bool { return c.online diff --git a/telegram/handlers.go b/telegram/handlers.go index 126d858..0d1cda9 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -203,11 +203,11 @@ func (c *Client) updateChatLastMessage(update *client.UpdateChatLastMessage) { // message received func (c *Client) updateNewMessage(update *client.UpdateNewMessage) { - go func() { - chatId := update.Message.ChatId + chatId := update.Message.ChatId - // guarantee sequential message delivering per chat - lock := c.getChatMessageLock(chatId) + // guarantee sequential message delivering per chat + lock := c.getChatMessageLock(chatId) + go func() { lock.Lock() defer lock.Unlock() @@ -223,13 +223,35 @@ func (c *Client) updateNewMessage(update *client.UpdateNewMessage) { }).Warn("New message from chat") c.ProcessIncomingMessage(chatId, update.Message) + + c.updateLastMessageHash(update.Message.ChatId, update.Message.Id, update.Message.Content) }() } // message content updated func (c *Client) updateMessageContent(update *client.UpdateMessageContent) { - markupFunction := formatter.EntityToXEP0393 - if update.NewContent.MessageContentType() == client.TypeMessageText { + markupFunction := c.getFormatter() + + defer c.updateLastMessageHash(update.ChatId, update.MessageId, update.NewContent) + + c.SendMessageLock.Lock() + c.SendMessageLock.Unlock() + xmppId, err := gateway.IdsDB.GetByTgIds(c.Session.Login, c.jid, update.ChatId, update.MessageId) + var ignoredResource string + if err == nil { + ignoredResource = c.popFromOutbox(xmppId) + } else { + log.Infof("Couldn't retrieve XMPP message ids for %v, an echo may happen", update.MessageId) + } + log.Infof("ignoredResource: %v", ignoredResource) + + jids := c.getCarbonFullJids(true, ignoredResource) + if len(jids) == 0 { + log.Info("The only resource is ignored, aborting") + return + } + + if update.NewContent.MessageContentType() == client.TypeMessageText && c.hasLastMessageHashChanged(update.ChatId, update.MessageId, update.NewContent) { textContent := update.NewContent.(*client.MessageText) var editChar string if c.Session.AsciiArrows { @@ -242,7 +264,9 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) { textContent.Text.Entities, markupFunction, )) - gateway.SendMessage(c.jid, strconv.FormatInt(update.ChatId, 10), text, c.xmpp) + for _, jid := range jids { + gateway.SendMessage(jid, strconv.FormatInt(update.ChatId, 10), text, "e"+strconv.FormatInt(update.MessageId, 10), c.xmpp, nil, false) + } } } @@ -256,7 +280,7 @@ func (c *Client) updateDeleteMessages(update *client.UpdateDeleteMessages) { deleteChar = "✗ " } text := deleteChar + strings.Join(int64SliceToStringSlice(update.MessageIds), ",") - gateway.SendMessage(c.jid, strconv.FormatInt(update.ChatId, 10), text, c.xmpp) + gateway.SendTextMessage(c.jid, strconv.FormatInt(update.ChatId, 10), text, c.xmpp) } } @@ -272,6 +296,11 @@ func (c *Client) updateAuthorizationState(update *client.UpdateAuthorizationStat // clean uploaded files func (c *Client) updateMessageSendSucceeded(update *client.UpdateMessageSendSucceeded) { + log.Debugf("replace message %v with %v", update.OldMessageId, update.Message.Id) + if err := gateway.IdsDB.ReplaceTgId(c.Session.Login, c.jid, update.Message.ChatId, update.OldMessageId, update.Message.Id); err != nil { + log.Errorf("failed to replace %v with %v: %v", update.OldMessageId, update.Message.Id, err.Error()) + } + file, _ := c.contentToFile(update.Message.Content) if file != nil && file.Local != nil { c.cleanTempFile(file.Local.Path) @@ -289,8 +318,13 @@ func (c *Client) updateChatTitle(update *client.UpdateChatTitle) { gateway.SetNickname(c.jid, strconv.FormatInt(update.ChatId, 10), update.Title, c.xmpp) // set also the status (for group chats only) - _, user, _ := c.GetContactByID(update.ChatId, nil) + chat, user, _ := c.GetContactByID(update.ChatId, nil) if user == nil { c.ProcessStatusUpdate(update.ChatId, update.Title, "chat", gateway.SPImmed(true)) } + + // update chat title in the cache + if chat != nil { + chat.Title = update.Title + } } diff --git a/telegram/utils.go b/telegram/utils.go index 377b784..08c45d2 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -2,8 +2,10 @@ package telegram import ( "crypto/sha1" + "encoding/binary" "fmt" "github.com/pkg/errors" + "hash/maphash" "io" "io/ioutil" "net/http" @@ -24,12 +26,23 @@ import ( "github.com/zelenin/go-tdlib/client" ) +type VCardInfo struct { + Fn string + Photo *client.File + Nicknames []string + Given string + Family string + Tel string + Info string +} + var errOffline = errors.New("TDlib instance is offline") var spaceRegex = regexp.MustCompile(`\s+`) var replyRegex = regexp.MustCompile("\\A>>? ?([0-9]+)\\n") const newlineChar string = "\n" +const messageHeaderSeparator string = " | " // GetContactByUsername resolves username to user id retrieves user and chat information func (c *Client) GetContactByUsername(username string) (*client.Chat, *client.User, error) { @@ -108,6 +121,33 @@ func (c *Client) GetContactByID(id int64, chat *client.Chat) (*client.Chat, *cli return chat, user, nil } +// IsPM checks if a chat is PM +func (c *Client) IsPM(id int64) (bool, error) { + if !c.Online() || id == 0 { + return false, errOffline + } + + var err error + + chat, ok := c.cache.GetChat(id) + if !ok { + chat, err = c.client.GetChat(&client.GetChatRequest{ + ChatId: id, + }) + if err != nil { + return false, err + } + + c.cache.SetChat(id, chat) + } + + chatType := chat.Type.ChatTypeType() + if chatType == client.TypeChatTypePrivate || chatType == client.TypeChatTypeSecret { + return true, nil + } + return false, nil +} + func (c *Client) userStatusToText(status client.UserStatus, chatID int64) (string, string, string) { var show, textStatus, presenceType string @@ -179,7 +219,7 @@ func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, o var photo string if chat != nil && chat.Photo != nil { - file, path, err := c.OpenPhotoFile(chat.Photo.Small, 1) + file, path, err := c.ForceOpenFile(chat.Photo.Small, 1) if err == nil { defer file.Close() @@ -203,15 +243,33 @@ func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, o cachedStatus, ok := c.cache.GetStatus(chatID) if status == "" { if ok { - show, status = cachedStatus.XMPP, cachedStatus.Description + var typ string + show, status, typ = cachedStatus.Destruct() + if presenceType == "" { + presenceType = typ + } + log.WithFields(log.Fields{ + "show": show, + "status": status, + "presenceType": presenceType, + }).Debug("Cached status") } else if user != nil && user.Status != nil { show, status, presenceType = c.userStatusToText(user.Status, chatID) + log.WithFields(log.Fields{ + "show": show, + "status": status, + "presenceType": presenceType, + }).Debug("Status to text") } else { show, status = "chat", chat.Title } } - c.cache.SetStatus(chatID, show, status) + cacheShow := show + if presenceType == "unavailable" { + cacheShow = presenceType + } + c.cache.SetStatus(chatID, cacheShow, status) newArgs := []args.V{ gateway.SPFrom(strconv.FormatInt(chatID, 10)), @@ -246,12 +304,15 @@ func (c *Client) formatContact(chatID int64) string { if chat != nil { str = fmt.Sprintf("%s (%v)", chat.Title, chat.Id) } else if user != nil { - username := user.Username - if username == "" { - username = strconv.FormatInt(user.Id, 10) + var usernames string + if user.Usernames != nil { + usernames = c.usernamesToString(user.Usernames.ActiveUsernames) + } + if usernames == "" { + usernames = strconv.FormatInt(user.Id, 10) } - str = fmt.Sprintf("%s %s (%v)", user.FirstName, user.LastName, username) + str = fmt.Sprintf("%s %s (%v)", user.FirstName, user.LastName, usernames) } else { str = strconv.FormatInt(chatID, 10) } @@ -261,6 +322,50 @@ func (c *Client) formatContact(chatID int64) string { return str } +func (c *Client) getSenderId(message *client.Message) (senderId int64) { + if message.SenderId != nil { + switch message.SenderId.MessageSenderType() { + case client.TypeMessageSenderUser: + senderUser, _ := message.SenderId.(*client.MessageSenderUser) + senderId = senderUser.UserId + case client.TypeMessageSenderChat: + senderChat, _ := message.SenderId.(*client.MessageSenderChat) + senderId = senderChat.ChatId + } + } + + return +} + +func (c *Client) formatSender(message *client.Message) string { + return c.formatContact(c.getSenderId(message)) +} + +func (c *Client) getMessageReply(message *client.Message) (reply *gateway.Reply, replyMsg *client.Message) { + if message.ReplyToMessageId != 0 { + var err error + replyMsg, err = c.client.GetMessage(&client.GetMessageRequest{ + ChatId: message.ChatId, + MessageId: message.ReplyToMessageId, + }) + if err != nil { + log.Errorf("<error fetching message: %s>", err.Error()) + return + } + + replyId, err := gateway.IdsDB.GetByTgIds(c.Session.Login, c.jid, message.ChatId, message.ReplyToMessageId) + if err != nil { + replyId = strconv.FormatInt(message.ReplyToMessageId, 10) + } + reply = &gateway.Reply{ + Author: fmt.Sprintf("%v@%s", c.getSenderId(replyMsg), gateway.Jid.Full()), + Id: replyId, + } + } + + return +} + func (c *Client) formatMessage(chatID int64, messageID int64, preview bool, message *client.Message) string { var err error if message == nil { @@ -279,18 +384,7 @@ func (c *Client) formatMessage(chatID int64, messageID int64, preview bool, mess var str strings.Builder // add messageid and sender - var senderId int64 - if message.SenderId != nil { - switch message.SenderId.MessageSenderType() { - case client.TypeMessageSenderUser: - senderUser, _ := message.SenderId.(*client.MessageSenderUser) - senderId = senderUser.UserId - case client.TypeMessageSenderChat: - senderChat, _ := message.SenderId.(*client.MessageSenderChat) - senderId = senderChat.ChatId - } - } - str.WriteString(fmt.Sprintf("%v | %s | ", message.Id, c.formatContact(senderId))) + str.WriteString(fmt.Sprintf("%v | %s | ", message.Id, c.formatSender(message))) // add date if !preview { str.WriteString( @@ -350,10 +444,24 @@ func (c *Client) formatForward(fwd *client.MessageForwardInfo) string { return "Unknown forward type" } -func (c *Client) formatFile(file *client.File, compact bool) string { +func (c *Client) formatFile(file *client.File, compact bool) (string, string) { + if file == nil { + return "", "" + } + src, link := c.PermastoreFile(file, false) + + if compact { + return link, link + } else { + return fmt.Sprintf("%s (%v kbytes) | %s", filepath.Base(src), file.Size/1024, link), link + } +} + +// PermastoreFile steals a file out of TDlib control into an independent shared directory +func (c *Client) PermastoreFile(file *client.File, clone bool) (string, string) { log.Debugf("file: %#v", file) if file == nil || file.Local == nil || file.Remote == nil { - return "" + return "", "" } gateway.StorageLock.Lock() @@ -367,7 +475,7 @@ func (c *Client) formatFile(file *client.File, compact bool) string { _, err := os.Stat(src) if err != nil { log.Errorf("Cannot access source file: %v", err) - return "" + return "", "" } size64 := uint64(file.Size) @@ -377,18 +485,57 @@ func (c *Client) formatFile(file *client.File, compact bool) string { dest := c.content.Path + "/" + basename // destination path link = c.content.Link + "/" + basename // download link - // move - err = os.Rename(src, dest) - if err != nil { - linkErr := err.(*os.LinkError) - if linkErr.Err.Error() == "file exists" { - log.Warn(err.Error()) + if clone { + file, path, err := c.ForceOpenFile(file, 1) + if err == nil { + defer file.Close() + + // mode + mode := os.FileMode(0644) + fi, err := os.Stat(path) + if err == nil { + mode = fi.Mode().Perm() + } + + // create destination + tempFile, err := os.OpenFile(dest, os.O_CREATE|os.O_EXCL|os.O_WRONLY, mode) + if err != nil { + pathErr := err.(*os.PathError) + if pathErr.Err.Error() == "file exists" { + log.Warn(err.Error()) + return src, link + } else { + log.Errorf("File creation error: %v", err) + return "<ERROR>", "" + } + } + defer tempFile.Close() + // copy + _, err = io.Copy(tempFile, file) + if err != nil { + log.Errorf("File copying error: %v", err) + return "<ERROR>", "" + } + } else if path != "" { + log.Errorf("Source file does not exist: %v", path) + return "<ERROR>", "" } else { - log.Errorf("File moving error: %v", err) - return "<ERROR>" + log.Errorf("PHOTO: %#v", err.Error()) + return "<ERROR>", "" + } + } else { + // move + err = os.Rename(src, dest) + if err != nil { + linkErr := err.(*os.LinkError) + if linkErr.Err.Error() == "file exists" { + log.Warn(err.Error()) + } else { + log.Errorf("File moving error: %v", err) + return "<ERROR>", "" + } } } - gateway.CachedStorageSize += size64 // chown if c.content.User != "" { @@ -407,13 +554,12 @@ func (c *Client) formatFile(file *client.File, compact bool) string { log.Errorf("Wrong user name for chown: %v", err) } } - } - if compact { - return link - } else { - return fmt.Sprintf("%s (%v kbytes) | %s", filepath.Base(src), file.Size/1024, link) + // copy or move should have succeeded at this point + gateway.CachedStorageSize += size64 } + + return src, link } func (c *Client) formatBantime(hours int64) int32 { @@ -441,7 +587,7 @@ func (c *Client) messageToText(message *client.Message, preview bool) string { return "<empty message>" } - markupFunction := formatter.EntityToXEP0393 + markupFunction := c.getFormatter() switch message.Content.MessageContentType() { case client.TypeMessageSticker: sticker, _ := message.Content.(*client.MessageSticker) @@ -612,6 +758,22 @@ func (c *Client) messageToText(message *client.Message, preview bool) string { return strings.Join(rows, "\n") } + case client.TypeMessageChatSetMessageAutoDeleteTime: + ttl, _ := message.Content.(*client.MessageChatSetMessageAutoDeleteTime) + name := c.formatContact(ttl.FromUserId) + if name == "" { + if ttl.MessageAutoDeleteTime == 0 { + return "The self-destruct timer was disabled" + } else { + return fmt.Sprintf("The self-destruct timer was set to %v seconds", ttl.MessageAutoDeleteTime) + } + } else { + if ttl.MessageAutoDeleteTime == 0 { + return fmt.Sprintf("%s disabled the self-destruct timer", name) + } else { + return fmt.Sprintf("%s set the self-destruct timer to %v seconds", name, ttl.MessageAutoDeleteTime) + } + } } return fmt.Sprintf("unknown message (%s)", message.Content.MessageContentType()) @@ -626,7 +788,7 @@ func (c *Client) contentToFile(content client.MessageContent) (*client.File, *cl case client.TypeMessageSticker: sticker, _ := content.(*client.MessageSticker) file := sticker.Sticker.Sticker - if sticker.Sticker.IsAnimated && sticker.Sticker.Thumbnail != nil && sticker.Sticker.Thumbnail.File != nil { + if sticker.Sticker.Format.StickerFormatType() == client.TypeStickerFormatTgs && sticker.Sticker.Thumbnail != nil && sticker.Sticker.Thumbnail.File != nil { file = sticker.Sticker.Thumbnail.File } return file, nil @@ -681,40 +843,62 @@ func (c *Client) contentToFile(content client.MessageContent) (*client.File, *cl return nil, nil } -func (c *Client) messageToPrefix(message *client.Message, previewString string, fileString string) string { +func (c *Client) countCharsInLines(lines *[]string) (count int) { + for _, line := range *lines { + count += len(line) + } + return +} + +func (c *Client) messageToPrefix(message *client.Message, previewString string, fileString string, replyMsg *client.Message) (string, int, int) { + isPM, err := c.IsPM(message.ChatId) + if err != nil { + log.Errorf("Could not determine if chat is PM: %v", err) + } + isCarbonsEnabled := gateway.MessageOutgoingPermissionVersion > 0 && c.Session.Carbons + // with carbons, hide for all messages in PM and only for outgoing in group chats + hideSender := isCarbonsEnabled && (message.IsOutgoing || isPM) + + var replyStart, replyEnd int prefix := []string{} // message direction var directionChar string - if c.Session.AsciiArrows { - if message.IsOutgoing { - directionChar = "> " - } else { - directionChar = "< " - } - } else { - if message.IsOutgoing { - directionChar = "➡ " + if !hideSender { + if c.Session.AsciiArrows { + if message.IsOutgoing { + directionChar = "> " + } else { + directionChar = "< " + } } else { - directionChar = "⬅ " + if message.IsOutgoing { + directionChar = "➡ " + } else { + directionChar = "⬅ " + } } } - prefix = append(prefix, directionChar+strconv.FormatInt(message.Id, 10)) + if !isPM || !c.Session.HideIds { + prefix = append(prefix, directionChar+strconv.FormatInt(message.Id, 10)) + } // show sender in group chats - if message.ChatId < 0 && message.SenderId != nil { - var senderId int64 - switch message.SenderId.MessageSenderType() { - case client.TypeMessageSenderUser: - senderUser, _ := message.SenderId.(*client.MessageSenderUser) - senderId = senderUser.UserId - case client.TypeMessageSenderChat: - senderChat, _ := message.SenderId.(*client.MessageSenderChat) - senderId = senderChat.ChatId + if !hideSender { + sender := c.formatSender(message) + if sender != "" { + prefix = append(prefix, sender) } - prefix = append(prefix, c.formatContact(senderId)) } // reply to if message.ReplyToMessageId != 0 { - prefix = append(prefix, "reply: "+c.formatMessage(message.ChatId, message.ReplyToMessageId, true, nil)) + if len(prefix) > 0 { + replyStart = c.countCharsInLines(&prefix) + (len(prefix)-1)*len(messageHeaderSeparator) + } + replyLine := "reply: " + c.formatMessage(message.ChatId, message.ReplyToMessageId, true, replyMsg) + prefix = append(prefix, replyLine) + replyEnd = replyStart + len(replyLine) + if len(prefix) > 0 { + replyEnd += len(messageHeaderSeparator) + } } if message.ForwardInfo != nil { prefix = append(prefix, "fwd: "+c.formatForward(message.ForwardInfo)) @@ -728,7 +912,7 @@ func (c *Client) messageToPrefix(message *client.Message, previewString string, prefix = append(prefix, "file: "+fileString) } - return strings.Join(prefix, " | ") + return strings.Join(prefix, messageHeaderSeparator), replyStart, replyEnd } func (c *Client) ensureDownloadFile(file *client.File) *client.File { @@ -749,7 +933,13 @@ func (c *Client) ensureDownloadFile(file *client.File) *client.File { // ProcessIncomingMessage transfers a message to XMPP side and marks it as read on Telegram side func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { - var text string + isCarbon := gateway.MessageOutgoingPermissionVersion > 0 && c.Session.Carbons && message.IsOutgoing + jids := c.getCarbonFullJids(isCarbon, "") + + var text, oob, auxText string + + reply, replyMsg := c.getMessageReply(message) + content := message.Content if content != nil && content.MessageContentType() == client.TypeMessageChatChangePhoto { chat, err := c.client.GetChat(&client.GetChatRequest{ @@ -764,27 +954,47 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { text = c.messageToText(message, false) // OTR support (I do not know why would you need it, seriously) - if !(strings.HasPrefix(text, "?OTR") || c.Session.RawMessages) { + if !(strings.HasPrefix(text, "?OTR") || (c.Session.RawMessages && !c.Session.OOBMode)) { file, preview := c.contentToFile(content) // download file and preview (if present) file = c.ensureDownloadFile(file) preview = c.ensureDownloadFile(preview) - var prefix strings.Builder - prefix.WriteString(c.messageToPrefix(message, c.formatFile(preview, true), c.formatFile(file, false))) - if text != "" { - // \n if it is groupchat and message is not empty - if chatId < 0 { - prefix.WriteString("\n") - } else if chatId > 0 { - prefix.WriteString(" | ") + previewName, _ := c.formatFile(preview, true) + fileName, link := c.formatFile(file, false) + + oob = link + if c.Session.OOBMode && oob != "" { + typ := message.Content.MessageContentType() + if typ != client.TypeMessageSticker { + auxText = text + } + text = oob + } else if !c.Session.RawMessages { + var newText strings.Builder + + prefix, replyStart, replyEnd := c.messageToPrefix(message, previewName, fileName, replyMsg) + newText.WriteString(prefix) + if reply != nil { + reply.Start = uint64(replyStart) + reply.End = uint64(replyEnd) } - prefix.WriteString(text) - } + if text != "" { + // \n if it is groupchat and message is not empty + if prefix != "" { + if chatId < 0 { + newText.WriteString("\n") + } else if chatId > 0 { + newText.WriteString(" | ") + } + } - text = prefix.String() + newText.WriteString(text) + } + text = newText.String() + } } } @@ -794,26 +1004,40 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { MessageIds: []int64{message.Id}, ForceRead: true, }) + // forward message to XMPP - gateway.SendMessage(c.jid, strconv.FormatInt(chatId, 10), text, c.xmpp) + sId := strconv.FormatInt(message.Id, 10) + sChatId := strconv.FormatInt(chatId, 10) + + for _, jid := range jids { + gateway.SendMessageWithOOB(jid, sChatId, text, sId, c.xmpp, reply, oob, isCarbon) + if auxText != "" { + gateway.SendMessage(jid, sChatId, auxText, sId, c.xmpp, reply, isCarbon) + } + } +} + +// PrepareMessageContent creates a simple text message +func (c *Client) PrepareOutgoingMessageContent(text string) client.InputMessageContent { + return c.prepareOutgoingMessageContent(text, nil) } -// ProcessOutgoingMessage executes commands or sends messages to mapped chats -func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid string) client.InputMessageContent { +// ProcessOutgoingMessage executes commands or sends messages to mapped chats, returns message id +func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid string, replyId int64, replaceId int64) int64 { if !c.Online() { // we're offline - return nil + return 0 } - if returnJid != "" && (strings.HasPrefix(text, "/") || strings.HasPrefix(text, "!")) { + if replaceId == 0 && (strings.HasPrefix(text, "/") || strings.HasPrefix(text, "!")) { // try to execute commands response, isCommand := c.ProcessChatCommand(chatID, text) if response != "" { - gateway.SendMessage(returnJid, strconv.FormatInt(chatID, 10), response, c.xmpp) + c.returnMessage(returnJid, chatID, response) } // do not send on success if isCommand { - return nil + return 0 } } @@ -821,67 +1045,41 @@ func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid str // quotations var reply int64 - replySlice := replyRegex.FindStringSubmatch(text) - if len(replySlice) > 1 { - reply, _ = strconv.ParseInt(replySlice[1], 10, 64) + if replaceId == 0 && replyId == 0 { + replySlice := replyRegex.FindStringSubmatch(text) + if len(replySlice) > 1 { + reply, _ = strconv.ParseInt(replySlice[1], 10, 64) + } + } else { + reply = replyId } // attach a file var file *client.InputFileLocal - if chatID != 0 && c.content.Upload != "" && strings.HasPrefix(text, c.content.Upload) { + if c.content.Upload != "" && strings.HasPrefix(text, c.content.Upload) { response, err := http.Get(text) if err != nil { - gateway.SendMessage( - returnJid, - strconv.FormatInt(chatID, 10), - fmt.Sprintf("Failed to fetch the uploaded file: %s", err.Error()), - c.xmpp, - ) - return nil + c.returnError(returnJid, chatID, "Failed to fetch the uploaded file", err) } if response != nil && response.Body != nil { defer response.Body.Close() if response.StatusCode != 200 { - gateway.SendMessage( - returnJid, - strconv.FormatInt(chatID, 10), - fmt.Sprintf("Received status code %v", response.StatusCode), - c.xmpp, - ) - return nil + c.returnMessage(returnJid, chatID, fmt.Sprintf("Received status code %v", response.StatusCode)) } tempDir, err := ioutil.TempDir("", "telegabber-*") if err != nil { - gateway.SendMessage( - returnJid, - strconv.FormatInt(chatID, 10), - fmt.Sprintf("Failed to create a temporary directory: %s", err.Error()), - c.xmpp, - ) - return nil + c.returnError(returnJid, chatID, "Failed to create a temporary directory", err) } tempFile, err := os.Create(filepath.Join(tempDir, filepath.Base(text))) if err != nil { - gateway.SendMessage( - returnJid, - strconv.FormatInt(chatID, 10), - fmt.Sprintf("Failed to create a temporary file: %s", err.Error()), - c.xmpp, - ) - return nil + c.returnError(returnJid, chatID, "Failed to create a temporary file", err) } _, err = io.Copy(tempFile, response.Body) if err != nil { - gateway.SendMessage( - returnJid, - strconv.FormatInt(chatID, 10), - fmt.Sprintf("Failed to write a temporary file: %s", err.Error()), - c.xmpp, - ) - return nil + c.returnError(returnJid, chatID, "Failed to write a temporary file", err) } file = &client.InputFileLocal{ @@ -891,7 +1089,7 @@ func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid str } // remove first line from text - if file != nil || reply != 0 { + if file != nil || (reply != 0 && replyId == 0) { newlinePos := strings.Index(text, newlineChar) if newlinePos != -1 { text = text[newlinePos+1:] @@ -900,42 +1098,60 @@ func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid str } } + content := c.prepareOutgoingMessageContent(text, file) + + if replaceId != 0 { + tgMessage, err := c.client.EditMessageText(&client.EditMessageTextRequest{ + ChatId: chatID, + MessageId: replaceId, + InputMessageContent: content, + }) + if err != nil { + c.returnError(returnJid, chatID, "Not edited", err) + return 0 + } + return tgMessage.Id + } + + tgMessage, err := c.client.SendMessage(&client.SendMessageRequest{ + ChatId: chatID, + ReplyToMessageId: reply, + InputMessageContent: content, + }) + if err != nil { + c.returnError(returnJid, chatID, "Not sent", err) + return 0 + } + return tgMessage.Id +} + +func (c *Client) returnMessage(returnJid string, chatID int64, text string) { + gateway.SendTextMessage(returnJid, strconv.FormatInt(chatID, 10), text, c.xmpp) +} + +func (c *Client) returnError(returnJid string, chatID int64, msg string, err error) { + c.returnMessage(returnJid, chatID, fmt.Sprintf("%s: %s", msg, err.Error())) +} + +func (c *Client) prepareOutgoingMessageContent(text string, file *client.InputFileLocal) client.InputMessageContent { formattedText := &client.FormattedText{ Text: text, } - var message client.InputMessageContent + var content client.InputMessageContent if file != nil { // we can try to send a document - message = &client.InputMessageDocument{ + content = &client.InputMessageDocument{ Document: file, Caption: formattedText, } } else { // compile our message - message = &client.InputMessageText{ + content = &client.InputMessageText{ Text: formattedText, } } - - if chatID != 0 { - _, err := c.client.SendMessage(&client.SendMessageRequest{ - ChatId: chatID, - ReplyToMessageId: reply, - InputMessageContent: message, - }) - if err != nil { - gateway.SendMessage( - returnJid, - strconv.FormatInt(chatID, 10), - fmt.Sprintf("Not sent: %s", err.Error()), - c.xmpp, - ) - } - return nil - } else { - return message - } + return content } // StatusesRange proxies the following function from unexported cache @@ -962,11 +1178,33 @@ func (c *Client) deleteResource(resource string) { } } +func (c *Client) resourcesRange() chan string { + c.locks.resourcesLock.Lock() + + resourceChan := make(chan string, 1) + + go func() { + defer func() { + c.locks.resourcesLock.Unlock() + close(resourceChan) + }() + + for resource := range c.resources { + resourceChan <- resource + } + }() + + return resourceChan +} + // resend statuses to (to another resource, for example) func (c *Client) roster(resource string) { + c.locks.resourcesLock.Lock() if _, ok := c.resources[resource]; ok { + c.locks.resourcesLock.Unlock() return // we know it } + c.locks.resourcesLock.Unlock() log.Warnf("Sending roster for %v", resource) @@ -980,7 +1218,7 @@ func (c *Client) roster(resource string) { } // get last messages from specified chat -func (c *Client) getLastMessages(id int64, query string, from int64, count int32) (*client.Messages, error) { +func (c *Client) getLastMessages(id int64, query string, from int64, count int32) (*client.FoundChatMessages, error) { return c.client.SearchChatMessages(&client.SearchChatMessagesRequest{ ChatId: id, Query: query, @@ -999,20 +1237,20 @@ func (c *Client) DownloadFile(id int32, priority int32, synchronous bool) (*clie }) } -// OpenPhotoFile reliably obtains a photo if possible -func (c *Client) OpenPhotoFile(photoFile *client.File, priority int32) (*os.File, string, error) { - if photoFile == nil { - return nil, "", errors.New("Photo file not found") +// ForceOpenFile reliably obtains a file if possible +func (c *Client) ForceOpenFile(tgFile *client.File, priority int32) (*os.File, string, error) { + if tgFile == nil { + return nil, "", errors.New("File not found") } - path := photoFile.Local.Path + path := tgFile.Local.Path file, err := os.Open(path) if err == nil { return file, path, nil } else // obtain the photo right now if still not downloaded - if !photoFile.Local.IsDownloadingCompleted { - tdFile, tdErr := c.DownloadFile(photoFile.Id, priority, true) + if !tgFile.Local.IsDownloadingCompleted { + tdFile, tdErr := c.DownloadFile(tgFile.Id, priority, true) if tdErr == nil { path = tdFile.Local.Path file, err = os.Open(path) @@ -1033,10 +1271,18 @@ func (c *Client) GetChatDescription(chat *client.Chat) string { UserId: privateType.UserId, }) if err == nil { - if fullInfo.Bio != "" { - return fullInfo.Bio - } else if fullInfo.Description != "" { - return fullInfo.Description + if fullInfo.Bio != nil && fullInfo.Bio.Text != "" { + return formatter.Format( + fullInfo.Bio.Text, + fullInfo.Bio.Entities, + c.getFormatter(), + ) + } else if fullInfo.BotInfo != nil { + if fullInfo.BotInfo.ShortDescription != "" { + return fullInfo.BotInfo.ShortDescription + } else { + return fullInfo.BotInfo.Description + } } } else { log.Warnf("Couldn't retrieve private chat info: %v", err.Error()) @@ -1155,3 +1401,162 @@ func (c *Client) prepareDiskSpace(size uint64) { } } } + +func (c *Client) GetVcardInfo(toID int64) (VCardInfo, error) { + var info VCardInfo + chat, user, err := c.GetContactByID(toID, nil) + if err != nil { + return info, err + } + + if chat != nil { + info.Fn = chat.Title + + if chat.Photo != nil { + info.Photo = chat.Photo.Small + } + info.Info = c.GetChatDescription(chat) + } + if user != nil { + if user.Usernames != nil { + info.Nicknames = make([]string, len(user.Usernames.ActiveUsernames)) + copy(info.Nicknames, user.Usernames.ActiveUsernames) + } + info.Given = user.FirstName + info.Family = user.LastName + info.Tel = user.PhoneNumber + } + + return info, nil +} + +func (c *Client) UpdateChatNicknames() { + for _, id := range c.cache.ChatsKeys() { + chat, ok := c.cache.GetChat(id) + if ok { + newArgs := []args.V{ + gateway.SPFrom(strconv.FormatInt(id, 10)), + gateway.SPNickname(chat.Title), + } + + cachedStatus, ok := c.cache.GetStatus(id) + if ok { + show, status, typ := cachedStatus.Destruct() + newArgs = append(newArgs, gateway.SPShow(show), gateway.SPStatus(status)) + if typ != "" { + newArgs = append(newArgs, gateway.SPType(typ)) + } + } + + gateway.SendPresence( + c.xmpp, + c.jid, + newArgs..., + ) + + gateway.SetNickname(c.jid, strconv.FormatInt(id, 10), chat.Title, c.xmpp) + } + } +} + +// AddToOutbox remembers the resource from which a message with given ID was sent +func (c *Client) AddToOutbox(xmppId, resource string) { + c.locks.outboxLock.Lock() + defer c.locks.outboxLock.Unlock() + + c.outbox[xmppId] = resource +} + +func (c *Client) popFromOutbox(xmppId string) string { + c.locks.outboxLock.Lock() + defer c.locks.outboxLock.Unlock() + + resource, ok := c.outbox[xmppId] + if ok { + delete(c.outbox, xmppId) + } else { + log.Warnf("No %v xmppId in outbox", xmppId) + } + return resource +} + +func (c *Client) getCarbonFullJids(isOutgoing bool, ignoredResource string) []string { + var jids []string + if isOutgoing { + for resource := range c.resourcesRange() { + if ignoredResource == "" || resource != ignoredResource { + jids = append(jids, c.jid+"/"+resource) + } + } + } else { + jids = []string{c.jid} + } + return jids +} + +func (c *Client) calculateMessageHash(messageId int64, content client.MessageContent) uint64 { + var h maphash.Hash + h.SetSeed(c.msgHashSeed) + + buf8 := make([]byte, 8) + binary.BigEndian.PutUint64(buf8, uint64(messageId)) + h.Write(buf8) + + if content != nil && content.MessageContentType() == client.TypeMessageText { + textContent, ok := content.(*client.MessageText) + if !ok { + uhOh() + } + + if textContent.Text != nil { + h.WriteString(textContent.Text.Text) + for _, entity := range textContent.Text.Entities { + buf4 := make([]byte, 4) + binary.BigEndian.PutUint32(buf4, uint32(entity.Offset)) + h.Write(buf4) + binary.BigEndian.PutUint32(buf4, uint32(entity.Length)) + h.Write(buf4) + h.WriteString(entity.Type.TextEntityTypeType()) + } + } + } + + return h.Sum64() +} + +func (c *Client) updateLastMessageHash(chatId, messageId int64, content client.MessageContent) { + c.locks.lastMsgHashesLock.Lock() + defer c.locks.lastMsgHashesLock.Unlock() + + c.lastMsgHashes[chatId] = c.calculateMessageHash(messageId, content) +} + +func (c *Client) hasLastMessageHashChanged(chatId, messageId int64, content client.MessageContent) bool { + c.locks.lastMsgHashesLock.Lock() + defer c.locks.lastMsgHashesLock.Unlock() + + oldHash, ok := c.lastMsgHashes[chatId] + newHash := c.calculateMessageHash(messageId, content) + + if !ok { + log.Warnf("Last message hash for chat %v does not exist", chatId) + } + log.WithFields(log.Fields{ + "old hash": oldHash, + "new hash": newHash, + }).Info("Message hashes") + + return !ok || oldHash != newHash +} + +func (c *Client) getFormatter() func(*client.TextEntity) (*formatter.Insertion, *formatter.Insertion) { + return formatter.EntityToXEP0393 +} + +func (c *Client) usernamesToString(usernames []string) string { + var atUsernames []string + for _, username := range usernames { + atUsernames = append(atUsernames, "@"+username) + } + return strings.Join(atUsernames, ", ") +} diff --git a/telegram/utils_test.go b/telegram/utils_test.go index f0140ae..e54ddb5 100644 --- a/telegram/utils_test.go +++ b/telegram/utils_test.go @@ -146,10 +146,13 @@ func TestFormatFile(t *testing.T) { }, } - content := c.formatFile(&file, false) + content, link := c.formatFile(&file, false) if content != ". (23 kbytes) | " { t.Errorf("Wrong file label: %v", content) } + if link != "" { + t.Errorf("Wrong file link: %v", link) + } } func TestFormatPreview(t *testing.T) { @@ -168,10 +171,13 @@ func TestFormatPreview(t *testing.T) { }, } - content := c.formatFile(&file, true) + content, link := c.formatFile(&file, true) if content != "" { t.Errorf("Wrong preview label: %v", content) } + if link != "" { + t.Errorf("Wrong preview link: %v", link) + } } func TestMessageToTextSticker(t *testing.T) { @@ -363,6 +369,53 @@ func TestMessageAnimation(t *testing.T) { } } +func TestMessageTtl1(t *testing.T) { + ttl := client.Message{ + Content: &client.MessageChatSetMessageAutoDeleteTime{}, + } + text := (&Client{}).messageToText(&ttl, false) + if text != "The self-destruct timer was disabled" { + t.Errorf("Wrong anonymous off ttl label: %v", text) + } +} + +func TestMessageTtl2(t *testing.T) { + ttl := client.Message{ + Content: &client.MessageChatSetMessageAutoDeleteTime{ + MessageAutoDeleteTime: 3, + }, + } + text := (&Client{}).messageToText(&ttl, false) + if text != "The self-destruct timer was set to 3 seconds" { + t.Errorf("Wrong anonymous ttl label: %v", text) + } +} + +func TestMessageTtl3(t *testing.T) { + ttl := client.Message{ + Content: &client.MessageChatSetMessageAutoDeleteTime{ + FromUserId: 3, + }, + } + text := (&Client{}).messageToText(&ttl, false) + if text != "unknown contact: TDlib instance is offline disabled the self-destruct timer" { + t.Errorf("Wrong off ttl label: %v", text) + } +} + +func TestMessageTtl4(t *testing.T) { + ttl := client.Message{ + Content: &client.MessageChatSetMessageAutoDeleteTime{ + FromUserId: 3, + MessageAutoDeleteTime: 3, + }, + } + text := (&Client{}).messageToText(&ttl, false) + if text != "unknown contact: TDlib instance is offline set the self-destruct timer to 3 seconds" { + t.Errorf("Wrong ttl label: %v", text) + } +} + func TestMessageUnknown(t *testing.T) { unknown := client.Message{ Content: &client.MessageExpiredPhoto{}, @@ -383,10 +436,16 @@ func TestMessageToPrefix1(t *testing.T) { }, }, } - prefix := (&Client{Session: &persistence.Session{}}).messageToPrefix(&message, "", "") + prefix, replyStart, replyEnd := (&Client{Session: &persistence.Session{}}).messageToPrefix(&message, "", "", nil) if prefix != "➡ 42 | fwd: ziz" { t.Errorf("Wrong prefix: %v", prefix) } + if replyStart != 0 { + t.Errorf("Wrong replyStart: %v", replyStart) + } + if replyEnd != 0 { + t.Errorf("Wrong replyEnd: %v", replyEnd) + } } func TestMessageToPrefix2(t *testing.T) { @@ -398,10 +457,16 @@ func TestMessageToPrefix2(t *testing.T) { }, }, } - prefix := (&Client{Session: &persistence.Session{}}).messageToPrefix(&message, "y.jpg", "") + prefix, replyStart, replyEnd := (&Client{Session: &persistence.Session{}}).messageToPrefix(&message, "y.jpg", "", nil) if prefix != "⬅ 56 | fwd: (zaz) | preview: y.jpg" { t.Errorf("Wrong prefix: %v", prefix) } + if replyStart != 0 { + t.Errorf("Wrong replyStart: %v", replyStart) + } + if replyEnd != 0 { + t.Errorf("Wrong replyEnd: %v", replyEnd) + } } func TestMessageToPrefix3(t *testing.T) { @@ -413,10 +478,16 @@ func TestMessageToPrefix3(t *testing.T) { }, }, } - prefix := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "", "a.jpg") + prefix, replyStart, replyEnd := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "", "a.jpg", nil) if prefix != "< 56 | fwd: (zuz) | file: a.jpg" { t.Errorf("Wrong prefix: %v", prefix) } + if replyStart != 0 { + t.Errorf("Wrong replyStart: %v", replyStart) + } + if replyEnd != 0 { + t.Errorf("Wrong replyEnd: %v", replyEnd) + } } func TestMessageToPrefix4(t *testing.T) { @@ -424,10 +495,16 @@ func TestMessageToPrefix4(t *testing.T) { Id: 23, IsOutgoing: true, } - prefix := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "", "") + prefix, replyStart, replyEnd := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "", "", nil) if prefix != "> 23" { t.Errorf("Wrong prefix: %v", prefix) } + if replyStart != 0 { + t.Errorf("Wrong replyStart: %v", replyStart) + } + if replyEnd != 0 { + t.Errorf("Wrong replyEnd: %v", replyEnd) + } } func TestMessageToPrefix5(t *testing.T) { @@ -439,8 +516,72 @@ func TestMessageToPrefix5(t *testing.T) { }, }, } - prefix := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "h.jpg", "a.jpg") + prefix, replyStart, replyEnd := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "h.jpg", "a.jpg", nil) if prefix != "< 560 | fwd: (zyz) | preview: h.jpg | file: a.jpg" { t.Errorf("Wrong prefix: %v", prefix) } + if replyStart != 0 { + t.Errorf("Wrong replyStart: %v", replyStart) + } + if replyEnd != 0 { + t.Errorf("Wrong replyEnd: %v", replyEnd) + } +} + +func TestMessageToPrefix6(t *testing.T) { + message := client.Message{ + Id: 23, + IsOutgoing: true, + ReplyToMessageId: 42, + } + reply := client.Message{ + Id: 42, + Content: &client.MessageText{ + Text: &client.FormattedText{ + Text: "tist", + }, + }, + } + prefix, replyStart, replyEnd := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "", "", &reply) + if prefix != "> 23 | reply: 42 | | tist" { + t.Errorf("Wrong prefix: %v", prefix) + } + if replyStart != 4 { + t.Errorf("Wrong replyStart: %v", replyStart) + } + if replyEnd != 26 { + t.Errorf("Wrong replyEnd: %v", replyEnd) + } +} + +func GetSenderIdEmpty(t *testing.T) { + message := client.Message{} + senderId := (&Client{}).getSenderId(&message) + if senderId != 0 { + t.Errorf("Wrong sender id: %v", senderId) + } +} + +func GetSenderIdUser(t *testing.T) { + message := client.Message{ + SenderId: &client.MessageSenderUser{ + UserId: 42, + }, + } + senderId := (&Client{}).getSenderId(&message) + if senderId != 42 { + t.Errorf("Wrong sender id: %v", senderId) + } +} + +func GetSenderIdChat(t *testing.T) { + message := client.Message{ + SenderId: &client.MessageSenderChat{ + ChatId: -42, + }, + } + senderId := (&Client{}).getSenderId(&message) + if senderId != -42 { + t.Errorf("Wrong sender id: %v", senderId) + } } |