chgksuite 0.25.0b4__py3-none-any.whl → 0.26.0__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.
- chgksuite/cli.py +292 -31
- chgksuite/common.py +4 -0
- chgksuite/composer/composer_common.py +2 -0
- chgksuite/composer/docx.py +520 -292
- chgksuite/composer/pptx.py +80 -47
- chgksuite/composer/telegram.py +76 -51
- chgksuite/handouter/__init__.py +0 -0
- chgksuite/handouter/gen.py +143 -0
- chgksuite/handouter/installer.py +245 -0
- chgksuite/handouter/pack.py +79 -0
- chgksuite/handouter/runner.py +237 -0
- chgksuite/handouter/tex_internals.py +47 -0
- chgksuite/handouter/utils.py +88 -0
- chgksuite/parser.py +210 -17
- chgksuite/resources/regexes_by.json +1 -1
- chgksuite/resources/regexes_en.json +1 -1
- chgksuite/resources/regexes_kz_cyr.json +1 -1
- chgksuite/resources/regexes_ru.json +2 -2
- chgksuite/resources/regexes_sr.json +1 -1
- chgksuite/resources/regexes_ua.json +1 -1
- chgksuite/resources/regexes_uz_cyr.json +1 -1
- chgksuite/version.py +1 -1
- {chgksuite-0.25.0b4.dist-info → chgksuite-0.26.0.dist-info}/METADATA +4 -2
- {chgksuite-0.25.0b4.dist-info → chgksuite-0.26.0.dist-info}/RECORD +28 -22
- {chgksuite-0.25.0b4.dist-info → chgksuite-0.26.0.dist-info}/WHEEL +1 -1
- chgksuite/resources/template_shorin.pptx +0 -0
- {chgksuite-0.25.0b4.dist-info → chgksuite-0.26.0.dist-info}/entry_points.txt +0 -0
- {chgksuite-0.25.0b4.dist-info → chgksuite-0.26.0.dist-info}/licenses/LICENSE +0 -0
- {chgksuite-0.25.0b4.dist-info → chgksuite-0.26.0.dist-info}/top_level.txt +0 -0
chgksuite/composer/pptx.py
CHANGED
|
@@ -3,15 +3,16 @@ import os
|
|
|
3
3
|
import re
|
|
4
4
|
|
|
5
5
|
import toml
|
|
6
|
+
|
|
7
|
+
from chgksuite.common import log_wrap, replace_escaped, tryint
|
|
8
|
+
from chgksuite.composer.composer_common import BaseExporter, backtick_replace, parseimg
|
|
6
9
|
from pptx import Presentation
|
|
7
10
|
from pptx.dml.color import RGBColor
|
|
8
11
|
from pptx.enum.text import MSO_AUTO_SIZE, MSO_VERTICAL_ANCHOR, PP_ALIGN
|
|
12
|
+
from pptx.enum.lang import MSO_LANGUAGE_ID
|
|
9
13
|
from pptx.util import Inches as PptxInches
|
|
10
14
|
from pptx.util import Pt as PptxPt
|
|
11
15
|
|
|
12
|
-
from chgksuite.common import log_wrap, replace_escaped, tryint
|
|
13
|
-
from chgksuite.composer.composer_common import BaseExporter, backtick_replace, parseimg
|
|
14
|
-
|
|
15
16
|
|
|
16
17
|
class PptxExporter(BaseExporter):
|
|
17
18
|
def __init__(self, *args, **kwargs):
|
|
@@ -48,6 +49,17 @@ class PptxExporter(BaseExporter):
|
|
|
48
49
|
textbox = slide.shapes.add_textbox(left, top, width, height)
|
|
49
50
|
return textbox
|
|
50
51
|
|
|
52
|
+
def add_run(self, para, text, color=None):
|
|
53
|
+
r = para.add_run()
|
|
54
|
+
r.text = text
|
|
55
|
+
if color is None:
|
|
56
|
+
color = self.c["textbox"].get("color")
|
|
57
|
+
if color:
|
|
58
|
+
r.font.color.rgb = RGBColor(*color)
|
|
59
|
+
if self.args.language == "ru":
|
|
60
|
+
r.font.language_id = MSO_LANGUAGE_ID.RUSSIAN
|
|
61
|
+
return r
|
|
62
|
+
|
|
51
63
|
def pptx_format(self, el, para, tf, slide, replace_spaces=True):
|
|
52
64
|
def r_sp(text):
|
|
53
65
|
if replace_spaces:
|
|
@@ -60,15 +72,13 @@ class PptxExporter(BaseExporter):
|
|
|
60
72
|
licount = 0
|
|
61
73
|
for li in el[1]:
|
|
62
74
|
licount += 1
|
|
63
|
-
|
|
64
|
-
r.text = "\n{}. ".format(licount)
|
|
75
|
+
self.add_run(para, "\n{}. ".format(licount))
|
|
65
76
|
self.pptx_format(li, para, tf, slide)
|
|
66
77
|
else:
|
|
67
78
|
licount = 0
|
|
68
79
|
for li in el:
|
|
69
80
|
licount += 1
|
|
70
|
-
|
|
71
|
-
r.text = "\n{}. ".format(licount)
|
|
81
|
+
self.add_run(para, "\n{}. ".format(licount))
|
|
72
82
|
self.pptx_format(li, para, tf, slide)
|
|
73
83
|
|
|
74
84
|
if isinstance(el, str):
|
|
@@ -77,23 +87,20 @@ class PptxExporter(BaseExporter):
|
|
|
77
87
|
|
|
78
88
|
for run in self.parse_4s_elem(el):
|
|
79
89
|
if run[0] == "screen":
|
|
80
|
-
|
|
81
|
-
r.text = r_sp(run[1]["for_screen"])
|
|
90
|
+
self.add_run(para, r_sp(run[1]["for_screen"]))
|
|
82
91
|
|
|
83
92
|
elif run[0] == "linebreak":
|
|
84
|
-
|
|
93
|
+
self.add_run(para, "\n")
|
|
85
94
|
|
|
86
95
|
elif run[0] == "strike":
|
|
87
|
-
r =
|
|
88
|
-
r.text = r_sp(run[1])
|
|
96
|
+
r = self.add_run(para, r_sp(run[1]))
|
|
89
97
|
r.font.strike = True # TODO: doesn't work as of 2023-12-24, cf. https://github.com/scanny/python-pptx/issues/339
|
|
90
98
|
|
|
91
99
|
elif run[0] == "img":
|
|
92
100
|
pass # image processing is moved to other places
|
|
93
101
|
|
|
94
102
|
else:
|
|
95
|
-
r =
|
|
96
|
-
r.text = r_sp(run[1])
|
|
103
|
+
r = self.add_run(para, r_sp(run[1]))
|
|
97
104
|
if "italic" in run[0]:
|
|
98
105
|
r.font.italic = True
|
|
99
106
|
if "bold" in run[0]:
|
|
@@ -166,25 +173,28 @@ class PptxExporter(BaseExporter):
|
|
|
166
173
|
txt = txt.upper()
|
|
167
174
|
self.set_question_number(slide, number=txt)
|
|
168
175
|
else:
|
|
169
|
-
r =
|
|
170
|
-
|
|
176
|
+
r = self.add_run(
|
|
177
|
+
p, self._replace_no_break(self.pptx_process_text(section[0][1]))
|
|
178
|
+
)
|
|
171
179
|
r.font.size = PptxPt(self.c["text_size_grid"]["section"])
|
|
172
180
|
add_line_break = True
|
|
173
181
|
if editor:
|
|
174
|
-
r =
|
|
175
|
-
|
|
176
|
-
(
|
|
177
|
-
|
|
178
|
-
|
|
182
|
+
r = self.add_run(
|
|
183
|
+
p,
|
|
184
|
+
self._replace_no_break(
|
|
185
|
+
("\n\n" if add_line_break else "")
|
|
186
|
+
+ self.pptx_process_text(editor[0][1])
|
|
187
|
+
),
|
|
179
188
|
)
|
|
180
189
|
add_line_break = True
|
|
181
190
|
if meta:
|
|
182
191
|
for element in meta:
|
|
183
|
-
r =
|
|
184
|
-
|
|
185
|
-
(
|
|
186
|
-
|
|
187
|
-
|
|
192
|
+
r = self.add_run(
|
|
193
|
+
p,
|
|
194
|
+
self._replace_no_break(
|
|
195
|
+
("\n\n" if add_line_break else "")
|
|
196
|
+
+ self.pptx_process_text(element[1])
|
|
197
|
+
),
|
|
188
198
|
)
|
|
189
199
|
add_line_break = True
|
|
190
200
|
|
|
@@ -211,8 +221,11 @@ class PptxExporter(BaseExporter):
|
|
|
211
221
|
title = slide.shapes.title
|
|
212
222
|
title.text = title_text[0][1]
|
|
213
223
|
if date_text:
|
|
214
|
-
|
|
215
|
-
|
|
224
|
+
try:
|
|
225
|
+
subtitle = slide.placeholders[1]
|
|
226
|
+
subtitle.text = date_text[0][1]
|
|
227
|
+
except KeyError:
|
|
228
|
+
pass
|
|
216
229
|
for block in (editor_block, section_block):
|
|
217
230
|
self._process_block(block)
|
|
218
231
|
|
|
@@ -223,11 +236,12 @@ class PptxExporter(BaseExporter):
|
|
|
223
236
|
qtf = qntextbox.text_frame
|
|
224
237
|
qtf_p = self.init_paragraph(qtf)
|
|
225
238
|
if self.c["number_textbox"].get("align"):
|
|
226
|
-
qtf_p.alignment = getattr(
|
|
227
|
-
|
|
239
|
+
qtf_p.alignment = getattr(
|
|
240
|
+
PP_ALIGN, self.c["number_textbox"]["align"].upper()
|
|
241
|
+
)
|
|
228
242
|
if self.c.get("question_number_format") == "caps" and tryint(number):
|
|
229
243
|
number = f"ВОПРОС {number}"
|
|
230
|
-
qtf_r
|
|
244
|
+
qtf_r = self.add_run(qtf_p, number)
|
|
231
245
|
if self.c["number_textbox"].get("bold"):
|
|
232
246
|
qtf_r.font.bold = True
|
|
233
247
|
if self.c["number_textbox"].get("color"):
|
|
@@ -278,6 +292,15 @@ class PptxExporter(BaseExporter):
|
|
|
278
292
|
base_top = PptxInches(self.c["textbox"]["top"])
|
|
279
293
|
base_width = PptxInches(self.c["textbox"]["width"])
|
|
280
294
|
base_height = PptxInches(self.c["textbox"]["height"])
|
|
295
|
+
if self.c.get("disable_autolayout"):
|
|
296
|
+
slide.shapes.add_picture(
|
|
297
|
+
image["imgfile"],
|
|
298
|
+
left=base_left,
|
|
299
|
+
top=base_top,
|
|
300
|
+
width=img_base_width,
|
|
301
|
+
height=img_base_height,
|
|
302
|
+
)
|
|
303
|
+
return self.get_textbox(slide), 1
|
|
281
304
|
big_mode = (
|
|
282
305
|
image["big"] and not self.c.get("text_is_duplicated") and allowbigimage
|
|
283
306
|
)
|
|
@@ -390,9 +413,13 @@ class PptxExporter(BaseExporter):
|
|
|
390
413
|
def process_question_text(self, q):
|
|
391
414
|
image = self._get_image_from_4s(q["question"])
|
|
392
415
|
handout = self._get_handout_from_4s(q["question"])
|
|
393
|
-
|
|
416
|
+
add_handout_on_separate_slide = self.c.get("add_handout_on_separate_slide")
|
|
417
|
+
add_handout_on_separate_slide = (
|
|
418
|
+
add_handout_on_separate_slide is None or add_handout_on_separate_slide
|
|
419
|
+
)
|
|
420
|
+
if image and add_handout_on_separate_slide:
|
|
394
421
|
self.add_slide_with_image(image, number=self.number)
|
|
395
|
-
elif handout:
|
|
422
|
+
elif handout and add_handout_on_separate_slide:
|
|
396
423
|
self.add_slide_with_handout(handout, number=self.number)
|
|
397
424
|
slide = self.prs.slides.add_slide(self.BLANK_SLIDE)
|
|
398
425
|
text_is_duplicated = bool(self.c.get("text_is_duplicated"))
|
|
@@ -411,9 +438,11 @@ class PptxExporter(BaseExporter):
|
|
|
411
438
|
fields = ["answer"]
|
|
412
439
|
if q.get("zachet") and self.c.get("add_zachet"):
|
|
413
440
|
fields.append("zachet")
|
|
441
|
+
if q.get("nezachet") and self.c.get("add_zachet"):
|
|
442
|
+
fields.append("nezachet")
|
|
414
443
|
if self.c["add_comment"] and "comment" in q:
|
|
415
444
|
fields.append("comment")
|
|
416
|
-
if self.c
|
|
445
|
+
if self.c.get("add_source") and "source" in q:
|
|
417
446
|
fields.append("source")
|
|
418
447
|
textbox = None
|
|
419
448
|
coeff = 1
|
|
@@ -435,6 +464,10 @@ class PptxExporter(BaseExporter):
|
|
|
435
464
|
text_for_size += "\n" + self.recursive_join(
|
|
436
465
|
self.pptx_process_text(q["zachet"], strip_brackets=False)
|
|
437
466
|
)
|
|
467
|
+
if q.get("nezachet") and self.c.get("add_zachet"):
|
|
468
|
+
text_for_size += "\n" + self.recursive_join(
|
|
469
|
+
self.pptx_process_text(q["nezachet"], strip_brackets=False)
|
|
470
|
+
)
|
|
438
471
|
if q.get("comment") and self.c.get("add_comment"):
|
|
439
472
|
text_for_size += "\n" + self.recursive_join(
|
|
440
473
|
self.pptx_process_text(q["comment"])
|
|
@@ -451,34 +484,34 @@ class PptxExporter(BaseExporter):
|
|
|
451
484
|
p = self.init_paragraph(tf, size=self.c["force_text_size_answer"])
|
|
452
485
|
else:
|
|
453
486
|
p = self.init_paragraph(tf, text=text_for_size, coeff=coeff)
|
|
454
|
-
r =
|
|
455
|
-
r.text = f"{self.get_label(q, 'answer')}: "
|
|
487
|
+
r = self.add_run(p, f"{self.get_label(q, 'answer')}: ")
|
|
456
488
|
r.font.bold = True
|
|
457
489
|
self.pptx_format(
|
|
458
490
|
self.pptx_process_text(q["answer"], strip_brackets=False), p, tf, slide
|
|
459
491
|
)
|
|
460
492
|
if q.get("zachet") and self.c.get("add_zachet"):
|
|
461
493
|
zachet_text = self.pptx_process_text(q["zachet"], strip_brackets=False)
|
|
462
|
-
r =
|
|
463
|
-
r.text = f"\n{self.get_label(q, 'zachet')}: "
|
|
494
|
+
r = self.add_run(p, f"\n{self.get_label(q, 'zachet')}: ")
|
|
464
495
|
r.font.bold = True
|
|
465
496
|
self.pptx_format(zachet_text, p, tf, slide)
|
|
497
|
+
if q.get("nezachet") and self.c.get("add_zachet"):
|
|
498
|
+
nezachet_text = self.pptx_process_text(q["nezachet"], strip_brackets=False)
|
|
499
|
+
r = self.add_run(p, f"\n{self.get_label(q, 'nezachet')}: ")
|
|
500
|
+
r.font.bold = True
|
|
501
|
+
self.pptx_format(nezachet_text, p, tf, slide)
|
|
466
502
|
if self.c["add_comment"] and "comment" in q:
|
|
467
503
|
comment_text = self.pptx_process_text(q["comment"])
|
|
468
|
-
r =
|
|
469
|
-
r.text = f"\n{self.get_label(q, 'comment')}: "
|
|
504
|
+
r = self.add_run(p, f"\n{self.get_label(q, 'comment')}: ")
|
|
470
505
|
r.font.bold = True
|
|
471
506
|
self.pptx_format(comment_text, p, tf, slide)
|
|
472
|
-
if self.c
|
|
507
|
+
if self.c.get("add_source") and "source" in q:
|
|
473
508
|
source_text = self.pptx_process_text(q["source"])
|
|
474
|
-
r =
|
|
475
|
-
r.text = f"\n{self.get_label(q, 'source')}: "
|
|
509
|
+
r = self.add_run(p, f"\n{self.get_label(q, 'source')}: ")
|
|
476
510
|
r.font.bold = True
|
|
477
511
|
self.pptx_format(source_text, p, tf, slide)
|
|
478
|
-
if self.c
|
|
512
|
+
if self.c.get("add_author") and "author" in q:
|
|
479
513
|
author_text = self.pptx_process_text(q["author"])
|
|
480
|
-
r =
|
|
481
|
-
r.text = f"\n{self.get_label(q, 'author')}: "
|
|
514
|
+
r = self.add_run(p, f"\n{self.get_label(q, 'author')}: ")
|
|
482
515
|
r.font.bold = True
|
|
483
516
|
self.pptx_format(author_text, p, tf, slide)
|
|
484
517
|
|
|
@@ -510,7 +543,7 @@ class PptxExporter(BaseExporter):
|
|
|
510
543
|
return element["size"]
|
|
511
544
|
return self.c["text_size_grid"]["smallest"]
|
|
512
545
|
|
|
513
|
-
def init_paragraph(self, text_frame, text=None, coeff=1, size=None):
|
|
546
|
+
def init_paragraph(self, text_frame, text=None, coeff=1, size=None, color=None):
|
|
514
547
|
p = text_frame.paragraphs[0]
|
|
515
548
|
p.font.name = self.c["font"]["name"]
|
|
516
549
|
if size:
|
chgksuite/composer/telegram.py
CHANGED
|
@@ -16,6 +16,11 @@ from chgksuite.composer.composer_common import BaseExporter, parseimg
|
|
|
16
16
|
from chgksuite.composer.telegram_bot import run_bot_in_thread
|
|
17
17
|
|
|
18
18
|
|
|
19
|
+
def get_text(msg_data):
|
|
20
|
+
if "message" in msg_data and "text" in msg_data["message"]:
|
|
21
|
+
return msg_data["message"]["text"]
|
|
22
|
+
|
|
23
|
+
|
|
19
24
|
class TelegramExporter(BaseExporter):
|
|
20
25
|
def __init__(self, *args, **kwargs):
|
|
21
26
|
super().__init__(*args, **kwargs)
|
|
@@ -37,6 +42,7 @@ class TelegramExporter(BaseExporter):
|
|
|
37
42
|
self.channel_id = None # Target channel ID
|
|
38
43
|
self.chat_id = None # Discussion group ID linked to the channel
|
|
39
44
|
self.auth_uuid = uuid.uuid4().hex[:8]
|
|
45
|
+
self.chat_auth_uuid = uuid.uuid4().hex[:8]
|
|
40
46
|
self.init_telegram()
|
|
41
47
|
|
|
42
48
|
def check_connectivity(self):
|
|
@@ -119,6 +125,9 @@ class TelegramExporter(BaseExporter):
|
|
|
119
125
|
|
|
120
126
|
if result:
|
|
121
127
|
msg_data = json.loads(result["raw_data"])
|
|
128
|
+
if msg_data["message"]["chat"]["type"] != "private":
|
|
129
|
+
print("You should post to the PRIVATE chat, not to the channel/group")
|
|
130
|
+
continue
|
|
122
131
|
self.control_chat_id = msg_data["message"]["chat"]["id"]
|
|
123
132
|
self.send_api_request(
|
|
124
133
|
"sendMessage",
|
|
@@ -852,7 +861,7 @@ class TelegramExporter(BaseExporter):
|
|
|
852
861
|
|
|
853
862
|
# Wait for a forwarded message with channel information
|
|
854
863
|
channel_id = self.wait_for_forwarded_message(
|
|
855
|
-
entity_type="channel", check_type=True
|
|
864
|
+
entity_type="channel", check_type=True
|
|
856
865
|
)
|
|
857
866
|
if channel_id:
|
|
858
867
|
self.save_username(channel_result, channel_id)
|
|
@@ -860,6 +869,7 @@ class TelegramExporter(BaseExporter):
|
|
|
860
869
|
raise Exception("Failed to get channel ID from forwarded message")
|
|
861
870
|
else:
|
|
862
871
|
raise Exception("Channel ID is undefined")
|
|
872
|
+
|
|
863
873
|
# Handle chat resolution
|
|
864
874
|
if isinstance(chat_result, int):
|
|
865
875
|
chat_id = chat_result
|
|
@@ -868,14 +878,15 @@ class TelegramExporter(BaseExporter):
|
|
|
868
878
|
if not chat_id:
|
|
869
879
|
print("\n" + "=" * 50)
|
|
870
880
|
print(
|
|
871
|
-
"Please
|
|
881
|
+
f"Please write a message in the discussion group with text: {self.chat_auth_uuid}"
|
|
872
882
|
)
|
|
873
883
|
print("This will allow me to extract the group ID automatically.")
|
|
884
|
+
print("The bot MUST be added do the group and made admin, else it won't work!")
|
|
874
885
|
print("=" * 50 + "\n")
|
|
875
886
|
|
|
876
887
|
# Wait for a forwarded message with chat information
|
|
877
888
|
chat_id = self.wait_for_forwarded_message(
|
|
878
|
-
entity_type="chat", check_type=False
|
|
889
|
+
entity_type="chat", check_type=False
|
|
879
890
|
)
|
|
880
891
|
if not chat_id:
|
|
881
892
|
self.logger.error("Failed to get chat ID from forwarded message")
|
|
@@ -883,15 +894,13 @@ class TelegramExporter(BaseExporter):
|
|
|
883
894
|
while chat_id == channel_id:
|
|
884
895
|
error_msg = (
|
|
885
896
|
"Chat ID and channel ID are the same. The problem may be that "
|
|
886
|
-
"you
|
|
887
|
-
"from the channel by Telegram. Please forward a message that was sent directly in the discussion group."
|
|
897
|
+
"you posted a message in the channel, not in the discussion group."
|
|
888
898
|
)
|
|
889
899
|
self.logger.error(error_msg)
|
|
890
900
|
chat_id = self.wait_for_forwarded_message(
|
|
891
901
|
entity_type="chat",
|
|
892
902
|
check_type=False,
|
|
893
903
|
add_msg=error_msg,
|
|
894
|
-
string_id=chat_result,
|
|
895
904
|
)
|
|
896
905
|
if chat_id:
|
|
897
906
|
self.save_username(chat_result, chat_id)
|
|
@@ -904,7 +913,10 @@ class TelegramExporter(BaseExporter):
|
|
|
904
913
|
raise Exception("Chat ID is undefined")
|
|
905
914
|
|
|
906
915
|
self.channel_id = f"-100{channel_id}"
|
|
907
|
-
|
|
916
|
+
if not str(chat_id).startswith("-100"):
|
|
917
|
+
self.chat_id = f"-100{chat_id}"
|
|
918
|
+
else:
|
|
919
|
+
self.chat_id = chat_id
|
|
908
920
|
|
|
909
921
|
self.logger.info(
|
|
910
922
|
f"Using channel ID {self.channel_id} and discussion group ID {self.chat_id}"
|
|
@@ -1088,7 +1100,10 @@ class TelegramExporter(BaseExporter):
|
|
|
1088
1100
|
failure_message = "❌ Failed to extract channel ID."
|
|
1089
1101
|
else:
|
|
1090
1102
|
entity_name = "discussion group"
|
|
1091
|
-
instruction_message =
|
|
1103
|
+
instruction_message = (
|
|
1104
|
+
f"🔄 Please post to the discussion group a message with text: {self.chat_auth_uuid}\n\n"
|
|
1105
|
+
"⚠️ IMPORTANT: Bot should be added to the discussion group and have ADMIN rights!"
|
|
1106
|
+
)
|
|
1092
1107
|
success_message = "✅ Successfully extracted discussion group ID: {}"
|
|
1093
1108
|
failure_message = "❌ Failed to extract discussion group ID."
|
|
1094
1109
|
|
|
@@ -1105,6 +1120,7 @@ class TelegramExporter(BaseExporter):
|
|
|
1105
1120
|
resolved = False
|
|
1106
1121
|
retry_count = 0
|
|
1107
1122
|
max_retries = 30 # 5 minutes (10 seconds per retry)
|
|
1123
|
+
extracted_id = None
|
|
1108
1124
|
|
|
1109
1125
|
# Extract channel ID for comparison if we're looking for a discussion group
|
|
1110
1126
|
channel_numeric_id = None
|
|
@@ -1117,11 +1133,15 @@ class TelegramExporter(BaseExporter):
|
|
|
1117
1133
|
|
|
1118
1134
|
# Look for a forwarded message in recent messages
|
|
1119
1135
|
cursor = self.db_conn.cursor()
|
|
1136
|
+
if self.created_at:
|
|
1137
|
+
threshold = "'" + self.created_at + "'"
|
|
1138
|
+
else:
|
|
1139
|
+
threshold = "datetime('now', '-2 minutes')"
|
|
1120
1140
|
cursor.execute(
|
|
1121
|
-
"""
|
|
1141
|
+
f"""
|
|
1122
1142
|
SELECT raw_data, created_at
|
|
1123
1143
|
FROM messages
|
|
1124
|
-
WHERE created_at >
|
|
1144
|
+
WHERE created_at > {threshold}
|
|
1125
1145
|
ORDER BY created_at DESC
|
|
1126
1146
|
"""
|
|
1127
1147
|
)
|
|
@@ -1129,58 +1149,63 @@ class TelegramExporter(BaseExporter):
|
|
|
1129
1149
|
messages = cursor.fetchall()
|
|
1130
1150
|
|
|
1131
1151
|
for row in messages:
|
|
1152
|
+
if self.args.debug:
|
|
1153
|
+
self.logger.info(row["raw_data"])
|
|
1132
1154
|
if self.created_at and row["created_at"] < self.created_at:
|
|
1133
1155
|
break
|
|
1134
1156
|
msg_data = json.loads(row["raw_data"])
|
|
1135
|
-
if
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
# Remove -100 prefix if present
|
|
1143
|
-
if str(chat_id).startswith("-100"):
|
|
1144
|
-
extracted_id = int(str(chat_id)[4:])
|
|
1145
|
-
else:
|
|
1146
|
-
extracted_id = chat_id
|
|
1147
|
-
|
|
1148
|
-
# If we're looking for a discussion group, verify it's not the same as the channel ID
|
|
1149
|
-
if entity_type == "chat" and channel_numeric_id:
|
|
1150
|
-
if extracted_id == channel_numeric_id:
|
|
1151
|
-
self.logger.warning(
|
|
1152
|
-
"User forwarded a message from the channel, not the discussion group"
|
|
1153
|
-
)
|
|
1154
|
-
self.send_api_request(
|
|
1155
|
-
"sendMessage",
|
|
1156
|
-
{
|
|
1157
|
-
"chat_id": self.control_chat_id,
|
|
1158
|
-
"text": "⚠️ You forwarded a message from the channel, not from the discussion group.\n\nPlease forward a message that was originally sent IN the discussion group, not an automatic repost from the channel.",
|
|
1159
|
-
},
|
|
1160
|
-
)
|
|
1161
|
-
# Skip this message and continue waiting
|
|
1162
|
-
continue
|
|
1163
|
-
|
|
1164
|
-
# For channels, check the type; for chats, accept any type except "channel" if check_type is False
|
|
1165
|
-
if (check_type and forward_info.get("type") == "channel") or (
|
|
1166
|
-
not check_type
|
|
1167
|
-
):
|
|
1168
|
-
resolved = True
|
|
1169
|
-
self.created_at = row["created_at"]
|
|
1170
|
-
self.logger.info(
|
|
1171
|
-
f"Extracted {entity_name} ID: {extracted_id} from forwarded message"
|
|
1157
|
+
if entity_type == "chat":
|
|
1158
|
+
if get_text(msg_data) != self.chat_auth_uuid:
|
|
1159
|
+
continue
|
|
1160
|
+
extracted_id = msg_data["message"]["chat"]["id"]
|
|
1161
|
+
if extracted_id == channel_numeric_id or extracted_id == self.control_chat_id:
|
|
1162
|
+
self.logger.warning(
|
|
1163
|
+
"User posted a message in the channel, not the discussion group"
|
|
1172
1164
|
)
|
|
1173
|
-
|
|
1174
|
-
# Send confirmation message
|
|
1175
1165
|
self.send_api_request(
|
|
1176
1166
|
"sendMessage",
|
|
1177
1167
|
{
|
|
1178
1168
|
"chat_id": self.control_chat_id,
|
|
1179
|
-
"text":
|
|
1169
|
+
"text": (
|
|
1170
|
+
"⚠️ You posted a message in the channel, not in the discussion group."
|
|
1171
|
+
)
|
|
1180
1172
|
},
|
|
1181
1173
|
)
|
|
1174
|
+
# Skip this message and continue waiting
|
|
1175
|
+
continue
|
|
1176
|
+
elif entity_type == "channel":
|
|
1177
|
+
if msg_data["message"]["chat"]["id"] != self.control_chat_id:
|
|
1178
|
+
continue
|
|
1179
|
+
if "message" in msg_data and "forward_from_chat" in msg_data["message"]:
|
|
1180
|
+
forward_info = msg_data["message"]["forward_from_chat"]
|
|
1181
|
+
|
|
1182
|
+
# Extract chat ID from the message
|
|
1183
|
+
chat_id = forward_info.get("id")
|
|
1184
|
+
# Remove -100 prefix if present
|
|
1185
|
+
if str(chat_id).startswith("-100"):
|
|
1186
|
+
extracted_id = int(str(chat_id)[4:])
|
|
1187
|
+
else:
|
|
1188
|
+
extracted_id = chat_id
|
|
1189
|
+
# For channels, check the type; for chats, accept any type except "channel" if check_type is False
|
|
1190
|
+
if extracted_id and ((check_type and forward_info.get("type") == "channel") or (
|
|
1191
|
+
not check_type
|
|
1192
|
+
)):
|
|
1193
|
+
resolved = True
|
|
1194
|
+
self.created_at = row["created_at"]
|
|
1195
|
+
self.logger.info(
|
|
1196
|
+
f"Extracted {entity_name} ID: {extracted_id} from forwarded message"
|
|
1197
|
+
)
|
|
1198
|
+
|
|
1199
|
+
# Send confirmation message
|
|
1200
|
+
self.send_api_request(
|
|
1201
|
+
"sendMessage",
|
|
1202
|
+
{
|
|
1203
|
+
"chat_id": self.control_chat_id,
|
|
1204
|
+
"text": success_message.format(extracted_id),
|
|
1205
|
+
},
|
|
1206
|
+
)
|
|
1182
1207
|
|
|
1183
|
-
|
|
1208
|
+
return extracted_id
|
|
1184
1209
|
|
|
1185
1210
|
retry_count += 1
|
|
1186
1211
|
|
|
File without changes
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
import itertools
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
from collections import defaultdict
|
|
7
|
+
|
|
8
|
+
import toml
|
|
9
|
+
|
|
10
|
+
from chgksuite.common import get_source_dirs
|
|
11
|
+
from chgksuite.composer.chgksuite_parser import parse_4s
|
|
12
|
+
from chgksuite.composer.composer_common import _parse_4s_elem, parseimg
|
|
13
|
+
from chgksuite.handouter.utils import read_file, write_file
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def postprocess(s):
|
|
17
|
+
return s.replace("\\_", "_")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def generate_handouts_list(handouts, output_dir, base_name, parsed):
|
|
21
|
+
"""Generate a human-readable file with question numbers that have handouts."""
|
|
22
|
+
question_numbers = sorted([int(h["for_question"]) for h in handouts])
|
|
23
|
+
|
|
24
|
+
content = "ВОПРОСЫ С РАЗДАТОЧНЫМ МАТЕРИАЛОМ:\n\n"
|
|
25
|
+
content += f"Сквозная нумерация:\n{', '.join(map(str, question_numbers))}\n\n"
|
|
26
|
+
|
|
27
|
+
content += "По турам:\n"
|
|
28
|
+
tour = 0
|
|
29
|
+
by_tour = {}
|
|
30
|
+
for tup in parsed:
|
|
31
|
+
if tup[0] == "section":
|
|
32
|
+
tour += 1
|
|
33
|
+
by_tour[tour] = []
|
|
34
|
+
if tup[0] == "Question":
|
|
35
|
+
if tour == 0:
|
|
36
|
+
tour = 1
|
|
37
|
+
by_tour[tour] = []
|
|
38
|
+
if tup[1]["number"] in question_numbers:
|
|
39
|
+
by_tour[tour].append(tup[1]["number"])
|
|
40
|
+
|
|
41
|
+
for tour in sorted(by_tour):
|
|
42
|
+
tour_handouts = by_tour[tour]
|
|
43
|
+
if tour_handouts:
|
|
44
|
+
content += f"Тур {tour}: {', '.join(map(str, tour_handouts))}\n"
|
|
45
|
+
else:
|
|
46
|
+
content += f"Тур {tour}: нет раздаток\n"
|
|
47
|
+
|
|
48
|
+
output_fn = os.path.join(output_dir, base_name + "_handouts_list.txt")
|
|
49
|
+
write_file(output_fn, content)
|
|
50
|
+
print(f"File with list of handouts: {output_fn}")
|
|
51
|
+
print(content)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def generate_handouts(args):
|
|
55
|
+
_, resourcedir = get_source_dirs()
|
|
56
|
+
labels = toml.loads(
|
|
57
|
+
read_file(os.path.join(resourcedir, f"labels_{args.lang}.toml"))
|
|
58
|
+
)
|
|
59
|
+
handout_re = re.compile(
|
|
60
|
+
"\\["
|
|
61
|
+
+ labels["question_labels"]["handout_short"]
|
|
62
|
+
+ ".+?:( |\n)(?P<handout_text>.+?)\\]",
|
|
63
|
+
flags=re.DOTALL,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
cnt = read_file(args.filename)
|
|
67
|
+
parsed = parse_4s(cnt)
|
|
68
|
+
|
|
69
|
+
questions = [q[1] for q in parsed if q[0] == "Question"]
|
|
70
|
+
handouts = []
|
|
71
|
+
for q in questions:
|
|
72
|
+
if isinstance(q["question"], list):
|
|
73
|
+
question_text = "\n".join(itertools.chain.from_iterable(q["question"]))
|
|
74
|
+
else:
|
|
75
|
+
question_text = q["question"]
|
|
76
|
+
question_text_lower = question_text.lower()
|
|
77
|
+
srch = handout_re.search(question_text)
|
|
78
|
+
if srch:
|
|
79
|
+
text = postprocess(srch.group("handout_text"))
|
|
80
|
+
elems = _parse_4s_elem(text)
|
|
81
|
+
img = [el for el in elems if el[0] == "img"]
|
|
82
|
+
if img:
|
|
83
|
+
try:
|
|
84
|
+
parsed_img = parseimg(img[0][1])
|
|
85
|
+
except:
|
|
86
|
+
print(
|
|
87
|
+
f"Image file for question {q['number']} not found, add it by hand"
|
|
88
|
+
)
|
|
89
|
+
continue
|
|
90
|
+
else:
|
|
91
|
+
parsed_img = None
|
|
92
|
+
res = {"for_question": q["number"]}
|
|
93
|
+
if parsed_img:
|
|
94
|
+
res["image"] = parsed_img["imgfile"]
|
|
95
|
+
else:
|
|
96
|
+
res["text"] = text
|
|
97
|
+
handouts.append(res)
|
|
98
|
+
elif (
|
|
99
|
+
"раздат" in question_text_lower
|
|
100
|
+
or "роздан" in question_text_lower
|
|
101
|
+
or "(img" in question_text_lower
|
|
102
|
+
):
|
|
103
|
+
print(f"probably badly formatted handout for question {q['number']}")
|
|
104
|
+
res = {"for_question": q["number"], "text": postprocess(question_text)}
|
|
105
|
+
handouts.append(res)
|
|
106
|
+
result = []
|
|
107
|
+
result_by_question = defaultdict(list)
|
|
108
|
+
for handout in handouts:
|
|
109
|
+
if "image" in handout:
|
|
110
|
+
key = "image"
|
|
111
|
+
prefix = "image: "
|
|
112
|
+
else:
|
|
113
|
+
key = "text"
|
|
114
|
+
prefix = ""
|
|
115
|
+
value = handout[key]
|
|
116
|
+
formatted = (
|
|
117
|
+
f"for_question: {handout['for_question']}\n" if not args.separate else ""
|
|
118
|
+
) + f"columns: 3\n\n{prefix}{value}"
|
|
119
|
+
result.append(formatted)
|
|
120
|
+
result_by_question[handout["for_question"]].append(formatted)
|
|
121
|
+
output_dir = os.path.dirname(os.path.abspath(args.filename))
|
|
122
|
+
bn, _ = os.path.splitext(os.path.basename(args.filename))
|
|
123
|
+
|
|
124
|
+
if args.separate:
|
|
125
|
+
for k, v in result_by_question.items():
|
|
126
|
+
if len(v) > 1:
|
|
127
|
+
for i, cnt in enumerate(v):
|
|
128
|
+
output_fn = os.path.join(
|
|
129
|
+
output_dir, f"{bn}_q{k.zfill(2)}_{i + 1}.txt"
|
|
130
|
+
)
|
|
131
|
+
print(output_fn)
|
|
132
|
+
write_file(output_fn, cnt)
|
|
133
|
+
else:
|
|
134
|
+
output_fn = os.path.join(output_dir, f"{bn}_q{str(k).zfill(2)}.txt")
|
|
135
|
+
print(output_fn)
|
|
136
|
+
write_file(output_fn, v[0])
|
|
137
|
+
else:
|
|
138
|
+
output_fn = os.path.join(output_dir, bn + "_handouts.txt")
|
|
139
|
+
print(f"output filename: {output_fn}")
|
|
140
|
+
write_file(output_fn, "\n---\n".join(result))
|
|
141
|
+
|
|
142
|
+
if args.list_handouts:
|
|
143
|
+
generate_handouts_list(handouts, output_dir, bn, parsed)
|