@ -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 . OpenPhoto File( chat . Photo . Small , 1 )
file , path , err := c . Force OpenFile( 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 , username s )
} 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 re placeId == 0 && ( strings . HasPrefix ( text , "/" ) || strings . HasPrefix ( text , "!" ) ) {
// try to execute commands
response , isCommand := c . ProcessChatCommand ( chatID , text )
if response != "" {
gateway. Send Message( returnJid , strconv. FormatInt ( chatID, 10 ) , response , c . xmpp )
c. return Message( 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 c hatID != 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 . FoundChat Messages, 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( photo File * client . File , priority int32 ) ( * os . File , string , error ) {
if photo File == nil {
return nil , "" , errors . New ( " Photo f ile not found")
// ForceOpenFile reliably obtains a file if possible
func ( c * Client ) ForceOpenFile( tg File * client . File , priority int32 ) ( * os . File , string , error ) {
if tg File == nil {
return nil , "" , errors . New ( " F ile not found")
}
path := photo File. Local . Path
path := tg File. 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 ! photo File. Local . IsDownloadingCompleted {
tdFile , tdErr := c . DownloadFile ( photo File. Id , priority , true )
if ! tg File. Local . IsDownloadingCompleted {
tdFile , tdErr := c . DownloadFile ( tg File. 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 , ", " )
}