diff options
-rw-r--r-- | .gitignore | 4 | ||||
-rw-r--r-- | Dockerfile | 1 | ||||
-rw-r--r-- | README | 10 | ||||
-rw-r--r-- | bot.py | 49 | ||||
-rw-r--r-- | config.ini.example | 9 | ||||
-rw-r--r-- | config.py | 13 | ||||
-rw-r--r-- | dbstore/dbm_store.py | 34 | ||||
-rw-r--r-- | foursquare/query_poi.py | 33 | ||||
-rw-r--r-- | mastodon/__init__.py | 0 | ||||
-rw-r--r-- | mastodon/toot.py | 21 | ||||
-rw-r--r-- | toot.py | 11 |
11 files changed, 147 insertions, 38 deletions
@@ -1,3 +1,7 @@ | |||
1 | foursquare/location_example.json | ||
2 | config.ini | ||
3 | .idea/ | ||
4 | fsq_poi.db | ||
1 | # Byte-compiled / optimized / DLL files | 5 | # Byte-compiled / optimized / DLL files |
2 | __pycache__/ | 6 | __pycache__/ |
3 | *.py[cod] | 7 | *.py[cod] |
diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2d13273 --- /dev/null +++ b/Dockerfile | |||
@@ -0,0 +1 @@ | |||
FROM python:3 | |||
@@ -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: |
@@ -22,7 +23,12 @@ if __version_info__ < (20, 0, 0, "alpha", 1): | |||
22 | f"visit https://docs.python-telegram-bot.org/en/v{TG_VER}/examples.html" | 23 | f"visit https://docs.python-telegram-bot.org/en/v{TG_VER}/examples.html" |
23 | ) | 24 | ) |
24 | from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update | 25 | from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update |
25 | from telegram.ext import Application, CallbackQueryHandler, CommandHandler, ContextTypes | 26 | from telegram.ext import Application, CallbackQueryHandler, CommandHandler, ContextTypes, MessageHandler, filters |
27 | |||
28 | from config import BOT_TOKEN | ||
29 | from foursquare.query_poi import query_poi | ||
30 | from dbstore.dbm_store import get_loc | ||
31 | from toot import mastodon_client | ||
26 | 32 | ||
27 | # Enable logging | 33 | # Enable logging |
28 | logging.basicConfig( | 34 | logging.basicConfig( |
@@ -32,29 +38,39 @@ logger = logging.getLogger(__name__) | |||
32 | 38 | ||
33 | 39 | ||
34 | async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: | 40 | async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: |
35 | """Sends a message with three inline buttons attached.""" | 41 | hello = "Hello, this is `checkin.bot`. \n\n" \ |
36 | keyboard = [ | 42 | "This is a Telegram bot with functionality similar to Foursquare Swarm, " \ |
37 | [ | 43 | "but check in and post your location to the Fediverse (Mastodon/Pleroma) instead of Twitter.\n\n" \ |
38 | InlineKeyboardButton("Option 1", callback_data="1"), | 44 | "Aware of privacy concerns, this bot will not store your location data."\ |
39 | InlineKeyboardButton("Option 2", callback_data="2"), | 45 | "*Be safe and cautious when sharing your real time location on the web.* \n\n"\ |
40 | ], | 46 | "Start using this bot by sharing your location using Telegram context menu to it." |
41 | [InlineKeyboardButton("Option 3", callback_data="3")], | ||
42 | ] | ||
43 | 47 | ||
44 | reply_markup = InlineKeyboardMarkup(keyboard) | 48 | await update.message.reply_text(hello, parse_mode=telegram.constants.ParseMode.MARKDOWN) |
45 | 49 | ||
46 | await update.message.reply_text("Please choose:", reply_markup=reply_markup) | 50 | |
51 | async def checkin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: | ||
52 | keyboard = [] | ||
53 | |||
54 | for poi in query_poi(update.message.location.latitude, update.message.location.longitude): | ||
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) | ||
47 | 61 | ||
48 | 62 | ||
49 | async def button(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: | 63 | async def button(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: |
50 | """Parses the CallbackQuery and updates the message text.""" | 64 | """Parses the CallbackQuery and updates the message text.""" |
51 | query = update.callback_query | 65 | query = update.callback_query |
52 | 66 | ||
53 | # CallbackQueries need to be answered, even if no notification to the user is needed | ||
54 | # Some clients may have trouble otherwise. See https://core.telegram.org/bots/api#callbackquery | ||
55 | 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") | ||
56 | 72 | ||
57 | await query.edit_message_text(text=f"Selected option: {query.data}") | 73 | await query.edit_message_text(text=f"Selected option: {poi}") |
58 | 74 | ||
59 | 75 | ||
60 | async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: | 76 | async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: |
@@ -63,12 +79,11 @@ async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No | |||
63 | 79 | ||
64 | 80 | ||
65 | def main() -> None: | 81 | def main() -> None: |
66 | """Run the bot.""" | 82 | application = Application.builder().token(BOT_TOKEN).build() |
67 | # Create the Application and pass it your bot's token. | ||
68 | application = Application.builder().token("TOKEN").build() | ||
69 | 83 | ||
70 | application.add_handler(CommandHandler("start", start)) | 84 | application.add_handler(CommandHandler("start", start)) |
71 | application.add_handler(CallbackQueryHandler(button)) | 85 | application.add_handler(CallbackQueryHandler(button)) |
86 | application.add_handler(MessageHandler(filters.LOCATION & ~filters.COMMAND, checkin)) | ||
72 | application.add_handler(CommandHandler("help", help_command)) | 87 | application.add_handler(CommandHandler("help", help_command)) |
73 | 88 | ||
74 | # Run the bot until the user presses Ctrl-C | 89 | # Run the bot until the user presses Ctrl-C |
diff --git a/config.ini.example b/config.ini.example new file mode 100644 index 0000000..4c29644 --- /dev/null +++ b/config.ini.example | |||
@@ -0,0 +1,9 @@ | |||
1 | [DEFAULT] | ||
2 | BOT_TOKEN = | ||
3 | FOURSQUARE_API_KEY = | ||
4 | |||
5 | [TOOT] | ||
6 | CLIENT_ID = | ||
7 | CLIENT_SECRET = | ||
8 | API_BASE_URL = https://mastodon.social | ||
9 | ACCESS_TOKEN = | ||
diff --git a/config.py b/config.py new file mode 100644 index 0000000..a403db0 --- /dev/null +++ b/config.py | |||
@@ -0,0 +1,13 @@ | |||
1 | # https://docs.python.org/3/library/configparser.html | ||
2 | |||
3 | import configparser | ||
4 | config = configparser.ConfigParser() | ||
5 | config.read("config.ini") | ||
6 | |||
7 | BOT_TOKEN = config["DEFAULT"]["BOT_TOKEN"] | ||
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 new file mode 100644 index 0000000..efdd1b1 --- /dev/null +++ b/foursquare/query_poi.py | |||
@@ -0,0 +1,33 @@ | |||
1 | import requests | ||
2 | import json | ||
3 | from config import FSQ_API_KEY | ||
4 | from dbstore.dbm_store import get_loc, store_loc | ||
5 | |||
6 | POI_API_ENDPOINT = "https://api.foursquare.com/v3/places/nearby?ll={}%2C{}" | ||
7 | OSM_ENDPOINT = "https://www.openstreetmap.org/?mlat={}&mlon={}&zoom=15&layers=M" | ||
8 | |||
9 | def query_poi(latitude, longitude): | ||
10 | locations = list() | ||
11 | |||
12 | url = POI_API_ENDPOINT.format(latitude, longitude) | ||
13 | headers = { | ||
14 | "accept": "application/json", | ||
15 | "Authorization": FSQ_API_KEY | ||
16 | } | ||
17 | |||
18 | response = requests.get(url, headers=headers) | ||
19 | |||
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) | ||