diff --git a/src/telegram/bot.ts b/src/telegram/bot.ts index cf0ca2c79..58a00060c 100644 --- a/src/telegram/bot.ts +++ b/src/telegram/bot.ts @@ -48,6 +48,32 @@ type MediaGroupEntry = { timer: ReturnType; }; +/** Telegram Location object */ +interface TelegramLocation { + latitude: number; + longitude: number; + horizontal_accuracy?: number; + live_period?: number; + heading?: number; +} + +/** Telegram Venue object */ +interface TelegramVenue { + location: TelegramLocation; + title: string; + address: string; + foursquare_id?: string; + foursquare_type?: string; + google_place_id?: string; + google_place_type?: string; +} + +/** Extended message type that may include location/venue */ +type TelegramMessageWithLocation = TelegramMessage & { + location?: TelegramLocation; + venue?: TelegramVenue; +}; + type TelegramContext = { message: TelegramMessage; me?: { username?: string }; @@ -226,7 +252,15 @@ export function createTelegramBot(opts: TelegramBotOptions) { else if (msg.document) placeholder = ""; const replyTarget = describeReplyTarget(msg); - const rawBody = (msg.text ?? msg.caption ?? placeholder).trim(); + const locationText = formatLocationMessage( + msg as TelegramMessageWithLocation, + ); + const rawBody = ( + msg.text ?? + msg.caption ?? + locationText ?? + placeholder + ).trim(); if (!rawBody && allMedia.length === 0) return; let bodyText = rawBody; @@ -248,6 +282,8 @@ export function createTelegramBot(opts: TelegramBotOptions) { body: `${bodyText}${replySuffix}`, }); + const locationData = extractLocationData(msg); + const ctxPayload = { Body: body, From: isGroup ? `group:${chatId}` : `telegram:${chatId}`, @@ -273,6 +309,11 @@ export function createTelegramBot(opts: TelegramBotOptions) { allMedia.length > 0 ? (allMedia.map((m) => m.contentType).filter(Boolean) as string[]) : undefined, + LocationLat: locationData?.latitude, + LocationLon: locationData?.longitude, + LocationAccuracy: locationData?.accuracy, + VenueName: locationData?.venueName, + VenueAddress: locationData?.venueAddress, CommandAuthorized: commandAuthorized, }; @@ -698,6 +739,10 @@ function describeReplyTarget(msg: TelegramMessage) { else if (reply.video) body = ""; else if (reply.audio || reply.voice) body = ""; else if (reply.document) body = ""; + else if ((reply as TelegramMessageWithLocation).location) + body = + formatLocationMessage(reply as TelegramMessageWithLocation) ?? + ""; } if (!body) return null; const sender = buildSenderName(reply); @@ -708,3 +753,61 @@ function describeReplyTarget(msg: TelegramMessage) { body, }; } + +/** + * Extract structured location data from a message. + */ +function extractLocationData(msg: TelegramMessage): { + latitude: number; + longitude: number; + accuracy?: number; + venueName?: string; + venueAddress?: string; +} | null { + const msgWithLocation = msg as TelegramMessageWithLocation; + const { venue, location } = msgWithLocation; + + if (venue) { + return { + latitude: venue.location.latitude, + longitude: venue.location.longitude, + venueName: venue.title, + venueAddress: venue.address, + }; + } + + if (location) { + return { + latitude: location.latitude, + longitude: location.longitude, + accuracy: location.horizontal_accuracy, + }; + } + + return null; +} + +/** + * Format location or venue message into text. + * Handles both raw location shares and venue shares (places with names). + */ +function formatLocationMessage( + msg: TelegramMessageWithLocation, +): string | null { + const { venue, location } = msg; + + if (venue) { + const { latitude, longitude } = venue.location; + return `[Venue: ${venue.title} - ${venue.address} (${latitude.toFixed(6)}, ${longitude.toFixed(6)})]`; + } + + if (location) { + const { latitude, longitude, horizontal_accuracy } = location; + const accuracy = horizontal_accuracy + ? ` ±${Math.round(horizontal_accuracy)}m` + : ""; + return `[Location: ${latitude.toFixed(6)}, ${longitude.toFixed(6)}${accuracy}]`; + } + + return null; +}