diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | README | 10 | ||||
-rw-r--r-- | bot.py | 47 | ||||
-rw-r--r-- | config.ini.example | 20 | ||||
-rw-r--r-- | config.py | 5 | ||||
-rw-r--r-- | dbstore/dbm_store.py | 34 | ||||
-rw-r--r-- | foursquare/query_poi.py | 23 | ||||
-rw-r--r-- | mastodon/__init__.py | 0 | ||||
-rw-r--r-- | mastodon/toot.py | 21 | ||||
-rw-r--r-- | toot.py | 11 |
10 files changed, 111 insertions, 61 deletions
@@ -1,6 +1,7 @@ | |||
1 | foursquare/location_example.json | 1 | foursquare/location_example.json |
2 | config.ini | 2 | config.ini |
3 | .idea/ | 3 | .idea/ |
4 | fsq_poi.db | ||
4 | # Byte-compiled / optimized / DLL files | 5 | # Byte-compiled / optimized / DLL files |
5 | __pycache__/ | 6 | __pycache__/ |
6 | *.py[cod] | 7 | *.py[cod] |
@@ -1,3 +1,13 @@ | |||
1 | # Swarm2Fediverse | 1 | # Swarm2Fediverse |
2 | 2 | ||
3 | Foursquare Swarm like Telegram bot to checkin at places and post to Fediverse (Mastodon/Pleroma) | 3 | Foursquare Swarm like Telegram bot to checkin at places and post to Fediverse (Mastodon/Pleroma) |
4 | |||
5 | TODO: | ||
6 | |||
7 | - Attach up to four images to each checkin | ||
8 | - Set default checkin visibility to followers-only and add option to set individual checkin visibility | ||
9 | - Amazon .bot domain | ||
10 | - i18n | ||
11 | - Anonymized analysis | ||
12 | - OAuth login to Mastodon/Pleroma | ||
13 | - Delayed checkins | ||
@@ -8,6 +8,7 @@ Basic example for a bot that uses inline keyboards. For an in-depth explanation, | |||
8 | """ | 8 | """ |
9 | import logging | 9 | import logging |
10 | 10 | ||
11 | import telegram.constants | ||
11 | from telegram import __version__ as TG_VER | 12 | from telegram import __version__ as TG_VER |
12 | 13 | ||
13 | try: | 14 | try: |
@@ -26,7 +27,8 @@ from telegram.ext import Application, CallbackQueryHandler, CommandHandler, Cont | |||
26 | 27 | ||
27 | from config import BOT_TOKEN | 28 | from config import BOT_TOKEN |
28 | from foursquare.query_poi import query_poi | 29 | from foursquare.query_poi import query_poi |
29 | 30 | from dbstore.dbm_store import get_loc | |
31 | from toot import mastodon_client | ||
30 | 32 | ||
31 | # Enable logging | 33 | # Enable logging |
32 | logging.basicConfig( | 34 | logging.basicConfig( |
@@ -36,36 +38,39 @@ logger = logging.getLogger(__name__) | |||
36 | 38 | ||
37 | 39 | ||
38 | async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: | 40 | async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: |
39 | """Sends a message with three inline buttons attached.""" | 41 | hello = "Hello, this is `checkin.bot`. \n\n" \ |
40 | keyboard = [ | 42 | "This is a Telegram bot with functionality similar to Foursquare Swarm, " \ |
41 | [ | 43 | "but check in and post your location to the Fediverse (Mastodon/Pleroma) instead of Twitter.\n\n" \ |
42 | InlineKeyboardButton("Option 1", callback_data="1"), | 44 | "Aware of privacy concerns, this bot will not store your location data."\ |
43 | InlineKeyboardButton("Option 2", callback_data="2"), | 45 | "*Be safe and cautious when sharing your real time location on the web.* \n\n"\ |
44 | ], | 46 | "Start using this bot by sharing your location using Telegram context menu to it." |
45 | [InlineKeyboardButton("Option 3", callback_data="3")], | ||
46 | ] | ||
47 | |||
48 | reply_markup = InlineKeyboardMarkup(keyboard) | ||
49 | 47 | ||
50 | await update.message.reply_text("Please choose:", reply_markup=reply_markup) | 48 | await update.message.reply_text(hello, parse_mode=telegram.constants.ParseMode.MARKDOWN) |
51 | 49 | ||
52 | 50 | ||
53 | async def checkin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: | 51 | async def checkin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: |
54 | print(update.message.location.latitude) | 52 | keyboard = [] |
55 | print(update.message.location.longitude) | 53 | |
56 | poi = query_poi(update.message.location.latitude, update.message.location.longitude) | 54 | for poi in query_poi(update.message.location.latitude, update.message.location.longitude): |
57 | await update.message.reply_text("Your location received: {}".format(poi)) | 55 | keyboard.append([ |
56 | InlineKeyboardButton(poi["name"], callback_data=poi["fsq_id"]), | ||
57 | ]) | ||
58 | |||
59 | reply_markup = InlineKeyboardMarkup(keyboard) | ||
60 | await update.message.reply_text("Select a place", reply_markup=reply_markup) | ||
58 | 61 | ||
59 | 62 | ||
60 | async def button(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: | 63 | async def button(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: |
61 | """Parses the CallbackQuery and updates the message text.""" | 64 | """Parses the CallbackQuery and updates the message text.""" |
62 | query = update.callback_query | 65 | query = update.callback_query |
63 | 66 | ||
64 | # CallbackQueries need to be answered, even if no notification to the user is needed | ||
65 | # Some clients may have trouble otherwise. See https://core.telegram.org/bots/api#callbackquery | ||
66 | await query.answer() | 67 | await query.answer() |
68 | poi = get_loc(query.data) | ||
69 | |||
70 | mastodon_client.status_post(f"I'm at {poi['name']} in {poi['locality']}, {poi['region']}, \n[OSM]({poi['osm_url']})", | ||
71 | visibility="private") | ||
67 | 72 | ||
68 | await query.edit_message_text(text=f"Selected option: {query.data}") | 73 | await query.edit_message_text(text=f"Selected option: {poi}") |
69 | 74 | ||
70 | 75 | ||
71 | async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: | 76 | 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 | |||
74 | 79 | ||
75 | 80 | ||
76 | def main() -> None: | 81 | def main() -> None: |
77 | """Run the bot.""" | ||
78 | # Create the Application and pass it your bot's token. | ||
79 | application = Application.builder().token(BOT_TOKEN).build() | 82 | application = Application.builder().token(BOT_TOKEN).build() |
80 | 83 | ||
81 | application.add_handler(CommandHandler("start", start)) | 84 | application.add_handler(CommandHandler("start", start)) |
82 | application.add_handler(CommandHandler("checkin", checkin)) | ||
83 | application.add_handler(CallbackQueryHandler(button)) | 85 | application.add_handler(CallbackQueryHandler(button)) |
84 | # on non command i.e message - echo the message on Telegram | ||
85 | application.add_handler(MessageHandler(filters.LOCATION & ~filters.COMMAND, checkin)) | 86 | application.add_handler(MessageHandler(filters.LOCATION & ~filters.COMMAND, checkin)) |
86 | application.add_handler(CommandHandler("help", help_command)) | 87 | application.add_handler(CommandHandler("help", help_command)) |
87 | 88 | ||
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 @@ | |||
1 | [DEFAULT] | 1 | [DEFAULT] |
2 | BOT_TOKEN="" | 2 | BOT_TOKEN = |
3 | FOURSQUARE_API_KEY = | ||
3 | 4 | ||
4 | ; [DEFAULT] | 5 | [TOOT] |
5 | ; ServerAliveInterval = 45 | 6 | CLIENT_ID = |
6 | ; Compression = yes | 7 | CLIENT_SECRET = |
7 | ; CompressionLevel = 9 | 8 | API_BASE_URL = https://mastodon.social |
8 | ; ForwardX11 = yes | 9 | ACCESS_TOKEN = |
9 | |||
10 | ; [forge.example] | ||
11 | ; User = hg | ||
12 | |||
13 | ; [topsecret.server.example] | ||
14 | ; Port = 50022 | ||
15 | ; ForwardX11 = no | ||
@@ -6,3 +6,8 @@ config.read("config.ini") | |||
6 | 6 | ||
7 | BOT_TOKEN = config["DEFAULT"]["BOT_TOKEN"] | 7 | BOT_TOKEN = config["DEFAULT"]["BOT_TOKEN"] |
8 | FSQ_API_KEY = config["DEFAULT"]["FOURSQUARE_API_KEY"] | 8 | FSQ_API_KEY = config["DEFAULT"]["FOURSQUARE_API_KEY"] |
9 | |||
10 | TOOT_API_BASE_URL = config["TOOT"]["API_BASE_URL"] | ||
11 | TOOT_CLIENT_ID = config["TOOT"]["CLIENT_ID"] | ||
12 | TOOT_CLIENT_SECRET = config["TOOT"]["CLIENT_SECRET"] | ||
13 | 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 @@ | |||
1 | import dbm | ||
2 | |||
3 | db = None | ||
4 | store_file = "fsq_poi.db" | ||
5 | |||
6 | |||
7 | def get_loc(fsq_id): | ||
8 | global db | ||
9 | if db is None: | ||
10 | db = dbm.open(store_file, 'c') | ||
11 | if fsq_id in db: | ||
12 | res = db[fsq_id].decode("utf-8").split("|") | ||
13 | return { | ||
14 | "name": res[0], | ||
15 | "locality": res[1], | ||
16 | "region": res[2], | ||
17 | "latitude": res[3], | ||
18 | "longitude": res[4], | ||
19 | "osm_url": res[5], | ||
20 | } | ||
21 | else: | ||
22 | return None | ||
23 | |||
24 | |||
25 | def store_loc(loc): | ||
26 | global db | ||
27 | if db is None: | ||
28 | db = dbm.open(store_file, 'c') | ||
29 | db[loc["fsq_id"]] = "{}|{}|{}|{}|{}|{}".format(loc["name"], | ||
30 | loc["locality"], | ||
31 | loc["region"], | ||
32 | loc["latitude"], | ||
33 | loc["longitude"], | ||
34 | 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 @@ | |||
1 | import requests | 1 | import requests |
2 | import json | 2 | import json |
3 | from config import FSQ_API_KEY | 3 | from config import FSQ_API_KEY |
4 | from dbstore.dbm_store import get_loc, store_loc | ||
4 | 5 | ||
5 | POI_API_ENDPOINT = "https://api.foursquare.com/v3/places/nearby?ll={}%2C{}" | 6 | POI_API_ENDPOINT = "https://api.foursquare.com/v3/places/nearby?ll={}%2C{}" |
6 | 7 | OSM_ENDPOINT = "https://www.openstreetmap.org/?mlat={}&mlon={}&zoom=15&layers=M" | |
7 | 8 | ||
8 | def query_poi(latitude, longitude): | 9 | def query_poi(latitude, longitude): |
9 | url = POI_API_ENDPOINT.format(latitude, longitude) | 10 | locations = list() |
10 | 11 | ||
12 | url = POI_API_ENDPOINT.format(latitude, longitude) | ||
11 | headers = { | 13 | headers = { |
12 | "accept": "application/json", | 14 | "accept": "application/json", |
13 | "Authorization": FSQ_API_KEY | 15 | "Authorization": FSQ_API_KEY |
14 | } | 16 | } |
15 | 17 | ||
16 | response = requests.get(url, headers=headers) | 18 | response = requests.get(url, headers=headers) |
17 | print(response.text) | 19 | |
18 | return json.loads(response.text)["results"][:2] | 20 | for poi in json.loads(response.text)["results"]: |
21 | loc = { | ||
22 | "fsq_id": poi["fsq_id"], | ||
23 | "name": poi["name"], | ||
24 | "locality": poi["location"]["locality"], | ||
25 | "region": poi["location"]["region"], | ||
26 | "latitude": poi["geocodes"]["main"]["latitude"], | ||
27 | "longitude": poi["geocodes"]["main"]["longitude"], | ||
28 | "osm_url": OSM_ENDPOINT.format(poi["geocodes"]["main"]["latitude"], poi["geocodes"]["main"]["longitude"]) | ||
29 | } | ||
30 | locations.append(loc) | ||
31 | store_loc(loc) | ||
32 | |||
33 | return locations | ||
diff --git a/mastodon/__init__.py b/mastodon/__init__.py deleted file mode 100644 index e69de29..0000000 --- a/mastodon/__init__.py +++ /dev/null | |||
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 @@ | |||
1 | from mastodon import Mastodon | ||
2 | |||
3 | ''' | ||
4 | https://mastodonpy.readthedocs.io/en/stable/index.html | ||
5 | |||
6 | Mastodon.create_app( | ||
7 | 'pytooterapp', | ||
8 | api_base_url = 'https://mastodon.social', | ||
9 | to_file = 'pytooter_clientcred.secret' | ||
10 | ) | ||
11 | ''' | ||
12 | |||
13 | # mastodon = Mastodon(client_id = 'pytooter_clientcred.secret',) | ||
14 | # mastodon.log_in( | ||
15 | # '[email protected]', | ||
16 | # 'incrediblygoodpassword', | ||
17 | # to_file = 'pytooter_usercred.secret' | ||
18 | # ) | ||
19 | |||
20 | mastodon = Mastodon(access_token = 'pytooter_usercred.secret') | ||
21 | mastodon.toot('Tooting from Python using #mastodonpy !') \ No newline at end of file | ||
@@ -0,0 +1,11 @@ | |||
1 | from mastodon import Mastodon | ||
2 | from config import TOOT_API_BASE_URL, TOOT_CLIENT_SECRET, TOOT_ACCESS_TOKEN, TOOT_CLIENT_ID | ||
3 | |||
4 | ''' | ||
5 | https://mastodonpy.readthedocs.io/en/stable/index.html | ||
6 | ''' | ||
7 | |||
8 | mastodon_client = Mastodon(client_id=TOOT_CLIENT_ID, | ||
9 | client_secret=TOOT_CLIENT_SECRET, | ||
10 | api_base_url=TOOT_API_BASE_URL, | ||
11 | access_token=TOOT_ACCESS_TOKEN) | ||