mirror of
https://dev.narayana.im/narayana/telegabber.git
synced 2026-10-07 08:41: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 (
|
||||
"bytes"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"github.com/pkg/errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime"
|
||||
"net/http"
|
||||
"os"
|
||||
osUser "os/user"
|
||||
|
|
@ -1860,6 +1862,73 @@ func (c *Client) contentToFile(content client.MessageContent) (*client.File, *cl
|
|||
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) {
|
||||
for _, line := range *lines {
|
||||
count += utf8.RuneCountInString(line)
|
||||
|
|
@ -2081,6 +2150,9 @@ func (c *Client) SendMessageToGateway(chatId int64, message *client.Message, id
|
|||
}
|
||||
|
||||
var text, oob, auxText string
|
||||
var rawCaption string
|
||||
var fileMetaName, fileMetaType, fileMetaHash string
|
||||
var fileMetaSize int64
|
||||
var reply *gateway.Reply
|
||||
var replyObtained bool
|
||||
|
||||
|
|
@ -2127,6 +2199,7 @@ func (c *Client) SendMessageToGateway(chatId int64, message *client.Message, id
|
|||
}
|
||||
} else {
|
||||
text = c.messageToText(message, false)
|
||||
rawCaption = text
|
||||
|
||||
// OTR support (I do not know why would you need it, seriously)
|
||||
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 {
|
||||
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
|
||||
replyObtained = true
|
||||
newText.WriteString(prefix)
|
||||
|
|
@ -2176,6 +2258,12 @@ func (c *Client) SendMessageToGateway(chatId int64, message *client.Message, id
|
|||
auxText = text
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
commonArgs := []args.V{
|
||||
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))...)
|
||||
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.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 != "" {
|
||||
gateway.SendMessage(jid, from, c.xmpp, append(commonArgs,
|
||||
auxArgs := append(commonArgs,
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -131,18 +131,19 @@ type Fallback struct {
|
|||
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 {
|
||||
XMLName xml.Name `xml:"urn:xmpp:fallback:0 body"`
|
||||
Start string `xml:"start,attr"`
|
||||
End string `xml:"end,attr"`
|
||||
Start string `xml:"start,attr,omitempty"`
|
||||
End string `xml:"end,attr,omitempty"`
|
||||
}
|
||||
|
||||
// FallbackSubject is from XEP-0428
|
||||
type FallbackSubject struct {
|
||||
XMLName xml.Name `xml:"urn:xmpp:fallback:0 subject"`
|
||||
Start string `xml:"start,attr"`
|
||||
End string `xml:"end,attr"`
|
||||
Start string `xml:"start,attr,omitempty"`
|
||||
End string `xml:"end,attr,omitempty"`
|
||||
}
|
||||
|
||||
// CarbonReceived is from XEP-0280
|
||||
|
|
@ -520,6 +521,44 @@ type AttachTo struct {
|
|||
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
|
||||
type Reactions struct {
|
||||
XMLName xml.Name `xml:"urn:xmpp:reactions:0 reactions"`
|
||||
|
|
@ -768,6 +807,11 @@ func (c AttachTo) Namespace() string {
|
|||
return c.XMLName.Space
|
||||
}
|
||||
|
||||
// Namespace is a namespace!
|
||||
func (c FileSharing) Namespace() string {
|
||||
return c.XMLName.Space
|
||||
}
|
||||
|
||||
// Namespace is a namespace!
|
||||
func (c Reactions) Namespace() string {
|
||||
return c.XMLName.Space
|
||||
|
|
|
|||
|
|
@ -53,6 +53,16 @@ type marker struct {
|
|||
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 {
|
||||
Affiliation string
|
||||
Jid string
|
||||
|
|
@ -328,6 +338,13 @@ var SMOOB = args.NewString()
|
|||
// SMReplaceId is a XEP-0308 replaced message id
|
||||
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
|
||||
var SMIsCarbon = args.NewBool()
|
||||
|
||||
|
|
@ -385,6 +402,8 @@ func sendMessageWrapper(to, from string, component *xmpp.Component, args ...args
|
|||
timestamp := SMTimestamp.Get(args)
|
||||
oob := SMOOB.Get(args)
|
||||
replaceId := SMReplaceId.Get(args)
|
||||
attachToId := SMAttachToId.Get(args)
|
||||
fileSharing, _ := SMFileSharing.Get(args).(*FileSharing)
|
||||
isCarbon := SMIsCarbon.Get(args)
|
||||
isGroupchat := SMIsGroupchat.Get(args)
|
||||
forceSubject := SMForceSubject.Get(args)
|
||||
|
|
@ -562,6 +581,33 @@ func sendMessageWrapper(to, from string, component *xmpp.Component, args ...args
|
|||
if 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 {
|
||||
reactionExts := make([]extensions.Reaction, len(reactions.Reactions))
|
||||
for i, r := range reactions.Reactions {
|
||||
|
|
|
|||
Loading…
Reference in a new issue