From 499678d0923b42c35e68df4ed2cfbfc01a5bfc81 Mon Sep 17 00:00:00 2001 From: Sumner Evans Date: Thu, 20 Jun 2024 16:49:45 -0600 Subject: [PATCH] media: handle location shares Signed-off-by: Sumner Evans --- pkg/connector/directdownload.go | 3 - pkg/connector/matrix.go | 22 +++++ pkg/connector/msgconv/geouri.go | 60 +++++++++++ pkg/connector/msgconv/tomatrix.go | 159 ++++++++++++++++++++---------- 4 files changed, 189 insertions(+), 55 deletions(-) create mode 100644 pkg/connector/msgconv/geouri.go diff --git a/pkg/connector/directdownload.go b/pkg/connector/directdownload.go index f118225f..6a6a95a9 100644 --- a/pkg/connector/directdownload.go +++ b/pkg/connector/directdownload.go @@ -94,13 +94,10 @@ func (tc *TelegramConnector) Download(ctx context.Context, mediaID networkid.Med data, err = download.DownloadDocument(ctx, client.client.API(), document) // TODO all of these - // case *tg.MessageMediaGeo: // messageMediaGeo#56e0d474 // case *tg.MessageMediaUnsupported: // messageMediaUnsupported#9f84f49e // case *tg.MessageMediaDocument: // messageMediaDocument#4cf4d72d - // 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 diff --git a/pkg/connector/matrix.go b/pkg/connector/matrix.go index 4240ff1d..7ad7b352 100644 --- a/pkg/connector/matrix.go +++ b/pkg/connector/matrix.go @@ -16,6 +16,7 @@ import ( "maunium.net/go/mautrix/event" "go.mau.fi/mautrix-telegram/pkg/connector/ids" + "go.mau.fi/mautrix-telegram/pkg/connector/msgconv" "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 if caption != "" { // TODO resolver? + // TODO HTML 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) } + 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: return nil, fmt.Errorf("unsupported message type %s", msg.Content.MsgType) } diff --git a/pkg/connector/msgconv/geouri.go b/pkg/connector/msgconv/geouri.go new file mode 100644 index 00000000..d32ec92c --- /dev/null +++ b/pkg/connector/msgconv/geouri.go @@ -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()) +} diff --git a/pkg/connector/msgconv/tomatrix.go b/pkg/connector/msgconv/tomatrix.go index ba00b303..232379ae 100644 --- a/pkg/connector/msgconv/tomatrix.go +++ b/pkg/connector/msgconv/tomatrix.go @@ -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 %s: %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 %s: %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: %s`, note, url, body), + }, + Extra: extra, + }, nil +}