feat(telegram): parse location and venue messages

- Add TelegramLocation, TelegramVenue, and TelegramMessageWithLocation types
- Add formatLocationMessage() to convert location/venue shares to text
- Add extractLocationData() for structured location access in ctxPayload
- Handle both raw location pins and venue shares (places with names)
- Include location in reply-to context for quoted messages

Location messages now appear as:
- [Location: lat, lon ±accuracy] for raw pins
- [Venue: Name - Address (lat, lon)] for places

ctxPayload includes LocationLat, LocationLon, LocationAccuracy,
VenueName, and VenueAddress fields for programmatic access.
This commit is contained in:
Nacho Iacovino
2026-01-04 20:55:58 +00:00
committed by Peter Steinberger
parent 9ffea23f31
commit 255e77f530

View File

@@ -48,6 +48,32 @@ type MediaGroupEntry = {
timer: ReturnType<typeof setTimeout>;
};
/** 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 = "<media:document>";
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 = "<media:video>";
else if (reply.audio || reply.voice) body = "<media:audio>";
else if (reply.document) body = "<media:document>";
else if ((reply as TelegramMessageWithLocation).location)
body =
formatLocationMessage(reply as TelegramMessageWithLocation) ??
"<location>";
}
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;
}