media: handle location shares
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
This commit is contained in:
@@ -94,13 +94,10 @@ func (tc *TelegramConnector) Download(ctx context.Context, mediaID networkid.Med
|
|||||||
data, err = download.DownloadDocument(ctx, client.client.API(), document)
|
data, err = download.DownloadDocument(ctx, client.client.API(), document)
|
||||||
|
|
||||||
// TODO all of these
|
// TODO all of these
|
||||||
// case *tg.MessageMediaGeo: // messageMediaGeo#56e0d474
|
|
||||||
// case *tg.MessageMediaUnsupported: // messageMediaUnsupported#9f84f49e
|
// case *tg.MessageMediaUnsupported: // messageMediaUnsupported#9f84f49e
|
||||||
// case *tg.MessageMediaDocument: // messageMediaDocument#4cf4d72d
|
// case *tg.MessageMediaDocument: // messageMediaDocument#4cf4d72d
|
||||||
// case *tg.MessageMediaVenue: // messageMediaVenue#2ec0533f
|
|
||||||
// case *tg.MessageMediaGame: // messageMediaGame#fdb19008
|
// case *tg.MessageMediaGame: // messageMediaGame#fdb19008
|
||||||
// case *tg.MessageMediaInvoice: // messageMediaInvoice#f6a548d3
|
// case *tg.MessageMediaInvoice: // messageMediaInvoice#f6a548d3
|
||||||
// case *tg.MessageMediaGeoLive: // messageMediaGeoLive#b940c666
|
|
||||||
// case *tg.MessageMediaPoll: // messageMediaPoll#4bd6e798
|
// case *tg.MessageMediaPoll: // messageMediaPoll#4bd6e798
|
||||||
// case *tg.MessageMediaDice: // messageMediaDice#3f7ee58b
|
// case *tg.MessageMediaDice: // messageMediaDice#3f7ee58b
|
||||||
// case *tg.MessageMediaStory: // messageMediaStory#68cb6283
|
// case *tg.MessageMediaStory: // messageMediaStory#68cb6283
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
"maunium.net/go/mautrix/event"
|
"maunium.net/go/mautrix/event"
|
||||||
|
|
||||||
"go.mau.fi/mautrix-telegram/pkg/connector/ids"
|
"go.mau.fi/mautrix-telegram/pkg/connector/ids"
|
||||||
|
"go.mau.fi/mautrix-telegram/pkg/connector/msgconv"
|
||||||
"go.mau.fi/mautrix-telegram/pkg/connector/waveform"
|
"go.mau.fi/mautrix-telegram/pkg/connector/waveform"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -66,6 +67,7 @@ func (t *TelegramClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.
|
|||||||
var styling []styling.StyledTextOption
|
var styling []styling.StyledTextOption
|
||||||
if caption != "" {
|
if caption != "" {
|
||||||
// TODO resolver?
|
// TODO resolver?
|
||||||
|
// TODO HTML
|
||||||
styling = append(styling, html.String(nil, caption))
|
styling = append(styling, html.String(nil, caption))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,6 +100,26 @@ func (t *TelegramClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.
|
|||||||
}
|
}
|
||||||
updates, err = builder.Media(ctx, media)
|
updates, err = builder.Media(ctx, media)
|
||||||
}
|
}
|
||||||
|
case event.MsgLocation:
|
||||||
|
var uri msgconv.GeoURI
|
||||||
|
uri, err = msgconv.ParseGeoURI(msg.Content.GeoURI)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var styling []styling.StyledTextOption
|
||||||
|
if location, ok := msg.Event.Content.Raw["org.matrix.msc3488.location"].(map[string]any); ok {
|
||||||
|
if desc, ok := location["description"].(string); ok {
|
||||||
|
// TODO resolver?
|
||||||
|
// TODO HTML
|
||||||
|
styling = append(styling, html.String(nil, desc))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updates, err = builder.Media(ctx, message.Media(&tg.InputMediaGeoPoint{
|
||||||
|
GeoPoint: &tg.InputGeoPoint{
|
||||||
|
Lat: uri.Lat,
|
||||||
|
Long: uri.Long,
|
||||||
|
},
|
||||||
|
}, styling...))
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported message type %s", msg.Content.MsgType)
|
return nil, fmt.Errorf("unsupported message type %s", msg.Content.MsgType)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package msgconv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GeoURI struct {
|
||||||
|
Lat float64
|
||||||
|
Long float64
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ json.Unmarshaler = (*GeoURI)(nil)
|
||||||
|
var _ json.Marshaler = (*GeoURI)(nil)
|
||||||
|
|
||||||
|
func GeoURIFromLatLong(lat, long float64) GeoURI {
|
||||||
|
return GeoURI{lat, long}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseGeoURI(uri string) (g GeoURI, err error) {
|
||||||
|
if !strings.HasPrefix(uri, "geo:") {
|
||||||
|
return g, fmt.Errorf("invalid geo URI: %s", uri)
|
||||||
|
}
|
||||||
|
coordinates := strings.Split(strings.TrimPrefix(uri, "geo:"), ";")[0]
|
||||||
|
parts := strings.Split(coordinates, ",")
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return g, fmt.Errorf("geo coordinates not formatted properly")
|
||||||
|
}
|
||||||
|
g.Lat, err = strconv.ParseFloat(parts[0], 64)
|
||||||
|
if err != nil {
|
||||||
|
return g, fmt.Errorf("failed to parse latitude: %w", err)
|
||||||
|
}
|
||||||
|
g.Long, err = strconv.ParseFloat(parts[1], 64)
|
||||||
|
if err != nil {
|
||||||
|
return g, fmt.Errorf("failed to parse longitude: %w", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g GeoURI) URI() string {
|
||||||
|
return fmt.Sprintf("geo:%f,%f", g.Lat, g.Long)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GeoURI) UnmarshalJSON(data []byte) (err error) {
|
||||||
|
var uri string
|
||||||
|
err = json.Unmarshal(data, &uri)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
geo, err := ParseGeoURI(uri)
|
||||||
|
g.Lat = geo.Lat
|
||||||
|
g.Long = geo.Long
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GeoURI) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(g.URI())
|
||||||
|
}
|
||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html"
|
"html"
|
||||||
"slices"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -30,14 +29,6 @@ type ttlable interface {
|
|||||||
GetTTLSeconds() (value int, ok bool)
|
GetTTLSeconds() (value int, ok bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
func mediaRequiringUpload(media tg.MessageMediaClass) bool {
|
|
||||||
allowed := []uint32{
|
|
||||||
tg.MessageMediaPhotoTypeID,
|
|
||||||
tg.MessageMediaDocumentTypeID,
|
|
||||||
}
|
|
||||||
return slices.Contains(allowed, media.TypeID())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mc *MessageConverter) ToMatrix(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI, msg *tg.Message) (*bridgev2.ConvertedMessage, error) {
|
func (mc *MessageConverter) ToMatrix(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI, msg *tg.Message) (*bridgev2.ConvertedMessage, error) {
|
||||||
log := zerolog.Ctx(ctx).With().Str("conversion_direction", "to_matrix").Logger()
|
log := zerolog.Ctx(ctx).With().Str("conversion_direction", "to_matrix").Logger()
|
||||||
ctx = log.WithContext(ctx)
|
ctx = log.WithContext(ctx)
|
||||||
@@ -67,8 +58,10 @@ func (mc *MessageConverter) ToMatrix(ctx context.Context, portal *bridgev2.Porta
|
|||||||
}
|
}
|
||||||
|
|
||||||
if media, ok := msg.GetMedia(); ok {
|
if media, ok := msg.GetMedia(); ok {
|
||||||
switch {
|
switch media.TypeID() {
|
||||||
case media.TypeID() == tg.MessageMediaUnsupportedTypeID:
|
case tg.MessageMediaWebPageTypeID:
|
||||||
|
// Already handled above
|
||||||
|
case tg.MessageMediaUnsupportedTypeID:
|
||||||
cm.Parts = append(cm.Parts, &bridgev2.ConvertedMessagePart{
|
cm.Parts = append(cm.Parts, &bridgev2.ConvertedMessagePart{
|
||||||
ID: networkid.PartID("unsupported_media"),
|
ID: networkid.PartID("unsupported_media"),
|
||||||
Type: event.EventMessage,
|
Type: event.EventMessage,
|
||||||
@@ -80,48 +73,23 @@ func (mc *MessageConverter) ToMatrix(ctx context.Context, portal *bridgev2.Porta
|
|||||||
"fi.mau.telegram.unsupported": true,
|
"fi.mau.telegram.unsupported": true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
case mediaRequiringUpload(media):
|
case tg.MessageMediaPhotoTypeID, tg.MessageMediaDocumentTypeID:
|
||||||
mediaParts, disappearingSetting, err := mc.convertMediaRequiringUpload(ctx, portal, intent, msg.ID, media)
|
mediaPart, disappearingSetting, err := mc.convertMediaRequiringUpload(ctx, portal, intent, msg.ID, media)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if disappearingSetting != nil {
|
if disappearingSetting != nil {
|
||||||
cm.Disappear = *disappearingSetting
|
cm.Disappear = *disappearingSetting
|
||||||
}
|
}
|
||||||
cm.Parts = append(cm.Parts, mediaParts)
|
cm.Parts = append(cm.Parts, mediaPart)
|
||||||
case media.TypeID() == tg.MessageMediaContactTypeID:
|
case tg.MessageMediaContactTypeID:
|
||||||
contact := media.(*tg.MessageMediaContact)
|
cm.Parts = append(cm.Parts, mc.convertContact(media))
|
||||||
name := util.FormatFullName(contact.FirstName, contact.LastName)
|
case tg.MessageMediaGeoTypeID, tg.MessageMediaGeoLiveTypeID, tg.MessageMediaVenueTypeID:
|
||||||
formattedPhone := fmt.Sprintf("+%s", strings.TrimPrefix(contact.PhoneNumber, "+"))
|
location, err := mc.convertLocation(media)
|
||||||
|
if err != nil {
|
||||||
content := event.MessageEventContent{
|
return nil, err
|
||||||
MsgType: event.MsgText,
|
|
||||||
Body: fmt.Sprintf("Shared contact info for %s: %s", name, formattedPhone),
|
|
||||||
}
|
}
|
||||||
if contact.UserID > 0 {
|
cm.Parts = append(cm.Parts, location)
|
||||||
content.Format = event.FormatHTML
|
|
||||||
content.FormattedBody = fmt.Sprintf(
|
|
||||||
`Shared contact info for <a href="https://matrix.to/#/%s">%s</a>: %s`,
|
|
||||||
mc.connector.FormatGhostMXID(ids.MakeUserID(contact.UserID)),
|
|
||||||
html.EscapeString(name),
|
|
||||||
html.EscapeString(formattedPhone),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
cm.Parts = append(cm.Parts, &bridgev2.ConvertedMessagePart{
|
|
||||||
ID: networkid.PartID("contact"),
|
|
||||||
Type: event.EventMessage,
|
|
||||||
Content: &content,
|
|
||||||
Extra: map[string]any{
|
|
||||||
"fi.mau.telegram.contact": map[string]any{
|
|
||||||
"user_id": contact.UserID,
|
|
||||||
"first_name": contact.FirstName,
|
|
||||||
"last_name": contact.LastName,
|
|
||||||
"phone_number": contact.PhoneNumber,
|
|
||||||
"vcard": contact.Vcard,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported media type %T", media)
|
return nil, fmt.Errorf("unsupported media type %T", media)
|
||||||
}
|
}
|
||||||
@@ -214,12 +182,9 @@ func (mc *MessageConverter) convertMediaRequiringUpload(ctx context.Context, por
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO all of these
|
// TODO all of these
|
||||||
// case *tg.MessageMediaGeo: // messageMediaGeo#56e0d474
|
|
||||||
// case *tg.MessageMediaUnsupported: // messageMediaUnsupported#9f84f49e
|
// case *tg.MessageMediaUnsupported: // messageMediaUnsupported#9f84f49e
|
||||||
// case *tg.MessageMediaVenue: // messageMediaVenue#2ec0533f
|
|
||||||
// case *tg.MessageMediaGame: // messageMediaGame#fdb19008
|
// case *tg.MessageMediaGame: // messageMediaGame#fdb19008
|
||||||
// case *tg.MessageMediaInvoice: // messageMediaInvoice#f6a548d3
|
// case *tg.MessageMediaInvoice: // messageMediaInvoice#f6a548d3
|
||||||
// case *tg.MessageMediaGeoLive: // messageMediaGeoLive#b940c666
|
|
||||||
// case *tg.MessageMediaPoll: // messageMediaPoll#4bd6e798
|
// case *tg.MessageMediaPoll: // messageMediaPoll#4bd6e798
|
||||||
// case *tg.MessageMediaDice: // messageMediaDice#3f7ee58b
|
// case *tg.MessageMediaDice: // messageMediaDice#3f7ee58b
|
||||||
// case *tg.MessageMediaStory: // messageMediaStory#68cb6283
|
// case *tg.MessageMediaStory: // messageMediaStory#68cb6283
|
||||||
@@ -275,12 +240,9 @@ func (mc *MessageConverter) convertMediaRequiringUpload(ctx context.Context, por
|
|||||||
data, err = download.DownloadDocument(ctx, mc.client.API(), document)
|
data, err = download.DownloadDocument(ctx, mc.client.API(), document)
|
||||||
|
|
||||||
// TODO all of these
|
// TODO all of these
|
||||||
// case *tg.MessageMediaGeo: // messageMediaGeo#56e0d474
|
|
||||||
// case *tg.MessageMediaUnsupported: // messageMediaUnsupported#9f84f49e
|
// case *tg.MessageMediaUnsupported: // messageMediaUnsupported#9f84f49e
|
||||||
// case *tg.MessageMediaVenue: // messageMediaVenue#2ec0533f
|
|
||||||
// case *tg.MessageMediaGame: // messageMediaGame#fdb19008
|
// case *tg.MessageMediaGame: // messageMediaGame#fdb19008
|
||||||
// case *tg.MessageMediaInvoice: // messageMediaInvoice#f6a548d3
|
// case *tg.MessageMediaInvoice: // messageMediaInvoice#f6a548d3
|
||||||
// case *tg.MessageMediaGeoLive: // messageMediaGeoLive#b940c666
|
|
||||||
// case *tg.MessageMediaPoll: // messageMediaPoll#4bd6e798
|
// case *tg.MessageMediaPoll: // messageMediaPoll#4bd6e798
|
||||||
// case *tg.MessageMediaDice: // messageMediaDice#3f7ee58b
|
// case *tg.MessageMediaDice: // messageMediaDice#3f7ee58b
|
||||||
// case *tg.MessageMediaStory: // messageMediaStory#68cb6283
|
// case *tg.MessageMediaStory: // messageMediaStory#68cb6283
|
||||||
@@ -335,3 +297,96 @@ func (mc *MessageConverter) convertMediaRequiringUpload(ctx context.Context, por
|
|||||||
Extra: extra,
|
Extra: extra,
|
||||||
}, disappearingSetting, nil
|
}, disappearingSetting, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (mc *MessageConverter) convertContact(media tg.MessageMediaClass) *bridgev2.ConvertedMessagePart {
|
||||||
|
contact := media.(*tg.MessageMediaContact)
|
||||||
|
name := util.FormatFullName(contact.FirstName, contact.LastName)
|
||||||
|
formattedPhone := fmt.Sprintf("+%s", strings.TrimPrefix(contact.PhoneNumber, "+"))
|
||||||
|
|
||||||
|
content := event.MessageEventContent{
|
||||||
|
MsgType: event.MsgText,
|
||||||
|
Body: fmt.Sprintf("Shared contact info for %s: %s", name, formattedPhone),
|
||||||
|
}
|
||||||
|
if contact.UserID > 0 {
|
||||||
|
content.Format = event.FormatHTML
|
||||||
|
content.FormattedBody = fmt.Sprintf(
|
||||||
|
`Shared contact info for <a href="https://matrix.to/#/%s">%s</a>: %s`,
|
||||||
|
mc.connector.FormatGhostMXID(ids.MakeUserID(contact.UserID)),
|
||||||
|
html.EscapeString(name),
|
||||||
|
html.EscapeString(formattedPhone),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &bridgev2.ConvertedMessagePart{
|
||||||
|
ID: networkid.PartID("contact"),
|
||||||
|
Type: event.EventMessage,
|
||||||
|
Content: &content,
|
||||||
|
Extra: map[string]any{
|
||||||
|
"fi.mau.telegram.contact": map[string]any{
|
||||||
|
"user_id": contact.UserID,
|
||||||
|
"first_name": contact.FirstName,
|
||||||
|
"last_name": contact.LastName,
|
||||||
|
"phone_number": contact.PhoneNumber,
|
||||||
|
"vcard": contact.Vcard,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type hasGeo interface {
|
||||||
|
GetGeo() tg.GeoPointClass
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mc *MessageConverter) convertLocation(media tg.MessageMediaClass) (*bridgev2.ConvertedMessagePart, error) {
|
||||||
|
g, ok := media.(hasGeo)
|
||||||
|
if !ok || g.GetGeo().TypeID() != tg.GeoPointTypeID {
|
||||||
|
return nil, fmt.Errorf("location didn't have geo or geo is wrong type")
|
||||||
|
}
|
||||||
|
point := g.GetGeo().(*tg.GeoPoint)
|
||||||
|
var longChar, latChar string
|
||||||
|
if point.Long > 0 {
|
||||||
|
longChar = "E"
|
||||||
|
} else {
|
||||||
|
longChar = "W"
|
||||||
|
}
|
||||||
|
if point.Lat > 0 {
|
||||||
|
latChar = "N"
|
||||||
|
} else {
|
||||||
|
latChar = "S"
|
||||||
|
}
|
||||||
|
|
||||||
|
geo := fmt.Sprintf("%f,%f", point.Lat, point.Long)
|
||||||
|
geoURI := GeoURIFromLatLong(point.Lat, point.Long)
|
||||||
|
body := fmt.Sprintf("%.4f° %s, %.4f° %s", point.Lat, latChar, point.Long, longChar)
|
||||||
|
url := fmt.Sprintf("https://maps.google.com/?q=%s", geo)
|
||||||
|
|
||||||
|
extra := map[string]any{}
|
||||||
|
var note string
|
||||||
|
if media.TypeID() == tg.MessageMediaGeoLiveTypeID {
|
||||||
|
note = "Live Location (see your Telegram client for live updates)"
|
||||||
|
} else if venue, ok := media.(*tg.MessageMediaVenue); ok {
|
||||||
|
note = venue.Title
|
||||||
|
body = fmt.Sprintf("%s (%s)", venue.Address, body)
|
||||||
|
extra["fi.mau.telegram.venue_id"] = venue.VenueID
|
||||||
|
} else {
|
||||||
|
note = "Location"
|
||||||
|
}
|
||||||
|
|
||||||
|
extra["org.matrix.msc3488.location"] = map[string]any{
|
||||||
|
"uri": geoURI,
|
||||||
|
"description": note,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &bridgev2.ConvertedMessagePart{
|
||||||
|
ID: networkid.PartID("location"),
|
||||||
|
Type: event.EventMessage,
|
||||||
|
Content: &event.MessageEventContent{
|
||||||
|
MsgType: event.MsgLocation,
|
||||||
|
GeoURI: geoURI.URI(),
|
||||||
|
Body: fmt.Sprintf("%s: %s\n%s", note, body, url),
|
||||||
|
Format: event.FormatHTML,
|
||||||
|
FormattedBody: fmt.Sprintf(`%s: <a href="%s">%s</a>`, note, url, body),
|
||||||
|
},
|
||||||
|
Extra: extra,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user