package telegram import ( "crypto/sha1" "encoding/binary" "fmt" "github.com/pkg/errors" "hash/maphash" "io" "io/ioutil" "net/http" "os" osUser "os/user" "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/zelenin/go-tdlib/client" ) type VCardInfo struct { Fn string Photo *client.File Nickname 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) { if !c.Online() { return nil, nil, errOffline } var chat *client.Chat var err error var userID int64 if strings.HasPrefix(username, "@") { chat, err = c.client.SearchPublicChat(&client.SearchPublicChatRequest{ Username: username, }) if err != nil { return nil, nil, err } userID = chat.Id } else { userID, err = strconv.ParseInt(username, 10, 64) if err != nil { return nil, nil, err } } return c.GetContactByID(userID, 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() || id == 0 { 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 } // 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 switch status.UserStatusType() { case client.TypeUserStatusOnline: onlineStatus, _ := status.(*client.UserStatusOnline) c.DelayedStatusesLock.Lock() c.DelayedStatuses[chatID] = &DelayedStatus{ TimestampOnline: time.Now().Unix(), TimestampExpired: int64(onlineStatus.Expires), } c.DelayedStatusesLock.Unlock() textStatus = "Online" case client.TypeUserStatusRecently: show, textStatus = "dnd", "Last seen recently" c.DelayedStatusesLock.Lock() delete(c.DelayedStatuses, chatID) c.DelayedStatusesLock.Unlock() case client.TypeUserStatusLastWeek: show, textStatus = "xa", "Last seen last week" case client.TypeUserStatusLastMonth: show, textStatus = "xa", "Last seen last month" case client.TypeUserStatusEmpty: presenceType, textStatus = "unavailable", "Last seen a long time ago" case client.TypeUserStatusOffline: offlineStatus, _ := status.(*client.UserStatusOffline) // this will stop working in 2038 O\ wasOnline := int64(offlineStatus.WasOnline) elapsed := time.Now().Unix() - wasOnline if elapsed < 3600 { show = "away" } else { show = "xa" } textStatus = c.LastSeenStatus(wasOnline) c.DelayedStatusesLock.Lock() delete(c.DelayedStatuses, chatID) c.DelayedStatusesLock.Unlock() } return show, textStatus, presenceType } // LastSeenStatus formats a timestamp to a "Last seen at" string func (c *Client) LastSeenStatus(timestamp int64) string { return time.Unix(int64(timestamp), 0). In(c.Session.TimezoneToLocation()). Format("Last seen at 15:04 02/01/2006") } // ProcessStatusUpdate sets contact status func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, oldArgs ...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 { file, path, err := c.ForceOpenFile(chat.Photo.Small, 1) 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) } } var presenceType string if gateway.SPType.IsSet(oldArgs) { presenceType = gateway.SPType.Get(oldArgs) } cachedStatus, ok := c.cache.GetStatus(chatID) if status == "" { if ok { show, status = cachedStatus.XMPP, cachedStatus.Description } else if user != nil && user.Status != nil { show, status, presenceType = c.userStatusToText(user.Status, chatID) } else { show, status = "chat", chat.Title } } c.cache.SetStatus(chatID, show, status) newArgs := []args.V{ gateway.SPFrom(strconv.FormatInt(chatID, 10)), gateway.SPShow(show), gateway.SPStatus(status), gateway.SPPhoto(photo), gateway.SPResource(gateway.Jid.Resource), gateway.SPImmed(gateway.SPImmed.Get(oldArgs)), } if presenceType != "" { newArgs = append(newArgs, gateway.SPType(presenceType)) } return gateway.SendPresence( c.xmpp, c.jid, newArgs..., ) } 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) 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("", 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 { 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 // add messageid and sender str.WriteString(fmt.Sprintf("%v | %s | ", message.Id, c.formatSender(message))) // add date if !preview { str.WriteString( time.Unix(int64(message.Date), 0). In(c.Session.TimezoneToLocation()). Format("02 Jan 2006 15:04:05 | "), ) } // text message var text string if message.Content != nil { text = c.messageToText(message, preview) } if text != "" { if !preview { str.WriteString(text) } else { newlinePos := strings.Index(text, newlineChar) if newlinePos == -1 { str.WriteString(text) } else { str.WriteString(text[0:newlinePos]) } } } return str.String() } func (c *Client) formatForward(fwd *client.MessageForwardInfo) string { switch fwd.Origin.MessageForwardOriginType() { case client.TypeMessageForwardOriginUser: originUser := fwd.Origin.(*client.MessageForwardOriginUser) return c.formatContact(originUser.SenderUserId) case client.TypeMessageForwardOriginChat: originChat := fwd.Origin.(*client.MessageForwardOriginChat) var signature string if originChat.AuthorSignature != "" { signature = fmt.Sprintf(" (%s)", originChat.AuthorSignature) } return c.formatContact(originChat.SenderChatId) + signature case client.TypeMessageForwardOriginHiddenUser: originUser := fwd.Origin.(*client.MessageForwardOriginHiddenUser) return originUser.SenderName case client.TypeMessageForwardOriginChannel: channel := fwd.Origin.(*client.MessageForwardOriginChannel) var signature string if channel.AuthorSignature != "" { signature = fmt.Sprintf(" (%s)", channel.AuthorSignature) } return c.formatContact(channel.ChatId) + signature case client.TypeMessageForwardOriginMessageImport: originImport := fwd.Origin.(*client.MessageForwardOriginMessageImport) return originImport.SenderName } return "Unknown forward type" } 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 "", "" } gateway.StorageLock.Lock() defer gateway.StorageLock.Unlock() var link string var src string if c.content.Path != "" && c.content.Link != "" { src = file.Local.Path // source path _, err := os.Stat(src) if err != nil { log.Errorf("Cannot access source file: %v", err) return "", "" } size64 := uint64(file.Size) c.prepareDiskSpace(size64) basename := file.Remote.UniqueId + filepath.Ext(src) dest := c.content.Path + "/" + basename // destination path link = c.content.Link + "/" + basename // download link 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 "", "" } } defer tempFile.Close() // copy _, err = io.Copy(tempFile, file) if err != nil { log.Errorf("File copying error: %v", err) return "", "" } } else if path != "" { log.Errorf("Source file does not exist: %v", path) return "", "" } else { log.Errorf("PHOTO: %#v", err.Error()) return "", "" } } 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 "", "" } } } // chown if c.content.User != "" { user, err := osUser.Lookup(c.content.User) if err == nil { uid, err := strconv.ParseInt(user.Uid, 10, 0) if err == nil { err = os.Chown(dest, int(uid), -1) if err != nil { log.Errorf("Chown error: %v", err) } } else { log.Errorf("Broken uid: %v", err) } } else { log.Errorf("Wrong user name for chown: %v", err) } } // copy or move should have succeeded at this point gateway.CachedStorageSize += size64 } return src, link } func (c *Client) formatBantime(hours int64) int32 { var until int32 if hours > 0 { until = int32(time.Now().Unix() + hours*3600) } return until } func (c *Client) formatLocation(location *client.Location) string { return fmt.Sprintf( "coordinates: %v,%v | https://www.google.com/maps/search/%v,%v/", location.Latitude, location.Longitude, location.Latitude, location.Longitude, ) } func (c *Client) messageToText(message *client.Message, preview bool) string { if message.Content == nil { log.Warnf("Unknown message: %#v", message) return "" } markupFunction := formatter.EntityToXEP0393 switch message.Content.MessageContentType() { case client.TypeMessageSticker: sticker, _ := message.Content.(*client.MessageSticker) return sticker.Sticker.Emoji case client.TypeMessageAnimatedEmoji: animatedEmoji, _ := message.Content.(*client.MessageAnimatedEmoji) return animatedEmoji.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, preview, 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 c.formatLocation(location.Location) case client.TypeMessageVenue: venue, _ := message.Content.(*client.MessageVenue) if preview { return venue.Venue.Title } else { return fmt.Sprintf( "*%s*\n%s\n%s", venue.Venue.Title, venue.Venue.Address, c.formatLocation(venue.Venue.Location), ) } case client.TypeMessagePhoto: photo, _ := message.Content.(*client.MessagePhoto) if preview { return photo.Caption.Text } else { return formatter.Format( photo.Caption.Text, photo.Caption.Entities, markupFunction, ) } case client.TypeMessageAudio: audio, _ := message.Content.(*client.MessageAudio) if preview { return audio.Caption.Text } else { return formatter.Format( audio.Caption.Text, audio.Caption.Entities, markupFunction, ) } case client.TypeMessageVideo: video, _ := message.Content.(*client.MessageVideo) if preview { return video.Caption.Text } else { return formatter.Format( video.Caption.Text, video.Caption.Entities, markupFunction, ) } case client.TypeMessageDocument: document, _ := message.Content.(*client.MessageDocument) if preview { return document.Caption.Text } else { return formatter.Format( document.Caption.Text, document.Caption.Entities, markupFunction, ) } case client.TypeMessageText: text, _ := message.Content.(*client.MessageText) if preview { return text.Text.Text } else { return formatter.Format( text.Text.Text, text.Text.Entities, markupFunction, ) } case client.TypeMessageVoiceNote: voice, _ := message.Content.(*client.MessageVoiceNote) if preview { return voice.Caption.Text } else { return formatter.Format( voice.Caption.Text, voice.Caption.Entities, markupFunction, ) } case client.TypeMessageVideoNote: return "" case client.TypeMessageAnimation: animation, _ := message.Content.(*client.MessageAnimation) if preview { return animation.Caption.Text } else { return formatter.Format( animation.Caption.Text, animation.Caption.Entities, markupFunction, ) } case client.TypeMessageContact: contact, _ := message.Content.(*client.MessageContact) if preview { return contact.Contact.FirstName + " " + contact.Contact.LastName } else { var jid string if contact.Contact.UserId != 0 { jid = fmt.Sprintf("%v@%s", contact.Contact.UserId, gateway.Jid.Bare()) } return fmt.Sprintf( "*%s %s*\n%s\n%s\n%s", contact.Contact.FirstName, contact.Contact.LastName, contact.Contact.PhoneNumber, contact.Contact.Vcard, jid, ) } case client.TypeMessageDice: dice, _ := message.Content.(*client.MessageDice) return fmt.Sprintf("%s 1d6: [%v]", dice.Emoji, dice.Value) case client.TypeMessagePoll: poll, _ := message.Content.(*client.MessagePoll) if preview { return poll.Poll.Question } else { rows := []string{} rows = append(rows, fmt.Sprintf("*%s*", poll.Poll.Question)) for _, option := range poll.Poll.Options { var tick string if option.IsChosen { tick = "x" } else { tick = " " } rows = append(rows, fmt.Sprintf( "[%s] %s | %v%% | %v vote", tick, option.Text, option.VotePercentage, option.VoterCount, )) } return strings.Join(rows, "\n") } } return fmt.Sprintf("unknown message (%s)", message.Content.MessageContentType()) } func (c *Client) contentToFile(content client.MessageContent) (*client.File, *client.File) { if content == nil { return nil, nil } switch content.MessageContentType() { case client.TypeMessageSticker: sticker, _ := content.(*client.MessageSticker) file := sticker.Sticker.Sticker if sticker.Sticker.IsAnimated && sticker.Sticker.Thumbnail != nil && sticker.Sticker.Thumbnail.File != nil { file = sticker.Sticker.Thumbnail.File } return file, nil case client.TypeMessageVoiceNote: voice, _ := content.(*client.MessageVoiceNote) return voice.VoiceNote.Voice, nil case client.TypeMessageVideoNote: video, _ := content.(*client.MessageVideoNote) var preview *client.File if video.VideoNote.Thumbnail != nil { preview = video.VideoNote.Thumbnail.File } return video.VideoNote.Video, preview case client.TypeMessageAnimation: animation, _ := content.(*client.MessageAnimation) var preview *client.File if animation.Animation.Thumbnail != nil { preview = animation.Animation.Thumbnail.File } return animation.Animation.Animation, preview case client.TypeMessagePhoto: photo, _ := content.(*client.MessagePhoto) sizes := photo.Photo.Sizes if len(sizes) >= 1 { file := sizes[len(sizes)-1].Photo return file, nil } return nil, nil case client.TypeMessageAudio: audio, _ := content.(*client.MessageAudio) var preview *client.File if audio.Audio.AlbumCoverThumbnail != nil { preview = audio.Audio.AlbumCoverThumbnail.File } return audio.Audio.Audio, preview case client.TypeMessageVideo: video, _ := content.(*client.MessageVideo) var preview *client.File if video.Video.Thumbnail != nil { preview = video.Video.Thumbnail.File } return video.Video.Video, preview case client.TypeMessageDocument: document, _ := content.(*client.MessageDocument) var preview *client.File if document.Document.Thumbnail != nil { preview = document.Document.Thumbnail.File } return document.Document.Document, preview } return nil, nil } 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) } var replyStart, replyEnd int prefix := []string{} // message direction var directionChar string if !isPM || !gateway.MessageOutgoingPermission || !c.Session.Carbons { if c.Session.AsciiArrows { if message.IsOutgoing { directionChar = "> " } else { directionChar = "< " } } else { if message.IsOutgoing { directionChar = "➡ " } else { directionChar = "⬅ " } } } if !isPM || !c.Session.HideIds { prefix = append(prefix, directionChar+strconv.FormatInt(message.Id, 10)) } // show sender in group chats if !isPM { sender := c.formatSender(message) if sender != "" { prefix = append(prefix, sender) } } // reply to if message.ReplyToMessageId != 0 { 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)) } // preview if previewString != "" { prefix = append(prefix, "preview: "+previewString) } // file if fileString != "" { prefix = append(prefix, "file: "+fileString) } return strings.Join(prefix, messageHeaderSeparator), replyStart, replyEnd } func (c *Client) ensureDownloadFile(file *client.File) *client.File { gateway.StorageLock.Lock() defer gateway.StorageLock.Unlock() if file != nil { c.prepareDiskSpace(uint64(file.Size)) newFile, err := c.DownloadFile(file.Id, 1, true) if err == nil { return newFile } } return 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, ignoredResource string) { var isPM bool var err error if gateway.MessageOutgoingPermission && c.Session.Carbons { isPM, err = c.IsPM(chatId) if err != nil { log.Errorf("Could not determine if chat is PM: %v", err) } } isOutgoing := message.IsOutgoing isCarbon := isPM && isOutgoing jids := c.getCarbonFullJids(isOutgoing, ignoredResource) if len(jids) == 0 { log.Info("The only resource is ignored, aborting") return } 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{ ChatId: chatId, }) if err == nil { c.cache.SetChat(chatId, chat) go c.ProcessStatusUpdate(chatId, "", "", gateway.SPImmed(true)) text = "" } } else { 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 && !c.Session.OOBMode)) { file, preview := c.contentToFile(content) // download file and preview (if present) file = c.ensureDownloadFile(file) preview = c.ensureDownloadFile(preview) 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) } 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(" | ") } } newText.WriteString(text) } text = newText.String() } } } // mark message as read c.client.ViewMessages(&client.ViewMessagesRequest{ ChatId: chatId, MessageIds: []int64{message.Id}, ForceRead: true, }) // forward message to 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, 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 0 } if replaceId == 0 && (strings.HasPrefix(text, "/") || strings.HasPrefix(text, "!")) { // try to execute commands response, isCommand := c.ProcessChatCommand(chatID, text) if response != "" { c.returnMessage(returnJid, chatID, response) } // do not send on success if isCommand { return 0 } } log.Warnf("Sending message to chat %v", chatID) // quotations var reply int64 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 c.content.Upload != "" && strings.HasPrefix(text, c.content.Upload) { response, err := http.Get(text) if err != 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 { c.returnMessage(returnJid, chatID, fmt.Sprintf("Received status code %v", response.StatusCode)) } tempDir, err := ioutil.TempDir("", "telegabber-*") if err != 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 { c.returnError(returnJid, chatID, "Failed to create a temporary file", err) } _, err = io.Copy(tempFile, response.Body) if err != nil { c.returnError(returnJid, chatID, "Failed to write a temporary file", err) } file = &client.InputFileLocal{ Path: tempFile.Name(), } } } // remove first line from text if file != nil || (reply != 0 && replyId == 0) { newlinePos := strings.Index(text, newlineChar) if newlinePos != -1 { text = text[newlinePos+1:] } else { text = "" } } 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 content client.InputMessageContent if file != nil { // we can try to send a document content = &client.InputMessageDocument{ Document: file, Caption: formattedText, } } else { // compile our message content = &client.InputMessageText{ Text: formattedText, } } return content } // 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) } } 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) for _, chat := range c.cache.ChatsKeys() { c.ProcessStatusUpdate(chat, "", "") } gateway.SendPresence(c.xmpp, c.jid, gateway.SPStatus("Logged in as: "+c.Session.Login)) c.addResource(resource) } // get last messages from specified chat func (c *Client) getLastMessages(id int64, query string, from int64, count int32) (*client.Messages, error) { return c.client.SearchChatMessages(&client.SearchChatMessagesRequest{ ChatId: id, Query: query, SenderId: &client.MessageSenderUser{UserId: from}, Filter: &client.SearchMessagesFilterEmpty{}, Limit: count, }) } // DownloadFile actually obtains a file by id given by TDlib func (c *Client) DownloadFile(id int32, priority int32, synchronous bool) (*client.File, error) { return c.client.DownloadFile(&client.DownloadFileRequest{ FileId: id, Priority: priority, Synchronous: synchronous, }) } // 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 := 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 !tgFile.Local.IsDownloadingCompleted { tdFile, tdErr := c.DownloadFile(tgFile.Id, priority, true) if tdErr == nil { path = tdFile.Local.Path file, err = os.Open(path) return file, path, err } } // give up return nil, path, err } // GetChatDescription obtains bio or description according to the chat type func (c *Client) GetChatDescription(chat *client.Chat) string { chatType := chat.Type.ChatTypeType() if chatType == client.TypeChatTypePrivate { privateType, _ := chat.Type.(*client.ChatTypePrivate) fullInfo, err := c.client.GetUserFullInfo(&client.GetUserFullInfoRequest{ UserId: privateType.UserId, }) if err == nil { if fullInfo.Bio != "" { return fullInfo.Bio } else if fullInfo.Description != "" { return fullInfo.Description } } else { log.Warnf("Coudln't retrieve private chat info: %v", err.Error()) } } else if chatType == client.TypeChatTypeBasicGroup { basicGroupType, _ := chat.Type.(*client.ChatTypeBasicGroup) fullInfo, err := c.client.GetBasicGroupFullInfo(&client.GetBasicGroupFullInfoRequest{ BasicGroupId: basicGroupType.BasicGroupId, }) if err == nil { return fullInfo.Description } else { log.Warnf("Coudln't retrieve basic group info: %v", err.Error()) } } else if chatType == client.TypeChatTypeSupergroup { supergroupType, _ := chat.Type.(*client.ChatTypeSupergroup) fullInfo, err := c.client.GetSupergroupFullInfo(&client.GetSupergroupFullInfoRequest{ SupergroupId: supergroupType.SupergroupId, }) if err == nil { return fullInfo.Description } else { log.Warnf("Coudln't retrieve supergroup info: %v", err.Error()) } } return "" } // subscribe to a Telegram ID func (c *Client) subscribeToID(id int64, chat *client.Chat) { var args []args.V args = append(args, gateway.SPFrom(strconv.FormatInt(id, 10))) args = append(args, gateway.SPType("subscribe")) if chat == nil { chat, _, _ = c.GetContactByID(id, nil) } if chat != nil { args = append(args, gateway.SPNickname(chat.Title)) gateway.SetNickname(c.jid, strconv.FormatInt(id, 10), chat.Title, c.xmpp) } gateway.SendPresence( c.xmpp, c.jid, args..., ) } func (c *Client) prepareDiskSpace(size uint64) { if gateway.StorageQuota > 0 && c.content.Path != "" { var loweredQuota uint64 if gateway.StorageQuota >= size { loweredQuota = gateway.StorageQuota - size } if gateway.CachedStorageSize >= loweredQuota { log.Warn("Storage is rapidly clogged") gateway.CleanOldFiles(c.content.Path, loweredQuota) } } } 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 { info.Nickname = user.Username 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 { gateway.SendPresence( c.xmpp, c.jid, gateway.SPFrom(strconv.FormatInt(id, 10)), gateway.SPNickname(chat.Title), ) 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 }