package telegram import ( "crypto/sha1" "crypto/sha256" "fmt" "github.com/pkg/errors" "io" "os" "path/filepath" "regexp" "strconv" "strings" "time" "dev.narayana.im/narayana/telegabber/telegram/cache" "dev.narayana.im/narayana/telegabber/telegram/formatter" "dev.narayana.im/narayana/telegabber/xmpp/gateway" log "github.com/sirupsen/logrus" "github.com/soheilhy/args" "github.com/godcong/go-tdlib/client" ) var errOffline = errors.New("TDlib instance is offline") var spaceRegex = regexp.MustCompile(`\s+`) var replyRegex = regexp.MustCompile("> ?([0-9]{10,})") const newlineChar string = "\n" // GetContactByUsername resolves username to user id retrieves user and chat information func (c *Client) GetContactByUsername(username string) (*client.Chat, *client.User, error) { if !c.Online() { return nil, nil, errOffline } chat, err := c.client.SearchPublicChat(&client.SearchPublicChatRequest{ Username: username, }) if err != nil { return nil, nil, err } return c.GetContactByID(chat.ID, chat) } // GetContactByID gets user and chat information from cache (or tries to retrieve it, if missing) func (c *Client) GetContactByID(id int64, chat *client.Chat) (*client.Chat, *client.User, error) { if !c.Online() { return nil, nil, errOffline } var user *client.User var cacheChat *client.Chat var ok bool var err error user, ok = c.cache.GetUser(id) if !ok && id > 0 { user, err = c.client.GetUser(&client.GetUserRequest{ UserID: id, }) if err == nil { c.cache.SetUser(id, user) } } cacheChat, ok = c.cache.GetChat(id) if !ok { if chat == nil { cacheChat, err = c.client.GetChat(&client.GetChatRequest{ ChatID: id, }) if err != nil { // error is irrelevant if the user was found successfully if user != nil { return nil, user, nil } return nil, nil, err } c.cache.SetChat(id, cacheChat) } else { c.cache.SetChat(id, chat) } } if chat == nil { chat = cacheChat } return chat, user, nil } func (c *Client) userStatusToText(status client.UserStatus) (string, string) { var show, textStatus string switch status.UserStatusType() { case client.TypeUserStatusOnline: textStatus = "Online" case client.TypeUserStatusRecently: show, textStatus = "dnd", "Last seen recently" case client.TypeUserStatusLastWeek: show, textStatus = "unavailable", "Last seen last week" case client.TypeUserStatusLastMonth: show, textStatus = "unavailable", "Last seen last month" case client.TypeUserStatusEmpty: show, textStatus = "unavailable", "Last seen a long time ago" case client.TypeUserStatusOffline: offlineStatus, _ := status.(*client.UserStatusOffline) // this will stop working in 2038 O\ elapsed := time.Now().Unix() - int64(offlineStatus.WasOnline) if elapsed < 3600 { show = "away" } else { show = "xa" } textStatus = time.Unix(int64(offlineStatus.WasOnline), 0). In(c.Session.TimezoneToLocation()). Format("Last seen at 15:04 02/01/2006") } return show, textStatus } // ProcessStatusUpdate sets contact status func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, args ...args.V) error { if !c.Online() { return nil } log.WithFields(log.Fields{ "chat_id": chatID, }).Info("Status update for") chat, user, err := c.GetContactByID(chatID, nil) if err != nil { return err } var photo string if chat != nil && chat.Photo != nil { path := chat.Photo.Small.Local.Path file, err := os.Open(path) if err == nil { defer file.Close() hash := sha1.New() _, err = io.Copy(hash, file) if err == nil { photo = fmt.Sprintf("%x", hash.Sum(nil)) } else { log.Errorf("Error calculating hash: %v", path) } } else if path != "" { log.Errorf("Photo does not exist: %v", path) } } if status == "" { if user != nil { show, status = c.userStatusToText(user.Status) } else { show, status = "chat", chat.Title } } c.cache.SetStatus(chatID, show, status) gateway.SendPresence( c.xmpp, c.jid, gateway.SPFrom(strconv.FormatInt(chatID, 10)), gateway.SPShow(show), gateway.SPStatus(status), gateway.SPPhoto(photo), gateway.SPImmed(gateway.SPImmed.Get(args)), ) return nil } func (c *Client) formatContact(chatID int64) string { if chatID == 0 { return "" } chat, user, err := c.GetContactByID(chatID, nil) if err != nil { return "unknown contact: " + err.Error() } var str 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) } str = fmt.Sprintf("%s %s (%v)", user.FirstName, user.LastName, username) } else { str = strconv.FormatInt(chatID, 10) } str = spaceRegex.ReplaceAllString(str, " ") return str } func (c *Client) formatMessage(chatID int64, messageID int64, preview bool, message *client.Message) string { var err error if message == nil { message, err = c.client.GetMessage(&client.GetMessageRequest{ ChatID: chatID, MessageID: messageID, }) if err != nil { return fmt.Sprintf("", err.Error()) } } if message == nil { return "" } var str strings.Builder var senderId int64 switch message.Sender.MessageSenderType() { case client.TypeMessageSenderUser: senderUser, _ := message.Sender.(*client.MessageSenderUser) senderId = senderUser.UserID case client.TypeMessageSenderChat: senderChat, _ := message.Sender.(*client.MessageSenderChat) senderId = senderChat.ChatID } str.WriteString(fmt.Sprintf("%v | %s | ", message.ID, c.formatContact(senderId))) if !preview { str.WriteString( time.Unix(int64(message.Date), 0). In(c.Session.TimezoneToLocation()). Format("02 Jan 2006 15:04:05 | "), ) } var text string if message.Content != nil { switch message.Content.MessageContentType() { case client.TypeMessageText: messageText, _ := message.Content.(*client.MessageText) text = messageText.Text.Text // TODO: handle other message types with labels (not supported in Zhabogram!) } } if text != "" { if !preview { str.WriteString(text) } else { newlinePos := strings.Index(text, newlineChar) if !preview || newlinePos == -1 { str.WriteString(text) } else { str.WriteString(text[0:newlinePos]) } } } return str.String() } func (c *Client) formatContent(file *client.File, filename string) string { if file == nil { return "" } return fmt.Sprintf( "%s (%v kbytes) | %s/%s%s", filename, file.Size/1024, c.content.Link, fmt.Sprintf("%x", sha256.Sum256([]byte(file.Remote.ID))), filepath.Ext(filename), ) } func (c *Client) messageToText(message *client.Message) string { if message.Content == nil { log.Warnf("Unknown message (big emoji?): %#v", message) return "" } markupFunction := formatter.EntityToXEP0393 switch message.Content.MessageContentType() { case client.TypeMessageSticker: sticker, _ := message.Content.(*client.MessageSticker) return sticker.Sticker.Emoji case client.TypeMessageBasicGroupChatCreate, client.TypeMessageSupergroupChatCreate: return "has created chat" case client.TypeMessageChatJoinByLink: return "joined chat via invite link" case client.TypeMessageChatAddMembers: addMembers, _ := message.Content.(*client.MessageChatAddMembers) text := "invited " if len(addMembers.MemberUserIDs) > 0 { text += c.formatContact(addMembers.MemberUserIDs[0]) } return text case client.TypeMessageChatDeleteMember: deleteMember, _ := message.Content.(*client.MessageChatDeleteMember) return "kicked " + c.formatContact(deleteMember.UserID) case client.TypeMessagePinMessage: pinMessage, _ := message.Content.(*client.MessagePinMessage) return "pinned message: " + c.formatMessage(message.ChatID, pinMessage.MessageID, false, nil) case client.TypeMessageChatChangeTitle: changeTitle, _ := message.Content.(*client.MessageChatChangeTitle) return "chat title set to: " + changeTitle.Title case client.TypeMessageLocation: location, _ := message.Content.(*client.MessageLocation) return fmt.Sprintf( "coordinates: %v,%v | https://www.google.com/maps/search/%v,%v/", location.Location.Latitude, location.Location.Longitude, location.Location.Latitude, location.Location.Longitude, ) case client.TypeMessagePhoto: photo, _ := message.Content.(*client.MessagePhoto) return formatter.Format( photo.Caption.Text, formatter.SortEntities(photo.Caption.Entities), markupFunction, ) case client.TypeMessageAudio: audio, _ := message.Content.(*client.MessageAudio) return formatter.Format( audio.Caption.Text, formatter.SortEntities(audio.Caption.Entities), markupFunction, ) case client.TypeMessageVideo: video, _ := message.Content.(*client.MessageVideo) return formatter.Format( video.Caption.Text, formatter.SortEntities(video.Caption.Entities), markupFunction, ) case client.TypeMessageDocument: document, _ := message.Content.(*client.MessageDocument) return formatter.Format( document.Caption.Text, formatter.SortEntities(document.Caption.Entities), markupFunction, ) case client.TypeMessageText: text, _ := message.Content.(*client.MessageText) return formatter.Format( text.Text.Text, formatter.SortEntities(text.Text.Entities), markupFunction, ) case client.TypeMessageVoiceNote: voice, _ := message.Content.(*client.MessageVoiceNote) return formatter.Format( voice.Caption.Text, formatter.SortEntities(voice.Caption.Entities), markupFunction, ) case client.TypeMessageVideoNote: return "" case client.TypeMessageAnimation: animation, _ := message.Content.(*client.MessageAnimation) return formatter.Format( animation.Caption.Text, formatter.SortEntities(animation.Caption.Entities), markupFunction, ) } return fmt.Sprintf("unknown message (%s)", message.Content.MessageContentType()) } func (c *Client) contentToFilename(content client.MessageContent) (*client.File, string) { if content == nil { return nil, "" } switch content.MessageContentType() { case client.TypeMessageSticker: sticker, _ := content.(*client.MessageSticker) return sticker.Sticker.Sticker, "sticker.webp" case client.TypeMessageVoiceNote: voice, _ := content.(*client.MessageVoiceNote) return voice.VoiceNote.Voice, fmt.Sprintf("voice note (%v s.).oga", voice.VoiceNote.Duration) case client.TypeMessageVideoNote: video, _ := content.(*client.MessageVideoNote) return video.VideoNote.Video, fmt.Sprintf("video note (%v s.).mp4", video.VideoNote.Duration) case client.TypeMessageAnimation: animation, _ := content.(*client.MessageAnimation) return animation.Animation.Animation, "animation.mp4" case client.TypeMessagePhoto: photo, _ := content.(*client.MessagePhoto) sizes := photo.Photo.Sizes if len(sizes) > 1 { file := sizes[len(sizes)-1].Photo return file, strconv.FormatInt(file.ID, 10) + ".jpg" } return nil, "" case client.TypeMessageAudio: audio, _ := content.(*client.MessageAudio) return audio.Audio.Audio, audio.Audio.FileName case client.TypeMessageVideo: video, _ := content.(*client.MessageVideo) return video.Video.Video, video.Video.FileName case client.TypeMessageDocument: document, _ := content.(*client.MessageDocument) return document.Document.Document, document.Document.FileName } return nil, "" } func (c *Client) messageToPrefix(message *client.Message, fileString string) string { prefix := []string{} // message direction var directionChar string if message.IsOutgoing { directionChar = "⇾ " } else { directionChar = "⇽ " } prefix = append(prefix, directionChar+strconv.FormatInt(message.ID, 10)) // show sender in group chats if message.ChatID < 0 && message.Sender != nil { var senderId int64 switch message.Sender.MessageSenderType() { case client.TypeMessageSenderUser: senderUser, _ := message.Sender.(*client.MessageSenderUser) senderId = senderUser.UserID case client.TypeMessageSenderChat: senderChat, _ := message.Sender.(*client.MessageSenderChat) senderId = senderChat.ChatID } prefix = append(prefix, c.formatContact(senderId)) } if message.ForwardInfo != nil { switch message.ForwardInfo.Origin.MessageForwardOriginType() { case client.TypeMessageForwardOriginUser: originUser := message.ForwardInfo.Origin.(*client.MessageForwardOriginUser) prefix = append(prefix, "fwd: "+c.formatContact(originUser.SenderUserID)) case client.TypeMessageForwardOriginChat: originChat := message.ForwardInfo.Origin.(*client.MessageForwardOriginChat) var signature string if originChat.AuthorSignature != "" { signature = fmt.Sprintf(" (%s)", originChat.AuthorSignature) } prefix = append(prefix, "fwd: "+c.formatContact(originChat.SenderChatID)+signature) case client.TypeMessageForwardOriginHiddenUser: originUser := message.ForwardInfo.Origin.(*client.MessageForwardOriginHiddenUser) prefix = append(prefix, fmt.Sprintf("fwd: anonymous (%s)", originUser.SenderName)) case client.TypeMessageForwardOriginChannel: channel := message.ForwardInfo.Origin.(*client.MessageForwardOriginChannel) var signature string if channel.AuthorSignature != "" { signature = fmt.Sprintf(" (%s)", channel.AuthorSignature) } prefix = append(prefix, "fwd: "+c.formatContact(channel.ChatID)+signature) } } // reply to if message.ReplyToMessageID != 0 { prefix = append(prefix, "reply: "+c.formatMessage(message.ChatID, message.ReplyToMessageID, true, nil)) } if fileString != "" { prefix = append(prefix, "file: "+fileString) } return strings.Join(prefix, " | ") } // ProcessOutgoingMessage executes commands or sends messages to mapped chats func (c *Client) ProcessOutgoingMessage(chatID int64, text string, messageID int64, returnJid string) { if messageID == 0 && strings.HasPrefix(text, "/") { // try to execute a command response, isCommand := c.ProcessChatCommand(chatID, text) if response != "" { gateway.SendMessage(returnJid, strconv.FormatInt(chatID, 10), response, c.xmpp) } // do not send on success if isCommand { return } } if !c.Online() { // we're offline return } log.Warnf("Send message to chat %v", chatID) if messageID != 0 { formattedText := &client.FormattedText{ Text: text, } // compile our message message := &client.InputMessageText{ Text: formattedText, } c.client.EditMessageText(&client.EditMessageTextRequest{ ChatID: chatID, MessageID: messageID, InputMessageContent: message, }) } else { // quotations var reply int64 replySlice := replyRegex.FindStringSubmatch(text) if len(replySlice) > 1 { reply, _ = strconv.ParseInt(replySlice[1], 10, 64) } // attach a file var file *client.InputFileRemote if c.content.Upload != "" && strings.HasPrefix(text, c.content.Upload) { file = &client.InputFileRemote{ ID: text, } } // remove first line from text if file != nil || reply != 0 { newlinePos := strings.Index(text, newlineChar) if newlinePos != -1 { text = text[newlinePos+1:] } } formattedText := &client.FormattedText{ Text: text, } var message client.InputMessageContent if file != nil { // we can try to send a document message = &client.InputMessageDocument{ Document: file, Caption: formattedText, } } else { // compile our message message = &client.InputMessageText{ Text: formattedText, } } _, err := c.client.SendMessage(&client.SendMessageRequest{ ChatID: chatID, ReplyToMessageID: reply, InputMessageContent: message, }) if err != nil { gateway.SendMessage( returnJid, strconv.FormatInt(chatID, 10), fmt.Sprintf("Message not sent: %s", err.Error()), c.xmpp, ) } } } // StatusesRange proxies the following function from unexported cache func (c *Client) StatusesRange() chan *cache.Status { return c.cache.StatusesRange() } func (c *Client) addResource(resource string) { if resource == "" { return } c.locks.resourcesLock.Lock() defer c.locks.resourcesLock.Unlock() c.resources[resource] = true } func (c *Client) deleteResource(resource string) { c.locks.resourcesLock.Lock() defer c.locks.resourcesLock.Unlock() if _, ok := c.resources[resource]; ok { delete(c.resources, resource) } } // refresh roster func (c *Client) refresh(resource string) { if _, ok := c.resources[resource]; ok { return } log.Warnf("Refreshing roster for resource %v", resource) for _, chat := range c.cache.ChatsKeys() { c.ProcessStatusUpdate(chat, "", "") } c.addResource(resource) }