aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--README1
-rw-r--r--bot.py51
-rw-r--r--callback.py51
-rw-r--r--command.py7
-rw-r--r--dbstore/dbm_store.py34
-rw-r--r--dbstore/peewee_store.py59
-rw-r--r--foursquare/poi.py5
-rw-r--r--requirements.txt15
9 files changed, 139 insertions, 85 deletions
diff --git a/.gitignore b/.gitignore
index f52378c..6b3bccc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@ foursquare/*_example.json
2config.ini 2config.ini
3.idea/ 3.idea/
4fsq_poi.db 4fsq_poi.db
5checkinbot.db
5# Byte-compiled / optimized / DLL files 6# Byte-compiled / optimized / DLL files
6__pycache__/ 7__pycache__/
7*.py[cod] 8*.py[cod]
diff --git a/README b/README
index b8176ad..2150cc4 100644
--- a/README
+++ b/README
@@ -11,3 +11,4 @@ TODO:
11- OAuth login to Mastodon/Pleroma 11- OAuth login to Mastodon/Pleroma
12- Delayed checkins 12- Delayed checkins
13- user usage count, add Telegram payment option 13- user usage count, add Telegram payment option
14- connect one Telegram user to multiple Fediverse accounts
diff --git a/bot.py b/bot.py
index 79fba5d..a059c9c 100644
--- a/bot.py
+++ b/bot.py
@@ -61,6 +61,7 @@ from config import (
61) 61)
62from mastodon import Mastodon 62from mastodon import Mastodon
63 63
64from dbstore.peewee_store import db, User
64 65
65# Enable logging 66# Enable logging
66logging.basicConfig( 67logging.basicConfig(
@@ -72,39 +73,35 @@ logger = logging.getLogger(__name__)
72@dataclass 73@dataclass
73class FediLoginCallbackUpdate: 74class FediLoginCallbackUpdate:
74 code: str 75 code: str
75 state: int 76 state: str
76 77
77 78
78class FediLoginCallbackContext(CallbackContext[ExtBot, dict, dict, dict]): 79class FediLoginCallbackContext(CallbackContext[ExtBot, dict, dict, dict]):
79 @classmethod 80 pass
80 def from_update(
81 cls,
82 update: object,
83 application: "Application",
84 ) -> "FediLoginCallbackContext":
85 if isinstance(update, FediLoginCallbackUpdate):
86 return cls(application=application, user_id=update.state)
87 return super().from_update(update, application)
88 81
89 82
90async def process_oauth_login_callback(update: FediLoginCallbackUpdate, context: FediLoginCallbackContext) -> None: 83async def process_oauth_login_callback(update: FediLoginCallbackUpdate, context: FediLoginCallbackContext) -> None:
91 # query client_id and client_secret from in memory database 84 state = update.state
92 client_id = "" 85
93 client_secret = "" 86 with db.connection_context():
94 home_instance = "" 87 user = User.get(User.state == state)
95
96 mastodon_client = Mastodon(client_id=client_id, client_secret=client_secret, api_base_url=home_instance)
97 access_token = mastodon_client.log_in(
98 code=update.code,
99 redirect_uri="{}{}".format(BOT_DOMAIN, FEDI_LOGIN_CALLBACK_URL),
100 scopes=BOT_SCOPE
101 )
102 88
103 # TODO 89 client_id = user.client_id
104 # save access_token to database 90 client_secret = user.client_secret
91 home_instance = user.home_instance
92
93 if len(user.access_key) == 0:
94 mastodon_client = Mastodon(client_id=client_id, client_secret=client_secret, api_base_url=home_instance)
95 access_token = mastodon_client.log_in(
96 code=update.code,
97 redirect_uri="{}{}".format(BOT_DOMAIN, FEDI_LOGIN_CALLBACK_URL),
98 scopes=BOT_SCOPE
99 )
100 user.access_key = access_token
101 user.save()
105 102
106 text = "You have successfully logged in to your Mastodon account!" 103 text = "You have successfully logged in to your Mastodon account!"
107 await context.bot.send_message(chat_id=update.state, text=text) 104 await context.bot.send_message(chat_id=user.telegram_user_id, text=text)
108 105
109 106
110async def main() -> None: 107async def main() -> None:
@@ -112,7 +109,7 @@ async def main() -> None:
112 # Here we set updater to None because we want our custom webhook server to handle the updates 109 # Here we set updater to None because we want our custom webhook server to handle the updates
113 # and hence we don't need an Updater instance 110 # and hence we don't need an Updater instance
114 application = ( 111 application = (
115 Application.builder().token(BOT_TOKEN).context_types(context_types).build() 112 Application.builder().updater(None).token(BOT_TOKEN).context_types(context_types).build()
116 ) 113 )
117 114
118 checkin_handler = ConversationHandler( 115 checkin_handler = ConversationHandler(
@@ -171,7 +168,7 @@ async def main() -> None:
171 """ 168 """
172 try: 169 try:
173 code = request.query_params["code"] 170 code = request.query_params["code"]
174 state = int(request.query_params.get("state")) 171 state = request.query_params.get("state")
175 except KeyError: 172 except KeyError:
176 return PlainTextResponse( 173 return PlainTextResponse(
177 status_code=HTTPStatus.BAD_REQUEST, 174 status_code=HTTPStatus.BAD_REQUEST,
diff --git a/callback.py b/callback.py
index c7cb60c..4fe0540 100644
--- a/callback.py
+++ b/callback.py
@@ -7,13 +7,23 @@ from telegram.error import BadRequest
7from telegram.ext import CallbackContext 7from telegram.ext import CallbackContext
8 8
9from command import * 9from command import *
10from dbstore.dbm_store import get_loc 10from dbstore.peewee_store import get_poi_by_fsq_id
11from foursquare.poi import OSM_ENDPOINT 11from foursquare.poi import OSM_ENDPOINT
12from foursquare.poi import query_poi 12from foursquare.poi import query_poi
13# from toot import mastodon_client
14from config import BOT_SCOPE 13from config import BOT_SCOPE
14from dbstore.peewee_store import User, db
15import uuid
15 16
16mastodon_client = None 17
18def generate_uuid():
19 return str(uuid.uuid4())
20
21
22def get_mastodon_client(user_id: int):
23 with db.connection_context():
24 user = User.get(User.telegram_user_id == user_id)
25 if user.home_instance and user.access_key:
26 return Mastodon(access_token=user.access_key, api_base_url=user.home_instance)
17 27
18 28
19def generate_toot_text(poi_name, poi_locality, poi_region, poi_lat, poi_lon): 29def generate_toot_text(poi_name, poi_locality, poi_region, poi_lat, poi_lon):
@@ -41,6 +51,7 @@ async def get_img_file_bytes(telegram_media_file):
41async def process_media_group(context: CallbackContext): 51async def process_media_group(context: CallbackContext):
42 context.job.data = cast(List[MsgDict], context.job.data) 52 context.job.data = cast(List[MsgDict], context.job.data)
43 53
54 mastodon_client = get_mastodon_client(context.user_data["user_id"])
44 media_id = [] 55 media_id = []
45 chat_id = context.job.data[0].get("chat_id") 56 chat_id = context.job.data[0].get("chat_id")
46 for media_dict in context.job.data: 57 for media_dict in context.job.data:
@@ -61,9 +72,6 @@ async def process_media_group(context: CallbackContext):
61 72
62 73
63async def callback_generate_fedi_login_url(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: 74async def callback_generate_fedi_login_url(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
64 # generate fedi OAuth login url
65 global mastodon_client
66
67 home_instance = update.effective_message.text 75 home_instance = update.effective_message.text
68 client_id, client_secret = Mastodon.create_app( 76 client_id, client_secret = Mastodon.create_app(
69 "Checkin.bot", 77 "Checkin.bot",
@@ -71,13 +79,25 @@ async def callback_generate_fedi_login_url(update: Update, context: ContextTypes
71 redirect_uris="{}{}".format(BOT_DOMAIN, FEDI_LOGIN_CALLBACK_URL), 79 redirect_uris="{}{}".format(BOT_DOMAIN, FEDI_LOGIN_CALLBACK_URL),
72 api_base_url=home_instance, 80 api_base_url=home_instance,
73 ) 81 )
82 print("client_id: {}".format(client_id))
83 print("client_secret: {}".format(client_secret))
84
74 m = Mastodon(client_id=client_id, client_secret=client_secret, api_base_url=home_instance) 85 m = Mastodon(client_id=client_id, client_secret=client_secret, api_base_url=home_instance)
75 86
76 # TODO 87 user_id = update.effective_user.id
77 # generate random string as OAuth state 88 state = generate_uuid()
89
90 db.connect()
91 u = User.get_or_none(telegram_user_id=user_id)
92 if u is None:
93 u = User.create(telegram_user_id=user_id, access_key="", home_instance=home_instance,
94 client_id=client_id, client_secret=client_secret, state=state)
95 u.save()
96 db.close()
97
78 oauth_url = m.auth_request_url(redirect_uris="{}{}".format(BOT_DOMAIN, FEDI_LOGIN_CALLBACK_URL), 98 oauth_url = m.auth_request_url(redirect_uris="{}{}".format(BOT_DOMAIN, FEDI_LOGIN_CALLBACK_URL),
79 scopes=BOT_SCOPE, 99 scopes=BOT_SCOPE,
80 state=update.effective_user.id) 100 state=state)
81 101
82 await update.message.reply_text(PROMPT_FEDI_LOGIN, 102 await update.message.reply_text(PROMPT_FEDI_LOGIN,
83 reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("Login", url=oauth_url)]]), 103 reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("Login", url=oauth_url)]]),
@@ -93,9 +113,9 @@ async def callback_location_sharing(update: Update, context: ContextTypes.DEFAUL
93 context.user_data["latitude"] = update.message.venue.location.latitude 113 context.user_data["latitude"] = update.message.venue.location.latitude
94 context.user_data["longitude"] = update.message.venue.location.longitude 114 context.user_data["longitude"] = update.message.venue.location.longitude
95 115
96 poi = get_loc(context.user_data.get("fsq_id")) 116 poi = get_poi_by_fsq_id(context.user_data.get("fsq_id"))
97 content = generate_toot_text(poi["name"], poi["locality"], poi["region"], poi["latitude"], poi["longitude"]) 117 content = generate_toot_text(poi["name"], poi["locality"], poi["region"], poi["latitude"], poi["longitude"])
98 status = mastodon_client.status_post(content, visibility=DEFAULT_TOOT_VISIBILITY, media_ids=[]) 118 status = get_mastodon_client(update.effective_user.id).status_post(content, visibility=DEFAULT_TOOT_VISIBILITY, media_ids=[])
99 119
100 context.user_data[KEY_TOOT_STATUS_ID] = status["id"] 120 context.user_data[KEY_TOOT_STATUS_ID] = status["id"]
101 context.user_data[KEY_TOOT_STATUS_CONTENT] = content 121 context.user_data[KEY_TOOT_STATUS_CONTENT] = content
@@ -165,12 +185,12 @@ async def callback_skip_location_keyword_search(update: Update, context: Context
165async def _process_location_selection(context: ContextTypes.DEFAULT_TYPE) -> int: 185async def _process_location_selection(context: ContextTypes.DEFAULT_TYPE) -> int:
166 poi_name = context.user_data.get("poi_name") 186 poi_name = context.user_data.get("poi_name")
167 if context.user_data.get("fsq_id") is not None: 187 if context.user_data.get("fsq_id") is not None:
168 poi = get_loc(context.user_data.get("fsq_id")) 188 poi = get_poi_by_fsq_id(context.user_data.get("fsq_id"))
169 content = generate_toot_text(poi["name"], poi["locality"], poi["region"], poi["latitude"], poi["longitude"]) 189 content = generate_toot_text(poi["name"], poi["locality"], poi["region"], poi["latitude"], poi["longitude"])
170 else: 190 else:
171 content = generate_toot_text(poi_name, "", "", context.user_data.get("latitude"), context.user_data.get("longitude")) 191 content = generate_toot_text(poi_name, "", "", context.user_data.get("latitude"), context.user_data.get("longitude"))
172 192
173 status = mastodon_client.status_post(content, visibility=DEFAULT_TOOT_VISIBILITY, media_ids=[]) 193 status = get_mastodon_client(context.user_data["user_id"]).status_post(content, visibility=DEFAULT_TOOT_VISIBILITY, media_ids=[])
174 194
175 context.user_data[KEY_TOOT_STATUS_ID] = status["id"] 195 context.user_data[KEY_TOOT_STATUS_ID] = status["id"]
176 context.user_data[KEY_TOOT_STATUS_CONTENT] = content 196 context.user_data[KEY_TOOT_STATUS_CONTENT] = content
@@ -192,6 +212,7 @@ async def callback_location_confirmation(update: Update, context: ContextTypes.D
192 query = update.callback_query 212 query = update.callback_query
193 await query.answer() 213 await query.answer()
194 context.user_data["fsq_id"] = query.data 214 context.user_data["fsq_id"] = query.data
215 context.user_data["user_id"] = update.effective_user.id
195 await query.delete_message() 216 await query.delete_message()
196 217
197 context.user_data["chat_id"] = update.effective_chat.id 218 context.user_data["chat_id"] = update.effective_chat.id
@@ -202,6 +223,7 @@ async def callback_location_confirmation(update: Update, context: ContextTypes.D
202async def callback_manual_location(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: 223async def callback_manual_location(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
203 context.user_data["poi_name"] = update.effective_message.text 224 context.user_data["poi_name"] = update.effective_message.text
204 context.user_data["chat_id"] = update.effective_chat.id 225 context.user_data["chat_id"] = update.effective_chat.id
226 context.user_data["user_id"] = update.effective_user.id
205 227
206 return await _process_location_selection(context) 228 return await _process_location_selection(context)
207 229
@@ -219,7 +241,7 @@ async def callback_add_comment(update: Update, context: ContextTypes.DEFAULT_TYP
219 await context.bot.delete_message(update.effective_chat.id, context.user_data.get(PROMPT_ADD_COMMENT)) 241 await context.bot.delete_message(update.effective_chat.id, context.user_data.get(PROMPT_ADD_COMMENT))
220 242
221 comment = update.effective_message.text 243 comment = update.effective_message.text
222 mastodon_client.status_update(id=context.user_data.get(KEY_TOOT_STATUS_ID), 244 get_mastodon_client(update.effective_user.id).status_update(id=context.user_data.get(KEY_TOOT_STATUS_ID),
223 status=f"{comment} " + context.user_data.get(KEY_TOOT_STATUS_CONTENT)) 245 status=f"{comment} " + context.user_data.get(KEY_TOOT_STATUS_CONTENT))
224 context.user_data[KEY_TOOT_STATUS_CONTENT] = f"{comment} " + context.user_data.get(KEY_TOOT_STATUS_CONTENT) 246 context.user_data[KEY_TOOT_STATUS_CONTENT] = f"{comment} " + context.user_data.get(KEY_TOOT_STATUS_CONTENT)
225 247
@@ -243,6 +265,7 @@ async def callback_add_media(update: Update, context: CallbackContext):
243 if "not found" in str(e.message): 265 if "not found" in str(e.message):
244 pass 266 pass
245 267
268 mastodon_client = get_mastodon_client(update.effective_user.id)
246 status_id = context.user_data.get(KEY_TOOT_STATUS_ID) 269 status_id = context.user_data.get(KEY_TOOT_STATUS_ID)
247 status_content = context.user_data.get(KEY_TOOT_STATUS_CONTENT) 270 status_content = context.user_data.get(KEY_TOOT_STATUS_CONTENT)
248 271
diff --git a/command.py b/command.py
index 837d3cd..e45a2e2 100644
--- a/command.py
+++ b/command.py
@@ -15,13 +15,6 @@ async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> i
15 15
16 16
17async def fedi_login_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: 17async def fedi_login_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
18 # generate fedi OAuth login url
19
20 # mastodon_client = Mastodon(client_id=MASTODON_CLIENT_ID_FILE, api_base_url=TOOT_API_BASE_URL)
21 # oauth_url = mastodon_client.auth_request_url(redirect_uris="{}/{}".format(BOT_DOMAIN, FEDI_LOGIN_CALLBACK_URL),
22 # scopes=['write:media', 'write:statuses'])
23 #
24 # await update.message.reply_text(PROMPT_FEDI_LOGIN.format(oauth_url), parse_mode=ParseMode.MARKDOWN)
25 await update.message.reply_text(PROMPT_FEDI_LOGIN_WHERE_IS_INSTANCE, parse_mode=ParseMode.MARKDOWN) 18 await update.message.reply_text(PROMPT_FEDI_LOGIN_WHERE_IS_INSTANCE, parse_mode=ParseMode.MARKDOWN)
26 return FEDI_LOGIN 19 return FEDI_LOGIN
27 20
diff --git a/dbstore/dbm_store.py b/dbstore/dbm_store.py
deleted file mode 100644
index fedb505..0000000
--- a/dbstore/dbm_store.py
+++ /dev/null
@@ -1,34 +0,0 @@
1import dbm
2
3db = None
4store_file = "fsq_poi.db"
5
6
7def 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
25def 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/dbstore/peewee_store.py b/dbstore/peewee_store.py
new file mode 100644
index 0000000..37b8c01
--- /dev/null
+++ b/dbstore/peewee_store.py
@@ -0,0 +1,59 @@
1from peewee import *
2
3db = SqliteDatabase("checkinbot.db")
4db.connect(reuse_if_open=True)
5
6
7class BaseModel(Model):
8 class Meta:
9 database = db
10
11
12class User(BaseModel):
13 telegram_user_id = CharField(unique=True, primary_key=True)
14 access_key = CharField(max_length=64)
15 home_instance = CharField(max_length=128)
16 state = CharField(max_length=128)
17 client_id = CharField(max_length=128)
18 client_secret = CharField(max_length=128)
19
20
21class Location(BaseModel):
22 fsq_id = CharField(unique=True, primary_key=True)
23 name = CharField(max_length=128)
24 locality = CharField(max_length=128)
25 region = CharField(max_length=128)
26 latitude = CharField(max_length=128)
27 longitude = CharField(max_length=128)
28
29
30with db.connection_context():
31 db.create_tables([User, Location])
32
33
34def get_poi_by_fsq_id(fsq_id) -> dict:
35 with db.connection_context():
36 try:
37 poi = Location.get(Location.fsq_id == fsq_id)
38 return {
39 "name": poi.name,
40 "locality": poi.locality,
41 "region": poi.region,
42 "latitude": poi.latitude,
43 "longitude": poi.longitude,
44 }
45 except DoesNotExist:
46 return {}
47
48
49def create_poi(poi: dict):
50 with db.connection_context():
51 poi = Location.create(
52 fsq_id=poi["fsq_id"],
53 name=poi["name"],
54 locality=poi["locality"],
55 region=poi["region"],
56 latitude=poi["latitude"],
57 longitude=poi["longitude"],
58 )
59 poi.save()
diff --git a/foursquare/poi.py b/foursquare/poi.py
index 5e67d1c..45b4fa9 100644
--- a/foursquare/poi.py
+++ b/foursquare/poi.py
@@ -3,7 +3,7 @@ import json
3import requests 3import requests
4 4
5from config import FSQ_API_KEY 5from config import FSQ_API_KEY
6from dbstore.dbm_store import store_loc 6from dbstore.peewee_store import create_poi
7 7
8POI_API_ENDPOINT = "https://api.foursquare.com/v3/places/nearby?ll={}%2C{}&limit=10" 8POI_API_ENDPOINT = "https://api.foursquare.com/v3/places/nearby?ll={}%2C{}&limit=10"
9POI_SEARCH_API_ENDPOINT = "https://api.foursquare.com/v3/places/search?query={}&ll={}%2C{}&radius=2000&limit=10" 9POI_SEARCH_API_ENDPOINT = "https://api.foursquare.com/v3/places/search?query={}&ll={}%2C{}&radius=2000&limit=10"
@@ -33,8 +33,7 @@ def query_poi(search, latitude, longitude):
33 "osm_url": OSM_ENDPOINT.format(poi["geocodes"]["main"]["latitude"], poi["geocodes"]["main"]["longitude"]) 33 "osm_url": OSM_ENDPOINT.format(poi["geocodes"]["main"]["latitude"], poi["geocodes"]["main"]["longitude"])
34 } 34 }
35 locations.append(loc) 35 locations.append(loc)
36 print(loc) 36 create_poi(loc)
37 store_loc(loc)
38 37
39 return locations 38 return locations
40 39
diff --git a/requirements.txt b/requirements.txt
index c031383..c3166b6 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,8 +1,12 @@
1anyio==3.6.2 1anyio==3.6.2
2APScheduler==3.10.0
3asgiref==3.6.0
2blurhash==1.1.4 4blurhash==1.1.4
3certifi==2022.12.7 5certifi==2022.12.7
4charset-normalizer==3.0.1 6charset-normalizer==3.0.1
7click==8.1.3
5decorator==5.1.1 8decorator==5.1.1
9greenlet==2.0.2
6h11==0.14.0 10h11==0.14.0
7h2==4.1.0 11h2==4.1.0
8hpack==4.0.0 12hpack==4.0.0
@@ -11,11 +15,22 @@ httpx==0.23.3
11hyperframe==6.0.1 15hyperframe==6.0.1
12idna==3.4 16idna==3.4
13Mastodon.py==1.8.0 17Mastodon.py==1.8.0
18peewee==3.15.4
19Pillow==9.4.0
14python-dateutil==2.8.2 20python-dateutil==2.8.2
15python-magic==0.4.27 21python-magic==0.4.27
16python-telegram-bot==20.1 22python-telegram-bot==20.1
23pytz==2022.7.1
24pytz-deprecation-shim==0.1.0.post0
17requests==2.28.2 25requests==2.28.2
18rfc3986==1.5.0 26rfc3986==1.5.0
19six==1.16.0 27six==1.16.0
20sniffio==1.3.0 28sniffio==1.3.0
29SQLAlchemy==2.0.4
30sqlparse==0.4.3
31starlette==0.25.0
32typing_extensions==4.5.0
33tzdata==2022.7
34tzlocal==4.2
21urllib3==1.26.14 35urllib3==1.26.14
36uvicorn==0.20.0
Powered by cgit v1.2.3 (git 2.41.0)