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
+}