mirror of
https://dev.narayana.im/narayana/telegabber.git
synced 2026-10-07 16:51:47 +00:00
Implement SFS and attach caption message to OOB message
This commit is contained in:
parent
930557bda5
commit
c9700e931e
3 changed files with 216 additions and 10 deletions
|
|
@ -3,11 +3,13 @@ package telegram
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
|
"crypto/sha256"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
osUser "os/user"
|
osUser "os/user"
|
||||||
|
|
@ -1860,6 +1862,73 @@ func (c *Client) contentToFile(content client.MessageContent) (*client.File, *cl
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fileMetadata extracts XEP-0446 name/media-type/size/hash metadata from
|
||||||
|
// message content, falling back to the extension-derived media type and
|
||||||
|
// the basename of fallbackLink where the content type carries no
|
||||||
|
// filename/mime type of its own (photos, stickers, video notes). hash is
|
||||||
|
// a base64 XEP-0300 sha-256 digest of the file's actual content, read
|
||||||
|
// from fallbackLink's permastored location - by this point formatFile
|
||||||
|
// has already moved the file there out of TDLib's own local path, so
|
||||||
|
// that's the only place it's still guaranteed to exist on disk.
|
||||||
|
func (c *Client) fileMetadata(content client.MessageContent, file *client.File, fallbackLink string) (name string, mediaType string, size int64, hash string) {
|
||||||
|
if file != nil {
|
||||||
|
size = file.Size
|
||||||
|
if size == 0 {
|
||||||
|
size = file.ExpectedSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if fallbackLink != "" && strings.HasPrefix(fallbackLink, c.content.Link) {
|
||||||
|
localPath := c.content.Path + strings.TrimPrefix(fallbackLink, c.content.Link)
|
||||||
|
if f, err := os.Open(localPath); err == nil {
|
||||||
|
defer f.Close()
|
||||||
|
digest := sha256.New()
|
||||||
|
if _, err := io.Copy(digest, f); err == nil {
|
||||||
|
hash = base64.StdEncoding.EncodeToString(digest.Sum(nil))
|
||||||
|
} else {
|
||||||
|
log.Errorf("Error hashing stored file: %v", localPath)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Errorf("Error opening stored file for hashing: %v", localPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch content.MessageContentType() {
|
||||||
|
case client.TypeMessageDocument:
|
||||||
|
document, _ := content.(*client.MessageDocument)
|
||||||
|
name = document.Document.FileName
|
||||||
|
mediaType = document.Document.MimeType
|
||||||
|
case client.TypeMessageVideo:
|
||||||
|
video, _ := content.(*client.MessageVideo)
|
||||||
|
name = video.Video.FileName
|
||||||
|
mediaType = video.Video.MimeType
|
||||||
|
case client.TypeMessageAudio:
|
||||||
|
audio, _ := content.(*client.MessageAudio)
|
||||||
|
name = audio.Audio.FileName
|
||||||
|
mediaType = audio.Audio.MimeType
|
||||||
|
case client.TypeMessageAnimation:
|
||||||
|
animation, _ := content.(*client.MessageAnimation)
|
||||||
|
name = animation.Animation.FileName
|
||||||
|
mediaType = animation.Animation.MimeType
|
||||||
|
case client.TypeMessageVoiceNote:
|
||||||
|
voice, _ := content.(*client.MessageVoiceNote)
|
||||||
|
mediaType = voice.VoiceNote.MimeType
|
||||||
|
case client.TypeMessagePhoto:
|
||||||
|
mediaType = "image/jpeg"
|
||||||
|
}
|
||||||
|
|
||||||
|
if name == "" {
|
||||||
|
name = filepath.Base(fallbackLink)
|
||||||
|
}
|
||||||
|
if mediaType == "" {
|
||||||
|
mediaType = mime.TypeByExtension(filepath.Ext(name))
|
||||||
|
}
|
||||||
|
if mediaType == "" {
|
||||||
|
mediaType = "application/octet-stream"
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) countCharsInLines(lines *[]string) (count int) {
|
func (c *Client) countCharsInLines(lines *[]string) (count int) {
|
||||||
for _, line := range *lines {
|
for _, line := range *lines {
|
||||||
count += utf8.RuneCountInString(line)
|
count += utf8.RuneCountInString(line)
|
||||||
|
|
@ -2081,6 +2150,9 @@ func (c *Client) SendMessageToGateway(chatId int64, message *client.Message, id
|
||||||
}
|
}
|
||||||
|
|
||||||
var text, oob, auxText string
|
var text, oob, auxText string
|
||||||
|
var rawCaption string
|
||||||
|
var fileMetaName, fileMetaType, fileMetaHash string
|
||||||
|
var fileMetaSize int64
|
||||||
var reply *gateway.Reply
|
var reply *gateway.Reply
|
||||||
var replyObtained bool
|
var replyObtained bool
|
||||||
|
|
||||||
|
|
@ -2127,6 +2199,7 @@ func (c *Client) SendMessageToGateway(chatId int64, message *client.Message, id
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
text = c.messageToText(message, false)
|
text = c.messageToText(message, false)
|
||||||
|
rawCaption = text
|
||||||
|
|
||||||
// OTR support (I do not know why would you need it, seriously)
|
// OTR support (I do not know why would you need it, seriously)
|
||||||
if !(strings.HasPrefix(text, "?OTR") || (c.Session.RawMessages && !c.Session.OOBMode)) {
|
if !(strings.HasPrefix(text, "?OTR") || (c.Session.RawMessages && !c.Session.OOBMode)) {
|
||||||
|
|
@ -2153,7 +2226,16 @@ func (c *Client) SendMessageToGateway(chatId int64, message *client.Message, id
|
||||||
if !c.Session.RawMessages && !ignorePrefix {
|
if !c.Session.RawMessages && !ignorePrefix {
|
||||||
var newText strings.Builder
|
var newText strings.Builder
|
||||||
|
|
||||||
prefix, prefixReply := c.messageToPrefix(message, previewName, fileName, false)
|
// Once oobSwap sends the caption as its own message, the
|
||||||
|
// "file: name (size) | link" line only ever ends up in
|
||||||
|
// that caption body - and by then the same name/size are
|
||||||
|
// already in the file-sharing metadata attached to the
|
||||||
|
// OOB message, so it'd just be a duplicate there.
|
||||||
|
prefixFileName := fileName
|
||||||
|
if oobSwap {
|
||||||
|
prefixFileName = ""
|
||||||
|
}
|
||||||
|
prefix, prefixReply := c.messageToPrefix(message, previewName, prefixFileName, false)
|
||||||
reply = prefixReply
|
reply = prefixReply
|
||||||
replyObtained = true
|
replyObtained = true
|
||||||
newText.WriteString(prefix)
|
newText.WriteString(prefix)
|
||||||
|
|
@ -2176,6 +2258,12 @@ func (c *Client) SendMessageToGateway(chatId int64, message *client.Message, id
|
||||||
auxText = text
|
auxText = text
|
||||||
}
|
}
|
||||||
text = oob
|
text = oob
|
||||||
|
if auxText != "" {
|
||||||
|
// Only worth hashing the file when there's a caption
|
||||||
|
// to attach file-sharing metadata to in the first
|
||||||
|
// place.
|
||||||
|
fileMetaName, fileMetaType, fileMetaSize, fileMetaHash = c.fileMetadata(content, file, link)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2301,6 +2389,19 @@ func (c *Client) SendMessageToGateway(chatId int64, message *client.Message, id
|
||||||
occupantId = gateway.CHATNODE(senderId)
|
occupantId = gateway.CHATNODE(senderId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// XEP-0447 metadata rides along on the OOB message itself (nesting
|
||||||
|
// its own source), so it's never split across a message the
|
||||||
|
// receiving client would need to reconstruct. Its <desc> duplicates
|
||||||
|
// rawCaption in the clear, so it's only populated when the caption
|
||||||
|
// itself isn't going out OMEMO-encrypted - otherwise the file-sharing
|
||||||
|
// element still describes the file, just without the caption, and
|
||||||
|
// the caption message is the sole (encrypted) copy.
|
||||||
|
hasSFS := oob != "" && auxText != ""
|
||||||
|
sfsDesc := ""
|
||||||
|
if hasSFS && auxEnvelope == nil {
|
||||||
|
sfsDesc = rawCaption
|
||||||
|
}
|
||||||
|
|
||||||
for _, jid := range jids {
|
for _, jid := range jids {
|
||||||
commonArgs := []args.V{
|
commonArgs := []args.V{
|
||||||
gateway.SMReply(reply), gateway.SMTimestamp(timestamp), gateway.SMIsCarbon(isCarbon),
|
gateway.SMReply(reply), gateway.SMTimestamp(timestamp), gateway.SMIsCarbon(isCarbon),
|
||||||
|
|
@ -2314,13 +2415,28 @@ func (c *Client) SendMessageToGateway(chatId int64, message *client.Message, id
|
||||||
gateway.SMBody(gateway.OMEMOSendFailedBody), gateway.SMId(sId), gateway.SMStanzaId(stanzaId))...)
|
gateway.SMBody(gateway.OMEMOSendFailedBody), gateway.SMId(sId), gateway.SMStanzaId(stanzaId))...)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
gateway.SendMessage(jid, from, c.xmpp, append(commonArgs,
|
textArgs := append(commonArgs,
|
||||||
gateway.SMBody(text), gateway.SMId(sId), gateway.SMStanzaId(stanzaId), gateway.SMOOB(oob),
|
gateway.SMBody(text), gateway.SMId(sId), gateway.SMStanzaId(stanzaId), gateway.SMOOB(oob),
|
||||||
gateway.SMOMEMOEnvelope(textEnvelope))...)
|
gateway.SMOMEMOEnvelope(textEnvelope))
|
||||||
|
if hasSFS {
|
||||||
|
textArgs = append(textArgs, gateway.SMFileSharing(&gateway.FileSharing{
|
||||||
|
Name: fileMetaName,
|
||||||
|
MediaType: fileMetaType,
|
||||||
|
Size: fileMetaSize,
|
||||||
|
Desc: sfsDesc,
|
||||||
|
HashAlgo: "sha-256",
|
||||||
|
HashValue: fileMetaHash,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
gateway.SendMessage(jid, from, c.xmpp, textArgs...)
|
||||||
if auxText != "" {
|
if auxText != "" {
|
||||||
gateway.SendMessage(jid, from, c.xmpp, append(commonArgs,
|
auxArgs := append(commonArgs,
|
||||||
gateway.SMBody(auxText), gateway.SMId(auxId), gateway.SMStanzaId(auxId),
|
gateway.SMBody(auxText), gateway.SMId(auxId), gateway.SMStanzaId(auxId),
|
||||||
gateway.SMOMEMOEnvelope(auxEnvelope))...)
|
gateway.SMOMEMOEnvelope(auxEnvelope))
|
||||||
|
if oob != "" {
|
||||||
|
auxArgs = append(auxArgs, gateway.SMAttachToId(sId))
|
||||||
|
}
|
||||||
|
gateway.SendMessage(jid, from, c.xmpp, auxArgs...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.UpdateLastChatMessageId(chatId, sId)
|
c.UpdateLastChatMessageId(chatId, sId)
|
||||||
|
|
|
||||||
|
|
@ -131,18 +131,19 @@ type Fallback struct {
|
||||||
Subject []FallbackSubject `xml:"urn:xmpp:fallback:0 subject"`
|
Subject []FallbackSubject `xml:"urn:xmpp:fallback:0 subject"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// FallbackBody is from XEP-0428
|
// FallbackBody is from XEP-0428. Start/End are omitted together to mark
|
||||||
|
// the whole body as fallback content, rather than a substring range.
|
||||||
type FallbackBody struct {
|
type FallbackBody struct {
|
||||||
XMLName xml.Name `xml:"urn:xmpp:fallback:0 body"`
|
XMLName xml.Name `xml:"urn:xmpp:fallback:0 body"`
|
||||||
Start string `xml:"start,attr"`
|
Start string `xml:"start,attr,omitempty"`
|
||||||
End string `xml:"end,attr"`
|
End string `xml:"end,attr,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// FallbackSubject is from XEP-0428
|
// FallbackSubject is from XEP-0428
|
||||||
type FallbackSubject struct {
|
type FallbackSubject struct {
|
||||||
XMLName xml.Name `xml:"urn:xmpp:fallback:0 subject"`
|
XMLName xml.Name `xml:"urn:xmpp:fallback:0 subject"`
|
||||||
Start string `xml:"start,attr"`
|
Start string `xml:"start,attr,omitempty"`
|
||||||
End string `xml:"end,attr"`
|
End string `xml:"end,attr,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CarbonReceived is from XEP-0280
|
// CarbonReceived is from XEP-0280
|
||||||
|
|
@ -520,6 +521,44 @@ type AttachTo struct {
|
||||||
Id string `xml:"id,attr"`
|
Id string `xml:"id,attr"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FileSharing is a XEP-0447 file-sharing element, with its source (a
|
||||||
|
// XEP-0066 OOB URL, typically) nested inline
|
||||||
|
type FileSharing struct {
|
||||||
|
XMLName xml.Name `xml:"urn:xmpp:sfs:0 file-sharing"`
|
||||||
|
File FileMetadata `xml:"urn:xmpp:file:metadata:0 file"`
|
||||||
|
Sources *Sources `xml:"urn:xmpp:sfs:0 sources,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileMetadata is from XEP-0446
|
||||||
|
type FileMetadata struct {
|
||||||
|
XMLName xml.Name `xml:"urn:xmpp:file:metadata:0 file"`
|
||||||
|
Name string `xml:"name,omitempty"`
|
||||||
|
MediaType string `xml:"media-type,omitempty"`
|
||||||
|
Size int64 `xml:"size,omitempty"`
|
||||||
|
Hashes []Hash `xml:"urn:xmpp:hashes:2 hash,omitempty"`
|
||||||
|
Desc string `xml:"desc,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash is a XEP-0300 cryptographic hash, used inline within XEP-0446
|
||||||
|
// file metadata
|
||||||
|
type Hash struct {
|
||||||
|
XMLName xml.Name `xml:"urn:xmpp:hashes:2 hash"`
|
||||||
|
Algo string `xml:"algo,attr"`
|
||||||
|
Value string `xml:",chardata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sources is a XEP-0447 sources element
|
||||||
|
type Sources struct {
|
||||||
|
XMLName xml.Name `xml:"urn:xmpp:sfs:0 sources"`
|
||||||
|
UrlData UrlData `xml:"http://jabber.org/protocol/url-data url-data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UrlData is the url-data source referenced by XEP-0447 Sources
|
||||||
|
type UrlData struct {
|
||||||
|
XMLName xml.Name `xml:"http://jabber.org/protocol/url-data url-data"`
|
||||||
|
Target string `xml:"target,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
// Reactions is from XEP-0444
|
// Reactions is from XEP-0444
|
||||||
type Reactions struct {
|
type Reactions struct {
|
||||||
XMLName xml.Name `xml:"urn:xmpp:reactions:0 reactions"`
|
XMLName xml.Name `xml:"urn:xmpp:reactions:0 reactions"`
|
||||||
|
|
@ -768,6 +807,11 @@ func (c AttachTo) Namespace() string {
|
||||||
return c.XMLName.Space
|
return c.XMLName.Space
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Namespace is a namespace!
|
||||||
|
func (c FileSharing) Namespace() string {
|
||||||
|
return c.XMLName.Space
|
||||||
|
}
|
||||||
|
|
||||||
// Namespace is a namespace!
|
// Namespace is a namespace!
|
||||||
func (c Reactions) Namespace() string {
|
func (c Reactions) Namespace() string {
|
||||||
return c.XMLName.Space
|
return c.XMLName.Space
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,16 @@ type marker struct {
|
||||||
Id string
|
Id string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FileSharing is a XEP-0447/XEP-0446 minimal file-sharing metadata set
|
||||||
|
type FileSharing struct {
|
||||||
|
Name string
|
||||||
|
MediaType string
|
||||||
|
Size int64
|
||||||
|
Desc string
|
||||||
|
HashAlgo string
|
||||||
|
HashValue string
|
||||||
|
}
|
||||||
|
|
||||||
type MUCUserItem struct {
|
type MUCUserItem struct {
|
||||||
Affiliation string
|
Affiliation string
|
||||||
Jid string
|
Jid string
|
||||||
|
|
@ -328,6 +338,13 @@ var SMOOB = args.NewString()
|
||||||
// SMReplaceId is a XEP-0308 replaced message id
|
// SMReplaceId is a XEP-0308 replaced message id
|
||||||
var SMReplaceId = args.NewString()
|
var SMReplaceId = args.NewString()
|
||||||
|
|
||||||
|
// SMAttachToId is a XEP-0367 attach-to reference to another message id
|
||||||
|
var SMAttachToId = args.NewString()
|
||||||
|
|
||||||
|
// SMFileSharing is a XEP-0447/XEP-0446 file-sharing set (*FileSharing),
|
||||||
|
// nesting the message's OOB URL as its source
|
||||||
|
var SMFileSharing = args.New()
|
||||||
|
|
||||||
// SMIsCarbon marks the message as a XEP-0280 carbon copy
|
// SMIsCarbon marks the message as a XEP-0280 carbon copy
|
||||||
var SMIsCarbon = args.NewBool()
|
var SMIsCarbon = args.NewBool()
|
||||||
|
|
||||||
|
|
@ -385,6 +402,8 @@ func sendMessageWrapper(to, from string, component *xmpp.Component, args ...args
|
||||||
timestamp := SMTimestamp.Get(args)
|
timestamp := SMTimestamp.Get(args)
|
||||||
oob := SMOOB.Get(args)
|
oob := SMOOB.Get(args)
|
||||||
replaceId := SMReplaceId.Get(args)
|
replaceId := SMReplaceId.Get(args)
|
||||||
|
attachToId := SMAttachToId.Get(args)
|
||||||
|
fileSharing, _ := SMFileSharing.Get(args).(*FileSharing)
|
||||||
isCarbon := SMIsCarbon.Get(args)
|
isCarbon := SMIsCarbon.Get(args)
|
||||||
isGroupchat := SMIsGroupchat.Get(args)
|
isGroupchat := SMIsGroupchat.Get(args)
|
||||||
forceSubject := SMForceSubject.Get(args)
|
forceSubject := SMForceSubject.Get(args)
|
||||||
|
|
@ -562,6 +581,33 @@ func sendMessageWrapper(to, from string, component *xmpp.Component, args ...args
|
||||||
if replaceId != "" {
|
if replaceId != "" {
|
||||||
message.Extensions = append(message.Extensions, extensions.Replace{Id: replaceId})
|
message.Extensions = append(message.Extensions, extensions.Replace{Id: replaceId})
|
||||||
}
|
}
|
||||||
|
if attachToId != "" {
|
||||||
|
message.Extensions = append(message.Extensions, extensions.AttachTo{Id: attachToId})
|
||||||
|
}
|
||||||
|
if fileSharing != nil {
|
||||||
|
file := extensions.FileMetadata{
|
||||||
|
Name: fileSharing.Name,
|
||||||
|
MediaType: fileSharing.MediaType,
|
||||||
|
Size: fileSharing.Size,
|
||||||
|
Desc: fileSharing.Desc,
|
||||||
|
}
|
||||||
|
if fileSharing.HashValue != "" {
|
||||||
|
file.Hashes = []extensions.Hash{{Algo: fileSharing.HashAlgo, Value: fileSharing.HashValue}}
|
||||||
|
}
|
||||||
|
fs := extensions.FileSharing{File: file}
|
||||||
|
if oob != "" {
|
||||||
|
fs.Sources = &extensions.Sources{UrlData: extensions.UrlData{Target: oob}}
|
||||||
|
}
|
||||||
|
message.Extensions = append(message.Extensions, fs)
|
||||||
|
if body != "" {
|
||||||
|
// XEP-0447 Example 2: a body that's just the plain OOB URL
|
||||||
|
// is a fallback for this same stanza's <file-sharing/>.
|
||||||
|
message.Extensions = append(message.Extensions, extensions.Fallback{
|
||||||
|
For: "urn:xmpp:sfs:0",
|
||||||
|
Body: []extensions.FallbackBody{{}},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
if reactions != nil {
|
if reactions != nil {
|
||||||
reactionExts := make([]extensions.Reaction, len(reactions.Reactions))
|
reactionExts := make([]extensions.Reaction, len(reactions.Reactions))
|
||||||
for i, r := range reactions.Reactions {
|
for i, r := range reactions.Reactions {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue