From f7f99f41e768e6740adbf2ee708488b29fe6265a Mon Sep 17 00:00:00 2001 From: clarkzjw Date: Mon, 20 Feb 2023 13:47:26 -0800 Subject: implemented basic functions: - send a location from Telegram to bot - query a list (7) of POIs from Foursquare - send user inline keyboard button to choose a location - post toot status update to Mastodon with a link to OSM - store previously seen locations in local db --- .gitignore | 1 + README | 10 ++++++++++ bot.py | 47 ++++++++++++++++++++++++----------------------- config.ini.example | 20 +++++++------------- config.py | 5 +++++ dbstore/dbm_store.py | 34 ++++++++++++++++++++++++++++++++++ foursquare/query_poi.py | 23 +++++++++++++++++++---- mastodon/__init__.py | 0 mastodon/toot.py | 21 --------------------- toot.py | 11 +++++++++++ 10 files changed, 111 insertions(+), 61 deletions(-) create mode 100644 dbstore/dbm_store.py delete mode 100644 mastodon/__init__.py delete mode 100644 mastodon/toot.py create mode 100644 toot.py diff --git a/.gitignore b/.gitignore index fdccc84..eb701d6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ foursquare/location_example.json config.ini .idea/ +fsq_poi.db # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/README b/README index 6c8ffd4..9b00713 100644 --- a/README +++ b/README @@ -1,3 +1,13 @@ # Swarm2Fediverse Foursquare Swarm like Telegram bot to checkin at places and post to Fediverse (Mastodon/Pleroma) + +TODO: + +- Attach up to four images to each checkin +- Set default checkin visibility to followers-only and add option to set individual checkin visibility +- Amazon .bot domain +- i18n +- Anonymized analysis +- OAuth login to Mastodon/Pleroma +- Delayed checkins diff --git a/bot.py b/bot.py index e010516..398e433 100644 --- a/bot.py +++ b/bot.py @@ -8,6 +8,7 @@ Basic example for a bot that uses inline keyboards. For an in-depth explanation, """ import logging +import telegram.constants from telegram import __version__ as TG_VER try: @@ -26,7 +27,8 @@ from telegram.ext import Application, CallbackQueryHandler, CommandHandler, Cont from config import BOT_TOKEN from foursquare.query_poi import query_poi - +from dbstore.dbm_store import get_loc +from toot import mastodon_client # Enable logging logging.basicConfig( @@ -36,36 +38,39 @@ logger = logging.getLogger(__name__) async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """Sends a message with three inline buttons attached.""" - keyboard = [ - [ - InlineKeyboardButton("Option 1", callback_data="1"), - InlineKeyboardButton("Option 2", callback_data="2"), - ], - [InlineKeyboardButton("Option 3", callback_data="3")], - ] - - reply_markup = InlineKeyboardMarkup(keyboard) + hello = "Hello, this is `checkin.bot`. \n\n" \ + "This is a Telegram bot with functionality similar to Foursquare Swarm, " \ + "but check in and post your location to the Fediverse (Mastodon/Pleroma) instead of Twitter.\n\n" \ + "Aware of privacy concerns, this bot will not store your location data."\ + "*Be safe and cautious when sharing your real time location on the web.* \n\n"\ + "Start using this bot by sharing your location using Telegram context menu to it." - await update.message.reply_text("Please choose:", reply_markup=reply_markup) + await update.message.reply_text(hello, parse_mode=telegram.constants.ParseMode.MARKDOWN) async def checkin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - print(update.message.location.latitude) - print(update.message.location.longitude) - poi = query_poi(update.message.location.latitude, update.message.location.longitude) - await update.message.reply_text("Your location received: {}".format(poi)) + keyboard = [] + + for poi in query_poi(update.message.location.latitude, update.message.location.longitude): + keyboard.append([ + InlineKeyboardButton(poi["name"], callback_data=poi["fsq_id"]), + ]) + + reply_markup = InlineKeyboardMarkup(keyboard) + await update.message.reply_text("Select a place", reply_markup=reply_markup) async def button(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Parses the CallbackQuery and updates the message text.""" query = update.callback_query - # CallbackQueries need to be answered, even if no notification to the user is needed - # Some clients may have trouble otherwise. See https://core.telegram.org/bots/api#callbackquery await query.answer() + poi = get_loc(query.data) + + mastodon_client.status_post(f"I'm at {poi['name']} in {poi['locality']}, {poi['region']}, \n[OSM]({poi['osm_url']})", + visibility="private") - await query.edit_message_text(text=f"Selected option: {query.data}") + await query.edit_message_text(text=f"Selected option: {poi}") async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: @@ -74,14 +79,10 @@ async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No def main() -> None: - """Run the bot.""" - # Create the Application and pass it your bot's token. application = Application.builder().token(BOT_TOKEN).build() application.add_handler(CommandHandler("start", start)) - application.add_handler(CommandHandler("checkin", checkin)) application.add_handler(CallbackQueryHandler(button)) - # on non command i.e message - echo the message on Telegram application.add_handler(MessageHandler(filters.LOCATION & ~filters.COMMAND, checkin)) application.add_handler(CommandHandler("help", help_command)) diff --git a/config.ini.example b/config.ini.example index f803e56..4c29644 100644 --- a/config.ini.example +++ b/config.ini.example @@ -1,15 +1,9 @@ [DEFAULT] -BOT_TOKEN="" +BOT_TOKEN = +FOURSQUARE_API_KEY = -; [DEFAULT] -; ServerAliveInterval = 45 -; Compression = yes -; CompressionLevel = 9 -; ForwardX11 = yes - -; [forge.example] -; User = hg - -; [topsecret.server.example] -; Port = 50022 -; ForwardX11 = no +[TOOT] +CLIENT_ID = +CLIENT_SECRET = +API_BASE_URL = https://mastodon.social +ACCESS_TOKEN = diff --git a/config.py b/config.py index 377a6c3..a403db0 100644 --- a/config.py +++ b/config.py @@ -6,3 +6,8 @@ config.read("config.ini") BOT_TOKEN = config["DEFAULT"]["BOT_TOKEN"] FSQ_API_KEY = config["DEFAULT"]["FOURSQUARE_API_KEY"] + +TOOT_API_BASE_URL = config["TOOT"]["API_BASE_URL"] +TOOT_CLIENT_ID = config["TOOT"]["CLIENT_ID"] +TOOT_CLIENT_SECRET = config["TOOT"]["CLIENT_SECRET"] +TOOT_ACCESS_TOKEN = config["TOOT"]["ACCESS_TOKEN"] diff --git a/dbstore/dbm_store.py b/dbstore/dbm_store.py new file mode 100644 index 0000000..fedb505 --- /dev/null +++ b/dbstore/dbm_store.py @@ -0,0 +1,34 @@ +import dbm + +db = None +store_file = "fsq_poi.db" + + +def get_loc(fsq_id): + global db + if db is None: + db = dbm.open(store_file, 'c') + if fsq_id in db: + res = db[fsq_id].decode("utf-8").split("|") + return { + "name": res[0], + "locality": res[1], + "region": res[2], + "latitude": res[3], + "longitude": res[4], + "osm_url": res[5], + } + else: + return None + + +def store_loc(loc): + global db + if db is None: + db = dbm.open(store_file, 'c') + db[loc["fsq_id"]] = "{}|{}|{}|{}|{}|{}".format(loc["name"], + loc["locality"], + loc["region"], + loc["latitude"], + loc["longitude"], + loc["osm_url"]) diff --git a/foursquare/query_poi.py b/foursquare/query_poi.py index 64ded55..efdd1b1 100644 --- a/foursquare/query_poi.py +++ b/foursquare/query_poi.py @@ -1,18 +1,33 @@ import requests import json from config import FSQ_API_KEY +from dbstore.dbm_store import get_loc, store_loc POI_API_ENDPOINT = "https://api.foursquare.com/v3/places/nearby?ll={}%2C{}" - +OSM_ENDPOINT = "https://www.openstreetmap.org/?mlat={}&mlon={}&zoom=15&layers=M" def query_poi(latitude, longitude): - url = POI_API_ENDPOINT.format(latitude, longitude) + locations = list() + url = POI_API_ENDPOINT.format(latitude, longitude) headers = { "accept": "application/json", "Authorization": FSQ_API_KEY } response = requests.get(url, headers=headers) - print(response.text) - return json.loads(response.text)["results"][:2] + + for poi in json.loads(response.text)["results"]: + loc = { + "fsq_id": poi["fsq_id"], + "name": poi["name"], + "locality": poi["location"]["locality"], + "region": poi["location"]["region"], + "latitude": poi["geocodes"]["main"]["latitude"], + "longitude": poi["geocodes"]["main"]["longitude"], + "osm_url": OSM_ENDPOINT.format(poi["geocodes"]["main"]["latitude"], poi["geocodes"]["main"]["longitude"]) + } + locations.append(loc) + store_loc(loc) + + return locations diff --git a/mastodon/__init__.py b/mastodon/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/mastodon/toot.py b/mastodon/toot.py deleted file mode 100644 index 2d07f0b..0000000 --- a/mastodon/toot.py +++ /dev/null @@ -1,21 +0,0 @@ -from mastodon import Mastodon - -''' -https://mastodonpy.readthedocs.io/en/stable/index.html - -Mastodon.create_app( - 'pytooterapp', - api_base_url = 'https://mastodon.social', - to_file = 'pytooter_clientcred.secret' -) -''' - -# mastodon = Mastodon(client_id = 'pytooter_clientcred.secret',) -# mastodon.log_in( -# 'my_login_email@example.com', -# 'incrediblygoodpassword', -# to_file = 'pytooter_usercred.secret' -# ) - -mastodon = Mastodon(access_token = 'pytooter_usercred.secret') -mastodon.toot('Tooting from Python using #mastodonpy !') \ No newline at end of file diff --git a/toot.py b/toot.py new file mode 100644 index 0000000..f1db858 --- /dev/null +++ b/toot.py @@ -0,0 +1,11 @@ +from mastodon import Mastodon +from config import TOOT_API_BASE_URL, TOOT_CLIENT_SECRET, TOOT_ACCESS_TOKEN, TOOT_CLIENT_ID + +''' +https://mastodonpy.readthedocs.io/en/stable/index.html +''' + +mastodon_client = Mastodon(client_id=TOOT_CLIENT_ID, + client_secret=TOOT_CLIENT_SECRET, + api_base_url=TOOT_API_BASE_URL, + access_token=TOOT_ACCESS_TOKEN) -- cgit v1.2.3