From b41ed0201c80e3de0078dbacd766f5ef62819363 Mon Sep 17 00:00:00 2001 From: clarkzjw Date: Mon, 20 Feb 2023 01:13:52 -0800 Subject: add python ini config example --- Dockerfile | 1 + config.ini | 12 ++++++++++++ config.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+) create mode 100644 Dockerfile create mode 100644 config.ini create mode 100644 config.py diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2d13273 --- /dev/null +++ b/Dockerfile @@ -0,0 +1 @@ +FROM python:3 diff --git a/config.ini b/config.ini new file mode 100644 index 0000000..8b4f7bf --- /dev/null +++ b/config.ini @@ -0,0 +1,12 @@ +; [DEFAULT] +; ServerAliveInterval = 45 +; Compression = yes +; CompressionLevel = 9 +; ForwardX11 = yes + +; [forge.example] +; User = hg + +; [topsecret.server.example] +; Port = 50022 +; ForwardX11 = no diff --git a/config.py b/config.py new file mode 100644 index 0000000..7f8e2f0 --- /dev/null +++ b/config.py @@ -0,0 +1,28 @@ +# https://docs.python.org/3/library/configparser.html + +# import configparser +# config = configparser.ConfigParser() +# config['DEFAULT'] = {'ServerAliveInterval': '45', +# 'Compression': 'yes', +# 'CompressionLevel': '9'} +# config['forge.example'] = {} +# config['forge.example']['User'] = 'hg' +# config['topsecret.server.example'] = {} +# topsecret = config['topsecret.server.example'] +# topsecret['Port'] = '50022' # mutates the parser +# topsecret['ForwardX11'] = 'no' # same here +# config['DEFAULT']['ForwardX11'] = 'yes' +# with open('example.ini', 'w') as configfile: +# config.write(configfile) + +# config = configparser.ConfigParser() +# config.sections() +# config.read('example.ini') +# config.sections() +# topsecret = config['topsecret.server.example'] +# topsecret['ForwardX11'] +# topsecret['Port'] +# for key in config['forge.example']: +# print(key) + +# config['forge.example']['ForwardX11'] -- cgit v1.2.3 From 48cd0ed8abf0e84fc5d013a126e84c65ef0ed87a Mon Sep 17 00:00:00 2001 From: clarkzjw Date: Mon, 20 Feb 2023 01:17:46 -0800 Subject: ignore config.ini in .gitignore --- .gitignore | 1 + config.ini | 12 ------------ config.ini.example | 15 +++++++++++++++ 3 files changed, 16 insertions(+), 12 deletions(-) delete mode 100644 config.ini create mode 100644 config.ini.example diff --git a/.gitignore b/.gitignore index 68bc17f..0671e2e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +config.ini # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/config.ini b/config.ini deleted file mode 100644 index 8b4f7bf..0000000 --- a/config.ini +++ /dev/null @@ -1,12 +0,0 @@ -; [DEFAULT] -; ServerAliveInterval = 45 -; Compression = yes -; CompressionLevel = 9 -; ForwardX11 = yes - -; [forge.example] -; User = hg - -; [topsecret.server.example] -; Port = 50022 -; ForwardX11 = no diff --git a/config.ini.example b/config.ini.example new file mode 100644 index 0000000..f803e56 --- /dev/null +++ b/config.ini.example @@ -0,0 +1,15 @@ +[DEFAULT] +BOT_TOKEN="" + +; [DEFAULT] +; ServerAliveInterval = 45 +; Compression = yes +; CompressionLevel = 9 +; ForwardX11 = yes + +; [forge.example] +; User = hg + +; [topsecret.server.example] +; Port = 50022 +; ForwardX11 = no -- cgit v1.2.3 From 3a230798be1bc63b363cf75b8b1cae3a508cca84 Mon Sep 17 00:00:00 2001 From: clarkzjw Date: Mon, 20 Feb 2023 01:55:20 -0800 Subject: 4sq: add query poi example --- .gitignore | 2 ++ bot.py | 18 ++++++++++++++++-- config.py | 30 +++++------------------------- foursquare/query_poi.py | 18 ++++++++++++++++++ 4 files changed, 41 insertions(+), 27 deletions(-) create mode 100644 foursquare/query_poi.py diff --git a/.gitignore b/.gitignore index 0671e2e..fdccc84 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ +foursquare/location_example.json config.ini +.idea/ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/bot.py b/bot.py index 517f810..e010516 100644 --- a/bot.py +++ b/bot.py @@ -22,7 +22,11 @@ if __version_info__ < (20, 0, 0, "alpha", 1): f"visit https://docs.python-telegram-bot.org/en/v{TG_VER}/examples.html" ) from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update -from telegram.ext import Application, CallbackQueryHandler, CommandHandler, ContextTypes +from telegram.ext import Application, CallbackQueryHandler, CommandHandler, ContextTypes, MessageHandler, filters + +from config import BOT_TOKEN +from foursquare.query_poi import query_poi + # Enable logging logging.basicConfig( @@ -46,6 +50,13 @@ async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: await update.message.reply_text("Please choose:", reply_markup=reply_markup) +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)) + + async def button(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Parses the CallbackQuery and updates the message text.""" query = update.callback_query @@ -65,10 +76,13 @@ 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("TOKEN").build() + 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)) # Run the bot until the user presses Ctrl-C diff --git a/config.py b/config.py index 7f8e2f0..377a6c3 100644 --- a/config.py +++ b/config.py @@ -1,28 +1,8 @@ # https://docs.python.org/3/library/configparser.html -# import configparser -# config = configparser.ConfigParser() -# config['DEFAULT'] = {'ServerAliveInterval': '45', -# 'Compression': 'yes', -# 'CompressionLevel': '9'} -# config['forge.example'] = {} -# config['forge.example']['User'] = 'hg' -# config['topsecret.server.example'] = {} -# topsecret = config['topsecret.server.example'] -# topsecret['Port'] = '50022' # mutates the parser -# topsecret['ForwardX11'] = 'no' # same here -# config['DEFAULT']['ForwardX11'] = 'yes' -# with open('example.ini', 'w') as configfile: -# config.write(configfile) +import configparser +config = configparser.ConfigParser() +config.read("config.ini") -# config = configparser.ConfigParser() -# config.sections() -# config.read('example.ini') -# config.sections() -# topsecret = config['topsecret.server.example'] -# topsecret['ForwardX11'] -# topsecret['Port'] -# for key in config['forge.example']: -# print(key) - -# config['forge.example']['ForwardX11'] +BOT_TOKEN = config["DEFAULT"]["BOT_TOKEN"] +FSQ_API_KEY = config["DEFAULT"]["FOURSQUARE_API_KEY"] diff --git a/foursquare/query_poi.py b/foursquare/query_poi.py new file mode 100644 index 0000000..64ded55 --- /dev/null +++ b/foursquare/query_poi.py @@ -0,0 +1,18 @@ +import requests +import json +from config import FSQ_API_KEY + +POI_API_ENDPOINT = "https://api.foursquare.com/v3/places/nearby?ll={}%2C{}" + + +def query_poi(latitude, longitude): + 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] -- cgit v1.2.3 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