Skip to content

Commit eb3160b

Browse files
committed
feat: remove scan cancel option, fetch deleted scan documents carefully, refactor interrupters
Fixes #56
1 parent ee783be commit eb3160b

9 files changed

Lines changed: 125 additions & 102 deletions

File tree

src/bot/api.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,12 +148,14 @@ async def cancel_manual_scan(self, telegram_id: int, scanner: Scanner, job_id: s
148148

149149
async def wait_and_merge_manual_scan(
150150
self, telegram_id: int, scanner: Scanner, job_id: str, prev_filename: str | None
151-
) -> ScanningResult:
151+
) -> ScanningResult | None:
152152
params = {"scanner_name": scanner.name, "job_id": job_id}
153153
if prev_filename:
154154
params["prev_filename"] = prev_filename
155155
async with self._create_client(telegram_id, 60 * 5) as client:
156156
response = await client.post("/scan/manual/wait_and_merge", params=params)
157+
if response.status_code == 404:
158+
return None
157159
response.raise_for_status()
158160
return ScanningResult.model_validate(response.json())
159161

src/bot/routers/printing/printing.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,9 @@ async def start_print_handler(callback: CallbackQuery, state: FSMContext, bot: B
213213

214214
while time.monotonic() - start_time < max_wait_time:
215215
iteration += 1
216+
217+
# Return if the job was changed (user sent a new document or /scan)
218+
await ensure_same_structural_message(callback.message, "confirmation_message_id", state)
216219
# Exit if state changed (user clicked cancel)
217220
if (await state.get_state()) == default_state:
218221
break

src/bot/routers/printing/printing_states.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@
77

88
from src.bot.api import api_client
99
from src.bot.logging_ import logger
10-
from src.bot.routers.printing.printing_tools import discard_job_settings_message, format_printing_message
10+
from src.bot.routers.printing.printing_tools import (
11+
discard_job_settings_message,
12+
format_configure_message,
13+
format_printing_message,
14+
)
15+
from src.bot.routers.tools import edit_message_text_anyway
1116

1217

1318
class PrintWork(StatesGroup):
@@ -39,14 +44,15 @@ async def gracefully_interrupt_printing_state(
3944
):
4045
await discard_job_settings_message(data, message, state, bot)
4146
if "confirmation_message_id" in data:
42-
try:
43-
await bot.edit_message_caption(
44-
caption=f"{html.bold("You've cancelled this print work 🤷‍♀️")}",
45-
chat_id=message.chat.id,
46-
message_id=data["confirmation_message_id"],
47-
)
48-
except TelegramBadRequest:
49-
pass
47+
printer_status = await api_client.get_printer_status(message.chat.id, data["printer"])
48+
text, _ = format_configure_message(data, printer_status)
49+
await edit_message_text_anyway(
50+
data["confirmation_message_id"],
51+
f"{text}\n{html.italic('This job has expired 🕒')}",
52+
None,
53+
message.chat.id,
54+
bot,
55+
)
5056
if "filename" in data:
5157
try:
5258
await api_client.cancel_not_started_job(message.chat.id, data["filename"])

src/bot/routers/scanning/scanning.py

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import httpx
66
from aiogram import Bot, F, Router, flags
77
from aiogram.enums import ChatAction
8+
from aiogram.exceptions import TelegramBadRequest
89
from aiogram.filters import Command, StateFilter
910
from aiogram.fsm.context import FSMContext
1011
from aiogram.fsm.state import default_state
@@ -27,13 +28,17 @@
2728
from src.bot.routers.scanning.scanning_states import ScanWork, gracefully_interrupt_scanning_state
2829
from src.bot.routers.scanning.scanning_tools import (
2930
ScanConfigureCallback,
30-
ScanningCallback,
3131
ScanningPausedCallback,
3232
format_configure_message,
3333
format_scanning_message,
3434
format_scanning_paused_message,
3535
)
36-
from src.bot.routers.tools import cancel_expiring, edit_message_text_anyway, make_expiring
36+
from src.bot.routers.tools import (
37+
cancel_expiring,
38+
edit_message_text_anyway,
39+
ensure_same_structural_message,
40+
make_expiring,
41+
)
3742
from src.bot.shared_messages import go_to_default_state
3843
from src.modules.scanning.entity_models import ScanningOptions
3944

@@ -96,6 +101,8 @@ async def continue_scan_handler(callback: CallbackQuery, state: FSMContext, pret
96101
await cancel_expiring(callback.message)
97102
await state.set_state(ScanWork.scanning)
98103
data = await state.get_data()
104+
text = format_scanning_message(data, None, "starting")
105+
message = await edit_message_text_anyway(message, text)
99106
scanner_status = await api_client.get_scanner_status(message.chat.id, data.get("scanner"))
100107
scanning_options = ScanningOptions(
101108
sides="false" if data["mode"] == "manual" else data.get("scan_sides", "false"),
@@ -107,7 +114,7 @@ async def continue_scan_handler(callback: CallbackQuery, state: FSMContext, pret
107114
# start scanning
108115
try:
109116
scan_job_id = await api_client.start_manual_scan(message.chat.id, scanner_status.scanner, scanning_options)
110-
await state.update_data(scan_job_id=scan_job_id)
117+
data = await state.update_data(scan_job_id=scan_job_id)
111118
except httpx.HTTPStatusError as e:
112119
if e.response.status_code == 503:
113120
await message.answer("Scanner is busy. Try pressing Cancel button on the device and try again.")
@@ -120,8 +127,6 @@ async def continue_scan_handler(callback: CallbackQuery, state: FSMContext, pret
120127
await edit_message_text_anyway(message, text, markup)
121128
return
122129
raise
123-
text, markup = format_scanning_message(data, scanner_status, "starting")
124-
await edit_message_text_anyway(message, text, markup)
125130

126131
scanning_result = asyncio.create_task(
127132
api_client.wait_and_merge_manual_scan(
@@ -138,14 +143,26 @@ async def continue_scan_handler(callback: CallbackQuery, state: FSMContext, pret
138143

139144
while time.monotonic() - start_time < max_wait_time:
140145
iteration += 1
141-
# Exit if state changed (user clicked cancel)
146+
147+
# Return if the job was changed (user sent a new document or /scan)
148+
try:
149+
await ensure_same_structural_message(message, "confirmation_message_id", state)
150+
except TelegramBadRequest:
151+
await scanning_result
152+
await api_client.cancel_manual_scan(message.chat.id, scanner_status.scanner, data["scan_job_id"])
153+
return
154+
# Return if the job was canceled
142155
if (await state.get_state()) == default_state:
143-
break
156+
await scanning_result
157+
await api_client.cancel_manual_scan(message.chat.id, scanner_status.scanner, data["scan_job_id"])
158+
return
144159

145-
text, markup = format_scanning_message(data, scanner_status, "scanning", iteration)
146-
await edit_message_text_anyway(message, text, markup)
160+
text = format_scanning_message(data, scanner_status, "scanning", iteration)
161+
message = await edit_message_text_anyway(message, text)
147162

148163
if scanning_result.done():
164+
if not (scanning_result := await scanning_result):
165+
return
149166
break
150167

151168
# Sleep for one second before next check
@@ -161,7 +178,7 @@ async def continue_scan_handler(callback: CallbackQuery, state: FSMContext, pret
161178
await edit_message_text_anyway(message, text, markup)
162179
return
163180

164-
scanning_result = await scanning_result
181+
await api_client.cancel_manual_scan(message.chat.id, scanner_status.scanner, data["scan_job_id"])
165182
data = await state.update_data(
166183
scan_server_name=scanning_result.filename,
167184
scan_result_pages_count=scanning_result.page_count,
@@ -184,20 +201,12 @@ async def send_new_started_scan_confirmation_message_handler(callback: CallbackQ
184201
scanner_status = await api_client.get_scanner_status(callback.message.chat.id, data.get("scanner"))
185202
caption, markup = format_scanning_paused_message(data, scanner_status, is_finished=True)
186203
await edit_message_text_anyway(callback.message, caption, markup)
187-
text, markup = format_scanning_message(data, scanner_status, "starting")
188-
msg = await callback.message.answer(text, reply_markup=markup)
204+
text = format_scanning_message(data, scanner_status, "starting")
205+
msg = await callback.message.answer(text)
189206
await state.update_data(confirmation_message_id=msg.message_id)
190207
await start_new_scan_handler(callback, state, bot, msg)
191208

192209

193-
@router.callback_query(ScanWork.scanning, ScanningCallback.filter(F.menu == "cancel"))
194-
async def scanning_cancel_handler(callback: CallbackQuery, state: FSMContext, bot: Bot):
195-
await callback.answer()
196-
await cancel_expiring(callback.message)
197-
await gracefully_interrupt_scanning_state(callback, state, bot)
198-
await go_to_default_state(callback, state)
199-
200-
201210
@router.callback_query(CallbackFromConfirmationMessageFilter(), ScanningPausedCallback.filter(F.menu == "remove-last"))
202211
async def scanning_paused_remove_last_handler(
203212
callback: CallbackQuery, callback_data: ScanningPausedCallback, state: FSMContext, bot: Bot

src/bot/routers/scanning/scanning_states.py

Lines changed: 20 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1-
from aiogram import Bot
1+
from aiogram import Bot, html
22
from aiogram.exceptions import TelegramBadRequest
33
from aiogram.fsm.context import FSMContext
44
from aiogram.fsm.state import State, StatesGroup
55
from aiogram.types import CallbackQuery, Message
66

77
from src.bot.api import api_client
88
from src.bot.routers.printing.printing_tools import discard_job_settings_message
9-
from src.bot.routers.scanning.scanning_tools import format_scanning_message, format_scanning_paused_message
9+
from src.bot.routers.scanning.scanning_tools import (
10+
format_configure_message,
11+
format_scanning_message,
12+
format_scanning_paused_message,
13+
)
14+
from src.bot.routers.tools import edit_message_text_anyway
1015

1116

1217
class ScanWork(StatesGroup):
@@ -39,35 +44,25 @@ async def gracefully_interrupt_scanning_state(
3944
):
4045
await discard_job_settings_message(data, message, state, bot)
4146
assert "confirmation_message_id" in data
42-
try:
43-
await bot.edit_message_text(
44-
text="Scanning cancelled", chat_id=message.chat.id, message_id=data["confirmation_message_id"]
45-
)
46-
except TelegramBadRequest:
47-
pass
47+
scanner_status = await api_client.get_scanner_status(message.chat.id, data["scanner"])
48+
text, _ = format_configure_message(data, scanner_status)
49+
await edit_message_text_anyway(
50+
data["confirmation_message_id"],
51+
f"{text}\n{html.italic('This job has expired 🕒')}",
52+
None,
53+
message.chat.id,
54+
bot,
55+
)
4856
elif current_state == ScanWork.scanning:
4957
assert "confirmation_message_id" in data
5058
scanner_status = await api_client.get_scanner_status(message.chat.id, data.get("scanner"))
51-
if scanner_status.scanner and "scan_job_id" in data:
52-
await api_client.cancel_manual_scan(message.chat.id, scanner_status.scanner, data["scan_job_id"])
53-
text, markup = format_scanning_message(data, scanner_status, "cancelled")
54-
try:
55-
await bot.edit_message_text(
56-
text=text, chat_id=message.chat.id, message_id=data["confirmation_message_id"], reply_markup=markup
57-
)
58-
except TelegramBadRequest:
59-
try:
60-
await bot.edit_message_caption(
61-
caption=text,
62-
chat_id=message.chat.id,
63-
message_id=data["confirmation_message_id"],
64-
reply_markup=markup,
65-
)
66-
except TelegramBadRequest:
67-
pass
59+
text = format_scanning_message(data, scanner_status, "cancelled")
60+
await edit_message_text_anyway(data["confirmation_message_id"], text, None, message.chat.id, bot)
6861
elif current_state == ScanWork.pause_menu:
6962
assert "confirmation_message_id" in data
7063
scanner_status = await api_client.get_scanner_status(message.chat.id, data.get("scanner"))
64+
if scanner_status.scanner and "scan_job_id" in data:
65+
await api_client.cancel_manual_scan(message.chat.id, scanner_status.scanner, data["scan_job_id"])
7166
caption, markup = format_scanning_paused_message(data, scanner_status, is_finished=True)
7267
try:
7368
await bot.edit_message_caption(

src/bot/routers/scanning/scanning_tools.py

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,6 @@ class ScanConfigureCallback(CallbackData, prefix="scan_menu"):
1414
menu: Literal["mode", "scanner", "quality", "sides", "crop", "cancel", "start"]
1515

1616

17-
class ScanningCallback(CallbackData, prefix="scanning"):
18-
menu: Literal["cancel"]
19-
20-
2117
class ScanningPausedCallback(CallbackData, prefix="scanning_paused"):
2218
menu: Literal["remove-last", "scan-more", "scan-new", "finish", "rename"]
2319

@@ -100,23 +96,17 @@ def format_scanning_message(
10096
scanner_status: ScannerStatus | None,
10197
status: Literal["starting", "scanning", "cancelled"],
10298
iteration: int = 0,
103-
) -> tuple[str, InlineKeyboardMarkup | None]:
99+
) -> str:
104100
text = scan_job_summary(data, scanner_status)
105101
if status == "starting":
106102
text += html.italic("⏳ Starting...\n")
107103
elif status == "scanning":
108104
text += html.italic(f"{'⤹⤿⤻⤺'[iteration % 4]} Scanning...\n")
109105
elif status == "cancelled":
110106
text += html.italic("❌ Cancelled\n")
111-
return text, None
107+
return text
112108

113-
return text, InlineKeyboardMarkup(
114-
inline_keyboard=[
115-
[
116-
InlineKeyboardButton(text="Cancel", callback_data=ScanningCallback(menu="cancel").pack()),
117-
]
118-
]
119-
)
109+
return text
120110

121111

122112
def scan_job_summary(data: FSMData, scanner_status: ScannerStatus | None) -> str:

src/bot/routers/tools.py

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import asyncio
22

3-
from aiogram import html
3+
from aiogram import Bot, html
44
from aiogram.exceptions import TelegramBadRequest
55
from aiogram.fsm.context import FSMContext
66
from aiogram.types import InlineKeyboardMarkup, Message
@@ -40,15 +40,36 @@ async def cancel_expiring(message: Message):
4040
async def mark_as_expired(message: Message, immediately: bool = False):
4141
await asyncio.sleep(0 if immediately else message_expiration_time)
4242
await edit_message_text_anyway(
43-
message=message, text=f"{message.caption or message.text}\n{html.italic('This job has expired 🕒')}"
43+
message_or_id=message, text=f"{message.caption or message.text}\n{html.italic('This job has expired 🕒')}"
4444
)
4545

4646

47-
async def edit_message_text_anyway(message: Message, text: str, reply_markup: InlineKeyboardMarkup | None = None):
48-
try:
49-
await message.edit_caption(caption=text, reply_markup=reply_markup)
50-
except TelegramBadRequest:
47+
async def edit_message_text_anyway(
48+
message_or_id: Message | int,
49+
text: str,
50+
reply_markup: InlineKeyboardMarkup | None = None,
51+
chat_id: int | None = None,
52+
bot: Bot | None = None,
53+
):
54+
after_edits = None
55+
if isinstance(message_or_id, Message):
5156
try:
52-
await message.edit_text(text=text, reply_markup=reply_markup)
57+
after_edits = await message_or_id.edit_caption(caption=text, reply_markup=reply_markup)
5358
except TelegramBadRequest:
54-
pass
59+
try:
60+
after_edits = await message_or_id.edit_text(text=text, reply_markup=reply_markup)
61+
except TelegramBadRequest:
62+
pass
63+
else:
64+
try:
65+
after_edits = await bot.edit_message_caption(
66+
chat_id=chat_id, message_id=message_or_id, caption=text, reply_markup=reply_markup
67+
)
68+
except TelegramBadRequest:
69+
try:
70+
after_edits = await bot.edit_message_text(
71+
chat_id=chat_id, message_id=message_or_id, text=text, reply_markup=reply_markup
72+
)
73+
except TelegramBadRequest:
74+
pass
75+
return after_edits

src/modules/scanning/repository.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ async def _is_scanner_offline(self, scanner: Scanner, client: httpx.AsyncClient)
123123
logger.warning(f"Scanner {scanner.name} is offline: {type(e)}")
124124
return True
125125

126-
async def scan_one_page(self, scanner: Scanner, options: ScanningOptions) -> bytes | None:
126+
async def scan_one_page_debug(self, scanner: Scanner, options: ScanningOptions) -> bytes | None:
127127
"""Scan using eSCL and return document as PDF bytes"""
128128
try:
129129
job_id = await self.start_scan_one(scanner, options)
@@ -158,11 +158,15 @@ async def start_scan_one(self, scanner: Scanner, options: ScanningOptions) -> st
158158
logger.warning(f"Scanner {scanner.name} returned None document url")
159159
return None
160160

161-
job_id = document_url.rsplit("/ScanJobs/", 1)[-1]
162-
return job_id
161+
return document_url[document_url.index("urn:uuid:") :]
163162

164163
async def fetch_scan_one(self, scanner: Scanner, job_id: str) -> bytes | None:
165-
document = await self.fetch_scanned_document(scanner, job_id)
164+
try:
165+
document = await self.fetch_scanned_document(scanner, job_id)
166+
except httpx.HTTPStatusError as e:
167+
if e.response.status_code in (404, 410):
168+
return None
169+
raise
166170
await self.delete_printer_scan_job(scanner, job_id)
167171
return document
168172

@@ -174,11 +178,16 @@ async def fetch_scanned_document(self, scanner: Scanner, job_id: str) -> bytes:
174178
return response.content # PDF bytes
175179

176180
async def delete_printer_scan_job(self, scanner: Scanner, job_id: str) -> None:
177-
"""Delete the scan job from the printer, so nobody can download the file"""
178-
async with httpx.AsyncClient(verify=False) as client:
179-
logger.info(f"Scanner {scanner.name} deleting document {job_id}")
180-
response = await client.delete(f"{scanner.escl}/ScanJobs/{job_id}")
181-
response.raise_for_status()
181+
"""Delete the document from the printer via its url"""
182+
try:
183+
async with httpx.AsyncClient(verify=False) as client:
184+
logger.info(f"Scanner {scanner.name} deleting document {job_id}")
185+
response = await client.delete(f"{scanner.escl}/ScanJobs/{job_id}")
186+
response.raise_for_status()
187+
except httpx.HTTPStatusError as e:
188+
if e.response.status_code == 410:
189+
return
190+
raise
182191

183192

184193
scanning_repository = ScanningRepository()

0 commit comments

Comments
 (0)