aboutsummaryrefslogtreecommitdiff
path: root/bot
diff options
context:
space:
mode:
Diffstat (limited to 'bot')
-rw-r--r--bot/__init__.py13
-rw-r--r--bot/admin.py5
-rw-r--r--bot/apps.py6
-rw-r--r--bot/bot.py300
-rw-r--r--bot/migrations/0001_initial.py44
-rw-r--r--bot/migrations/__init__.py0
-rw-r--r--bot/models.py9
-rw-r--r--bot/tests.py3
-rw-r--r--bot/urls.py7
-rw-r--r--bot/views.py5
10 files changed, 79 insertions, 313 deletions
diff --git a/bot/__init__.py b/bot/__init__.py
index 5e3c341..e69de29 100644
--- a/bot/__init__.py
+++ b/bot/__init__.py
@@ -1,13 +0,0 @@
1from telegram import __version__ as TG_VER
2
3try:
4 from telegram import __version_info__
5except ImportError:
6 __version_info__ = (0, 0, 0, 0, 0) # type: ignore[assignment]
7
8if __version_info__ < (20, 0, 0, "alpha", 1):
9 raise RuntimeError(
10 f"This example is not compatible with your current PTB version {TG_VER}. To view the "
11 f"{TG_VER} version of this example, "
12 f"visit https://docs.python-telegram-bot.org/en/v{TG_VER}/examples.html"
13 )
diff --git a/bot/admin.py b/bot/admin.py
new file mode 100644
index 0000000..f91be8f
--- /dev/null
+++ b/bot/admin.py
@@ -0,0 +1,5 @@
1from django.contrib import admin
2from django.contrib.auth.admin import UserAdmin
3from .models import User
4
5admin.site.register(User, UserAdmin)
diff --git a/bot/apps.py b/bot/apps.py
new file mode 100644
index 0000000..1cd7ff2
--- /dev/null
+++ b/bot/apps.py
@@ -0,0 +1,6 @@
1from django.apps import AppConfig
2
3
4class BotConfig(AppConfig):
5 default_auto_field = 'django.db.models.BigAutoField'
6 name = 'bot'
diff --git a/bot/bot.py b/bot/bot.py
deleted file mode 100644
index 2710184..0000000
--- a/bot/bot.py
+++ /dev/null
@@ -1,300 +0,0 @@
1#!/usr/bin/env python
2# pylint: disable=unused-argument, wrong-import-position
3# This program is dedicated to the public domain under the CC0 license.
4
5import logging
6import io
7import telegram.constants
8from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update, ReplyKeyboardMarkup, KeyboardButton
9from telegram.ext import Application, CallbackQueryHandler, CommandHandler, ContextTypes, MessageHandler, filters, ConversationHandler, CallbackContext
10from ..config import BOT_TOKEN
11from foursquare.poi import query_poi
12from dbstore.dbm_store import get_loc
13from toot import mastodon_client
14from typing import TypedDict, List, cast
15
16scheduler = None
17PRIVACY, TOOT = map(chr, range(8, 10))
18
19WAIT_LOC, LOCATION, PHOTO, PROCESS_PHOTO, FINAL, SETTING = range(6)
20
21# Enable logging
22logging.basicConfig(
23 format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO
24)
25logger = logging.getLogger(__name__)
26
27MAIN_MENU = ReplyKeyboardMarkup([
28 [telegram.KeyboardButton(text="/check in", request_location=True)],
29 [telegram.KeyboardButton(text="/cancel")],
30 [telegram.KeyboardButton(text="/setting")]
31])
32
33SKIP_MENU = ReplyKeyboardMarkup([[telegram.KeyboardButton(text="/skip")]])
34SETTING_MENU = ReplyKeyboardMarkup(
35 [
36 [KeyboardButton(text="/tos")],
37 [telegram.KeyboardButton(text="/back")],
38 ]
39)
40
41
42async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
43 hello = "Hello, this is `checkin.bot`. \n\n" \
44 "This is a Telegram bot with functionality similar to Foursquare Swarm, " \
45 "but check in and post your location to the Fediverse (Mastodon/Pleroma) instead of Twitter.\n\n" \
46 "Aware of privacy concerns, this bot will not store your location data." \
47 "*Be safe and cautious when sharing your real time location on the web.* \n\n" \
48 "Start using this bot by sharing your location using Telegram context menu to it."
49
50 await update.message.reply_text(hello, parse_mode=telegram.constants.ParseMode.MARKDOWN)
51 await update.message.reply_text("Use bot keyboard to choose an action", reply_markup=MAIN_MENU)
52
53 return LOCATION
54
55
56async def checkin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
57 keyboard = []
58
59 for poi in query_poi(update.message.location.latitude, update.message.location.longitude):
60 keyboard.append([
61 InlineKeyboardButton(poi["name"], callback_data=poi["fsq_id"]),
62 ])
63
64 reply_markup = InlineKeyboardMarkup(keyboard)
65 await update.message.reply_text("Where are you?", reply_markup=reply_markup)
66
67 return WAIT_LOC
68
69
70async def process_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
71 query = update.callback_query
72 await query.answer()
73 print(query.data)
74 context.user_data["fsq_id"] = query.data
75 await query.delete_message()
76
77 poi = get_loc(context.user_data["fsq_id"])
78 media_id = []
79 content = f"I'm at {poi['name']} in {poi['locality']}, {poi['region']}, {poi['osm_url']}"
80 status = mastodon_client.status_post(
81 content,
82 visibility="private",
83 media_ids=media_id)
84
85 context.user_data["status_id"] = status["id"]
86 context.user_data["status_content"] = content
87
88 print("status_id", context.user_data["status_id"])
89
90 await query.message.reply_text(
91 text=f"Selected place: {poi['name']}, `{query.data}`\nPosted to Mastodon: {status['url']}",
92 parse_mode=telegram.constants.ParseMode.MARKDOWN,
93 reply_markup=MAIN_MENU
94 )
95
96 await query.message.reply_text("You can continue attaching photos, or press skip to finish", reply_markup=SKIP_MENU)
97 return PHOTO
98
99
100async def tos(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
101 await update.message.reply_text("TOS", reply_markup=MAIN_MENU)
102
103
104async def setting(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
105 await update.message.reply_text("Setting", reply_markup=SETTING_MENU)
106 return SETTING
107
108
109async def setting_process_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
110 await update.message.reply_text("Setting Process Callback", reply_markup=SETTING_MENU)
111 return ConversationHandler.END
112
113
114async def process_location(update: Update, context: ContextTypes.DEFAULT_TYPE):
115 await update.message.reply_chat_action(telegram.constants.ChatAction.TYPING)
116
117 fsq_id = context.user_data["fsq_id"]
118 poi = get_loc(context.user_data["fsq_id"])
119 media_id = []
120
121 if context.user_data.get("photo") is not None:
122 media = mastodon_client.media_post(context.user_data.get("photo"), mime_type="image/jpeg")
123 media_id = [media["id"]]
124 # else:
125 # photo_url = get_poi_top_photo(context.user_data["fsq_id"])
126 # if photo_url is not None:
127 # with urllib.request.urlopen(photo_url) as response:
128 # data = response.read()
129 # media = mastodon_client.media_post(data, mime_type="image/jpeg")
130 # media_id = [media["id"]]
131
132 mastodon_client.status_post(
133 f"I'm at {poi['name']} in {poi['locality']}, {poi['region']}, {poi['osm_url']}",
134 visibility="private",
135 media_ids=media_id)
136
137 await update.message.delete_message()
138 return ConversationHandler.END
139
140
141async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
142 """Displays info on how to use the bot."""
143 await update.message.reply_text("Use /start to test this bot.")
144
145
146async def setting_cancel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
147 """Cancels and ends the conversation."""
148 user = update.message.from_user
149 logger.info("User %s canceled the conversation.", user.first_name)
150 await update.message.reply_text(
151 text="Setting canceled.",
152 # "Bye! I hope we can talk again some day.",
153 reply_markup=MAIN_MENU
154 )
155
156 return ConversationHandler.END
157
158
159async def cancel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
160 """Cancels and ends the conversation."""
161 user = update.message.from_user
162 logger.info("User %s canceled the conversation.", user.first_name)
163 await update.message.reply_text(
164 text="Canceled.",
165 # "Bye! I hope we can talk again some day.",
166 reply_markup=MAIN_MENU
167 )
168
169 return ConversationHandler.END
170
171
172class MsgDict(TypedDict):
173 media_id: str
174 caption: str
175 status_id: int
176 content: str
177 chat_id: int
178
179
180async def media_group_sender(context: CallbackContext):
181 context.job.data = cast(List[MsgDict], context.job.data)
182
183 media_id = []
184 chat_id = context.job.data[0].get("chat_id")
185 for msg_dict in context.job.data:
186 if len(media_id) >= 4:
187 print("Cannot attach more than 4 photos")
188 break
189 file = await context.bot.get_file(msg_dict.get("media_id"))
190 img = io.BytesIO()
191 await file.download_to_memory(img)
192
193 img.seek(0)
194
195 media = mastodon_client.media_post(img.read(), mime_type="image/jpeg")
196 media_id.append(media["id"])
197
198 mastodon_client.status_update(
199 status=msg_dict.get("content"),
200 id=msg_dict.get("status_id"),
201 media_ids=media_id)
202
203 await context.bot.send_message(chat_id=chat_id, text="Done",
204 reply_markup=MAIN_MENU
205 )
206
207
208async def photo(update: Update, context: CallbackContext):
209 """Stores the photo and asks for a location."""
210 global scheduler
211 await update.message.reply_chat_action(telegram.constants.ChatAction.TYPING)
212
213 status_id = context.user_data["status_id"]
214 status_content = context.user_data["status_content"]
215
216 message = update.effective_message
217 context.user_data["media"] = []
218 if message.media_group_id:
219 media_id = message.photo[-1].file_id if message.photo else message.effective_attachment.file_id
220 msg_dict = {
221 "media_id": media_id,
222 "caption": message.caption_html,
223 "status_id": status_id,
224 "content": status_content,
225 "chat_id": message.chat_id,
226 }
227 jobs = context.job_queue.get_jobs_by_name(str(message.media_group_id))
228 if jobs:
229 jobs[0].data.append(msg_dict)
230 else:
231 context.job_queue.run_once(callback=media_group_sender, when=5, data=[msg_dict],
232 name=str(message.media_group_id))
233 else:
234 file = await update.message.effective_attachment[-1].get_file()
235 img = io.BytesIO()
236 await file.download_to_memory(img)
237 img.seek(0)
238
239 media = mastodon_client.media_post(img.read(), mime_type="image/jpeg")
240 mastodon_client.status_update(
241 status=status_content,
242 id=status_id,
243 media_ids=media["id"])
244
245 await update.message.reply_text(text="Done",
246 reply_markup=MAIN_MENU
247 )
248
249
250async def skip_photo(update: Update, context: ContextTypes.DEFAULT_TYPE):
251 print(context.user_data)
252 await update.message.reply_text(
253 text="Done.", reply_markup=MAIN_MENU
254 )
255 return ConversationHandler.END
256
257
258def main() -> None:
259 application = Application.builder().token(BOT_TOKEN).build()
260
261 checkin_handler = ConversationHandler(
262 entry_points=[
263 CommandHandler("start", start),
264 MessageHandler(filters.LOCATION, checkin),
265 ],
266 states={
267 LOCATION: [
268 MessageHandler(filters.LOCATION, checkin),
269 ],
270 WAIT_LOC: [CallbackQueryHandler(process_callback)],
271 PHOTO: [MessageHandler(filters.PHOTO, photo),
272 CommandHandler("skip", skip_photo)],
273 },
274 fallbacks=[CommandHandler("cancel", cancel)],
275 per_message=False,
276 allow_reentry=True,
277 )
278
279 setting_conv_handler = ConversationHandler(
280 entry_points=[CommandHandler("setting", setting)],
281 states={
282 SETTING: [
283 CallbackQueryHandler(setting_process_callback),
284 ],
285 },
286 fallbacks=[CommandHandler("back", setting_cancel)],
287 per_message=False,
288 allow_reentry=True,
289 )
290
291 application.add_handler(CommandHandler("tos", tos))
292 application.add_handler(setting_conv_handler, 2)
293 application.add_handler(checkin_handler, 1)
294
295 # Run the bot until the user presses Ctrl-C
296 application.run_polling()
297
298
299if __name__ == "__main__":
300 main()
diff --git a/bot/migrations/0001_initial.py b/bot/migrations/0001_initial.py
new file mode 100644
index 0000000..6b4f210
--- /dev/null
+++ b/bot/migrations/0001_initial.py
@@ -0,0 +1,44 @@
1# Generated by Django 4.1.7 on 2023-02-22 00:44
2
3import django.contrib.auth.models
4from django.db import migrations, models
5import django.utils.timezone
6
7
8class Migration(migrations.Migration):
9
10 initial = True
11
12 dependencies = [
13 ('auth', '0012_alter_user_first_name_max_length'),
14 ]
15
16 operations = [
17 migrations.CreateModel(
18 name='User',
19 fields=[
20 ('password', models.CharField(max_length=128, verbose_name='password')),
21 ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
22 ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
23 ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
24 ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
25 ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
26 ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
27 ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
28 ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
29 ('name', models.CharField(blank=True, max_length=50)),
30 ('telegram_id', models.PositiveBigIntegerField(primary_key=True, serialize=False, unique=True)),
31 ('username', models.CharField(max_length=32)),
32 ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
33 ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
34 ],
35 options={
36 'verbose_name': 'user',
37 'verbose_name_plural': 'users',
38 'abstract': False,
39 },
40 managers=[
41 ('objects', django.contrib.auth.models.UserManager()),
42 ],
43 ),
44 ]
diff --git a/bot/migrations/__init__.py b/bot/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/bot/migrations/__init__.py
diff --git a/bot/models.py b/bot/models.py
new file mode 100644
index 0000000..c6df3e7
--- /dev/null
+++ b/bot/models.py
@@ -0,0 +1,9 @@
1from django.contrib.auth.models import AbstractUser
2from django.db import models
3
4
5class User(AbstractUser):
6 name = models.CharField(max_length=50, blank=True)
7 telegram_id = models.PositiveBigIntegerField(unique=True, primary_key=True)
8 username = models.CharField(max_length=32, blank=False)
9 USERNAME_FIELD = 'telegram_id'
diff --git a/bot/tests.py b/bot/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/bot/tests.py
@@ -0,0 +1,3 @@
1from django.test import TestCase
2
3# Create your tests here.
diff --git a/bot/urls.py b/bot/urls.py
new file mode 100644
index 0000000..88a9cac
--- /dev/null
+++ b/bot/urls.py
@@ -0,0 +1,7 @@
1from django.urls import path
2
3from . import views
4
5urlpatterns = [
6 path('', views.index, name='index'),
7]
diff --git a/bot/views.py b/bot/views.py
new file mode 100644
index 0000000..963b6f7
--- /dev/null
+++ b/bot/views.py
@@ -0,0 +1,5 @@
1from django.http import HttpResponse
2
3
4def index(request):
5 return HttpResponse("Hello, world. You're at the polls index.")
Powered by cgit v1.2.3 (git 2.41.0)