jims-telegram 0.1.0.dev4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- jims_telegram/__init__.py +270 -0
- jims_telegram/main.py +77 -0
- jims_telegram/md2tgmd.py +353 -0
- jims_telegram/py.typed +0 -0
- jims_telegram-0.1.0.dev4.dist-info/METADATA +13 -0
- jims_telegram-0.1.0.dev4.dist-info/RECORD +8 -0
- jims_telegram-0.1.0.dev4.dist-info/WHEEL +4 -0
- jims_telegram-0.1.0.dev4.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import uuid
|
|
3
|
+
from contextlib import suppress
|
|
4
|
+
from typing import Any, Awaitable, overload
|
|
5
|
+
|
|
6
|
+
from aiogram import Bot, Dispatcher, F
|
|
7
|
+
from aiogram.filters import CommandStart
|
|
8
|
+
from aiogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message
|
|
9
|
+
from jims_core.app import JimsApp
|
|
10
|
+
from jims_core.schema import Pipeline
|
|
11
|
+
from jims_core.thread.thread_context import StatusUpdater
|
|
12
|
+
from jims_core.thread.thread_controller import ThreadController
|
|
13
|
+
from jims_core.util import uuid7
|
|
14
|
+
from loguru import logger
|
|
15
|
+
from opentelemetry import trace
|
|
16
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
17
|
+
from typing_extensions import TypedDict
|
|
18
|
+
|
|
19
|
+
from jims_telegram.md2tgmd import escape
|
|
20
|
+
|
|
21
|
+
tracer = trace.get_tracer("jims_tui")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TelegramSettings(BaseSettings):
|
|
25
|
+
model_config = SettingsConfigDict(
|
|
26
|
+
env_prefix="TELEGRAM_",
|
|
27
|
+
env_file=".env",
|
|
28
|
+
env_file_encoding="utf-8",
|
|
29
|
+
extra="ignore",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
bot_token: str
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
settings = TelegramSettings() # type: ignore
|
|
36
|
+
|
|
37
|
+
uuid_namespace = uuid.UUID("00000000-0000-0000-0000-000000000000")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def uuid_from_int(value: int) -> uuid.UUID:
|
|
41
|
+
if not 0 <= value < 2**64:
|
|
42
|
+
raise ValueError(f"Integer value must be between 0 and 2^64-1, got {value}")
|
|
43
|
+
|
|
44
|
+
# Convert the integer to bytes in big-endian order
|
|
45
|
+
name = value.to_bytes(8, byteorder="big")
|
|
46
|
+
|
|
47
|
+
# Generate a version 5 UUID
|
|
48
|
+
return uuid.uuid5(uuid_namespace, name)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class TelegramStatusUpdater(StatusUpdater):
|
|
52
|
+
def __init__(self, bot: Bot, chat_id: int) -> None:
|
|
53
|
+
self.bot = bot
|
|
54
|
+
self.chat_id = chat_id
|
|
55
|
+
|
|
56
|
+
async def update_status(self, status: str) -> None:
|
|
57
|
+
await self.bot.send_chat_action(
|
|
58
|
+
chat_id=self.chat_id,
|
|
59
|
+
action="typing",
|
|
60
|
+
)
|
|
61
|
+
logger.debug(f"Status updated to: {status}")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class TelegramButton(TypedDict):
|
|
65
|
+
text: str
|
|
66
|
+
id: str
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class TelegramController:
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
app: JimsApp,
|
|
73
|
+
) -> None:
|
|
74
|
+
self.app = app
|
|
75
|
+
self.bot = Bot(token=settings.bot_token)
|
|
76
|
+
|
|
77
|
+
self.dispatcher = Dispatcher()
|
|
78
|
+
|
|
79
|
+
self.dispatcher.message.register(self.command_start, CommandStart())
|
|
80
|
+
self.dispatcher.message.register(self.handle_message)
|
|
81
|
+
self.dispatcher.callback_query.register(self.handle_callback, F.data.startswith("btn:"))
|
|
82
|
+
|
|
83
|
+
@overload
|
|
84
|
+
@classmethod
|
|
85
|
+
async def create(cls, app: JimsApp) -> "TelegramController":
|
|
86
|
+
...
|
|
87
|
+
|
|
88
|
+
@overload
|
|
89
|
+
@classmethod
|
|
90
|
+
async def create(cls, app: Awaitable[JimsApp]) -> "TelegramController":
|
|
91
|
+
...
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
async def create(cls, app: JimsApp | Awaitable[JimsApp]) -> "TelegramController":
|
|
95
|
+
if isinstance(app, Awaitable):
|
|
96
|
+
app = await app
|
|
97
|
+
return cls(app)
|
|
98
|
+
|
|
99
|
+
async def _run_pipeline(self, ctl: ThreadController, chat_id: Any, pipeline: Pipeline) -> None:
|
|
100
|
+
ctx = await ctl.make_context()
|
|
101
|
+
ctx = ctx.with_status_updater(TelegramStatusUpdater(self.bot, chat_id))
|
|
102
|
+
|
|
103
|
+
async def status_updater():
|
|
104
|
+
try:
|
|
105
|
+
while True:
|
|
106
|
+
await ctx.update_agent_status("thinking")
|
|
107
|
+
await asyncio.sleep(5)
|
|
108
|
+
except asyncio.CancelledError:
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
updater_task = asyncio.create_task(status_updater())
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
events = await ctl.run_pipeline_with_context(pipeline, ctx)
|
|
115
|
+
finally:
|
|
116
|
+
updater_task.cancel()
|
|
117
|
+
with suppress(asyncio.CancelledError):
|
|
118
|
+
await updater_task
|
|
119
|
+
|
|
120
|
+
for event in events:
|
|
121
|
+
if event.event_type == "comm.assistant_message":
|
|
122
|
+
await self.bot.send_message(
|
|
123
|
+
chat_id=chat_id,
|
|
124
|
+
text=escape(event.event_data["content"]),
|
|
125
|
+
parse_mode="MarkdownV2",
|
|
126
|
+
)
|
|
127
|
+
elif event.event_type == "comm.assistant_buttons":
|
|
128
|
+
buttons = event.event_data.get("buttons", [])
|
|
129
|
+
reply_markup = self._build_inline_keyboard(buttons)
|
|
130
|
+
await self.bot.send_message(
|
|
131
|
+
chat_id=chat_id,
|
|
132
|
+
text=escape(event.event_data.get("content", "")),
|
|
133
|
+
parse_mode="MarkdownV2",
|
|
134
|
+
reply_markup=reply_markup,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
async def command_start(self, message: Message) -> None:
|
|
138
|
+
logger.debug(f"Received command start from {message.chat.id}")
|
|
139
|
+
if not message.from_user: # messages sent on behalf of chats, channels, or by tg
|
|
140
|
+
logger.error("from_user.id not found in message")
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
from_id = message.from_user.id # type: ignore[union-attr]
|
|
144
|
+
|
|
145
|
+
thread_id = uuid7()
|
|
146
|
+
ctl = await self.app.new_thread(
|
|
147
|
+
contact_id=f"telegram:{from_id}",
|
|
148
|
+
thread_id=thread_id,
|
|
149
|
+
thread_config={
|
|
150
|
+
"interface": "telegram",
|
|
151
|
+
"telegram_chat_id": message.chat.id,
|
|
152
|
+
"telegram_user_id": from_id,
|
|
153
|
+
"telegram_user_name": message.from_user.username, # type: ignore[union-attr]
|
|
154
|
+
},
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
if message.text:
|
|
158
|
+
await ctl.store_user_message(
|
|
159
|
+
event_id=uuid_from_int(message.message_id),
|
|
160
|
+
content=message.text,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
if self.app.conversation_start_pipeline is not None:
|
|
164
|
+
await self._run_pipeline(ctl, message.chat.id, self.app.conversation_start_pipeline)
|
|
165
|
+
|
|
166
|
+
async def handle_message(self, message: Message) -> None:
|
|
167
|
+
with tracer.start_as_current_span("jims_telegram.handle_message"):
|
|
168
|
+
logger.debug(f"Received message {message.text=} from {message.chat.id=}")
|
|
169
|
+
if not message.from_user: # messages sent on behalf of chats, channels, or by tg
|
|
170
|
+
logger.error("from_user.id not found in message")
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
from_id = message.from_user.id # type: ignore[union-attr]
|
|
174
|
+
|
|
175
|
+
ctl = await ThreadController.latest_thread_from_contact_id(self.app.sessionmaker, f"telegram:{from_id}")
|
|
176
|
+
if ctl is None:
|
|
177
|
+
logger.warning(f"Thread with id {message.chat.id} not found, recreating")
|
|
178
|
+
thread_id = uuid7()
|
|
179
|
+
ctl = await ThreadController.new_thread(
|
|
180
|
+
self.app.sessionmaker,
|
|
181
|
+
contact_id=f"telegram:{from_id}",
|
|
182
|
+
thread_id=thread_id,
|
|
183
|
+
thread_config={
|
|
184
|
+
"interface": "telegram",
|
|
185
|
+
"telegram_chat_id": message.chat.id,
|
|
186
|
+
"telegram_user_id": from_id,
|
|
187
|
+
"telegram_user_name": message.from_user.username, # type: ignore[union-attr]
|
|
188
|
+
},
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
if message.text:
|
|
192
|
+
await ctl.store_user_message(
|
|
193
|
+
event_id=uuid_from_int(message.message_id),
|
|
194
|
+
content=message.text,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
await self._run_pipeline(ctl, message.chat.id, self.app.pipeline)
|
|
198
|
+
|
|
199
|
+
async def handle_callback(self, callback: CallbackQuery) -> None:
|
|
200
|
+
if not callback.from_user:
|
|
201
|
+
return
|
|
202
|
+
from_id = callback.from_user.id # type: ignore[union-attr]
|
|
203
|
+
|
|
204
|
+
ctl = await ThreadController.latest_thread_from_contact_id(self.app.sessionmaker, f"telegram:{from_id}")
|
|
205
|
+
if ctl is None:
|
|
206
|
+
thread_id = uuid7()
|
|
207
|
+
ctl = await ThreadController.new_thread(
|
|
208
|
+
self.app.sessionmaker,
|
|
209
|
+
contact_id=f"telegram:{from_id}",
|
|
210
|
+
thread_id=thread_id,
|
|
211
|
+
thread_config={
|
|
212
|
+
"interface": "telegram",
|
|
213
|
+
"telegram_chat_id": callback.message.chat.id if callback.message else None,
|
|
214
|
+
"telegram_user_id": from_id,
|
|
215
|
+
"telegram_user_name": callback.message.from_user.username, # type: ignore[union-attr]
|
|
216
|
+
},
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
data = callback.data or ""
|
|
220
|
+
try:
|
|
221
|
+
await ctl.store_event_dict(
|
|
222
|
+
event_id=uuid7(),
|
|
223
|
+
event_type="comm.user_button_click",
|
|
224
|
+
event_data={
|
|
225
|
+
"role": "user",
|
|
226
|
+
"content": data,
|
|
227
|
+
"message_id": callback.message.message_id if callback.message else None,
|
|
228
|
+
},
|
|
229
|
+
)
|
|
230
|
+
except Exception as e:
|
|
231
|
+
logger.error(f"Failed to store button click event: {e}")
|
|
232
|
+
|
|
233
|
+
with suppress(Exception):
|
|
234
|
+
await callback.answer()
|
|
235
|
+
|
|
236
|
+
chat_id = callback.message.chat.id if callback.message else from_id
|
|
237
|
+
await self._run_pipeline(ctl, chat_id, self.app.pipeline)
|
|
238
|
+
|
|
239
|
+
@staticmethod
|
|
240
|
+
def _build_inline_keyboard(
|
|
241
|
+
buttons: list[list[TelegramButton]] | list[TelegramButton] | list[Any],
|
|
242
|
+
) -> InlineKeyboardMarkup | None:
|
|
243
|
+
"""
|
|
244
|
+
Parse buttons dict into keyboard
|
|
245
|
+
"""
|
|
246
|
+
rows: list[list[InlineKeyboardButton]] = []
|
|
247
|
+
if buttons and isinstance(buttons, list):
|
|
248
|
+
# normalize single row form
|
|
249
|
+
if buttons and all(isinstance(b, dict) for b in buttons):
|
|
250
|
+
buttons = [buttons] # type: ignore[assignment]
|
|
251
|
+
for row in buttons:
|
|
252
|
+
if not isinstance(row, list):
|
|
253
|
+
continue
|
|
254
|
+
kb_row: list[InlineKeyboardButton] = []
|
|
255
|
+
for b in row:
|
|
256
|
+
if not isinstance(b, dict):
|
|
257
|
+
continue
|
|
258
|
+
label_v = b.get("text")
|
|
259
|
+
if label_v is None:
|
|
260
|
+
continue
|
|
261
|
+
label = str(label_v)
|
|
262
|
+
btn_id = str(b.get("id") or b.get("callback_data") or label)
|
|
263
|
+
kb_row.append(InlineKeyboardButton(text=label, callback_data=f"btn:{btn_id}"))
|
|
264
|
+
if kb_row:
|
|
265
|
+
rows.append(kb_row)
|
|
266
|
+
return InlineKeyboardMarkup(inline_keyboard=rows) if rows else None
|
|
267
|
+
|
|
268
|
+
async def run(self):
|
|
269
|
+
logger.debug("Starting Telegram bot")
|
|
270
|
+
await self.dispatcher.start_polling(self.bot)
|
jims_telegram/main.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from aiohttp import web
|
|
5
|
+
from jims_core.util import (
|
|
6
|
+
load_jims_app,
|
|
7
|
+
setup_monitoring_and_tracing_with_sentry,
|
|
8
|
+
setup_prometheus_metrics,
|
|
9
|
+
setup_verbose_logging,
|
|
10
|
+
)
|
|
11
|
+
from loguru import logger
|
|
12
|
+
|
|
13
|
+
from jims_telegram import TelegramController
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def healthcheck(host: str = "0.0.0.0", port: int = 8000):
|
|
17
|
+
async def health(request: web.Request):
|
|
18
|
+
return web.json_response({"status": "ok"})
|
|
19
|
+
|
|
20
|
+
app = web.Application()
|
|
21
|
+
app.add_routes([web.get("/health", health)])
|
|
22
|
+
app.add_routes([web.get("/healthz", health)])
|
|
23
|
+
|
|
24
|
+
runner = web.AppRunner(app)
|
|
25
|
+
await runner.setup()
|
|
26
|
+
site = web.TCPSite(runner, host, port)
|
|
27
|
+
await site.start()
|
|
28
|
+
return runner
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@click.command()
|
|
32
|
+
@click.option("--app", type=click.STRING, default="app")
|
|
33
|
+
@click.option("--enable-sentry", is_flag=True, help="Enable tracing to Sentry", default=False)
|
|
34
|
+
@click.option("--enable-healthcheck", is_flag=True, help="Enable healthcheck endpoint", default=True)
|
|
35
|
+
@click.option("--healthcheck-port", type=click.INT, default=9000)
|
|
36
|
+
@click.option("--metrics-port", type=click.INT, default=8000)
|
|
37
|
+
@click.option("--verbose", is_flag=True, default=False)
|
|
38
|
+
def cli(
|
|
39
|
+
app: str,
|
|
40
|
+
enable_sentry: bool,
|
|
41
|
+
enable_healthcheck: bool,
|
|
42
|
+
healthcheck_port: int,
|
|
43
|
+
metrics_port: int,
|
|
44
|
+
verbose: bool,
|
|
45
|
+
) -> None:
|
|
46
|
+
if verbose:
|
|
47
|
+
setup_verbose_logging()
|
|
48
|
+
|
|
49
|
+
setup_prometheus_metrics(port=metrics_port)
|
|
50
|
+
|
|
51
|
+
if enable_sentry:
|
|
52
|
+
setup_monitoring_and_tracing_with_sentry()
|
|
53
|
+
|
|
54
|
+
jims_app = load_jims_app(app)
|
|
55
|
+
|
|
56
|
+
async def run_telegram_bot():
|
|
57
|
+
if enable_healthcheck:
|
|
58
|
+
await healthcheck(port=healthcheck_port)
|
|
59
|
+
|
|
60
|
+
telegram_controller = await TelegramController.create(jims_app)
|
|
61
|
+
await telegram_controller.run()
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
asyncio.run(run_telegram_bot())
|
|
65
|
+
except KeyboardInterrupt:
|
|
66
|
+
logger.info("Bot stopped by user")
|
|
67
|
+
except Exception as e:
|
|
68
|
+
logger.error(f"Bot crashed: {e}")
|
|
69
|
+
exit(1)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def main():
|
|
73
|
+
cli(auto_envvar_prefix="JIMS")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
if __name__ == "__main__":
|
|
77
|
+
main()
|
jims_telegram/md2tgmd.py
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
"""
|
|
2
|
+
conveniently borrowed from https://github.com/yym68686/md2tgmd
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def find_all_index(str, pattern):
|
|
9
|
+
index_list = [0]
|
|
10
|
+
for match in re.finditer(pattern, str, re.MULTILINE):
|
|
11
|
+
if match.group(1) is not None:
|
|
12
|
+
start = match.start(1)
|
|
13
|
+
end = match.end(1)
|
|
14
|
+
index_list += [start, end]
|
|
15
|
+
index_list.append(len(str))
|
|
16
|
+
return index_list
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def replace_all(text, pattern, function):
|
|
20
|
+
poslist = [0]
|
|
21
|
+
strlist = []
|
|
22
|
+
originstr = []
|
|
23
|
+
poslist = find_all_index(text, pattern)
|
|
24
|
+
for i in range(1, len(poslist[:-1]), 2):
|
|
25
|
+
start, end = poslist[i : i + 2]
|
|
26
|
+
strlist.append(function(text[start:end]))
|
|
27
|
+
for i in range(0, len(poslist), 2):
|
|
28
|
+
j, k = poslist[i : i + 2]
|
|
29
|
+
originstr.append(text[j:k])
|
|
30
|
+
if len(strlist) < len(originstr):
|
|
31
|
+
strlist.append("")
|
|
32
|
+
else:
|
|
33
|
+
originstr.append("")
|
|
34
|
+
new_list = [item for pair in zip(originstr, strlist) for item in pair]
|
|
35
|
+
return "".join(new_list)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def escapeshape(text):
|
|
39
|
+
return "▎*" + " ".join(text.split()[1:]) + "*\n\n"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def escapeminus(text):
|
|
43
|
+
return "\\" + text
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def escapeminus2(text):
|
|
47
|
+
return r"@+>@"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def escapebackquote(text):
|
|
51
|
+
return r"\`\`"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def escapebackquoteincode(text):
|
|
55
|
+
return r"@->@"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def escapeplus(text):
|
|
59
|
+
return "\\" + text
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def escape_all_backquote(text):
|
|
63
|
+
return "\\" + text
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def dedent_space(text):
|
|
67
|
+
import textwrap
|
|
68
|
+
|
|
69
|
+
return "\n\n" + textwrap.dedent(text).strip() + "\n\n"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def split_code(text):
|
|
73
|
+
split_list = []
|
|
74
|
+
if len(text) > 2300:
|
|
75
|
+
split_str_list = text.split("\n\n")
|
|
76
|
+
|
|
77
|
+
conversation_len = len(split_str_list)
|
|
78
|
+
message_index = 1
|
|
79
|
+
while message_index < conversation_len:
|
|
80
|
+
if split_str_list[message_index].startswith(" "):
|
|
81
|
+
split_str_list[message_index - 1] += "\n\n" + split_str_list[message_index]
|
|
82
|
+
split_str_list.pop(message_index)
|
|
83
|
+
conversation_len = conversation_len - 1
|
|
84
|
+
else:
|
|
85
|
+
message_index = message_index + 1
|
|
86
|
+
|
|
87
|
+
split_index = 0
|
|
88
|
+
for index, _ in enumerate(split_str_list):
|
|
89
|
+
if len("".join(split_str_list[:index])) < len(text) // 2:
|
|
90
|
+
split_index += 1
|
|
91
|
+
continue
|
|
92
|
+
else:
|
|
93
|
+
break
|
|
94
|
+
str1 = "\n\n".join(split_str_list[:split_index])
|
|
95
|
+
if not str1.strip().endswith("```"):
|
|
96
|
+
str1 = str1 + "\n```"
|
|
97
|
+
split_list.append(str1)
|
|
98
|
+
code_type = text.split("\n")[0]
|
|
99
|
+
str2 = "\n\n".join(split_str_list[split_index:])
|
|
100
|
+
str2 = code_type + "\n" + str2
|
|
101
|
+
if not str2.strip().endswith("```"):
|
|
102
|
+
str2 = str2 + "\n```"
|
|
103
|
+
split_list.append(str2)
|
|
104
|
+
else:
|
|
105
|
+
split_list.append(text)
|
|
106
|
+
|
|
107
|
+
if len(split_list) > 1:
|
|
108
|
+
split_list = "\n@|@|@|@\n\n".join(split_list)
|
|
109
|
+
else:
|
|
110
|
+
split_list = split_list[0]
|
|
111
|
+
return split_list
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def find_lines_with_char(s, char, min_count):
|
|
115
|
+
"""
|
|
116
|
+
Find lines containing a specific character at least min_count times.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
s (str): String to process
|
|
120
|
+
char (str): Character to count
|
|
121
|
+
min_count (int): Minimum occurrence count
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
str: String with escaped characters
|
|
125
|
+
"""
|
|
126
|
+
lines = s.split("\n") # Split string by lines
|
|
127
|
+
|
|
128
|
+
for index, line in enumerate(lines):
|
|
129
|
+
if re.sub(r"```", "", line).count(char) % 2 != 0 or (
|
|
130
|
+
not line.strip().startswith("```") and line.count(char) % 2 != 0
|
|
131
|
+
):
|
|
132
|
+
lines[index] = replace_all(lines[index], r"\\`|(`)", escape_all_backquote)
|
|
133
|
+
|
|
134
|
+
return "\n".join(lines)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def escape(text, flag=0, italic=True):
|
|
138
|
+
"""
|
|
139
|
+
Convert Markdown to Telegram Markdown format.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
text (str): Input Markdown text
|
|
143
|
+
flag (int): Processing flag
|
|
144
|
+
italic (bool): Whether to process italic formatting
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
str: Telegram-formatted markdown
|
|
148
|
+
"""
|
|
149
|
+
# In all other places characters
|
|
150
|
+
# _ * [ ] ( ) ~ ` > # + - = | { } . !
|
|
151
|
+
# must be escaped with the preceding character '\'.
|
|
152
|
+
text = re.sub(r"\\\[", "@->@", text)
|
|
153
|
+
text = re.sub(r"\\]", "@<-@", text)
|
|
154
|
+
text = re.sub(r"\\\(", "@-->@", text)
|
|
155
|
+
text = re.sub(r"\\\)", "@<--@", text)
|
|
156
|
+
if flag:
|
|
157
|
+
text = re.sub(r"\\\\", "@@@", text)
|
|
158
|
+
text = re.sub(r"\\`", "@<@", text)
|
|
159
|
+
text = re.sub(r"\\", r"\\\\", text)
|
|
160
|
+
if flag:
|
|
161
|
+
text = re.sub(r"@{3}", r"\\\\", text)
|
|
162
|
+
# _italic_
|
|
163
|
+
if italic:
|
|
164
|
+
text = re.sub(r"_(.*?)_", "@@@\\1@@@", text)
|
|
165
|
+
text = re.sub(r"_", r"\_", text)
|
|
166
|
+
text = re.sub(r"@{3}(.*?)@{3}", "_\\1_", text)
|
|
167
|
+
else:
|
|
168
|
+
text = re.sub(r"_", r"\_", text)
|
|
169
|
+
|
|
170
|
+
text = re.sub(r"\*{2}(.*?)\*{2}", "@@@\\1@@@", text)
|
|
171
|
+
text = re.sub(r"\n{1,2}\*\s", "\n\n• ", text)
|
|
172
|
+
text = re.sub(r"\*", r"\*", text)
|
|
173
|
+
text = re.sub(r"@{3}(.*?)@{3}", "*\\1*", text)
|
|
174
|
+
text = re.sub(r"!?\[(.*?)]\((.*?)\)", "@@@\\1@@@^^^\\2^^^", text)
|
|
175
|
+
text = re.sub(r"\[", r"\[", text)
|
|
176
|
+
text = re.sub(r"]", r"\]", text)
|
|
177
|
+
text = re.sub(r"\(", r"\(", text)
|
|
178
|
+
text = re.sub(r"\)", r"\)", text)
|
|
179
|
+
text = re.sub(r"@->@", r"\[", text)
|
|
180
|
+
text = re.sub(r"@<-@", r"\]", text)
|
|
181
|
+
text = re.sub(r"@-->@", r"\(", text)
|
|
182
|
+
text = re.sub(r"@<--@", r"\)", text)
|
|
183
|
+
text = re.sub(r"@{3}(.*?)@{3}\^{3}(.*?)\^{3}", "[\\1](\\2)", text)
|
|
184
|
+
|
|
185
|
+
# ~strikethrough~
|
|
186
|
+
text = re.sub(r"~{2}(.*?)~{2}", "@@@\\1@@@", text)
|
|
187
|
+
text = re.sub(r"~", r"\~", text)
|
|
188
|
+
text = re.sub(r"@{3}(.*?)@{3}", "~\\1~", text)
|
|
189
|
+
|
|
190
|
+
text = re.sub(r"\n>\s", "\n@@@ ", text)
|
|
191
|
+
text = re.sub(r">", r"\>", text)
|
|
192
|
+
text = re.sub(r"@{3}", ">", text)
|
|
193
|
+
|
|
194
|
+
text = replace_all(text, r"(^#+\s.+?\n+)|```[\D\d\s]+?```", escapeshape)
|
|
195
|
+
text = re.sub(r"#", r"\#", text)
|
|
196
|
+
text = replace_all(text, r"(\+)|\n[\s]*-\s|```[\D\d\s]+?```|`[\D\d\s]*?`", escapeplus)
|
|
197
|
+
|
|
198
|
+
# Numbered lists
|
|
199
|
+
text = re.sub(r"\n{1,2}(\s*\d{1,2}\.\s)", "\n\n\\1", text)
|
|
200
|
+
|
|
201
|
+
# Replace - outside code blocks
|
|
202
|
+
text = replace_all(text, r"```[\D\d\s]+?```|(-)", escapeminus2)
|
|
203
|
+
text = re.sub(r"-", "@<+@", text)
|
|
204
|
+
text = re.sub(r"@\+>@", "-", text)
|
|
205
|
+
|
|
206
|
+
text = re.sub(r"\n{1,2}(\s*)-\s", "\n\n\\1• ", text)
|
|
207
|
+
text = re.sub(r"@<+@", r"\-", text)
|
|
208
|
+
text = replace_all(text, r"(-)|\n[\s]*-\s|```[\D\d\s]+?```|`[\D\d\s]*?`", escapeminus)
|
|
209
|
+
text = re.sub(r"```([\D\d\s]+?)```", "@@@\\1@@@", text)
|
|
210
|
+
# Replace backticks in code blocks
|
|
211
|
+
text = replace_all(text, r"\@\@\@[\s\d\D]+?\@\@\@|(`)", escapebackquoteincode)
|
|
212
|
+
text = re.sub(r"`", r"\`", text)
|
|
213
|
+
text = re.sub(r"@<@", r"\`", text)
|
|
214
|
+
text = re.sub(r"@->@", "`", text)
|
|
215
|
+
text = re.sub(r"\s`\\`\s", r" `\\\\\` ", text)
|
|
216
|
+
|
|
217
|
+
text = replace_all(text, r"(``)", escapebackquote)
|
|
218
|
+
text = re.sub(r"@{3}([\D\d\s]+?)@{3}", "```\\1```", text)
|
|
219
|
+
text = re.sub(r"=", r"\=", text)
|
|
220
|
+
text = re.sub(r"\|", r"\|", text)
|
|
221
|
+
# text = re.sub(r"\@\!\@", '||', text)
|
|
222
|
+
text = re.sub(r"{", r"\{", text)
|
|
223
|
+
text = re.sub(r"}", r"\}", text)
|
|
224
|
+
text = re.sub(r"\.", r"\.", text)
|
|
225
|
+
text = re.sub(r"!", r"\!", text)
|
|
226
|
+
text = find_lines_with_char(text, "`", 5)
|
|
227
|
+
text = replace_all(text, r"(\n+\x20*```[\D\d\s]+?```\n+)", dedent_space)
|
|
228
|
+
return text
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
test_text = r"""
|
|
232
|
+
# title
|
|
233
|
+
|
|
234
|
+
### `probs.scatter_(1, ind`
|
|
235
|
+
|
|
236
|
+
**bold**
|
|
237
|
+
```
|
|
238
|
+
# comment
|
|
239
|
+
print(qwer) # ferfe
|
|
240
|
+
ni1
|
|
241
|
+
```
|
|
242
|
+
# bn
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
# b
|
|
246
|
+
|
|
247
|
+
# Header
|
|
248
|
+
## Subheader
|
|
249
|
+
|
|
250
|
+
[1.0.0](http://version.com)
|
|
251
|
+

|
|
252
|
+
|
|
253
|
+
- item 1 -
|
|
254
|
+
- item 1 -
|
|
255
|
+
- item 1 -
|
|
256
|
+
* item 2 #
|
|
257
|
+
* item 3 ~
|
|
258
|
+
|
|
259
|
+
1. item 1
|
|
260
|
+
2. item 2
|
|
261
|
+
|
|
262
|
+
1. item 1
|
|
263
|
+
```python
|
|
264
|
+
|
|
265
|
+
# comment
|
|
266
|
+
print("1.1\n")_
|
|
267
|
+
\subsubsection{1.1}
|
|
268
|
+
- item 1 -
|
|
269
|
+
```
|
|
270
|
+
2. item 2
|
|
271
|
+
|
|
272
|
+
sudo apt install package # Install command
|
|
273
|
+
|
|
274
|
+
\subsubsection{1.1}
|
|
275
|
+
|
|
276
|
+
And simple text `with-dashes` `with+plus` + some - **symbols**.
|
|
277
|
+
|
|
278
|
+
```
|
|
279
|
+
print("Hello, World!") -
|
|
280
|
+
app.listen(PORT, () => {
|
|
281
|
+
console.log(`Server is running on http://localhost:${PORT}`);
|
|
282
|
+
});
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Cxy = abs (Pxy)**2/ (Pxx*Pyy)
|
|
286
|
+
|
|
287
|
+
`a`a-b-c`n`
|
|
288
|
+
\[ E[X^4] = \int_{-\infty}^{\infty} x^4 f(x) dx \]
|
|
289
|
+
|
|
290
|
+
`-a----++++`++a-b-c`-n-`
|
|
291
|
+
`[^``]*`a``b-c``d``
|
|
292
|
+
# pattern = r"`[^`]*`-([^`-]*)"``
|
|
293
|
+
w`-a----`ccccc`-n-`bbbb``a
|
|
294
|
+
|
|
295
|
+
1. Open VSCode terminal: Go to `View` > `Terminal` or use `Ctrl+``
|
|
296
|
+
|
|
297
|
+
How to write: `line.strip().startswith("```")`?
|
|
298
|
+
|
|
299
|
+
`View` > `Terminal`
|
|
300
|
+
|
|
301
|
+
Escape example: `\``
|
|
302
|
+
|
|
303
|
+
- `Path.open()` method opens the `README.md` file with UTF-8 encoding.
|
|
304
|
+
|
|
305
|
+
3. `(`
|
|
306
|
+
|
|
307
|
+
3. Parentheses example: `(`
|
|
308
|
+
|
|
309
|
+
According to Euler's totient function, for \( n = p_1^{k_1} \times p_2^{k_2} \times \cdots \times p_r^{k_r} \) (where \( p_1, p_2, \ldots, p_r \) are distinct primes):
|
|
310
|
+
|
|
311
|
+
\[ \varphi(n) = n \left(1 - \frac{1}{p_1}\right) \left(1 - \frac{1}{p_2}\right) \cdots \left(1 - \frac{1}{p_r}\right) \]
|
|
312
|
+
|
|
313
|
+
Therefore:
|
|
314
|
+
|
|
315
|
+
\[ \varphi(35) = 35 \left(1 - \frac{1}{5}\right) \left(1 - \frac{1}{7}\right) \]
|
|
316
|
+
|
|
317
|
+
Calculating step by step:
|
|
318
|
+
|
|
319
|
+
\[ \varphi(35) = 35 \left(\frac{4}{5}\right) \left(\frac{6}{7}\right) \]
|
|
320
|
+
|
|
321
|
+
\[ \varphi(35) = 35 \times \frac{24}{35} \]
|
|
322
|
+
|
|
323
|
+
\[ \varphi(35) = 24 \]
|
|
324
|
+
|
|
325
|
+
To calculate acceleration \( a \), use the formula:
|
|
326
|
+
|
|
327
|
+
\[ a = \frac{\Delta v}{\Delta t} \]
|
|
328
|
+
|
|
329
|
+
Where:
|
|
330
|
+
- \(\Delta v\) is the change in velocity
|
|
331
|
+
- \(\Delta t\) is the change in time
|
|
332
|
+
|
|
333
|
+
Given:
|
|
334
|
+
- Initial velocity \( v_0 = 0 \) m/s
|
|
335
|
+
- Final velocity \( v = 27.8 \) m/s
|
|
336
|
+
- Time \( \Delta t = 3.85 \) s
|
|
337
|
+
|
|
338
|
+
Substituting values:
|
|
339
|
+
|
|
340
|
+
\[ a = \frac{27.8 \, \text{m/s} - 0 \, \text{m/s}}{3.85 \, \text{s}} \]
|
|
341
|
+
|
|
342
|
+
Calculating:
|
|
343
|
+
|
|
344
|
+
\[ a = \frac{27.8}{3.85} \approx 7.22 \, \text{m/s}^2 \]
|
|
345
|
+
|
|
346
|
+
Therefore, the car's acceleration is approximately 7.22 m/s².
|
|
347
|
+
|
|
348
|
+
Thus, Euler's totient function \( \varphi(35) \) equals 24.
|
|
349
|
+
"""
|
|
350
|
+
|
|
351
|
+
if __name__ == "__main__":
|
|
352
|
+
test_text_escaped = escape(test_text)
|
|
353
|
+
print(test_text_escaped)
|
jims_telegram/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: jims-telegram
|
|
3
|
+
Version: 0.1.0.dev4
|
|
4
|
+
Summary: Telegram interface for JIMS applications
|
|
5
|
+
Author-email: Andrey Tatarinov <a@tatarinov.co>, Timur Sheydaev <tsheyd@epoch8.co>
|
|
6
|
+
Requires-Python: >=3.12
|
|
7
|
+
Requires-Dist: aiogram>=3.18
|
|
8
|
+
Requires-Dist: aiohttp>=3.11.16
|
|
9
|
+
Requires-Dist: click>=8.0
|
|
10
|
+
Requires-Dist: jims-core
|
|
11
|
+
Requires-Dist: loguru>=0.7.3
|
|
12
|
+
Requires-Dist: pydantic-settings>=2.8.1
|
|
13
|
+
Requires-Dist: pydantic>=2.4.1
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
jims_telegram/__init__.py,sha256=0wEcTNYCGnmVcc9n8wfID7vtBfYhmJryrYx-8Sg1qc4,9970
|
|
2
|
+
jims_telegram/main.py,sha256=inu_Nh6W3OxDvlPZbYZZRK7bxHog3Dg_akKfiteKVrQ,2059
|
|
3
|
+
jims_telegram/md2tgmd.py,sha256=xermMXAr8tgbsFp7itxSPPJ0NfEe5A66fwoBxrO-__o,9461
|
|
4
|
+
jims_telegram/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
jims_telegram-0.1.0.dev4.dist-info/METADATA,sha256=m4hW97HNPY7glFnRR3aPz3cIqEwE5YHVNBwP55yR3DU,431
|
|
6
|
+
jims_telegram-0.1.0.dev4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
7
|
+
jims_telegram-0.1.0.dev4.dist-info/entry_points.txt,sha256=eiEkp0hbAskEaY7YfCcWDXMs9NM_TdCg88UMA6pVx5Y,58
|
|
8
|
+
jims_telegram-0.1.0.dev4.dist-info/RECORD,,
|