Live Location tracking with Telegram

We recently launched a new feature in HelpYouFind.Me, it's built on top of Telegram Bot Platform, and uses Python Telegram Bot as a wrapper, I'm happy to introduce Footsteps.

Telegram

The Implementation

The first thing you need to know is that Python-Telegram-Bot has something called "filters", and well filters are just that, if a pattern happens and you have a filter for that pattern it will call that filter.

MessageHandler(Filters.reply & (~Filters.command), handle_reply)
MessageHandler(Filters.location, handle_location)

On the example above, every time the bot receive a message, it will check that the message received is a location object or not, if it is then it will execute whatever function we have defined.

Now, speaking of Filter.location, if you search the PTB documentation for you'll see that there's no filter for a "live location" object, because in the end the end a "live location" and a "static location" are the same, in fact PTB differentiate it by adding a live_period attribute to the location object, so if live_location has a value it's live, if not it's static.

Live Location Filter

We'll start by declaring the same handler:

MessageHandler(Filters.location, handle_location)

If you run your bot and share a live location you'll see that the handle_location function is called several times, sometimes it might be every second, this means that the filter also works for live location and static locations, but also means there's no way to differentiate the one from another.

Then in order to differentiate the two types of locations we need to check the edited_message attribute - why? - every time the location is updated the Telegram API will return you the same message with an updated date and a new location:

msg_type = 0
if message["edit_date"] is not None:
    msg_type += 1

if message["location"]["live_period"] is not None:
    msg_type += 1 << 1

What we're doing in the code above is:

  1. Create a helper variable and init that to zero.
  2. Sum one to the variable if the edit_date is not none.
  3. If live_period is not none sum the result of right and let the leftmost bits.

    << is called Bitwise Left Shift Operator or Zero-Fill Left-Shift and is used to push zero bits on the rightmost side and let the leftmost bits to overflow.

After that we just only need a simple if/else validation in order to execute one action or another based on the msg_type value:

if msg_type == 0:
    context.bot.send_message(user.id, "Single (non-live) location update.")
elif msg_type == 1:
    context.bot.send_message(user.id, "End of live period.")
elif msg_type == 2:
    context.bot.send_message(user.id, "Start of live period")
elif msg_type == 3:
    context.bot.send_message(user.id, "Live location update.")

Explaining a bit that code:

  • It will be zero if the edit_date in the message is none and the live_period in the location object is empty too.
  • It will be one if the message has and edit_date.
  • It will be two if the message doesn't have an edit_date but the live_period exists.
  • And finally three if the message has an edit_date and as well the live_period is not none.

Glue'ing everything all together, the handle_location function might look like this:

def handle_location(update: Update, context: CallbackContext):
    user = update.effective_user
    msg_type = 0

    if update.edited_message:
        message = update.edited_message
    else:
        message = update.message

    if message["edit_date"] is not None:
        msg_type += 1
    if message["location"]["live_period"] is not None:
        msg_type += 1 << 1

    if msg_type == 0:
        context.bot.send_message(user.id, "Single (non-live) location update.")
    elif msg_type == 1:
        context.bot.send_message(user.id, "End of live period.")
    elif msg_type == 2:
        context.bot.send_message(user.id, "Start of live period")
    elif msg_type == 3:
        context.bot.send_message(user.id, "Live location update.")

One Last Thing

As you might think, the live location can be also stopped by user interaction and not by the end of the live_period, but also there's no notification from the Telegram API once the live location has been completed, that gives you two validations that are needed once the live location has stopped:

  • User manually stops the live location.
  • Live location reachs the live_period time

In that case a simple solution might be to just start a timer once the user starts a live location:

my_timer = threading.Timer(
    interval=message.location.live_period,
    function=end_live_location,
    args=(update, context)
)
my_timer.name = 'some unique id'

With that, the end_live_location will be executed once the Timer reachs the end of the interval, and finally when the user stops the live location manually just stop the timer by their id:

timer_threads = filter(
    lambda t: isinstance(t, threading.Timer), threading.enumerate()
)
current_user_thread = filter(
    lambda t: 'some unique id' in t.name,
    timer_threads,
)
for t in current_user_thread:
    t.cancel()

And that's it, I hope this entry helps you with the implementation of live location tracking using the Telegram Bot Platform.