media: handle location shares

Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
This commit is contained in:
Sumner Evans
2024-06-20 16:49:45 -06:00
parent 9d9c82c9e9
commit 499678d092
4 changed files with 189 additions and 55 deletions
+60
View File
@@ -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())
}
+107 -52
View File
@@ -4,7 +4,6 @@ import (
"context"
"fmt"
"html"
"slices"
"strings"
"time"
@@ -30,14 +29,6 @@ type ttlable interface {
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) {
log := zerolog.Ctx(ctx).With().Str("conversion_direction", "to_matrix").Logger()
ctx = log.WithContext(ctx)
@@ -67,8 +58,10 @@ func (mc *MessageConverter) ToMatrix(ctx context.Context, portal *bridgev2.Porta
}
if media, ok := msg.GetMedia(); ok {
switch {
case media.TypeID() == tg.MessageMediaUnsupportedTypeID:
switch media.TypeID() {
case tg.MessageMediaWebPageTypeID:
// Already handled above
case tg.MessageMediaUnsupportedTypeID:
cm.Parts = append(cm.Parts, &bridgev2.ConvertedMessagePart{
ID: networkid.PartID("unsupported_media"),
Type: event.EventMessage,
@@ -80,48 +73,23 @@ func (mc *MessageConverter) ToMatrix(ctx context.Context, portal *bridgev2.Porta
"fi.mau.telegram.unsupported": true,
},
})
case mediaRequiringUpload(media):
mediaParts, disappearingSetting, err := mc.convertMediaRequiringUpload(ctx, portal, intent, msg.ID, media)
case tg.MessageMediaPhotoTypeID, tg.MessageMediaDocumentTypeID:
mediaPart, disappearingSetting, err := mc.convertMediaRequiringUpload(ctx, portal, intent, msg.ID, media)
if err != nil {
return nil, err
}
if disappearingSetting != nil {
cm.Disappear = *disappearingSetting
}
cm.Parts = append(cm.Parts, mediaParts)
case media.TypeID() == tg.MessageMediaContactTypeID:
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),
cm.Parts = append(cm.Parts, mediaPart)
case tg.MessageMediaContactTypeID:
cm.Parts = append(cm.Parts, mc.convertContact(media))
case tg.MessageMediaGeoTypeID, tg.MessageMediaGeoLiveTypeID, tg.MessageMediaVenueTypeID:
location, err := mc.convertLocation(media)
if err != nil {
return nil, err
}
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),
)
}
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,
},
},
})
cm.Parts = append(cm.Parts, location)
default:
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
// case *tg.MessageMediaGeo: // messageMediaGeo#56e0d474
// case *tg.MessageMediaUnsupported: // messageMediaUnsupported#9f84f49e
// case *tg.MessageMediaVenue: // messageMediaVenue#2ec0533f
// case *tg.MessageMediaGame: // messageMediaGame#fdb19008
// case *tg.MessageMediaInvoice: // messageMediaInvoice#f6a548d3
// case *tg.MessageMediaGeoLive: // messageMediaGeoLive#b940c666
// case *tg.MessageMediaPoll: // messageMediaPoll#4bd6e798
// case *tg.MessageMediaDice: // messageMediaDice#3f7ee58b
// 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)
// TODO all of these
// case *tg.MessageMediaGeo: // messageMediaGeo#56e0d474
// case *tg.MessageMediaUnsupported: // messageMediaUnsupported#9f84f49e
// case *tg.MessageMediaVenue: // messageMediaVenue#2ec0533f
// case *tg.MessageMediaGame: // messageMediaGame#fdb19008
// case *tg.MessageMediaInvoice: // messageMediaInvoice#f6a548d3
// case *tg.MessageMediaGeoLive: // messageMediaGeoLive#b940c666
// case *tg.MessageMediaPoll: // messageMediaPoll#4bd6e798
// case *tg.MessageMediaDice: // messageMediaDice#3f7ee58b
// case *tg.MessageMediaStory: // messageMediaStory#68cb6283
@@ -335,3 +297,96 @@ func (mc *MessageConverter) convertMediaRequiringUpload(ctx context.Context, por
Extra: extra,
}, 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
}