1
0
mirror of https://github.com/civsocit/olgram.git synced 2025-07-12 22:33:24 +00:00

Compare commits

...

261 Commits
v0.0.3 ... main

Author SHA1 Message Date
er8dd
2305590899 f 2024-03-22 04:16:38 +04:00
er8dd
a34e633b98 version bump 2024-03-22 04:14:07 +04:00
er8dd
16da3634db telegram anti-flood 2024-03-22 04:13:32 +04:00
er8dd
a0c6c9415e tags 2024-03-22 03:55:50 +04:00
er8dd
dab803a4e8 tags 2024-03-22 03:49:05 +04:00
er8dd
0ceea778fe merge 2024-03-22 03:46:34 +04:00
er8dd
7ce6df7fd9 bot tags 2024-03-22 03:44:34 +04:00
er8dd
59da56d463 locale 2024-03-17 14:35:34 +04:00
er8dd
1c12730a4e oh you are idiot 2024-03-17 14:25:07 +04:00
er8dd
d0b570baa9 version 2024-03-15 03:07:31 +04:00
er8dd
1b1fe239f8 tag first iteration 2024-03-15 02:59:49 +04:00
er8dd
214824db14 typo 2024-03-15 01:46:51 +04:00
er8dd
147fc2a665 docs 2024-03-03 10:21:00 +04:00
er8dd
c027ec656b minor fixes 2024-03-03 00:30:23 +04:00
er8dd
f79a9f317d Merge branch 'main' into stable 2024-03-03 00:10:32 +04:00
er8dd
eeb6c65f36 minor fixes 2024-03-03 00:07:45 +04:00
er8dd
7a0ce10c56 mailing size limit 2024-03-02 20:50:40 +04:00
er8dd
44f39e4de0 Merge branch 'main' into stable 2024-03-02 20:05:23 +04:00
er8dd
120fdef189 mailing second iteration 2024-03-02 20:03:42 +04:00
er8dd
f26958518c mailing first iteration 2024-03-01 22:12:05 +04:00
er8dd
acb62fb644 "interrupt threads" option 2024-03-01 19:39:46 +04:00
jjki3d
ae45374490 flake8 2024-02-17 04:30:05 +04:00
jjki3d
7aacb2e38f Merge branch 'main' into stable 2024-02-17 03:56:11 +04:00
jjki3d
0881c86349 second message option 2024-02-17 03:53:43 +04:00
jjki3d
1dd4d4d7fd Merge branch 'main' into stable 2024-01-13 02:29:57 +04:00
jjki3d
82b68d1d9f multi-lang fix 2024-01-13 02:29:36 +04:00
jjki3d
02e91a596c Merge branch 'main' into stable 2024-01-13 02:20:53 +04:00
jjki3d
a5e6fbce34 multi-lang 2024-01-12 22:35:01 +04:00
walker
28ed36ffeb rm xmr 2023-06-23 12:43:18 +04:00
walker
601c16622d update address 2023-06-17 02:38:37 +04:00
walker
9e46041d0f update year 2023-02-14 23:04:30 +04:00
walker
f41e17a15c rebuild 2023-02-14 22:59:58 +04:00
walker
a262d4e488 Merge branch 'main' into stable 2022-11-05 06:11:27 +04:00
walker
bb1456dda1 fix location forwarding 2022-11-05 00:42:11 +04:00
walker
756f0bd89a minor fixes 2022-11-05 00:40:26 +04:00
walker
6acc2068de fix for prev 2022-10-29 20:14:50 +04:00
walker
d478e9d8e9 version bump 2022-10-29 19:34:13 +04:00
walker
52864ed729 fix #19 2022-10-29 19:32:56 +04:00
mihalin
ac09e42f94
Merge pull request #24 from arcxio/multiple_admins
support multiple comma-separated values in ADMIN_ID
2022-10-29 19:20:27 +04:00
arĉi
afc5389520 support multiple comma-separated values in ADMIN_ID 2022-10-29 18:52:08 +06:00
mihalin
3e1f89034a fix translations 2022-09-16 21:30:38 +04:00
mihalin
30ab7c84b4 update translations 2022-09-02 05:12:53 +04:00
mihalin
9d8f5a97f7 Revert "debug print"
This reverts commit 16e944707f.
2022-09-02 04:59:18 +04:00
mihalin
16e944707f debug print 2022-09-02 04:48:16 +04:00
mihalin
9723c70deb leave chat button first iteration 2022-09-02 04:28:51 +04:00
mihalin
6e2ee437ba more nice menu 2022-09-02 04:09:28 +04:00
mihalin
6789d23c28 #ID more useful tag in user info 2022-08-03 00:02:32 +03:00
mihalin
0fd8d541f7 add SUPERVISOR_ID to env example 2022-08-02 23:58:41 +03:00
mihalin
65bc807ab7 version bump 2022-08-01 01:55:45 +03:00
mihalin
f6d47f729d #15 antiflood 2022-08-01 01:53:59 +03:00
mihalin
62d00cbd5f #15 antiflood 2022-08-01 01:52:01 +03:00
mihalin
7bb0951e7f fix for prev 2022-07-23 10:15:05 +03:00
mihalin
3f978c8d1c #16 smart auto-reply 2022-07-23 09:59:37 +03:00
mihalin
c7a52ea9fd typo 2022-07-06 00:50:02 +03:00
mihalin
a4ae50dbbe handle all exceptions on message forwarding 2022-07-06 00:45:51 +03:00
mihalin
d886061981 version bump 2022-07-04 02:50:57 +03:00
mihalin
087891010d fix "two botx in one chat" 2022-07-04 02:46:32 +03:00
mihalin
aa456d3e8d fix 2022-06-30 01:45:44 +03:00
mihalin
4b62762c13 logging print datetime 2022-06-30 01:28:03 +03:00
mihalin
fa2f3f9037 no preview on /start 2022-06-30 01:15:20 +03:00
mihalin
b0d4bc6f27 no removeprefix method 2022-06-30 01:10:18 +03:00
mihalin
55e99becd0 python3.8 2022-06-30 01:09:51 +03:00
mihalin
83db08c93c python3.9 2022-06-26 02:58:19 +03:00
mihalin
03fb55bf12 poetry update 2022-06-26 02:58:05 +03:00
mihalin
f7a4188a53 chat not found handle 2022-06-26 02:51:19 +03:00
mihalin
bfcf8ca414 bump python version 2022-06-25 01:04:38 +03:00
mihalin
8262854acb changelogs 2022-06-25 00:56:33 +03:00
mihalin
948f6af924 changelogs 2022-06-25 00:55:58 +03:00
mihalin
960dd8be5e civsocit link 2022-06-25 00:42:52 +03:00
mihalin
afe3f83d32 fix html second text 2022-06-25 00:26:10 +03:00
mihalin
9f45fb5338 update lang 2022-06-25 00:21:52 +03:00
mihalin
e2e14cfdc1 Revert "try fix migrations"
This reverts commit 1c33d602e0.
2022-06-25 00:14:31 +03:00
mihalin
1c33d602e0 try fix migrations 2022-06-25 00:07:22 +03:00
mihalin
944c5ce002 html support in /start message 2022-06-24 23:58:59 +03:00
mihalin
4063f9f336 don't accept bot own token (for self-hosted projects) 2022-06-24 23:33:01 +03:00
mihalin
b229a2c7e2 promo minor changes 2022-06-24 23:18:23 +03:00
mihalin
74a04c2792 version bump 2022-06-24 23:09:43 +03:00
mihalin
2debd22333 handle more errors 2022-06-24 23:08:32 +03:00
mihalin
09416e94f5 edited message experimental 2022-06-16 04:23:57 +03:00
mihalin
93d65d87c6 Merge branch 'stable' 2022-06-16 04:14:36 +03:00
mihalin
d6b80b8f66 fix additional info messages 2022-06-16 04:10:08 +03:00
mihalin
e3d579fa02 edited message handler experimental 2022-06-16 03:53:19 +03:00
mihalin
27fe37bd6b freeze redis version 2022-06-16 03:42:20 +03:00
mihalin
03437146f1 max bot count for promo 2022-06-16 03:22:15 +03:00
mihalin
c58a4b90d5 flake8 2022-06-16 03:10:09 +03:00
mihalin
3196eed2ac minor fix for prev 2022-06-16 03:07:53 +03:00
mihalin
883879e390 user info minor improvement 2022-06-16 02:59:38 +03:00
mihalin
0d31679280 version inc 2022-05-26 13:53:10 +03:00
mihalin
b2243587a5 увеличить время хранения идентификаторов для уже состоявшихся диалогов 2022-05-26 13:52:47 +03:00
mihalin
e268e5a895 increase redis timeout 2022-05-26 13:16:16 +03:00
mihalin
3725e3fff2 fix notify 2022-05-17 11:34:48 +03:00
mihalin
2891d1cd8b забытые changelogs 2022-05-14 09:57:32 +03:00
mihalin
2909410ce6 minor fixes for prev 2022-05-12 16:17:06 +03:00
mihalin
d5c003400a fix for prev 2022-05-12 16:07:21 +03:00
mihalin
15083fed8d notification, first iteration 2022-05-12 15:59:37 +03:00
mihalin
80f52d0713 Merge branch 'main' into stable 2022-04-12 16:31:54 +03:00
mihalin
dd916da876 забытая надпись в переводе 2022-04-12 16:10:39 +03:00
mihalin
09fc309e38 ignore mo pot 2022-04-12 16:01:28 +03:00
mihalin
483aa4165d english 2022-04-11 18:16:00 +03:00
mihalin
0455c6d022 fix empty locale 2022-04-11 17:24:13 +03:00
mihalin
a7ae47f2a7 flake8 fix 2022-04-11 17:17:35 +03:00
mihalin
cae7822ce3 fix for prev 2022-04-11 16:59:42 +03:00
mihalin
f5407d744d version inc 2022-04-11 15:54:56 +03:00
mihalin
059e97a96d автоматический перевод некоторых сообщений в зависимости от локали устройства 2022-04-11 15:51:00 +03:00
mihalin
b09f8d9cb6 Слава Україні (uk language support) 2022-04-09 07:15:27 +03:00
mihalin
1c4ce35829 возможность отзывать токен 2022-04-09 06:10:48 +03:00
mihalin
e78b0c1150 fix for prev 2022-04-09 05:58:37 +03:00
mihalin
ff28f5cea5 "Этот бот создан с помощью...." возможность выключать в промо 2022-04-09 05:42:52 +03:00
mihalin
7e016a0eb2 Revert "hostname debug"
This reverts commit 042daf90c9.
2022-04-06 22:10:07 +03:00
mihalin
042daf90c9 hostname debug 2022-04-06 22:00:59 +03:00
mihalin
5ce03ca50f малая правка текста 2022-04-02 00:54:27 +03:00
mihalin
654d0047da promo first iteration 2022-03-29 23:17:25 +03:00
mihalin
b9fd2881d9 promo first iteration 2022-03-29 22:36:50 +03:00
mihalin
50ed0ac142 Merge branch 'main' into stable 2022-03-26 21:44:51 +03:00
mihalin
512a892bb9 fix #14 2022-03-26 21:11:17 +03:00
mihalin
83a4f6ae2e no additional text for chinese 2022-03-22 08:22:39 +03:00
mihalin
df2d54156b no additional text for chinese 2022-03-22 08:20:31 +03:00
mihalin
9e3ed843e3 some translation fixes 2022-03-22 07:37:56 +03:00
mihalin
a008d09369 some documentation 2022-03-22 07:25:19 +03:00
mihalin
e209d56ce8 flake8 2022-03-22 06:56:39 +03:00
mihalin
5d5b47ea50 OlgramBot text translation 2022-03-22 06:52:00 +03:00
mihalin
14c85ce634 no technical support for self-hosted chinese bots 2022-03-22 06:31:57 +03:00
mihalin
1a9646d607 Chinese language support (suddenly!) 2022-03-22 05:43:10 +03:00
mihalin
db54473e0f minor fix for prev 2022-03-18 00:21:44 +03:00
mihalin
1c22d2d8d7 minor fix for prev 2022-03-17 23:45:48 +03:00
mihalin
f860fb1815 text fixes 2022-03-17 11:25:06 +03:00
mihalin
9d5bf0de53 fix image 2022-03-17 10:54:54 +03:00
mihalin
9101a81640 fix for prev 2022-03-17 10:22:07 +03:00
mihalin
765676b6e1 забытая картинка 2022-03-17 10:13:30 +03:00
mihalin
fd3645fa52 политика конфиденциальности 2022-03-17 09:44:24 +03:00
mihalin
02e06863e7 documentation 2022-03-17 09:11:15 +03:00
mihalin
8efc40730f рабочий ответ на info сообщение 2022-03-17 08:33:40 +03:00
mihalin
afdb623358 fix for prev, minor refactoring 2022-03-17 08:26:14 +03:00
mihalin
3b26fda9e7 fix for prev 2022-03-17 08:08:50 +03:00
mihalin
1779a5607d #11 additional user info 2022-03-17 08:05:03 +03:00
mihalin
90997f5adb increase redis timeout 2022-03-14 02:53:01 +03:00
mihalin
5ed24b9f42 Revert "debug info in /info command"
This reverts commit 3aa878ff87.
2022-03-13 18:12:22 +03:00
mihalin
569e9f6ccb Revert "debug host print"
This reverts commit cc9479327b.
2022-03-13 18:12:22 +03:00
mihalin
cc9479327b debug host print 2022-03-13 17:04:15 +03:00
mihalin
3aa878ff87 debug info in /info command 2022-03-13 16:45:31 +03:00
mihalin
ce408591c4 fix for prev 2022-03-13 02:29:34 +03:00
mihalin
2e03be7829 pass command-line arguments docker 2022-03-13 02:26:22 +03:00
mihalin
10e140814d some debug info 2022-03-13 02:19:33 +03:00
mihalin
59408aaacd readme 2022-03-08 04:21:38 +03:00
mihalin
73bcdcc3c3 fix migration 2022-02-20 10:55:11 +03:00
mihalin
31d2acc7fa doc 2022-02-19 20:45:53 +03:00
mihalin
715d516952 enable and disable threads 2022-02-19 20:40:56 +03:00
mihalin
64ba75e8cb version bump 2022-02-19 05:54:29 +03:00
mihalin
fc607cee5c запуск без olgram и без сервера (переход на два контейнера) 2022-02-19 05:53:53 +03:00
mihalin
6004f9d9af не увеличивать incoming messages при возможных ошибках на бекенде 2022-02-19 05:38:03 +03:00
mihalin
994d96885f about threads 2022-02-19 03:22:07 +03:00
mihalin
773c55f8c0 about threads 2022-02-19 03:20:01 +03:00
mihalin
35148883db Merge branch 'main' into stable 2022-02-19 02:43:50 +03:00
mihalin
88752a01dd reply exception skip 2022-02-19 02:41:59 +03:00
mihalin
bb49f6a702 some changelogs 2022-02-19 02:29:35 +03:00
mihalin
c5c7468e36 Merge branch 'main' into stable 2022-02-19 00:59:03 +03:00
mihalin
2bba944dc0 симметрия в тексте 2022-02-19 00:58:31 +03:00
mihalin
23dacbfe8f Merge branch 'main' into stable 2022-02-19 00:56:10 +03:00
mihalin
4be45985a0 minor edition 2022-02-18 22:09:37 +03:00
mihalin
1768d9e7ea Немного статистики 2022-02-18 21:50:23 +03:00
mihalin
6f602f417f Немного статистики 2022-02-18 21:47:40 +03:00
mihalin
5cff8da9cd webhook less connections 2022-02-18 07:51:51 +03:00
mihalin
36a0bc0f95 ещё немного статистики 2022-02-17 02:56:11 +03:00
mihalin
878abc6a0f threads first iteration 2022-02-16 20:52:08 +03:00
mihalin
d4582d9a9d threads first iteration 2022-02-16 19:56:03 +03:00
mihalin
02df39c9fd redis timeout 2022-02-16 18:45:44 +03:00
mihalin
a504d38418 handle deactivated error 2022-02-12 03:40:28 +03:00
mihalin
4c22563974 Merge branch 'main' into stable 2022-02-12 03:39:29 +03:00
mihalin
24710d6b5f backup ignore 2022-02-12 03:37:55 +03:00
mihalin
2164ee6f2c templates minor improvements 2022-02-12 01:28:10 +03:00
mihalin
a3eb985d28 minor fixes 2022-02-11 21:19:30 +03:00
mihalin
d53a574377 add images 2022-02-11 16:27:21 +03:00
mihalin
fbd546e59a inline doc first step 2022-02-11 16:15:50 +03:00
mihalin
bd31e21699 no spam in hroup chat 2022-02-11 04:42:19 +03:00
mihalin
767cfe64ee inline cache 2022-02-11 04:38:07 +03:00
mihalin
7c3069ccb8 fix for prev 2022-02-11 04:26:09 +03:00
mihalin
a5a6d5beac inline permissions 2022-02-11 04:24:11 +03:00
mihalin
0fbfa9bd1e flake8 2022-02-11 04:08:14 +03:00
mihalin
9e21b15781 fix phrases 2022-02-11 04:06:28 +03:00
mihalin
bd239f6b2f print debug 2022-02-11 03:59:58 +03:00
mihalin
6b3383418e logging 2022-02-11 02:09:09 +03:00
mihalin
a7a08639cf inlines first iteration 2022-02-11 02:02:28 +03:00
mihalin
177603606f flake8 2022-02-11 01:04:15 +03:00
mihalin
96853f4e09 version inc 2022-02-11 01:02:43 +03:00
mihalin
45e28bf9b7 templates first iteration 2022-02-11 01:02:23 +03:00
mihalin
ea5249d1b8 flake8 2022-01-27 04:12:14 +03:00
mihalin
bea77807af
Merge pull request #9 from BelarusRazam/main
Some improvements to development process. Improve Docker compatibility.
2022-01-27 04:05:46 +03:00
GordonFreeman-BY
880269d9d8 Add possibility to set loglevel from environment. 2022-01-24 03:43:16 +03:00
GordonFreeman-BY
d8b580d81b Remove host varible from web server. 2022-01-24 03:35:24 +03:00
mihalin
8bdd8307d5 handle deactivated error 2022-01-23 00:25:49 +03:00
mihalin
01bcbbb052 fix directory creation 2022-01-22 20:31:36 +03:00
mihalin
47cd78a349 $$ 2022-01-19 22:24:48 +03:00
mihalin
fd8c87fb78 doc fix 2022-01-19 21:22:54 +03:00
mihalin
151a4d9cb7 doc 2022-01-19 16:29:35 +03:00
mihalin
cf937f8dc2 fix for prev 2022-01-19 16:15:03 +03:00
mihalin
450e283e50 fix for prev 2022-01-19 16:12:43 +03:00
mihalin
164a251310 fix for prev 2022-01-19 16:09:29 +03:00
mihalin
8d5723e062 no more requirements.txt 2022-01-19 16:05:10 +03:00
mihalin
bc5186ba26 ban unban 2022-01-19 15:56:39 +03:00
mihalin
d0f9042fb6 logging 2022-01-19 15:48:50 +03:00
mihalin
173014fda0 loop 2022-01-19 00:42:29 +03:00
mihalin
9f0c03fb68 version bump 2022-01-19 00:10:37 +03:00
mihalin
603ae506f2 banned migration 2022-01-19 00:09:23 +03:00
mihalin
68502b7756 fix aerich version 2022-01-18 23:42:42 +03:00
mihalin
363391b575 ban\unban commands 2022-01-18 23:28:03 +03:00
mihalin
59b73c33dc add poetry 2022-01-18 03:30:39 +03:00
mihalin
067fbc2736 use poetry 2022-01-18 03:21:28 +03:00
mihalin
4aced9af91 оказывается, мы не поддерживаем heroku 2022-01-14 23:27:41 +03:00
mihalin
facdbbc2fe fox for prev 2022-01-14 20:47:53 +03:00
mihalin
9ce03e048e правки по инструкции после обращения в тех. поддержку: heroku, кавычки в .env 2022-01-12 00:40:39 +03:00
mihalin
47d9f510a9 custom certificate create directory 2022-01-12 00:17:33 +03:00
mihalin
46ba3a57aa fix info 2021-12-24 19:56:01 +03:00
mihalin
971fd178f2 info 2021-12-24 19:46:46 +03:00
mihalin
d83ff39067 enable info command 2021-12-24 19:39:25 +03:00
mihalin
2786af259a info 2021-12-24 19:20:53 +03:00
mihalin
1a1d382243 last changes back 2021-12-24 19:06:48 +03:00
mihalin
74ae7c5d14 debug 2021-12-23 21:35:53 +03:00
mihalin
2c6ef7bed9 search bug ... 2021-12-23 01:00:16 +03:00
mihalin
9d6fccd204 search bug ... 2021-12-23 00:06:38 +03:00
mihalin
c686d8d2d6 fix #8 2021-12-22 23:46:09 +03:00
mihalin
fcba54ccf5 typo 2021-12-22 22:24:41 +03:00
mihalin
b38834b265 При создании группового чата он добавляется в список olgram 2021-12-15 00:14:49 +03:00
mihalin
dcee7a98df example.env мелкие правки 2021-12-14 23:57:43 +03:00
mihalin
645357995b миграция ID чата (fix #7) 2021-12-14 23:55:19 +03:00
mihalin
ba0c2752a1 более понятный текст кнопки автоответчика 2021-12-09 23:05:56 +03:00
mihalin
b2cc2a4827 Merge remote-tracking branch 'origin/main' 2021-10-23 20:34:30 +03:00
mihalin
5a2e950839 Мелкие правки по инструкции 2021-10-23 20:33:33 +03:00
mihalin
6c98c988ca
Update README.md 2021-10-16 02:50:48 +03:00
mihalin
c6266cfdf2
Update README.md 2021-10-16 02:50:35 +03:00
mihalin
de68f0d002
Merge pull request #5 from milksense/patch-1
Update developer.rst
2021-10-11 18:39:20 +03:00
ᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠ
bef0e183b4
Update developer.rst 2021-10-11 16:33:52 +01:00
mihalin
04c7711b74 fix для ограничения прав 2021-10-02 14:47:33 +03:00
mihalin
3bdae028c3 version bump 2021-10-01 22:48:35 +03:00
mihalin
942862f171 update docker-compose-full example 2021-10-01 20:08:42 +03:00
mihalin
e95f21d413 ADMIN_ID документация 2021-10-01 20:06:09 +03:00
mihalin
1aeec0c9d8 Возможность ограничивать права на бота 2021-10-01 19:48:23 +03:00
mihalin
5fcb5b8900 запуск в режиме отладки 2021-09-28 01:43:57 +03:00
mihalin
f0237ecb0b добавил про шифрование в документацию 2021-09-27 05:01:28 +03:00
mihalin
4790a21f60 version increment 2021-09-26 20:42:30 +03:00
mihalin
ea8d251142 flake8 fix, server back 2021-09-26 20:37:51 +03:00
mihalin
2e61640f5a Шифрование токенов 2021-09-26 20:36:05 +03:00
mihalin
188b58d8e2 Добавлен второй текст бота 2021-09-26 19:06:03 +03:00
mihalin
0f84b67b49 Добавлен второй текст бота 2021-09-26 18:15:46 +03:00
mihalin
8013c8c8e4 мелкая правка по докам 2021-09-24 15:11:24 +03:00
mihalin
11f8004c55 убран tab в команде start 2021-09-24 14:38:15 +03:00
mihalin
0487838942 опечатка 2021-09-24 14:29:25 +03:00
mihalin
ddea5ba06c больше ботов на одного пользователя 2021-09-24 14:09:01 +03:00
mihalin
5c7ced1549 dot 2021-09-24 03:07:49 +03:00
mihalin
118b24df8f flake8 fixes 2021-09-24 02:59:14 +03:00
mihalin
0d8081be35 docks in start message 2021-09-24 02:57:57 +03:00
mihalin
c59fc4ebc1 minor documentation changes 2021-09-24 02:55:21 +03:00
mihalin
64538aa17f minor вщс changes 2021-09-24 02:02:13 +03:00
mihalin
7e3bc13f14 minor changes 2021-09-22 21:41:28 +03:00
mihalin
e4ec20a5c4 minor changes 2021-09-22 21:38:22 +03:00
mihalin
cbc3b586a7 minor changes 2021-09-22 21:34:15 +03:00
mihalin
5cf69e3ea9 minor changes 2021-09-22 21:17:26 +03:00
mihalin
04a3efd3fb quick start 2021-09-22 21:00:50 +03:00
mihalin
29d7118833 documentation first iteration 2021-09-22 19:19:11 +03:00
mihalin
7c6abd5558 flake8 2021-09-17 21:26:52 +03:00
mihalin
d9d37f4a5f version bump 2021-09-17 21:20:20 +03:00
mihalin
71e905b0be chat menu more description 2021-09-17 21:20:09 +03:00
92 changed files with 5844 additions and 219 deletions

View File

@ -2,3 +2,4 @@
venv
config.json
*.yaml
docs/

View File

@ -1,3 +1,3 @@
[flake8]
exclude = .git,__pycache__,venv
exclude = .git,__pycache__,venv,.venv
max-line-length = 120

View File

@ -4,6 +4,8 @@ on: push
env:
PYTHONUNBUFFERED: 1
POETRY_VERSION: 1.1.2
POETRY_VIRTUALENVS_CREATE: "false"
jobs:
lint:
@ -17,7 +19,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install flake8
pip install poetry
poetry install
- name: Check flake8
run: python -m flake8 .
run: flake8 .

7
.gitignore vendored
View File

@ -6,3 +6,10 @@ __pycache__
*.pyc
config.json
docker-compose-release.yaml
docs/build
ad.md
release.env
test.py
backup
*.mo
*.pot

25
.readthedocs.yaml Normal file
View File

@ -0,0 +1,25 @@
# Read the Docs configuration file for Sphinx projects
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
# Set the OS, Python version and other tools you might need
build:
os: ubuntu-22.04
tools:
python: "3.8"
jobs:
post_create_environment:
- python -m pip install sphinx_rtd_theme
# Build documentation in the "docs/" directory with Sphinx
sphinx:
configuration: docs/source/conf.py

View File

@ -1,11 +1,26 @@
FROM python:3.8-buster
COPY . /app
ENV PYTHONUNBUFFERED=1 \
POETRY_VERSION=1.5.1 \
POETRY_VIRTUALENVS_CREATE="false"
RUN apt-get update && \
apt-get install -y gettext build-essential && \
apt-get clean && rm -rf /var/cache/apt/* && rm -rf /var/lib/apt/lists/* && rm -rf /tmp/*
RUN pip install "poetry==$POETRY_VERSION"
WORKDIR /app
RUN pip install --upgrade pip && \
pip install -r requirements.txt
COPY pyproject.toml poetry.lock docker-entrypoint.sh ./
RUN poetry install --no-interaction --no-ansi --no-dev
COPY . /app
RUN msgfmt locales/zh/LC_MESSAGES/olgram.po -o locales/zh/LC_MESSAGES/olgram.mo --use-fuzzy
RUN msgfmt locales/uk/LC_MESSAGES/olgram.po -o locales/uk/LC_MESSAGES/olgram.mo --use-fuzzy
RUN msgfmt locales/en/LC_MESSAGES/olgram.po -o locales/en/LC_MESSAGES/olgram.mo --use-fuzzy
EXPOSE 80

View File

@ -1,44 +1,26 @@
# OLGram
[@olgram](https://t.me/olgrambot) - конструктор ботов обратной связи в Telegram
[![Static Analysis Status](https://github.com/civsocit/olgram/workflows/Linter/badge.svg)](https://github.com/civsocit/olgram/actions?workflow=Linter)
[![Deploy Status](https://github.com/civsocit/olgram/workflows/Deploy/badge.svg)](https://github.com/civsocit/olgram/actions?workflow=Deploy)
[![Documentation](https://readthedocs.org/projects/olgram/badge/?version=latest)](https://olgram.readthedocs.io)
![Logo](media/logo1_big.png)
[@OlgramBot](https://t.me/olgrambot) - конструктор ботов обратной связи в Telegram
## Возможности и преимущества Olgram Bot
* **Общение с клиентами**. После подключения бота, вы сможете общаться с вашими пользователями бота через диалог с
ботом, либо подключенный отдельно чат, где может находиться ваш колл-центр.
* **Все типы сообщений**. Livegram боты поддерживают все типы сообщений — текст, фото, видео, голосовые сообщения и
стикеры.
* **Open-source**. В отличие от известного проекта Livegram код нашего конструктора полностью открыт.
* **Self-hosted**. Вы можете развернуть свой собственный конструктор, если не доверяете нашему.
* **Безопасность**. В отличие от Livegram, мы не храним сообщения, которые вы отправляете в бот. А наши сервера
располагаются в Германии, что делает проект неподконтрольным российским властям.
Документация: https://olgram.readthedocs.io
По любым вопросам, связанным с Olgram, пишите в наш бот обратной связи
[@civsocit_feedback_bot](https://t.me/civsocit_feedback_bot)
**Olgram** [@OlgramBot](https://t.me/olgrambot) это конструктор, который позволяет создавать боты обратной связи
в Telegram. После подключения к Olgram пользователи вашего бота смогут писать сообщения, которые будут
пересылаться вам в чат, где вы сможете на них ответить.
### Для разработчиков: сборка и запуск проекта
Такие боты могут вам пригодиться, например:
Вам потребуется собственный VPS или любой хост со статическим адресом или доменом.
* Создайте файл .env и заполните его по образцу example.env. Вам нужно заполнить переменные:
* BOT_TOKEN - токен нового бота, получить у [@botfather](https://t.me/botfather)
* POSTGRES_PASSWORD - любой случайный пароль
* WEBHOOK_HOST - IP адрес или доменное имя сервера, на котором запускается проект
* Сохраните файл docker-compose.yaml и соберите его:
```
sudo docker-compose up -d
```
*Пример 1.* Вы администрируете Telegram-канал и хотите дать своим подписчикам возможность связаться с вами,
но не хотите оставлять свои личные контакты. Тогда вы можете создать бота обратной связи: подписчики будут писать
боту, вы будете отвечать через бота анонимно.
В docker-compose.yaml минимальная конфигурация. Для использования в серьёзных проектах мы советуем:
* Приобрести домен и настроить его на свой хост
* Наладить реверс-прокси и автоматическое обновление сертификатов - например, с помощью
[Traefik](https://github.com/traefik/traefik)
* Скрыть IP сервера с помощью [Cloudflire](https://www.cloudflare.com), чтобы пользователи ботов не могли найти IP адрес
хоста по Webhook бота.
*Пример 2.* Вы организуете небольшой call-центр в Telegram или группу технической поддержки. С помощью бота обратной
связи вы можете принимать заявки от пользователей в общий чат ваших специалистов, обсуждать эти заявки и отвечать
пользователям прямо из этого чата.
Пример более сложной конфигурации есть в файле docker-compose-full.yaml
Читайте больше: https://olgram.readthedocs.io

View File

@ -1,7 +1,9 @@
# Конфигурация, удобная для разработки в PyCharm: бот запускается без docker, порты postgres и redis открыты на localhost
# Не используйте её в production!
version: '3'
services:
postgres:
image: kartoza/postgis
image: postgres:14
environment:
- POSTGRES_USER=test_user
- POSTGRES_PASSWORD=test_passwd
@ -11,7 +13,7 @@ services:
volumes:
- database:/var/lib/postgresql/data
redis:
image: 'bitnami/redis:latest'
image: 'bitnami/redis:6.2.7'
environment:
- ALLOW_EMPTY_PASSWORD=yes
volumes:

View File

@ -1,7 +1,8 @@
# Пример сложной конфигурации сервера: реверс-прокси, автоматическое обновление github
version: '3'
services:
postgres:
image: postgres
image: postgres:13.4
restart: unless-stopped
env_file:
- release.env
@ -9,8 +10,10 @@ services:
- database:/var/lib/postgresql/data
networks:
- traefik
labels:
- 'com.centurylinklabs.watchtower.enable="false"'
redis:
image: 'bitnami/redis:latest'
image: 'bitnami/redis:6.2.7'
restart: unless-stopped
environment:
- ALLOW_EMPTY_PASSWORD=yes
@ -20,6 +23,8 @@ services:
- release.env
networks:
- traefik
labels:
- 'com.centurylinklabs.watchtower.enable="false"'
olgram:
image: ghcr.io/civsocit/olgram/bot:stable
restart: unless-stopped
@ -71,6 +76,8 @@ services:
- --certificatesresolvers.le.acme.tlschallenge=false
- --certificatesresolvers.le.acme.httpchallenge=true
- --certificatesresolvers.le.acme.httpchallenge.entrypoint=web
labels:
- 'com.centurylinklabs.watchtower.enable="false"'
volumes:
database:

36
docker-compose-src.yaml Normal file
View File

@ -0,0 +1,36 @@
# Минимальная конфигурация сервера, код собирается из текущей директории
version: '3'
services:
postgres:
image: postgres:13.4
restart: unless-stopped
env_file:
- .env
volumes:
- database:/var/lib/postgresql/data
redis:
image: 'bitnami/redis:latest'
restart: unless-stopped
environment:
- ALLOW_EMPTY_PASSWORD=yes
volumes:
- redis-db:/bitnami/redis/data
env_file:
- .env
olgram:
build: .
restart: unless-stopped
env_file:
- .env
volumes:
- olgram-cert:/cert
ports:
- "${WEBHOOK_PORT}:80"
depends_on:
- postgres
- redis
volumes:
database:
redis-db:
olgram-cert:

View File

@ -1,3 +1,4 @@
# Минимальная конфигурация сервера
version: '3'
services:
postgres:
@ -8,7 +9,7 @@ services:
volumes:
- database:/var/lib/postgresql/data
redis:
image: 'bitnami/redis:latest'
image: 'bitnami/redis:6.2.7'
restart: unless-stopped
environment:
- ALLOW_EMPTY_PASSWORD=yes

View File

@ -5,10 +5,11 @@ if [ ! -z "${CUSTOM_CERT}" ]; then
echo "Use custom certificate"
if [ ! -f /cert/private.key ]; then
echo "Generate new certificate"
openssl req -newkey rsa:2048 -sha256 -nodes -keyout /cert/private.key -x509 -days 1000 -out /cert/public.pem -subj "/C=US/ST=Berlin/L=Berlin/O=my_org/CN=${WEBHOOK_HOST}"
openssl req -newkey rsa:2048 -sha256 -nodes -keyout /cert/private.key -x509 -days 10000 -out /cert/public.pem -subj "/C=US/ST=Berlin/L=Berlin/O=my_org/CN=${WEBHOOK_HOST}"
fi
fi
sleep 10
aerich upgrade
python main.py
python migrate.py
python main.py $@

20
docs/Makefile Normal file
View File

@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = source
BUILDDIR = build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

BIN
docs/images/addbot.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

BIN
docs/images/added.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

BIN
docs/images/ban1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 KiB

BIN
docs/images/ban2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 KiB

BIN
docs/images/botfather.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 560 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 KiB

BIN
docs/images/chat1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

BIN
docs/images/chat2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

BIN
docs/images/inline.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 65 KiB

BIN
docs/images/logo1_big.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

View File

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

BIN
docs/images/start.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

BIN
docs/images/test.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

BIN
docs/images/test2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

BIN
docs/images/text1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

BIN
docs/images/text2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

BIN
docs/images/text3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

BIN
docs/images/thread.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

BIN
docs/images/user_info.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

35
docs/make.bat Normal file
View File

@ -0,0 +1,35 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=source
set BUILDDIR=build
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd

41
docs/source/about.rst Normal file
View File

@ -0,0 +1,41 @@
О проекте
===================================
Зачем нужен Olgram
------------
Olgram - это конструктор ботов обратной связи. Такие боты могут вам пригодиться, например:
*Пример 1.* Вы администрируете Telegram-канал и хотите дать своим подписчикам возможность связаться с вами,
но не хотите оставлять свои личные контакты. Тогда вы можете создать бота обратной связи: подписчики будут писать
боту, вы будете отвечать через бота анонимно.
*Пример 2.* Вы организуете небольшой call-центр в Telegram или группу технической поддержки. С помощью бота обратной
связи вы можете принимать заявки от пользователей в общий чат ваших специалистов, обсуждать эти заявки и отвечать
пользователям прямо из этого чата.
.. note::
Olgram - молодой развивающийся проект. Мы готовы расширить функционал бота под ваш сценарий использования, если он
не является слишком узкоспециализированным и пригодится другим пользователям. Напишите нам по этому вопросу
`@civsocit_feedback_bot <https://t.me/civsocit_feedback_bot>`_.
Почему не Livegram?
------------
Наше принципиальное отличие от `Livegram <https://t.me/LivegramBot>`_ это открытость и безопасность.
* Olgram не хранит сообщения, которые вы пересылаете через него
* Код нашего проекта `полностью открыт <https://github.com/civsocit/olgram>`_
* Вы можете развернуть Olgram на своём собственном сервере (читайте :doc:`developer`)
* Наши сервера находятся в Германии, мы не подконтрольны российскому или белорусскому правительству
Всё это позволяет вам использовать Olgram (в отличие от Livegram) в политических и других серьёзных проектах.
Чтобы приступить к созданию своего первого бота, откройте главу :doc:`quick_start`
.. note::
Если у вас возникли вопросы по использованию бота, или вы нашли ошибку - напишите
нам `@civsocit_feedback_bot <https://t.me/civsocit_feedback_bot>`_.

View File

@ -0,0 +1,52 @@
Дополнительно
=============
Донаты
----------------
На аренду сервера для этого проекта
Bitcoin:
``bc1qlq7cm5chc8flr3fy8ewk967aknq3dwmxtwn9hl``
Litecoin:
``ltc1qxajsvz0lw44aa5nytuch8cp2g8x7a4cdase4y7``
Monero:
``84ymMfpw3vxFxsgmYbFURMiZLgQCmhKsZNiZiqZRbpH2WRka2UDjyDVZpX8XH1cZ9d5EghvPXrF5hEuzvK5NvHGE8za4Gmk``
Как убрать "Этот бот создан с помощью ..."
----------------
Напишите нам на `@civsocit_feedback_bot <https://t.me/civsocit_feedback_bot>`_.
История изменений
----------------
- `2024-03-15` Тэги
- `2024-03-02` Рассылки
- `2024-03-01` Непрерывные потоки сообщений (опция)
- `2024-02-17` Опция смены режима работы автоответчика: автоответчик отвечает на КАЖДОЕ сообщение
- `2024-01-12` Мультиязычность (стартовое сообщение и автоответчик)
- `2022-08-01` Защита от флуда
- `2022-07-23` Автоответчик не пишет сообщение лишний раз
- `2022-07-04` Поддержка двух ботов в одном чате
- `2022-06-25` Поддержка HTML\Markdown в стартовом сообщении и автоответчике
- `2022-06-25` Пересылка отредактированных сообщений
- `2022-06-16` User info по возможности отправляется в том же сообщении, что и сообщение пользователя
- `2022-05-26` Возможность отвечать на более старые сообщения (1/2 года)
- `2022-04-11` Частичная поддержка украинского, английского языка
- `2022-04-09` 'Этот бот создан с помощью...' возможность выключать по промо
- `2022-03-17` Политика конфиденциальности
- `2022-03-17` Дополнительная информация о пользователях (имя пользователя и тд)
- `2022-02-19` Статистика использования бота
- `2022-02-16` Потоки сообщений
- `2022-02-16` Очистка Redis по timeout
- `2022-02-12` Шаблоны ответов
- `2022-01-27` Настройки логирования
- `2022-01-18` Команды /ban и /unban (возможность банить пользователей)
- `2021-12-14` Bugfix обработка изменения ID чата
- `2021-10-01` Возможность ограничивать права на бота (ADMIN_ID)
- `2021-09-26` Шифрование токенов
- `2021-09-26` Добавлен автоответчик
- `2021-09-24` Initial

35
docs/source/conf.py Normal file
View File

@ -0,0 +1,35 @@
# Configuration file for the Sphinx documentation builder.
# -- Project information
project = 'Olgram'
copyright = '2024, Civsocit'
author = 'civsocit'
release = '0.1'
version = '0.1.0'
# -- General configuration
extensions = [
'sphinx.ext.duration',
'sphinx.ext.doctest',
'sphinx.ext.autodoc',
'sphinx.ext.autosummary',
'sphinx.ext.intersphinx',
]
intersphinx_mapping = {
'python': ('https://docs.python.org/3/', None),
'sphinx': ('https://www.sphinx-doc.org/en/master/', None),
}
intersphinx_disabled_domains = ['std']
templates_path = ['_templates']
# -- Options for HTML output
html_theme = 'sphinx_rtd_theme'
# -- Options for EPUB output
epub_show_urls = 'footnote'

80
docs/source/developer.rst Normal file
View File

@ -0,0 +1,80 @@
Для разработчиков
=================
.. _run:
Сборка и запуск
---------------
Вы можете развернуть Olgram на своём сервере. Вам потребуется собственный VPS или любой хост со статическим адресом
или доменом.
1. Создайте файл .env и заполните его по образцу `example.env <https://github.com/civsocit/olgram/blob/main/example.env>`_
Вам нужно заполнить переменные:
* ``BOT_TOKEN`` - токен нового бота, получить у `@botfather <https://t.me/botfather>`_
* ``POSTGRES_PASSWORD`` - любой случайный пароль
* ``TOKEN_ENCRYPTION_KEY`` - любой случайный пароль, отличный от POSTGRES_PASSWORD
* ``WEBHOOK_HOST`` - IP адрес или доменное имя сервера, на котором запускается проект
2. Рядом с файлом .env сохраните файл
`docker-compose.yaml <https://github.com/civsocit/olgram/blob/main/docker-compose.yaml>`_ и соберите его:
.. code-block:: console
(bash) $ sudo docker-compose up -d
Готово, ваш собственный Olgram запущен!
.. warning::
Не потеряйте TOKEN_ENCRYPTION_KEY! Его нельзя восстановить. В случае утери TOKEN_ENCRYPTION_KEY вы потеряете
токены всех ботов, которые пользователи зарегистрировали в вашем боте.
Возможно, вы захотите внести изменения в проект и запустить бот с этими изменениями. Тогда:
1. Склонируйте репозиторий
.. code-block:: console
(bash) $ git clone https://github.com/civsocit/olgram
2. Внесите в код все изменения, которые хотите внести
3. В каталоге с репозиторием (рядом с файлами .yaml) создайте файл .env и заполните его, как в инструкции выше
4. Соберите и запустите сервер:
.. code-block:: console
(bash) $ sudo docker-compose -f docker-compose-src.yaml up -d
Дополнительно
-------------
В docker-compose.yaml приведена минимальная конфигурация. Для использования в серьёзных проектах мы советуем:
* Приобрести домен и настроить его на свой хост
* Наладить реверс-прокси и автоматическое обновление сертификатов - например, с помощью `Traefik <https://github.com/traefik/traefik>`_
* Скрыть IP сервера с помощью `Cloudflare <https://www.cloudflare.com>`_, чтобы пользователи ботов не могли найти IP адрес хоста по Webhook бота.
Пример более сложной конфигурации есть в файле `docker-compose-full.yaml <https://github.com/civsocit/olgram/blob/main/docker-compose-full.yaml>`_
Как ограничить доступ к своему боту
-----------------------------------
По-умолчанию все пользователи Telegram могут писать в ваш Olgram и регистрировать там своих ботов. Чтобы ограничить
доступ к боту, укажите в переменных окружения (файл .env):
``ADMIN_ID=<идентификатор чата>``
Идентификатор чата это либо ваш Telegram ID, либо ID группового чата Telegram. Идентификатор можно посмотреть
командой /chatid.
Настройка языка
---------------
Язык по-умолчанию - русский. Поддержку другого языка можно добавлять по образцу китайского в папку locales/
(китайский - zh). Код языка указать в настройках .env
``O_LANG=<идентификатор языка>``

25
docs/source/index.rst Normal file
View File

@ -0,0 +1,25 @@
Добро пожаловать в документацию Olgram
===================================
**Olgram** `@olgrambot <https://t.me/olgrambot>`_ это конструктор, который позволяет создавать боты обратной связи
в Telegram. После подключения к Olgram пользователи вашего бота смогут писать сообщения, которые будут
пересылаться вам в чат, где вы сможете на них ответить. Читайте больше о проекте в главе :doc:`about`.
Откройте главу :doc:`quick_start` чтобы приступить к созданию своего первого бота!
Оглавление
--------
.. toctree::
about
quick_start
templates
options
developer
additional
.. note::
Если у вас возникли вопросы по использованию бота, или вы нашли ошибку - напишите
нам `@civsocit_feedback_bot <https://t.me/civsocit_feedback_bot>`_.

83
docs/source/options.rst Normal file
View File

@ -0,0 +1,83 @@
Опции
=============
.. _threads:
Потоки сообщений
----------------
Olgram пересылает сообщения так, чтобы сообщения от одного и того же пользователя оставались в одном и том же
потоке сообщений. Тогда по кнопке View Replies можно увидеть диалог с этим пользователем, а все остальные сообщения из
чата скрываются:
.. image:: ../images/thread.gif
:width: 300
**Как настроить потоки сообщений**
Привяжите вашего feedback бота к групповому чату :doc:`quick_start`. В настройках группового чата откройте историю
чата для новых участников чата ("Chat history for new members -> Visible"). Изменение этой настройки превращает чат в
`супергруппу <https://telegram.org/blog/supergroups5k>`_: потоки сообщений работают только в таких группах
Включите потоки в настройках бота Olgram Опции->Потоки сообщений
.. _user_info:
Данные пользователя
-------------------
При получении входящего сообщения Olgram может пересылать дополнительную информацию об отправителе. Имя, username и
идентификатор пользователя. Например так:
.. image:: ../images/user_info.jpg
:width: 300
Эта функция может быть полезной, чтобы отличить одного пользователя от другого. Имя и username можно сменить, но
идентификатор #id остаётся неизменным для одного и того же аккаунта.
Включить эту функцию можно в настройках бота Olgram Опции->Данные пользователя
.. note::
Включение этой опции меняет текст политики конфиденциальности вашего feedback бота (команда /security_policy)
и может отпугнуть некоторых пользователей. Не включайте эту опцию без необходимости.
.. _antiflood:
Защита от флуда
---------------
При включении этой опции пользователю запрещается отправлять больше одного сообщения в минуту. Используйте её, если
не успеваете обрабатывать входящие сообщения.
.. _always_second_message:
Использовать автоответчик всегда
--------------------------------
По-умолчанию автоответчик отвечает только на первое сообщение в диалоге с пользователем. Чтобы автоответчик отвечал на
КАЖДОЕ входящее сообщение, включите эту опцию.
.. thread_interrupt:
Прерывать поток
--------------------------------
По-умолчанию поток сообщений от одного пользователя прерывается каждые 24 часа. Без этой опции поток сообщений не
прерывается никогда.
.. _mailing:
Рассылка
---------------
После включения этой опции ваш бот будет запоминать всех пользователей, которые пишут в ваш бот.
Вы сможете запустить рассылку по этим пользователям.
.. note::
Включение этой опции меняет текст политики конфиденциальности вашего feedback бота (команда /security_policy)
и может отпугнуть некоторых пользователей. Не включайте эту опцию без необходимости.

128
docs/source/quick_start.rst Normal file
View File

@ -0,0 +1,128 @@
Быстрый старт
=============
Как создать бота
----------------
Перейдите по ссылке `@OlgramBot <https://t.me/olgrambot>`_ и нажмите Запустить:
.. image:: ../images/start.jpg
:width: 300
Нажмите ссылку "addbot":
.. image:: ../images/addbot.jpg
:width: 300
И перейдите по ссылке создания бота:
.. image:: ../images/botfather.jpg
:width: 300
BotFather - это официальный бот Telegram, создающий другие боты, которые и будут помогать вам управлять каналами.
Запустите его как и Olgram bot и кликните по ссылке "/newbot":
.. image:: ../images/botfathernew.png
:width: 300
В диалоге прописываете, как вы хотите что бы назывался бот, и название ссылки, ведущей к этому боту. В итоге вы
получаете токен:
.. image:: ../images/botfathertoken.png
:width: 300
Скопируйте этот токен и отправьте в Olgram:
.. image:: ../images/added.jpg
:width: 300
Готово! Бот добавлен в Olgram. Теперь для человека,желающего что-то спросить, бот будет выглядеть примерно так:
.. image:: ../images/test.jpg
:width: 300
Для вас же это будет выглядеть так:
.. image:: ../images/test2.jpg
:width: 300
Как изменить текст приветствия
------------------------------
По-умолчанию ваш бот после запуска отправляет приветственное сообщение:
Здравствуйте! Напишите свой вопрос, и мы ответим вам в ближайшее время
Вы можете настроить этот текст. Для этого откройте список ботов командой /mybots и выберите нужного бота:
.. image:: ../images/text1.jpg
:width: 300
В появившемся меню выберите "Текст"
.. image:: ../images/text2.jpg
:width: 300
Теперь просто отправьте новый текст приветствия.
.. note::
Чтобы настроить особый текст приветствия для, например, русскоязычных пользователей (т.е. тех пользователей, у
которых в настройках Telegram выставлена русская локализация), нажмите кнопку "Руссикй 🇷🇺" и только потом отправьте
текст приветствия. Чтобы отредактировать текст приветствия для всех остальных языков, нажмите "[все языки]".
Как привязать бота к групповому чату
------------------------------------
По-умолчанию ваш бот пересылает сообщения от пользователей вам в личные сообщения. Бота можно привязать к групповому
чату. Для этого добавьте его в групповой чат. Затем откройте список ботов, как в примере выше, выберите нужного бота
и нажмите кнопку "Чат":
.. image:: ../images/chat1.jpg
:width: 300
Затем выберите в списке тот чат, в который добавили бота
.. image:: ../images/chat2.jpg
:width: 300
Готово. Теперь сообщения от пользователей будут пересылаться в групповой чат.
.. note::
Нужно сначала зарегистрировать своего бота в Olgram, и только потом добавить в групповой чат. Если бот уже
добавлен в групповой чат, удалите его оттуда и добавьте заново - тогда Olgram сможет пересылать туда сообщения.
Как блокировать и разблокировать пользователей
------------------------------------
Вы можете отправлять в бан пользователей feedback бота. Для этого есть команды /ban и /unban.
Например так:
.. image:: ../images/ban2.png
:width: 300
Со стороны пользователя этот диалог будет выглядеть так:
.. image:: ../images/ban1.png
:width: 300
.. note::
Если у вас возникли вопросы по использованию бота, или вы нашли ошибку - напишите
нам `@civsocit_feedback_bot <https://t.me/civsocit_feedback_bot>`_.
Тэги пользователей
------------------
Пользователям можно проставлять теги. Например, в ответ на сообщение пользователя написать:
```
/tag #important
```
Тогда в user info (см. раздел опции) помимо информации о пользователе будет тег #important

32
docs/source/templates.rst Normal file
View File

@ -0,0 +1,32 @@
Шаблоны ответов
=============
Иногда в поддержке приходится отвечать на однотипные вопросы однотипными ответами. Например:
Q. ``Здравствуйте! Когда будет доставлен мой заказ?``
A. ``Добрый день. Ваш заказ принят в обработку. Среднее время доставки 2-4 дня. Мы уведомим вас об изменении статуса заказа``
Чтобы не печатать каждый раз одинаковые тексты, в Olgram можно задать список шаблонных ответов. Тогда диалог с
пользователем может выглядеть так:
.. image:: ../images/inline.gif
:width: 300
Заметьте, чтобы увидеть список вариантов ответов, нужно упомянуть вашего feedback бота и нажать пробел
Как настроить шаблоны
---------------------
Шаблоны можно задать в меню Olgram бота Текст -> Автоответчик -> Шаблоны ответов.
.. image:: ../images/settemplates.jpg
:width: 300
Обязательно включите inline mode в вашем feedback боте. Для этого отправьте @BotFather команду ``/setinline``
и следуйте инструкциям
.. note::
Может пройти несколько минут, прежде чем добавленные в OlgramBot шаблоны появятся в списке вашего feedback бота

View File

@ -1,12 +1,35 @@
BOT_TOKEN=YOUR_BOT_TOKEN_HERE # example: 123456789:AAAA-abc123_AbcdEFghijKLMnopqrstu12
# example: 123456789:AAAA-abc123_AbcdEFghijKLMnopqrstu12 (without quotes!)
BOT_TOKEN=YOUR_BOT_TOKEN_HERE
POSTGRES_USER=olgram
POSTGRES_PASSWORD=SOME_RANDOM_PASSWORD_HERE # example: x2y0n27ihiez93kmzj82
# example: x2y0n27ihiez93kmzj82 (without quotes!)
POSTGRES_PASSWORD=SOME_RANDOM_PASSWORD_HERE
POSTGRES_DB=olgram
POSTGRES_HOST=postgres
WEBHOOK_HOST=YOUR_HOST_HERE # example: 11.143.142.140 or my_domain.com
WEBHOOK_PORT=8443 # allowed: 80, 443, 8080, 8443
CUSTOM_CERT=true # use that if you don't set up your own domain and let's encrypt certificate
# example: i7flci0mx4z5patxnl6m (without quotes!)
TOKEN_ENCRYPTION_KEY=SOME_RANDOM_PASSWORD_HERE
# use your user id or group chat id to restrict access to the bot
# ADMIN_ID=223453418
# use your user id or group chat id to give selected users access to the bot's general statistics (/info command)
# SUPERVISOR_ID=223453419
# example: 11.143.142.140 or my_domain.com (without quotes, without 'https://' prefix!)
WEBHOOK_HOST=YOUR_HOST_HERE
# allowed: 80, 443, 8080, 8443
WEBHOOK_PORT=8443
# use that if you don't set up your own domain and let's encrypt certificate
CUSTOM_CERT=true
REDIS_PATH=redis://redis
# Set log level, can be CRITICAL, ERROR, WARNING, INFO, DEBUG. By default it set to WARNING.
LOGLEVEL=
# Uncomment this to switch bot language to English
# O_LANG=en

View File

@ -0,0 +1,813 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR ORGANIZATION
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: \n"
"POT-Creation-Date: 2024-03-02 19:47+0400\n"
"PO-Revision-Date: 2024-03-02 19:48+0400\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: en_US\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"Generated-By: pygettext.py 1.5\n"
"X-Generator: Poedit 3.4.2\n"
#: olgram/commands/admin.py:21 olgram/commands/info.py:21
#: olgram/commands/promo.py:23 olgram/commands/promo.py:39
msgid "Недостаточно прав"
msgstr "Not enough permissions"
#: olgram/commands/admin.py:27
msgid "Нужно указать имя бота"
msgstr "You need to specify the bot's name"
#: olgram/commands/admin.py:33
msgid "Такого бота нет в системе"
msgstr "There is no such bot"
#: olgram/commands/admin.py:39 olgram/commands/admin.py:53
msgid "Пропустить"
msgstr "Skip"
#: olgram/commands/admin.py:42
msgid ""
"Введите текст, который будет отправлен владельцу бота {0}. Напишите "
"'Пропустить' чтобы отменить"
msgstr ""
"Enter the text that will be sent to the owner of the bot {0}. Write 'Skip' "
"to cancel"
#: olgram/commands/admin.py:50
msgid "Поддерживается только текст"
msgstr "Only text is supported"
#: olgram/commands/admin.py:55 olgram/commands/admin.py:71
msgid "Отменено"
msgstr "Cancelled"
#: olgram/commands/admin.py:61 olgram/commands/admin.py:69
msgid "Отправить"
msgstr "Send"
#: olgram/commands/admin.py:62
msgid "Отменить"
msgstr "Cancel"
#: olgram/commands/admin.py:81
msgid "Отправлено"
msgstr "Sent"
#: olgram/commands/bot_actions.py:27
msgid "Бот удалён"
msgstr "Bot removed"
#: olgram/commands/bot_actions.py:49 olgram/commands/bot_actions.py:67
msgid "Текст сброшен"
msgstr "Text is reset"
#: olgram/commands/bot_actions.py:81
msgid "Выбран личный чат"
msgstr "Personal chat selected"
#: olgram/commands/bot_actions.py:94
msgid "Бот вышел из чатов"
msgstr "Bot leaved chats"
#: olgram/commands/bot_actions.py:100
msgid "Нельзя привязать бота к этому чату"
msgstr "You can't bind a bot to this chat room"
#: olgram/commands/bot_actions.py:104
msgid "Выбран чат {0}"
msgstr "Selected chat {0}"
#: olgram/commands/bots.py:46
msgid ""
"У вас уже слишком много ботов. Удалите какой-нибудь свой бот из Olgram(/"
"mybots -> (Выбрать бота) -> Удалить бот)"
msgstr ""
"You already have too many bots. Remove any of your bots from Olgram(/mybots -"
"> (Select bot) -> Remove bot)"
#: olgram/commands/bots.py:50
msgid ""
"\n"
" Чтобы подключить бот, вам нужно выполнить три действия:\n"
"\n"
" 1. Перейдите в бот @BotFather, нажмите START и отправьте команду /"
"newbot\n"
" 2. Введите название бота, а потом username бота.\n"
" 3. После создания бота перешлите ответное сообщение в этот бот или "
"скопируйте и пришлите token бота.\n"
"\n"
" Важно: не подключайте боты, которые используются в других сервисах "
"(Manybot, Chatfuel, Livegram и других).\n"
" "
msgstr ""
"\n"
" To connect the bot, you need to follow three steps:\n"
"\n"
" 1. Go to bot @BotFather, press START and send command /newbot\n"
" 2. Enter the bot's name and then the bot's username.\n"
" 3. Once the bot is created, forward a reply message to this bot or copy "
"and send the bot's token.\n"
"\n"
" Important: do not connect bots that are used in other services (Manybot, "
"Chatfuel, Livegram and others).\n"
" "
#: olgram/commands/bots.py:70
msgid ""
"\n"
" Это не токен бота.\n"
"\n"
" Токен выглядит вот так: 123456789:AAAA-"
"abc123_AbcdEFghijKLMnopqrstu12\n"
" "
msgstr ""
"\n"
" This is not a bot token.\n"
"\n"
" The token looks like this: 123456789:AAAA-"
"abc123_AbcdEFghijKLMnopqrstu12\n"
" "
#: olgram/commands/bots.py:77
msgid ""
"\n"
" Не удалось запустить этого бота: неверный токен\n"
" "
msgstr ""
"\n"
" Failed to start this bot: Wrong token\n"
" "
#: olgram/commands/bots.py:82
msgid ""
"\n"
" Не удалось запустить этого бота: непредвиденная ошибка\n"
" "
msgstr ""
"\n"
" Failed to start this bot: unexpected error\n"
" "
#: olgram/commands/bots.py:87
msgid ""
"\n"
" Такой бот уже есть в базе данных\n"
" "
msgstr ""
"\n"
" Such a bot is already in the database\n"
" "
#: olgram/commands/bots.py:122
msgid "Бот добавлен! Список ваших ботов: /mybots"
msgstr "Bot added! List of your bots: /mybots"
#: olgram/commands/info.py:34
msgid "Количество ботов: {0}\n"
msgstr "Number of bots: {0}\n"
#: olgram/commands/info.py:35
msgid "Количество пользователей (у конструктора): {0}\n"
msgstr "Number of users (at the constructor): {0}\n"
#: olgram/commands/info.py:36
msgid "Шаблонов ответов: {0}\n"
msgstr "Answer templates: {0}\n"
#: olgram/commands/info.py:37
msgid "Входящих сообщений у всех ботов: {0}\n"
msgstr "Incoming messages from all bots: {0}\n"
#: olgram/commands/info.py:38
msgid "Исходящих сообщений у всех ботов: {0}\n"
msgstr "All bots have outgoing messages: {0}\n"
#: olgram/commands/info.py:39
msgid "Промо-кодов выдано: {0}\n"
msgstr "Promo codes issued: {0}\n"
#: olgram/commands/info.py:40
msgid "Рекламную плашку выключили: {0}\n"
msgstr "Ad disabled:: {0}\n"
#: olgram/commands/menu.py:33
msgid ""
"\n"
" У вас нет добавленных ботов.\n"
"\n"
" Отправьте команду /addbot, чтобы добавить бот.\n"
" "
msgstr ""
"\n"
" You do not have any bots added.\n"
"\n"
" Send the command /addbot to add a bot.\n"
" "
#: olgram/commands/menu.py:48
msgid "Ваши боты"
msgstr "Your bots"
#: olgram/commands/menu.py:69
msgid "Личные сообщения"
msgstr "Personal messages"
#: olgram/commands/menu.py:74
msgid "❗️ Выйти из всех чатов"
msgstr "❗️ Leave all chats"
#: olgram/commands/menu.py:79 olgram/commands/menu.py:124
#: olgram/commands/menu.py:156 olgram/commands/menu.py:209
#: olgram/commands/menu.py:390
msgid "<< Назад"
msgstr "<< Back"
#: olgram/commands/menu.py:85
msgid ""
"\n"
" Этот бот не добавлен в чаты, поэтому все сообщения будут приходить "
"вам в бот.\n"
" Чтобы подключить чат — добавьте бот @{0} в чат, откройте это меню "
"ещё раз и выберите добавленный чат.\n"
" Если ваш бот состоял в групповом чате до того, как его добавили в "
"Olgram - удалите бота из чата и добавьте\n"
" снова.\n"
" "
msgstr ""
"\n"
" This bot is not added to the chats, so all messages will come to you "
"in the bot.\n"
" To add a chat - add the bot @{0} to the chat, open this menu again "
"and select the added chat.\n"
" If your bot was in a group chat before you added it to Olgram - "
"remove the bot from the chat and add\n"
" again.\n"
" "
#: olgram/commands/menu.py:92
msgid ""
"\n"
" В этом разделе вы можете привязать бота @{0} к чату.\n"
" Выберите чат, куда бот будет пересылать сообщения.\n"
" "
msgstr ""
"\n"
" In this section you can bind the @{0} bot to a chat room.\n"
" Select the chat room where the bot will forward messages.\n"
" "
#: olgram/commands/menu.py:104
msgid "Текст"
msgstr "Text"
#: olgram/commands/menu.py:109
msgid "Чат"
msgstr "Chat"
#: olgram/commands/menu.py:114
msgid "Удалить бот"
msgstr "Delete bot"
#: olgram/commands/menu.py:119
msgid "Статистика"
msgstr "Statistics"
#: olgram/commands/menu.py:128
msgid "Опции"
msgstr "Options"
#: olgram/commands/menu.py:134 olgram/commands/menu.py:190
msgid "Рассылка"
msgstr "Mailing"
#: olgram/commands/menu.py:139
msgid ""
"\n"
" Управление ботом @{0}.\n"
"\n"
" Если у вас возникли вопросы по настройке бота, то посмотрите нашу "
"справку /help или напишите нам\n"
" @civsocit_feedback_bot\n"
" "
msgstr ""
"\n"
" Bot management @{0}.\n"
"\n"
" If you have any questions about configuring the bot, see our help /help "
"or email us\n"
" @civsocit_feedback_bot\n"
" "
#: olgram/commands/menu.py:151
msgid "Да, удалить бот"
msgstr "Yes, delete the bot"
#: olgram/commands/menu.py:160
msgid ""
"\n"
" Вы уверены, что хотите удалить бота @{0}?\n"
" "
msgstr ""
"\n"
" Are you sure you want to delete the bot @{0}?\n"
" "
#: olgram/commands/menu.py:169
msgid "Потоки сообщений"
msgstr "Message threads"
#: olgram/commands/menu.py:174
msgid "Данные пользователя"
msgstr "User data"
#: olgram/commands/menu.py:179
msgid "Антифлуд"
msgstr "Antiflood"
#: olgram/commands/menu.py:184
msgid "Автоответчик всегда"
msgstr "Autorespond always"
#: olgram/commands/menu.py:195
msgid "Прерывать поток"
msgstr "Inteeupt thread"
#: olgram/commands/menu.py:203
msgid "Olgram подпись"
msgstr "Olgram signature"
#: olgram/commands/menu.py:214 olgram/commands/menu.py:215
msgid "включены"
msgstr "enabled"
#: olgram/commands/menu.py:214 olgram/commands/menu.py:215
msgid "выключены"
msgstr "disabled"
#: olgram/commands/menu.py:216
#, fuzzy
#| msgid "включены"
msgid "включен"
msgstr "enabled"
#: olgram/commands/menu.py:216 olgram/commands/menu.py:217
#, fuzzy
#| msgid "выключены"
msgid "выключен"
msgstr "disabled"
#: olgram/commands/menu.py:217
#, fuzzy
#| msgid "включены"
msgid "включён"
msgstr "enabled"
#: olgram/commands/menu.py:218
msgid "да"
msgstr "yes"
#: olgram/commands/menu.py:218
msgid "нет"
msgstr "no"
#: olgram/commands/menu.py:219 olgram/commands/menu.py:231
msgid "включена"
msgstr "enabled"
#: olgram/commands/menu.py:219 olgram/commands/menu.py:231
msgid "выключена"
msgstr "disabled"
#: olgram/commands/menu.py:220
msgid ""
"\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options."
"html#threads\">Потоки сообщений</a>: <b>{0}</b>\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options.html#user-"
"info\">Данные пользователя</a>: <b>{1}</b>\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options."
"html#antiflood\">Антифлуд</a>: <b>{2}</b>\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options."
"html#always_second_message\">Автоответчик всегда</a>: <b>{3}</b>\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options."
"html#thread_interrupt\">Прерывать поток</a>: <b>{4}</b>\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options."
"html#mailing\">Рассылка</a>: <b>{5}</b>\n"
" "
msgstr ""
"\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options."
"html#threads\">Threads</a>: <b>{0}</b>\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options.html#user-"
"info\">User data</a>: <b>{1}</b>\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options."
"html#antiflood\">Anti-flood</a>: <b>{2}</b>\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options."
"html#always_second_message\">Autorespond always</a>: <b>{3}</b>\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options."
"html#thread_interrupt\">Interrupt threads</a>: <b>{4}</b>\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options."
"html#mailing\">Mailing</a>: <b>{5}</b>\n"
" "
#: olgram/commands/menu.py:232
msgid "Olgram подпись: <b>{0}</b>"
msgstr "Olgram signature: <b>{0}</b>"
#: olgram/commands/menu.py:259 olgram/commands/menu.py:421
#: olgram/commands/menu.py:480
msgid "<< Завершить редактирование"
msgstr "<< Finish editing"
#: olgram/commands/menu.py:263
msgid "Автоответчик"
msgstr "Autoresponder"
#: olgram/commands/menu.py:268 olgram/commands/menu.py:435
msgid "Сбросить текст"
msgstr "Reset text"
#: olgram/commands/menu.py:273 olgram/commands/menu.py:440
msgid "[все языки]"
msgstr "[all languages]"
#: olgram/commands/menu.py:290
msgid ""
"\n"
" Сейчас вы редактируете текст, который отправляется после того, как "
"пользователь отправит вашему боту @{0}\n"
" команду /start\n"
"\n"
" Текущий текст{2}:\n"
" <pre>{1}</pre>\n"
" Отправьте сообщение, чтобы изменить текст.\n"
" "
msgstr ""
#: olgram/commands/menu.py:300 olgram/commands/menu.py:467
msgid " (для языка {0})"
msgstr " (for language {0})"
#: olgram/commands/menu.py:313
msgid "<< Отменить рассылку"
msgstr "<< Cancel"
#: olgram/commands/menu.py:317
msgid ""
"\n"
" Напишите сообщение, которое нужно разослать всем подписчикам вашего бота "
"@{0}. \n"
" У сообщения будет до {1} получателей. \n"
" Учтите, что\n"
" 1. Рассылается только одно сообщение за раз (в т.ч. только одна "
"картинка)\n"
" 2. Когда рассылка запущена, её нельзя отменить \n"
" "
msgstr ""
"\n"
" Please send mailing message to send all @{0} subscribers. \n"
" Message will have up to {1} recipients. \n"
" Take note:\n"
" 1. Only one message per mailing\n"
" 2.Mailing cant be interrupted \n"
" "
#: olgram/commands/menu.py:367
msgid "Не удалось загрузить файл (слишком большой размер?)"
msgstr ""
#: olgram/commands/menu.py:374
msgid "Да, начать рассылку"
msgstr "Yes, start mailing"
#: olgram/commands/menu.py:394
msgid ""
"\n"
" Статистика по боту @{0}\n"
"\n"
" Входящих сообщений: <b>{1}</b>\n"
" Ответных сообщений: <b>{2}</b>\n"
" Шаблоны ответов: <b>{3}</b>\n"
" Забанено пользователей: <b>{4}</b>\n"
" "
msgstr ""
"\n"
" Statistics @{0}\n"
"\n"
" Income messages: <b>{1}</b>\n"
" Response messages: <b>{2}</b>\n"
" Tempaltes: <b>{3}</b>\n"
" Banned users: <b>{4}</b>\n"
" "
#: olgram/commands/menu.py:425
msgid "Предыдущий текст"
msgstr "Previous text"
#: olgram/commands/menu.py:430
msgid "Шаблоны ответов..."
msgstr "Answer templates..."
#: olgram/commands/menu.py:457
msgid ""
"\n"
" Сейчас вы редактируете текст автоответчика. Это сообщение отправляется в "
"ответ на все входящие сообщения @{0} автоматически. По умолчанию оно "
"отключено.\n"
"\n"
" Текущий текст{2}:\n"
" <pre>{1}</pre>\n"
" Отправьте сообщение, чтобы изменить текст.\n"
" "
msgstr ""
#: olgram/commands/menu.py:466
msgid "отключено"
msgstr "disabled"
#: olgram/commands/menu.py:484
msgid ""
"\n"
" Сейчас вы редактируете шаблоны ответов для @{0}. Текущие шаблоны:\n"
"\n"
" <pre>\n"
" {1}\n"
" </pre>\n"
" Отправьте какую-нибудь фразу (например: \"Ваш заказ готов, ожидайте!\"), "
"чтобы добавить её в шаблон.\n"
" Чтобы удалить шаблон из списка, отправьте его номер в списке (например, "
"4)\n"
" "
msgstr ""
"\n"
" You are currently editing the answer templates for @{0}. Current "
"templates:\n"
"\n"
" <pre>\n"
" {1}\n"
" </pre>.\n"
" Send some phrase (e.g., \"Your order is ready, wait!\") to add to the "
"template.\n"
" To remove a template from the list, send its number in the list (for "
"example, 4) "
#: olgram/commands/menu.py:503
msgid "(нет шаблонов)"
msgstr "(no templates)"
#: olgram/commands/menu.py:565
msgid "У вас нет шаблонов, чтобы их удалять"
msgstr "You don't have templates to delete them"
#: olgram/commands/menu.py:567
msgid "Неправильное число. Чтобы удалить шаблон, введите число от 0 до {0}"
msgstr "To delete a template, enter a number between 0 and {0}"
#: olgram/commands/menu.py:575
msgid "У вашего бота уже слишком много шаблонов"
msgstr "Your bot already has too many templates"
#: olgram/commands/menu.py:579
msgid "Такой текст уже есть в списке шаблонов"
msgstr "This text is already in the list of templates"
#: olgram/commands/menu.py:597
msgid "У вас нет прав на этого бота"
msgstr "You have no permissions to this bot"
#: olgram/commands/menu.py:617 olgram/commands/menu.py:643
msgid "Рассылка была совсем недавно, подождите немного"
msgstr "Mailing was recently, wait a bit please"
#: olgram/commands/menu.py:619 olgram/commands/menu.py:645
msgid "Нет пользователей для рассылки"
msgstr "No users for mailing"
#: olgram/commands/menu.py:647
msgid "Рассылка запущена"
msgstr "Mailing started"
#: olgram/commands/menu.py:649
msgid "Рассылка завершена, отправлено {0} сообщений"
msgstr "Mailing completed, {0} messages sent"
#: olgram/commands/menu.py:651
msgid "Устарело, создайте новую рассылку"
msgstr "Expired, please create new mailing"
#: olgram/commands/promo.py:27
msgid ""
"Новый промокод\n"
"```{0}```"
msgstr ""
"New promo code\n"
"```{0}```"
#: olgram/commands/promo.py:46
msgid "Неправильный токен"
msgstr "Incorrect token"
#: olgram/commands/promo.py:49
msgid "Такого кода не существует"
msgstr "There is no such code"
#: olgram/commands/promo.py:59
msgid "Промокод отозван"
msgstr "Promotion code withdrawn"
#: olgram/commands/promo.py:70
msgid ""
"Укажите аргумент: промокод. Например: <pre>/setpromo my-promo-code</pre>"
msgstr ""
"Specify the argument: promo code. For example: <pre>/setpromo my-promo-code</"
"pre>"
#: olgram/commands/promo.py:78 olgram/commands/promo.py:82
msgid "Промокод не найден"
msgstr "Promo code not found"
#: olgram/commands/promo.py:85
msgid "Промокод уже использован"
msgstr "Promo code has already been used"
#: olgram/commands/promo.py:91
msgid "Промокод активирован! Спасибо 🙌"
msgstr "Promo code activated! Thank you 🙌"
#: olgram/commands/start.py:23
msgid ""
"\n"
" Olgram Bot — это конструктор ботов обратной связи в Telegram. Подробнее "
"<a href=\"https://olgram.readthedocs.io\">читайте здесь</a>. Следите за "
"обновлениями <a href=\"https://t.me/civsoc_it\">здесь</a>.\n"
"\n"
" Используйте эти команды, чтобы управлять этим ботом:\n"
"\n"
" /addbot - добавить бот\n"
" /mybots - управление ботами\n"
"\n"
" /help - помощь\n"
" "
msgstr ""
"\n"
" Olgram Bot is a feedback bot contructor for Telegram. More info <a "
"href=\"https://olgram.readthedocs.io\">here</a>.\n"
"\n"
" Use that commands to control bot:\n"
"\n"
" /addbot - add bot\n"
" /mybots - bot control\n"
"\n"
" /help - help\n"
" "
#: olgram/commands/start.py:43
msgid ""
"\n"
" Читайте инструкции на нашем сайте https://olgram.readthedocs.io\n"
" Техническая поддержка: @civsocit_feedback_bot\n"
" Версия {0}\n"
" "
msgstr ""
"\n"
" Read the instructions on our website at https://olgram.readthedocs.io\n"
" Technical support: @civsocit_feedback_bot\n"
" Version {0}\n"
" "
#: olgram/models/models.py:30
msgid ""
"\n"
" Здравствуйте!\n"
" Напишите ваш вопрос и мы ответим вам в ближайшее время.\n"
" "
msgstr ""
"\n"
" Hello!\n"
" Write your question and we will answer you shortly.\n"
" "
#: olgram/utils/permissions.py:41
msgid "Владелец бота ограничил доступ к этому функционалу 😞"
msgstr "The bot owner has restricted access to this functionality 😞"
#: olgram/utils/permissions.py:53
msgid "Владелец бота ограничил доступ к этому функционалу😞"
msgstr "The owner of the bot has restricted access to this function😞"
#: server/custom.py:57
msgid ""
"<b>Политика конфиденциальности</b>\n"
"\n"
"Этот бот не хранит ваши сообщения, имя пользователя и @username. При "
"отправке сообщения (кроме команд /start и /security_policy) ваш "
"идентификатор пользователя записывается в кеш на некоторое время и потом "
"удаляется из кеша. Этот идентификатор используется для общения с "
"оператором.\n"
"\n"
msgstr ""
"<b>Privacy Policy</b>.\n"
"\n"
"This bot does not store your messages, username and @username. When you send "
"a message (except for /start and /security_policy), your username is cached "
"for a while and then deleted from the cache. This ID is used for "
"communicating with the operator\n"
"\n"
#: server/custom.py:62
msgid ""
"При отправке сообщения (кроме команд /start и /security_policy) оператор "
"<b>видит</b> ваши имя пользователя, @username и идентификатор пользователя в "
"силу настроек, которые оператор указал при создании бота.\n"
"\n"
msgstr ""
"When sending a message (except /start and /security_policy), the operator "
"<b>sees</b> your username, @username and user ID by virtue of the settings "
"that the operator specified when creating the bot.\n"
"\n"
#: server/custom.py:66
msgid ""
"В зависимости от ваших настроек конфиденциальности Telegram, оператор может "
"видеть ваш username, имя пользователя и другую информацию.\n"
"\n"
msgstr ""
"Depending on your Telegram privacy settings, the operator may see your "
"username, username and other information.\n"
"\n"
#: server/custom.py:70
msgid ""
"В этом боте включена массовая рассылка в силу настроек, которые оператор "
"указал при создании бота. Ваш идентификатор пользователя может быть записан "
"в базу данных на долгое время"
msgstr "Mailing enabled for this bot"
#: server/custom.py:73
msgid "В этом боте нет массовой рассылки сообщений"
msgstr "Mailing disabled for this bot"
#: server/custom.py:83
msgid "Сообщение от пользователя "
msgstr "Message from the user "
#: server/custom.py:157
msgid "Вы заблокированы в этом боте"
msgstr "You are blocked in this bot"
#: server/custom.py:163
msgid "Слишком много сообщений, подождите одну минуту"
msgstr "Too many messages, wait one minute"
#: server/custom.py:170
msgid "Не удаётся связаться с владельцем бота"
msgstr "Cannot contact the owner of the bot"
#: server/custom.py:202
msgid ""
"<i>Невозможно переслать сообщение: автор не найден (сообщение слишком "
"старое?)</i>"
msgstr ""
"<i>Cannot forward this message: author not found (message too old?)</i>"
#: server/custom.py:210
msgid "Пользователь заблокирован"
msgstr "User is blocked"
#: server/custom.py:215
msgid "Пользователь не был забанен"
msgstr "The user was not banned"
#: server/custom.py:218
msgid "Пользователь разбанен"
msgstr "A user has been unlocked"
#: server/custom.py:223
msgid "<i>Невозможно переслать сообщение (автор заблокировал бота?)</i>"
msgstr "<i>Cannot forward the message (has the author blocked the bot?)</i>"
#: server/server.py:41
msgid "(Пере)запустить бота"
msgstr "(Re)launch the bot"
#: server/server.py:42
msgid "Политика конфиденциальности"
msgstr "Privacy Policy"
#~ msgid ""
#~ "\n"
#~ "\n"
#~ "Этот бот создан с помощью @OlgramBot"
#~ msgstr ""
#~ "\n"
#~ "\n"
#~ "This bot was created using @OlgramBot"

25
locales/locale.py Normal file
View File

@ -0,0 +1,25 @@
import gettext
from olgram.settings import BotSettings
from os.path import dirname
locales_dir = dirname(__file__)
def dummy_translator(x: str) -> str:
return x
lang = BotSettings.language()
if lang == "ru":
_ = dummy_translator
else:
t = gettext.translation("olgram", localedir=locales_dir, languages=[lang])
_ = t.gettext
translators = {
"ru": dummy_translator,
"uk": gettext.translation("olgram", localedir=locales_dir, languages=["uk"]).gettext,
"zh": gettext.translation("olgram", localedir=locales_dir, languages=["zh"]).gettext,
"en": gettext.translation("olgram", localedir=locales_dir, languages=["en"]).gettext,
}

View File

@ -0,0 +1,810 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR ORGANIZATION
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: \n"
"POT-Creation-Date: 2024-03-02 19:47+0400\n"
"PO-Revision-Date: 2024-03-02 19:48+0400\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: uk_UA\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);\n"
"Generated-By: pygettext.py 1.5\n"
"X-Generator: Poedit 3.4.2\n"
#: olgram/commands/admin.py:21 olgram/commands/info.py:21
#: olgram/commands/promo.py:23 olgram/commands/promo.py:39
msgid "Недостаточно прав"
msgstr "Недостатньо прав"
#: olgram/commands/admin.py:27
msgid "Нужно указать имя бота"
msgstr "Потрібно вказати ім'я бота"
#: olgram/commands/admin.py:33
msgid "Такого бота нет в системе"
msgstr "Такого бота немає в системі"
#: olgram/commands/admin.py:39 olgram/commands/admin.py:53
msgid "Пропустить"
msgstr "Пропустити"
#: olgram/commands/admin.py:42
msgid ""
"Введите текст, который будет отправлен владельцу бота {0}. Напишите "
"'Пропустить' чтобы отменить"
msgstr ""
"Введіть текст, який буде надіслано власнику бота {0}. Напишіть 'Пропустити', "
"щоб скасувати"
#: olgram/commands/admin.py:50
msgid "Поддерживается только текст"
msgstr "Підтримується лише текст"
#: olgram/commands/admin.py:55 olgram/commands/admin.py:71
msgid "Отменено"
msgstr "Скасовано"
#: olgram/commands/admin.py:61 olgram/commands/admin.py:69
msgid "Отправить"
msgstr "Надіслати"
#: olgram/commands/admin.py:62
msgid "Отменить"
msgstr "Скасувати"
#: olgram/commands/admin.py:81
msgid "Отправлено"
msgstr "Надіслано"
#: olgram/commands/bot_actions.py:27
msgid "Бот удалён"
msgstr "Бот видалений"
#: olgram/commands/bot_actions.py:49 olgram/commands/bot_actions.py:67
msgid "Текст сброшен"
msgstr "Текст скинутий"
#: olgram/commands/bot_actions.py:81
msgid "Выбран личный чат"
msgstr "Вибраний особистий чат"
#: olgram/commands/bot_actions.py:94
msgid "Бот вышел из чатов"
msgstr "Бот вийшов із чатів"
#: olgram/commands/bot_actions.py:100
msgid "Нельзя привязать бота к этому чату"
msgstr "Не можна прив'язати робота до цього чату"
#: olgram/commands/bot_actions.py:104
msgid "Выбран чат {0}"
msgstr "Вибраний чат {0}"
#: olgram/commands/bots.py:46
msgid ""
"У вас уже слишком много ботов. Удалите какой-нибудь свой бот из Olgram(/"
"mybots -> (Выбрать бота) -> Удалить бот)"
msgstr ""
"У вас вже надто багато роботів. Видаліть якийсь свій бот з Olgram(/mybots -> "
"(Вибрати бота) -> Видалити бот)"
#: olgram/commands/bots.py:50
msgid ""
"\n"
" Чтобы подключить бот, вам нужно выполнить три действия:\n"
"\n"
" 1. Перейдите в бот @BotFather, нажмите START и отправьте команду /"
"newbot\n"
" 2. Введите название бота, а потом username бота.\n"
" 3. После создания бота перешлите ответное сообщение в этот бот или "
"скопируйте и пришлите token бота.\n"
"\n"
" Важно: не подключайте боты, которые используются в других сервисах "
"(Manybot, Chatfuel, Livegram и других).\n"
" "
msgstr ""
"\n"
" Щоб підключити бот, вам потрібно виконати три дії:\n"
"\n"
" 1. Перейдіть до бот @BotFather, натисніть START і надішліть команду /"
"newbot\n"
" 2. Введіть назву бота, а потім username бота.\n"
" 3. Після створення бота перешліть повідомлення у цей бот або скопіюйте "
"і надішліть token бота.\n"
"\n"
" Важливо: не підключайте роботи, які використовуються в інших сервісах "
"(Manybot, Chatfuel, Livegram та інших).\n"
" \n"
" "
#: olgram/commands/bots.py:70
msgid ""
"\n"
" Это не токен бота.\n"
"\n"
" Токен выглядит вот так: 123456789:AAAA-"
"abc123_AbcdEFghijKLMnopqrstu12\n"
" "
msgstr ""
"\n"
" Це не токен робота.\n"
"\n"
" Токен виглядає ось так: 123456789:AAAA-"
"abc123_AbcdEFghijKLMnopqrstu12\n"
" "
#: olgram/commands/bots.py:77
msgid ""
"\n"
" Не удалось запустить этого бота: неверный токен\n"
" "
msgstr ""
"\n"
" Не вдалося запустити цього бота: неправильний токен\n"
" "
#: olgram/commands/bots.py:82
msgid ""
"\n"
" Не удалось запустить этого бота: непредвиденная ошибка\n"
" "
msgstr ""
"\n"
" Не вдалося запустити цього бота: непередбачена помилка\n"
" "
#: olgram/commands/bots.py:87
msgid ""
"\n"
" Такой бот уже есть в базе данных\n"
" "
msgstr ""
"\n"
" Такий бот вже є у базі даних\n"
" "
#: olgram/commands/bots.py:122
msgid "Бот добавлен! Список ваших ботов: /mybots"
msgstr "Бот доданий! Список ваших роботів: /mybots"
#: olgram/commands/info.py:34
msgid "Количество ботов: {0}\n"
msgstr "Кількість ботів: {0}\n"
#: olgram/commands/info.py:35
msgid "Количество пользователей (у конструктора): {0}\n"
msgstr "Кількість користувачів (у конструктора): {0}\n"
#: olgram/commands/info.py:36
msgid "Шаблонов ответов: {0}\n"
msgstr "Шаблонів відповідей: {0}\n"
#: olgram/commands/info.py:37
msgid "Входящих сообщений у всех ботов: {0}\n"
msgstr "Вхідних повідомлень у всіх роботів: {0}\n"
#: olgram/commands/info.py:38
msgid "Исходящих сообщений у всех ботов: {0}\n"
msgstr "Вихідних повідомлень у всіх роботів: {0}\n"
#: olgram/commands/info.py:39
msgid "Промо-кодов выдано: {0}\n"
msgstr "Промо-кодів видано: {0}\n"
#: olgram/commands/info.py:40
msgid "Рекламную плашку выключили: {0}\n"
msgstr ""
#: olgram/commands/menu.py:33
msgid ""
"\n"
" У вас нет добавленных ботов.\n"
"\n"
" Отправьте команду /addbot, чтобы добавить бот.\n"
" "
msgstr ""
"\n"
" У вас немає доданих роботів.\n"
"\n"
" Надішліть команду /addbot, щоб додати бот.\n"
" \n"
" "
#: olgram/commands/menu.py:48
msgid "Ваши боты"
msgstr "Ваші боти"
#: olgram/commands/menu.py:69
msgid "Личные сообщения"
msgstr "Особисті повідомлення"
#: olgram/commands/menu.py:74
msgid "❗️ Выйти из всех чатов"
msgstr "❗️ Вийти зі всіх чатів"
#: olgram/commands/menu.py:79 olgram/commands/menu.py:124
#: olgram/commands/menu.py:156 olgram/commands/menu.py:209
#: olgram/commands/menu.py:390
msgid "<< Назад"
msgstr "<< Назад"
#: olgram/commands/menu.py:85
msgid ""
"\n"
" Этот бот не добавлен в чаты, поэтому все сообщения будут приходить "
"вам в бот.\n"
" Чтобы подключить чат — добавьте бот @{0} в чат, откройте это меню "
"ещё раз и выберите добавленный чат.\n"
" Если ваш бот состоял в групповом чате до того, как его добавили в "
"Olgram - удалите бота из чата и добавьте\n"
" снова.\n"
" "
msgstr ""
"\n"
" Цей бот не доданий до чатів, тому всі повідомлення будуть приходити "
"вам у бот.\n"
" Щоб підключити чат — додайте бот @{0} до чату, відкрийте це меню ще "
"раз і виберіть доданий чат.\n"
" Якщо ваш бот перебував у груповому чаті до того, як його додали до "
"Olgram - видаліть бота з чату та додайте\n"
" знову.\n"
" \n"
" "
#: olgram/commands/menu.py:92
msgid ""
"\n"
" В этом разделе вы можете привязать бота @{0} к чату.\n"
" Выберите чат, куда бот будет пересылать сообщения.\n"
" "
msgstr ""
"\n"
" У цьому розділі ви можете прив'язати робота @{0} до чату.\n"
" Виберіть чат, куди бот пересилатиме повідомлення.\n"
" \n"
" "
#: olgram/commands/menu.py:104
msgid "Текст"
msgstr "Текст"
#: olgram/commands/menu.py:109
msgid "Чат"
msgstr "Чат"
#: olgram/commands/menu.py:114
msgid "Удалить бот"
msgstr "Видалити бот"
#: olgram/commands/menu.py:119
msgid "Статистика"
msgstr "Статистика"
#: olgram/commands/menu.py:128
msgid "Опции"
msgstr "Опції"
#: olgram/commands/menu.py:134 olgram/commands/menu.py:190
msgid "Рассылка"
msgstr ""
#: olgram/commands/menu.py:139
msgid ""
"\n"
" Управление ботом @{0}.\n"
"\n"
" Если у вас возникли вопросы по настройке бота, то посмотрите нашу "
"справку /help или напишите нам\n"
" @civsocit_feedback_bot\n"
" "
msgstr ""
"\n"
" Управління роботом @{0}.\n"
"\n"
" Якщо у вас виникли питання з налаштування бота, подивіться нашу довідку /"
"help або напишіть нам\n"
" @civsocit_feedback_bot\n"
" "
#: olgram/commands/menu.py:151
msgid "Да, удалить бот"
msgstr "Так, видалити бот"
#: olgram/commands/menu.py:160
msgid ""
"\n"
" Вы уверены, что хотите удалить бота @{0}?\n"
" "
msgstr ""
"\n"
" Ви впевнені, що хочете видалити бота @{0}?\n"
" "
#: olgram/commands/menu.py:169
msgid "Потоки сообщений"
msgstr "Потоки повідомлень"
#: olgram/commands/menu.py:174
msgid "Данные пользователя"
msgstr "Дані користувача"
#: olgram/commands/menu.py:179
msgid "Антифлуд"
msgstr "Антифлуд"
#: olgram/commands/menu.py:184
#, fuzzy
#| msgid "Автоответчик"
msgid "Автоответчик всегда"
msgstr "Автовідповідач"
#: olgram/commands/menu.py:195
msgid "Прерывать поток"
msgstr ""
#: olgram/commands/menu.py:203
msgid "Olgram подпись"
msgstr "Olgram підпис"
#: olgram/commands/menu.py:214 olgram/commands/menu.py:215
msgid "включены"
msgstr "включені"
#: olgram/commands/menu.py:214 olgram/commands/menu.py:215
msgid "выключены"
msgstr "вимкнені"
#: olgram/commands/menu.py:216
#, fuzzy
#| msgid "включены"
msgid "включен"
msgstr "включені"
#: olgram/commands/menu.py:216 olgram/commands/menu.py:217
#, fuzzy
#| msgid "выключены"
msgid "выключен"
msgstr "вимкнені"
#: olgram/commands/menu.py:217
#, fuzzy
#| msgid "включены"
msgid "включён"
msgstr "включені"
#: olgram/commands/menu.py:218
msgid "да"
msgstr "Ча"
#: olgram/commands/menu.py:218
msgid "нет"
msgstr ""
#: olgram/commands/menu.py:219 olgram/commands/menu.py:231
msgid "включена"
msgstr "включена"
#: olgram/commands/menu.py:219 olgram/commands/menu.py:231
msgid "выключена"
msgstr "вимкнена"
#: olgram/commands/menu.py:220
msgid ""
"\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options."
"html#threads\">Потоки сообщений</a>: <b>{0}</b>\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options.html#user-"
"info\">Данные пользователя</a>: <b>{1}</b>\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options."
"html#antiflood\">Антифлуд</a>: <b>{2}</b>\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options."
"html#always_second_message\">Автоответчик всегда</a>: <b>{3}</b>\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options."
"html#thread_interrupt\">Прерывать поток</a>: <b>{4}</b>\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options."
"html#mailing\">Рассылка</a>: <b>{5}</b>\n"
" "
msgstr ""
#: olgram/commands/menu.py:232
msgid "Olgram подпись: <b>{0}</b>"
msgstr "Olgram підпис: <b>{0}</b>"
#: olgram/commands/menu.py:259 olgram/commands/menu.py:421
#: olgram/commands/menu.py:480
msgid "<< Завершить редактирование"
msgstr "<< Завершити редагування"
#: olgram/commands/menu.py:263
msgid "Автоответчик"
msgstr "Автовідповідач"
#: olgram/commands/menu.py:268 olgram/commands/menu.py:435
msgid "Сбросить текст"
msgstr "Скинути текст"
#: olgram/commands/menu.py:273 olgram/commands/menu.py:440
msgid "[все языки]"
msgstr ""
#: olgram/commands/menu.py:290
msgid ""
"\n"
" Сейчас вы редактируете текст, который отправляется после того, как "
"пользователь отправит вашему боту @{0}\n"
" команду /start\n"
"\n"
" Текущий текст{2}:\n"
" <pre>{1}</pre>\n"
" Отправьте сообщение, чтобы изменить текст.\n"
" "
msgstr ""
#: olgram/commands/menu.py:300 olgram/commands/menu.py:467
msgid " (для языка {0})"
msgstr ""
#: olgram/commands/menu.py:313
#, fuzzy
#| msgid "Отменить"
msgid "<< Отменить рассылку"
msgstr "Скасувати"
#: olgram/commands/menu.py:317
msgid ""
"\n"
" Напишите сообщение, которое нужно разослать всем подписчикам вашего бота "
"@{0}. \n"
" У сообщения будет до {1} получателей. \n"
" Учтите, что\n"
" 1. Рассылается только одно сообщение за раз (в т.ч. только одна "
"картинка)\n"
" 2. Когда рассылка запущена, её нельзя отменить \n"
" "
msgstr ""
#: olgram/commands/menu.py:367
msgid "Не удалось загрузить файл (слишком большой размер?)"
msgstr ""
#: olgram/commands/menu.py:374
msgid "Да, начать рассылку"
msgstr ""
#: olgram/commands/menu.py:394
msgid ""
"\n"
" Статистика по боту @{0}\n"
"\n"
" Входящих сообщений: <b>{1}</b>\n"
" Ответных сообщений: <b>{2}</b>\n"
" Шаблоны ответов: <b>{3}</b>\n"
" Забанено пользователей: <b>{4}</b>\n"
" "
msgstr ""
"\n"
" Статистика з роботи @{0}\n"
"\n"
" Вхідних повідомлень: <b>{1}</b>\n"
" Повідомлень у відповідь: <b>{2}</b>\n"
" Шаблони відповідей: <b>{3}</b>\n"
" Забанено користувачів: <b>{4}</b>\n"
" "
#: olgram/commands/menu.py:425
msgid "Предыдущий текст"
msgstr "Попередній текст"
#: olgram/commands/menu.py:430
msgid "Шаблоны ответов..."
msgstr "Шаблони відповідей..."
#: olgram/commands/menu.py:457
msgid ""
"\n"
" Сейчас вы редактируете текст автоответчика. Это сообщение отправляется в "
"ответ на все входящие сообщения @{0} автоматически. По умолчанию оно "
"отключено.\n"
"\n"
" Текущий текст{2}:\n"
" <pre>{1}</pre>\n"
" Отправьте сообщение, чтобы изменить текст.\n"
" "
msgstr ""
#: olgram/commands/menu.py:466
msgid "отключено"
msgstr "відключено"
#: olgram/commands/menu.py:484
msgid ""
"\n"
" Сейчас вы редактируете шаблоны ответов для @{0}. Текущие шаблоны:\n"
"\n"
" <pre>\n"
" {1}\n"
" </pre>\n"
" Отправьте какую-нибудь фразу (например: \"Ваш заказ готов, ожидайте!\"), "
"чтобы добавить её в шаблон.\n"
" Чтобы удалить шаблон из списка, отправьте его номер в списке (например, "
"4)\n"
" "
msgstr ""
"\n"
" Зараз ви редагуєте шаблони для @{0}. Поточні шаблони:\n"
"\n"
" <pre>\n"
" {1}\n"
" </pre>\n"
" Надішліть якусь фразу (наприклад: \"Ваше замовлення готове, чекайте!\"), "
"щоб додати її до шаблону.\n"
" Щоб видалити шаблон зі списку, відправте його у списку (наприклад, 4)\n"
" \n"
" "
#: olgram/commands/menu.py:503
msgid "(нет шаблонов)"
msgstr "(Немає шаблонів)"
#: olgram/commands/menu.py:565
msgid "У вас нет шаблонов, чтобы их удалять"
msgstr "У вас немає шаблонів, щоб їх видаляти"
#: olgram/commands/menu.py:567
msgid "Неправильное число. Чтобы удалить шаблон, введите число от 0 до {0}"
msgstr "Неправильне число. Щоб видалити шаблон, введіть число від 0 до {0}"
#: olgram/commands/menu.py:575
msgid "У вашего бота уже слишком много шаблонов"
msgstr "У вашого бота вже дуже багато шаблонів"
#: olgram/commands/menu.py:579
msgid "Такой текст уже есть в списке шаблонов"
msgstr "Такий текст вже є у списку шаблонів"
#: olgram/commands/menu.py:597
msgid "У вас нет прав на этого бота"
msgstr "У вас немає прав на цього бота"
#: olgram/commands/menu.py:617 olgram/commands/menu.py:643
msgid "Рассылка была совсем недавно, подождите немного"
msgstr ""
#: olgram/commands/menu.py:619 olgram/commands/menu.py:645
msgid "Нет пользователей для рассылки"
msgstr ""
#: olgram/commands/menu.py:647
msgid "Рассылка запущена"
msgstr "Розсилка запущена"
#: olgram/commands/menu.py:649
msgid "Рассылка завершена, отправлено {0} сообщений"
msgstr "Розсилка завершена, надіслано {0} повідомлень"
#: olgram/commands/menu.py:651
msgid "Устарело, создайте новую рассылку"
msgstr "Застаріло, створіть нову розсилку"
#: olgram/commands/promo.py:27
msgid ""
"Новый промокод\n"
"```{0}```"
msgstr ""
"Новий промокод\n"
"```{0}```"
#: olgram/commands/promo.py:46
msgid "Неправильный токен"
msgstr "Неправильний токен"
#: olgram/commands/promo.py:49
msgid "Такого кода не существует"
msgstr "Такого коду не існує"
#: olgram/commands/promo.py:59
msgid "Промокод отозван"
msgstr "Промокод відкликаний"
#: olgram/commands/promo.py:70
msgid ""
"Укажите аргумент: промокод. Например: <pre>/setpromo my-promo-code</pre>"
msgstr ""
"Зазначте аргумент: промокод. Наприклад: <pre>/setpromo my-promo-code</pre>"
#: olgram/commands/promo.py:78 olgram/commands/promo.py:82
msgid "Промокод не найден"
msgstr "Промокод не знайдено"
#: olgram/commands/promo.py:85
msgid "Промокод уже использован"
msgstr "Промокод уже використаний"
#: olgram/commands/promo.py:91
msgid "Промокод активирован! Спасибо 🙌"
msgstr "Промокод активовано! Дякую 🙌"
#: olgram/commands/start.py:23
msgid ""
"\n"
" Olgram Bot — это конструктор ботов обратной связи в Telegram. Подробнее "
"<a href=\"https://olgram.readthedocs.io\">читайте здесь</a>. Следите за "
"обновлениями <a href=\"https://t.me/civsoc_it\">здесь</a>.\n"
"\n"
" Используйте эти команды, чтобы управлять этим ботом:\n"
"\n"
" /addbot - добавить бот\n"
" /mybots - управление ботами\n"
"\n"
" /help - помощь\n"
" "
msgstr ""
"\n"
" Olgram Bot - це конструктор роботів зворотного зв'язку в Telegram. "
"Докладніше <a href=\"https://olgram.readthedocs.io\">читайте тут</a>. "
"Слідкуйте за оновленнями <a href=\"https://t.me/civsoc_it\">тут</a>.\n"
"\n"
" Використовуйте ці команди, щоб керувати цим ботом:\n"
"\n"
" /addbot - додати бот\n"
" /mybots - керування ботами\n"
"\n"
" /help - допомога\n"
" \n"
" "
#: olgram/commands/start.py:43
msgid ""
"\n"
" Читайте инструкции на нашем сайте https://olgram.readthedocs.io\n"
" Техническая поддержка: @civsocit_feedback_bot\n"
" Версия {0}\n"
" "
msgstr ""
"\n"
" Читайте інструкції на нашому сайті https://olgram.readthedocs.io\n"
" Технічна підтримка: @civsocit_feedback_bot\n"
" Версія {0}\n"
" \n"
" "
#: olgram/models/models.py:30
msgid ""
"\n"
" Здравствуйте!\n"
" Напишите ваш вопрос и мы ответим вам в ближайшее время.\n"
" "
msgstr ""
"\n"
" Доброго дня!\n"
" Напишіть ваше запитання, і ми відповімо вам найближчим часом.\n"
" \n"
" "
#: olgram/utils/permissions.py:41
msgid "Владелец бота ограничил доступ к этому функционалу 😞"
msgstr "Власник бота обмежив доступ до цього функціоналу 😞"
#: olgram/utils/permissions.py:53
msgid "Владелец бота ограничил доступ к этому функционалу😞"
msgstr "Власник бота обмежив доступ до цього функціоналу 😞"
#: server/custom.py:57
msgid ""
"<b>Политика конфиденциальности</b>\n"
"\n"
"Этот бот не хранит ваши сообщения, имя пользователя и @username. При "
"отправке сообщения (кроме команд /start и /security_policy) ваш "
"идентификатор пользователя записывается в кеш на некоторое время и потом "
"удаляется из кеша. Этот идентификатор используется для общения с "
"оператором.\n"
"\n"
msgstr ""
"<b>Політика конфіденційності</b>\n"
"\n"
"Цей бот не зберігає ваші повідомлення, ім'я користувача та @username. При "
"надсиланні повідомлення (крім команд /start та /security_policy) ваш "
"ідентифікатор користувача записується в кеш на деякий час і потім "
"видаляється з кеша. Цей ідентифікатор використовується для спілкування з "
"оператором.\n"
"\n"
#: server/custom.py:62
msgid ""
"При отправке сообщения (кроме команд /start и /security_policy) оператор "
"<b>видит</b> ваши имя пользователя, @username и идентификатор пользователя в "
"силу настроек, которые оператор указал при создании бота.\n"
"\n"
msgstr ""
"При надсиланні повідомлення (крім команд /start та /security_policy) "
"оператор <b>бачить</b> ваше ім'я користувача, @username та ідентифікатор "
"користувача через налаштування, які оператор вказав при створенні бота.\n"
"\n"
#: server/custom.py:66
msgid ""
"В зависимости от ваших настроек конфиденциальности Telegram, оператор может "
"видеть ваш username, имя пользователя и другую информацию.\n"
"\n"
msgstr ""
"Залежно від ваших налаштувань конфіденційності Telegram оператор може бачити "
"ваш username, ім'я користувача та іншу інформацію.\n"
"\n"
#: server/custom.py:70
msgid ""
"В этом боте включена массовая рассылка в силу настроек, которые оператор "
"указал при создании бота. Ваш идентификатор пользователя может быть записан "
"в базу данных на долгое время"
msgstr ""
"У цьому роботі включено масове розсилання в силу налаштувань, які оператор "
"вказав при створенні робота. Ваш ідентифікатор користувача може бути "
"записаний до бази даних на довгий час"
#: server/custom.py:73
msgid "В этом боте нет массовой рассылки сообщений"
msgstr "У цьому роботі немає масової розсилки повідомлень"
#: server/custom.py:83
msgid "Сообщение от пользователя "
msgstr "Допис від користувача "
#: server/custom.py:157
msgid "Вы заблокированы в этом боте"
msgstr "Ви заблоковані у цьому боті"
#: server/custom.py:163
msgid "Слишком много сообщений, подождите одну минуту"
msgstr "Забагато повідомлень, зачекайте одну хвилину"
#: server/custom.py:170
msgid "Не удаётся связаться с владельцем бота"
msgstr "Не вдається зв'язатися з власником бота"
#: server/custom.py:202
msgid ""
"<i>Невозможно переслать сообщение: автор не найден (сообщение слишком "
"старое?)</i>"
msgstr ""
"<i>Неможливо надіслати повідомлення: автора не знайдено (повідомлення "
"занадто старе?)</i>"
#: server/custom.py:210
msgid "Пользователь заблокирован"
msgstr "Користувач заблоковано"
#: server/custom.py:215
msgid "Пользователь не был забанен"
msgstr "Користувач не був забанений"
#: server/custom.py:218
msgid "Пользователь разбанен"
msgstr "Користувач розбанений"
#: server/custom.py:223
msgid "<i>Невозможно переслать сообщение (автор заблокировал бота?)</i>"
msgstr "<i>Неможливо надіслати повідомлення (автор заблокував робота?)</i>"
#: server/server.py:41
msgid "(Пере)запустить бота"
msgstr "(Пере) запустити бота"
#: server/server.py:42
msgid "Политика конфиденциальности"
msgstr "Політика конфіденційності"
#~ msgid ""
#~ "\n"
#~ "\n"
#~ "Этот бот создан с помощью @OlgramBot"
#~ msgstr ""
#~ "\n"
#~ "\n"
#~ "Цей бот створено за допомогою @OlgramBot"

View File

@ -0,0 +1,557 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR ORGANIZATION
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: \n"
"POT-Creation-Date: 2022-03-22 04:36+0300\n"
"PO-Revision-Date: 2022-03-22 04:55+0300\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: pygettext.py 1.5\n"
"X-Generator: Poedit 3.0\n"
"Last-Translator: \n"
"Plural-Forms: nplurals=1; plural=0;\n"
"Language: zh_CN\n"
#: olgram/commands/bot_actions.py:21
msgid "Бот удалён"
msgstr "移除机器人"
#: olgram/commands/bot_actions.py:37 olgram/commands/bot_actions.py:49
msgid "Текст сброшен"
msgstr "重置文本"
#: olgram/commands/bot_actions.py:63
msgid "Выбран личный чат"
msgstr "选择了私聊"
#: olgram/commands/bot_actions.py:68
msgid "Нельзя привязать бота к этому чату"
msgstr "你不能将机器人链接到这个群聊"
#: olgram/commands/bot_actions.py:72
msgid "Выбран чат {0}"
msgstr "聊天选择 {0}"
#: olgram/commands/bots.py:42
msgid "У вас уже слишком много ботов."
msgstr "你已经有太多的机器人了。"
#: olgram/commands/bots.py:45
msgid ""
"\n"
" Чтобы подключить бот, вам нужно выполнить три действия:\n"
"\n"
" 1. Перейдите в бот @BotFather, нажмите START и отправьте команду /"
"newbot\n"
" 2. Введите название бота, а потом username бота.\n"
" 3. После создания бота перешлите ответное сообщение в этот бот или "
"скопируйте и пришлите token бота.\n"
"\n"
" Важно: не подключайте боты, которые используются в других сервисах "
"(Manybot, Chatfuel, Livegram и других).\n"
" "
msgstr ""
"\n"
" 要连接机器人,你需要遵循三个步骤。\n"
"\n"
" 1. 转到机器人@BotFather按/START键并发送/newbot\n"
" 2. 输入机器人的名字,然后输入机器人的用户名。\n"
" 3. 一旦创建了机器人,就向这个机器人转发一条回复信息,或者复制并发送机器人"
"的令牌。\n"
"\n"
" 重要不要连接用于其他服务的机器人Manybot、Chatfuel、Livegram和其"
"他)。\n"
" "
#: olgram/commands/bots.py:65
msgid ""
"\n"
" Это не токен бота.\n"
"\n"
" Токен выглядит вот так: 123456789:AAAA-"
"abc123_AbcdEFghijKLMnopqrstu12\n"
" "
msgstr ""
"\n"
" 这不是一个机器人令牌。\n"
"\n"
" 该令牌看起来像这样123456789:AAAA-abc123_AbcdEFghijKLMnopqrstu12\n"
" "
#: olgram/commands/bots.py:72
msgid ""
"\n"
" Не удалось запустить этого бота: неверный токен\n"
" "
msgstr ""
"\n"
" 运行此机器人失败:错误的令牌\n"
" "
#: olgram/commands/bots.py:77
msgid ""
"\n"
" Не удалось запустить этого бота: непредвиденная ошибка\n"
" "
msgstr ""
"\n"
" 该机器人无法启动:意外错误\n"
" "
#: olgram/commands/bots.py:82
msgid ""
"\n"
" Такой бот уже есть в базе данных\n"
" "
msgstr ""
"\n"
" 这个机器人已经在数据库中出现了\n"
" "
#: olgram/commands/bots.py:114
msgid "Бот добавлен! Список ваших ботов: /mybots"
msgstr "机器人已加入! 你的机器人列表:/mybots"
#: olgram/commands/info.py:21
msgid "Недостаточно прав"
msgstr "没有足够的权利"
#: olgram/commands/info.py:32
msgid "Количество ботов: {0}\n"
msgstr "机器人的数量。{0}\n"
#: olgram/commands/info.py:33
msgid "Количество пользователей (у конструктора): {0}\n"
msgstr "用户的数量(在构造器处)。{0}\n"
#: olgram/commands/info.py:34
msgid "Шаблонов ответов: {0}\n"
msgstr "回复模板。{0}\n"
#: olgram/commands/info.py:35
msgid "Входящих сообщений у всех ботов: {0}\n"
msgstr "所有的机器人都有传入的信息。{0}\n"
#: olgram/commands/info.py:36
msgid "Исходящих сообщений у всех ботов: {0}\n"
msgstr "所有的机器人都有外发信息。{0}\n"
#: olgram/commands/menu.py:31
msgid ""
"\n"
" У вас нет добавленных ботов.\n"
"\n"
" Отправьте команду /addbot, чтобы добавить бот.\n"
" "
msgstr ""
"\n"
" 你没有添加任何机器人。\n"
"\n"
" 发送命令/addbot来添加一个机器人。\n"
" "
#: olgram/commands/menu.py:46
msgid "Ваши боты"
msgstr "你的机器人"
#: olgram/commands/menu.py:67
msgid "Личные сообщения"
msgstr "个人留言"
#: olgram/commands/menu.py:72 olgram/commands/menu.py:117
#: olgram/commands/menu.py:143 olgram/commands/menu.py:166
#: olgram/commands/menu.py:222
msgid "<< Назад"
msgstr "<< 返回"
#: olgram/commands/menu.py:78
msgid ""
"\n"
" Этот бот не добавлен в чаты, поэтому все сообщения будут приходить "
"вам в бот.\n"
" Чтобы подключить чат — добавьте бот @{0} в чат, откройте это меню "
"ещё раз и выберите добавленный чат.\n"
" Если ваш бот состоял в групповом чате до того, как его добавили в "
"Olgram - удалите бота из чата и добавьте\n"
" снова.\n"
" "
msgstr ""
"\n"
" 这个机器人没有被添加到聊天记录中,所以所有的信息都会在机器人中找到"
"你。\n"
" 要连接群聊--将机器人@{0}添加到群聊中,再次打开此菜单并选择添加的群"
"聊。\n"
" 如果你的机器人在添加到Olgram之前是在群组中请将其从群聊中删"
"除,然后添加到群组中。\n"
" 再次。\n"
" "
#: olgram/commands/menu.py:85
msgid ""
"\n"
" В этом разделе вы можете привязать бота @{0} к чату.\n"
" Выберите чат, куда бот будет пересылать сообщения.\n"
" "
msgstr ""
"\n"
" 在本节中,您可以将@{0}机器人绑定到一个群聊中。\n"
" 选择机器人将转发消息的群聊。\n"
" "
#: olgram/commands/menu.py:97
msgid "Текст"
msgstr "自动回复"
#: olgram/commands/menu.py:102
msgid "Чат"
msgstr "群聊"
#: olgram/commands/menu.py:107
msgid "Удалить бот"
msgstr "删除机器人"
#: olgram/commands/menu.py:112
msgid "Статистика"
msgstr "统计数据"
#: olgram/commands/menu.py:121
msgid "Опции"
msgstr "选择"
#: olgram/commands/menu.py:126
msgid ""
"\n"
" Управление ботом @{0}.\n"
"\n"
" Если у вас возникли вопросы по настройке бота, то посмотрите нашу "
"справку /help или напишите нам\n"
" @civsocit_feedback_bot\n"
" "
msgstr ""
"\n"
" 机器人管理@{0}。\n"
"\n"
" 如果你有任何关于机器人配置的问题,请参阅我们的帮助/help\n"
" "
#: olgram/commands/menu.py:138
msgid "Да, удалить бот"
msgstr "是的,删除该机器人"
#: olgram/commands/menu.py:147
msgid ""
"\n"
" Вы уверены, что хотите удалить бота @{0}?\n"
" "
msgstr ""
"\n"
" 你确定要删除机器人@{0}吗?\n"
" "
#: olgram/commands/menu.py:156
msgid "Потоки сообщений"
msgstr "信息流"
#: olgram/commands/menu.py:161
msgid "Данные пользователя"
msgstr "用户数据"
#: olgram/commands/menu.py:171 olgram/commands/menu.py:172
msgid "включены"
msgstr "包括"
#: olgram/commands/menu.py:171 olgram/commands/menu.py:172
msgid "выключены"
msgstr "关闭"
#: olgram/commands/menu.py:173
msgid ""
"\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options.html#threads"
"\">Потоки сообщений</a>: <b>{0}</b>\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options.html#user-info"
"\">Данные пользователя</a>: <b>{1}</b>\n"
" "
msgstr ""
"\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options.html#threads\">"
"信息流</a>: <b>{0}</b>\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options.html#user-info"
"\">用户数据</a>: <b>{1}</b>\n"
" "
#: olgram/commands/menu.py:185 olgram/commands/menu.py:247
#: olgram/commands/menu.py:289
msgid "<< Завершить редактирование"
msgstr "<< 完成编辑"
#: olgram/commands/menu.py:189
msgid "Автоответчик"
msgstr "自动回复"
#: olgram/commands/menu.py:194 olgram/commands/menu.py:261
msgid "Сбросить текст"
msgstr "重置文本"
#: olgram/commands/menu.py:199
msgid ""
"\n"
" Сейчас вы редактируете текст, который отправляется после того, как "
"пользователь отправит вашему боту @{0}\n"
" команду /start\n"
"\n"
" Текущий текст:\n"
" <pre>{1}</pre>\n"
" Отправьте сообщение, чтобы изменить текст.\n"
" "
msgstr ""
"\n"
" 你现在正在编辑用户向你的机器人发送@{0}之后的文本。\n"
" /start\n"
"\n"
" 目前的文本。\n"
" <pre>{1}</pre>\n"
" 发送消息,改变文本。\n"
" "
#: olgram/commands/menu.py:226
msgid ""
"\n"
" Статистика по боту @{0}\n"
"\n"
" Входящих сообщений: <b>{1}</b>\n"
" Ответных сообщений: <b>{2}</b>\n"
" Шаблоны ответов: <b>{3}</b>\n"
" Забанено пользователей: <b>{4}</b>\n"
" "
msgstr ""
"\n"
" 机器人统计 @{0}\n"
"\n"
" 收到的信息: <b>{1}</b>\n"
" 回复信息: <b>{2}</b>\n"
" 回复模板: <b>{3}</b>\n"
" 被禁止的用户: <b>{4}</b>\n"
" "
#: olgram/commands/menu.py:251
msgid "Предыдущий текст"
msgstr ""
#: olgram/commands/menu.py:256
msgid "Шаблоны ответов..."
msgstr "回复模板..."
#: olgram/commands/menu.py:266
msgid ""
"\n"
" Сейчас вы редактируете текст автоответчика. Это сообщение отправляется в "
"ответ на все входящие сообщения @{0} автоматически. По умолчанию оно "
"отключено.\n"
"\n"
" Текущий текст:\n"
" <pre>{1}</pre>\n"
" Отправьте сообщение, чтобы изменить текст.\n"
" "
msgstr ""
"\n"
" 你现在正在编辑自动回复的文本。该信息会自动响应所有收到的@{0}信息而发送。"
"默认情况下,它是禁用的。\n"
"\n"
" 目前的文本。\n"
" <pre>{1}</pre>。\n"
" 发送消息,改变文本。\n"
" "
#: olgram/commands/menu.py:276
msgid "(отключено)"
msgstr "(关闭)"
#: olgram/commands/menu.py:293
msgid ""
"\n"
" Сейчас вы редактируете шаблоны ответов для @{0}. Текущие шаблоны:\n"
"\n"
" <pre>\n"
" {1}\n"
" </pre>\n"
" Отправьте какую-нибудь фразу (например: \"Ваш заказ готов, ожидайте!\"), "
"чтобы добавить её в шаблон.\n"
" Чтобы удалить шаблон из списка, отправьте его номер в списке (например, "
"4)\n"
" "
msgstr ""
"\n"
" 你现在正在编辑@{0}的回复模板。目前的模板。\n"
"\n"
" <pre>\n"
" {1}\n"
" </pre>。\n"
" 发送一个短语(例如:\"您的订单已准备好,请等待!\"),将其添加到模板"
"中。\n"
" 要从列表中删除一个模板请发送它在列表中的编号如4。\n"
" "
#: olgram/commands/menu.py:312
msgid "(нет шаблонов)"
msgstr "(没有模板)"
#: olgram/commands/menu.py:351
msgid "У вас нет шаблонов, чтобы их удалять"
msgstr "你没有模板来删除它们"
#: olgram/commands/menu.py:353
msgid "Неправильное число. Чтобы удалить шаблон, введите число от 0 до {0}"
msgstr "不正确的数字。要删除一个模式请在0和{0}之间输入一个数字。"
#: olgram/commands/menu.py:361
msgid "У вашего бота уже слишком много шаблонов"
msgstr "你的机器人已经有太多的模式了"
#: olgram/commands/menu.py:365
msgid "Такой текст уже есть в списке шаблонов"
msgstr "此文本已在模板列表中"
#: olgram/commands/menu.py:383
msgid "У вас нет прав на этого бота"
msgstr "你对这个机器人没有任何权利"
#: olgram/commands/start.py:23
msgid ""
"\n"
" Olgram Bot — это конструктор ботов обратной связи в Telegram. Подробнее "
"<a href=\"https://olgram.readthedocs.io\">читайте здесь</a>.\n"
"\n"
" Используйте эти команды, чтобы управлять этим ботом:\n"
"\n"
" /addbot - добавить бот\n"
" /mybots - управление ботами\n"
"\n"
" /help - помощь\n"
" "
msgstr ""
"\n"
" Olgram Bot — 是一个Telegram反馈机器人的构建者。阅读更多 <a href="
"\"https://olgram.readthedocs.io\">在此阅读</a>.\n"
"\n"
" 使用这些命令来控制这个机器人:\n"
"\n"
" /addbot - 绑定机器人\n"
" /mybots - 机器人控制\n"
"\n"
" /help - 帮助\n"
" "
#: olgram/commands/start.py:42
msgid ""
"\n"
" Читайте инструкции на нашем сайте https://olgram.readthedocs.io\n"
" Техническая поддержка: @civsocit_feedback_bot\n"
" Версия {0}\n"
" "
msgstr ""
"\n"
" 请阅读我们网站上的说明 https://olgram.readthedocs.io\n"
" 版本{0}\n"
" "
#: olgram/models/models.py:30
msgid ""
"\n"
" Здравствуйте!\n"
" Напишите ваш вопрос и мы ответим вам в ближайшее время.\n"
" "
msgstr ""
"\n"
" 你好!\n"
" 请写下您的问题,我们将很快给您答复。\n"
" "
#: olgram/utils/permissions.py:40
msgid "Владелец бота ограничил доступ к этому функционалу 😞"
msgstr "机器人所有者已经限制了对该功能的访问 😞"
#: olgram/utils/permissions.py:52
msgid "Владелец бота ограничил доступ к этому функционалу😞"
msgstr "机器人主人限制了对该功能的访问😞。"
#: server/custom.py:40
msgid ""
"<b>Политика конфиденциальности</b>\n"
"\n"
"Этот бот не хранит ваши сообщения, имя пользователя и @username. При "
"отправке сообщения (кроме команд /start и /security_policy) ваш "
"идентификатор пользователя записывается в кеш на некоторое время и потом "
"удаляется из кеша. Этот идентификатор используется только для общения с "
"оператором; боты Olgram не делают массовых рассылок.\n"
"\n"
msgstr ""
"<b>隐私政策</b\n"
"\n"
"这个机器人不存储你的信息、用户名或@用户名。当你发送消息时(除/start和/"
"security_policy外你的用户名会被缓存一段时间然后从缓存中删除。这个ID只用"
"于与运营商沟通Olgram机器人不做批量信息发送。\n"
"\n"
#: server/custom.py:46
msgid ""
"При отправке сообщения (кроме команд /start и /security_policy) оператор "
"<b>видит</b> ваши имя пользователя, @username и идентификатор пользователя в "
"силу настроек, которые оператор указал при создании бота."
msgstr ""
"当发送消息时(除了/start和/security_policy操作者<b>看到</b>你的用户名、@"
"用户名和用户ID凭借的是操作者在创建机器人时指定的设置。"
#: server/custom.py:50
msgid ""
"В зависимости от ваших настроек конфиденциальности Telegram, оператор может "
"видеть ваш username, имя пользователя и другую информацию."
msgstr ""
"根据你的Telegram隐私设置运营商可能会看到你的用户名用户名和其他信息。"
#: server/custom.py:61
msgid "Сообщение от пользователя "
msgstr "用户的信息 "
#: server/custom.py:88
msgid "Вы заблокированы в этом боте"
msgstr "你在这个机器人中被封锁了"
#: server/custom.py:128
msgid ""
"<i>Невозможно переслать сообщение: автор не найден (сообщение слишком "
"старое?)</i>"
msgstr "无法转发信息:找不到作者(信息太旧?)"
#: server/custom.py:136
msgid "Пользователь заблокирован"
msgstr "用户被封锁了"
#: server/custom.py:141
msgid "Пользователь не был забанен"
msgstr "该用户没有被禁止"
#: server/custom.py:144
msgid "Пользователь разбанен"
msgstr "解禁用户"
#: server/custom.py:149
msgid "<i>Невозможно переслать сообщение (автор заблокировал бота?)</i>"
msgstr "无法转发该信息(作者已经屏蔽了机器人?)"
#: server/server.py:41
msgid "(Пере)запустить бота"
msgstr "(重新)启动机器人"
#: server/server.py:42
msgid "Политика конфиденциальности"
msgstr "隐私政策"
msgid "\n\nЭтот бот создан с помощью @OlgramBot"
msgstr "\n\n "

41
main.py
View File

@ -1,34 +1,38 @@
import asyncio
import argparse
from tortoise import Tortoise
from olgram.router import dp
from olgram.settings import TORTOISE_ORM
from olgram.settings import TORTOISE_ORM, OlgramSettings
from olgram.utils.permissions import AccessMiddleware
from server.custom import init_redis
import olgram.commands.menu # noqa: F401
import olgram.commands.bots # noqa: F401
import olgram.commands.start # noqa: F401
import olgram.commands.menu # noqa: F401
import olgram.commands.bot_actions # noqa: F401
import olgram.commands.info # noqa: F401
import olgram.commands.promo # noqa: F401
import olgram.commands.admin # noqa: F401
from locales.locale import _
from server.server import main as server_main
import logging
logging.basicConfig(level=logging.INFO)
async def init_database():
await Tortoise.init(config=TORTOISE_ORM)
async def init_olgram():
from olgram.router import bot
from olgram.router import bot, dp
dp.setup_middleware(AccessMiddleware(OlgramSettings.admin_ids()))
from aiogram.types import BotCommand
await bot.set_my_commands(
[
BotCommand("start", "Запустить бота"),
BotCommand("addbot", "Добавить бот"),
BotCommand("mybots", "Управление ботами"),
BotCommand("help", "Справка")
BotCommand("start", _("Запустить бота")),
BotCommand("addbot", _("Добавить бот")),
BotCommand("mybots", _("Управление ботами")),
BotCommand("help", _("Справка"))
]
)
@ -40,14 +44,21 @@ async def initialization():
def main():
"""
Classic polling
"""
parser = argparse.ArgumentParser("Olgram bot and feedback server")
group = parser.add_mutually_exclusive_group()
group.add_argument("--noserver", help="Не запускать сервер обратной связи, только сам Olgram", action="store_true")
group.add_argument("--onlyserver", help="Запустить только сервер обратной связи, без Olgram", action="store_true")
args = parser.parse_args()
loop = asyncio.get_event_loop()
loop.run_until_complete(initialization())
loop.create_task(dp.start_polling())
loop.create_task(server_main().start())
if not args.onlyserver:
print("Run olgram polling")
loop.create_task(dp.start_polling())
if not args.noserver:
print("Run olgram server")
loop.create_task(server_main().start())
loop.run_forever()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 KiB

6
migrate.py Normal file
View File

@ -0,0 +1,6 @@
import asyncio
from olgram.migrations.custom import migrate
if __name__ == '__main__':
asyncio.get_event_loop().run_until_complete(migrate())

81
olgram/commands/admin.py Normal file
View File

@ -0,0 +1,81 @@
"""
Здесь некоторые команды администратора
"""
from aiogram import types
from aiogram.dispatcher import FSMContext
from olgram.models import models
from olgram.router import dp
from olgram.settings import OlgramSettings
from locales.locale import _
@dp.message_handler(commands=["notifyowner"], state="*")
async def notify(message: types.Message, state: FSMContext):
"""
Команда /notify-owner
"""
if message.chat.id != OlgramSettings.supervisor_id():
await message.answer(_("Недостаточно прав"))
return
bot_name = message.get_args()
if not bot_name:
await message.answer(_("Нужно указать имя бота"))
return
bot = await models.Bot.filter(name=bot_name.replace("@", "")).first()
if not bot:
await message.answer(_("Такого бота нет в системе"))
return
await state.set_state("wait_owner_notify_message")
await state.update_data({"notify_to_bot": bot.id})
markup = types.ReplyKeyboardMarkup([[types.KeyboardButton(text=_("Пропустить"))]],
resize_keyboard=True)
await message.answer(_("Введите текст, который будет отправлен владельцу бота {0}. "
"Напишите 'Пропустить' чтобы отменить").format(bot_name), reply_markup=markup)
@dp.message_handler(state="wait_owner_notify_message")
async def on_notify_text(message: types.Message, state: FSMContext):
if not message.text:
await state.reset_state(with_data=True)
await message.answer(_("Поддерживается только текст"), reply_markup=types.ReplyKeyboardRemove())
return
if message.text == _("Пропустить"):
await state.reset_state(with_data=True)
await message.answer(_("Отменено"), reply_markup=types.ReplyKeyboardRemove())
return
await state.update_data({"notify_text": message.text})
await state.set_state("wait_owner_notify_message_confirm")
markup = types.ReplyKeyboardMarkup([[types.KeyboardButton(text=_("Отправить")),
types.KeyboardButton(text=_("Отменить"))]], resize_keyboard=True)
await message.answer("Точно отправить?", reply_markup=markup)
@dp.message_handler(state="wait_owner_notify_message_confirm")
async def on_notify_message_confirm(message: types.Message, state: FSMContext):
if not message.text or (message.text != _("Отправить")):
await state.reset_state(with_data=True)
await message.answer(_("Отменено"), reply_markup=types.ReplyKeyboardRemove())
return
data = await state.get_data()
bot = await models.Bot.get(pk=data["notify_to_bot"])
text = data["notify_text"]
chat_id = (await bot.owner).telegram_id
await state.reset_state(with_data=True)
await message.bot.send_message(chat_id, text=text)
await message.answer(_("Отправлено"), reply_markup=types.ReplyKeyboardRemove())

View File

@ -1,35 +1,70 @@
"""
Здесь работа с конкретным ботом
"""
import logging
from aiogram import types
from aiogram.utils.exceptions import TelegramAPIError
from olgram.models.models import Bot
from asyncio import sleep
from datetime import datetime
from olgram.utils.mix import send_stored_message
from aiogram.utils import exceptions
from aiogram import Bot as AioBot
from olgram.models.models import Bot, BotStartMessage, BotSecondMessage
from server.server import unregister_token
from locales.locale import _
async def delete_bot(bot: Bot, call: types.CallbackQuery):
"""
Пользователь решил удалить бота
"""
await unregister_token(bot.token)
try:
await unregister_token(bot.decrypted_token())
except exceptions.Unauthorized:
# Вероятно пользователь сбросил токен или удалил бот, это уже не наши проблемы
pass
await bot.delete()
await call.answer("Бот удалён")
await call.answer(_("Бот удалён"))
try:
await call.message.delete()
except TelegramAPIError:
except exceptions.TelegramAPIError:
pass
async def reset_bot_text(bot: Bot, call: types.CallbackQuery):
async def reset_bot_text(bot: Bot, call: types.CallbackQuery, state):
"""
Пользователь решил сбросить текст бота к default
:param bot:
:param call:
:return:
"""
bot.start_text = bot._meta.fields_map['start_text'].default
await bot.save()
await call.answer("Текст сброшен")
async with state.proxy() as proxy:
lang = proxy.get("lang", "none")
if lang == "none":
await BotStartMessage.filter(bot=bot).delete()
bot.start_text = bot._meta.fields_map['start_text'].default
await bot.save(update_fields=["start_text"])
else:
await BotStartMessage.filter(bot=bot, locale=lang).delete()
await call.answer(_("Текст сброшен"))
async def reset_bot_second_text(bot: Bot, call: types.CallbackQuery, state):
"""
Пользователь решил сбросить second text бота
:param bot:
:param call:
:return:
"""
async with state.proxy() as proxy:
lang = proxy.get("lang", "none")
if lang == "none":
await BotSecondMessage.filter(bot=bot).delete()
bot.second_text = bot._meta.fields_map['second_text'].default
await bot.save(update_fields=["second_text"])
else:
await BotSecondMessage.filter(bot=bot, locale=lang).delete()
await call.answer(_("Текст сброшен"))
async def select_chat(bot: Bot, call: types.CallbackQuery, chat: str):
@ -43,13 +78,98 @@ async def select_chat(bot: Bot, call: types.CallbackQuery, chat: str):
if chat == "personal":
bot.group_chat = None
await bot.save()
await call.answer("Выбран личный чат")
await call.answer(_("Выбран личный чат"))
return
if chat == "leave":
bot.group_chat = None
await bot.save()
chats = await bot.group_chats.all()
a_bot = AioBot(bot.decrypted_token())
for chat in chats:
try:
await chat.delete()
await a_bot.leave_chat(chat.chat_id)
except exceptions.TelegramAPIError:
pass
await call.answer(_("Бот вышел из чатов"))
await a_bot.session.close()
return
chat_obj = await bot.group_chats.filter(id=chat).first()
if not chat_obj:
await call.answer("Нельзя привязать бота к этому чату")
await call.answer(_("Нельзя привязать бота к этому чату"))
return
bot.group_chat = chat_obj
await bot.save()
await call.answer(f"Выбран чат {chat_obj.name}")
await call.answer(_("Выбран чат {0}").format(chat_obj.name))
async def threads(bot: Bot, call: types.CallbackQuery):
bot.enable_threads = not bot.enable_threads
await bot.save(update_fields=["enable_threads"])
async def additional_info(bot: Bot, call: types.CallbackQuery):
bot.enable_additional_info = not bot.enable_additional_info
await bot.save(update_fields=["enable_additional_info"])
async def always_second_message(bot: Bot, call: types.CallbackQuery):
bot.enable_always_second_message = not bot.enable_always_second_message
await bot.save(update_fields=["enable_always_second_message"])
async def thread_interrupt(bot: Bot, call: types.CallbackQuery):
bot.enable_thread_interrupt = not bot.enable_thread_interrupt
await bot.save(update_fields=["enable_thread_interrupt"])
async def olgram_text(bot: Bot, call: types.CallbackQuery):
if await bot.is_promo():
bot.enable_olgram_text = not bot.enable_olgram_text
await bot.save(update_fields=["enable_olgram_text"])
async def antiflood(bot: Bot, call: types.CallbackQuery):
bot.enable_antiflood = not bot.enable_antiflood
await bot.save(update_fields=["enable_antiflood"])
async def mailing(bot: Bot, call: types.CallbackQuery):
bot.enable_mailing = not bot.enable_mailing
await bot.save(update_fields=["enable_mailing"])
async def tags(bot: Bot, call: types.CallbackQuery):
bot.enable_tags = not bot.enable_tags
await bot.save(update_fields=["enable_tags"])
async def go_mailing(bot: Bot, context: dict) -> int:
users = await bot.mailing_users
a_bot = AioBot(bot.decrypted_token())
count = 0
for user in users:
bot.last_mailing_at = datetime.now()
await bot.save(update_fields=["last_mailing_at"])
try:
await sleep(0.05)
try:
file_id = await send_stored_message(context, a_bot, user.telegram_id)
except exceptions.RetryAfter as err:
await sleep(err.timeout)
file_id = await send_stored_message(context, a_bot, user.telegram_id)
if file_id:
context["mailing_id"] = file_id
count += 1
except (exceptions.ChatNotFound, exceptions.BotBlocked, exceptions.UserDeactivated):
await user.delete()
except Exception as err:
logging.error("mailing error")
logging.error(err, exc_info=True)
pass
return count

View File

@ -9,9 +9,10 @@ import re
from textwrap import dedent
from olgram.models.models import Bot, User
from olgram.settings import OlgramSettings
from olgram.settings import OlgramSettings, BotSettings
from olgram.commands.menu import send_bots_menu
from server.server import register_token
from locales.locale import _
from olgram.router import dp
@ -36,12 +37,17 @@ async def add_bot(message: types.Message, state: FSMContext):
"""
Команда /addbot (добавить бота)
"""
user = await User.get_or_none(telegram_id=message.from_user.id)
max_bot_count = OlgramSettings.max_bots_per_user()
if user and await user.is_promo():
max_bot_count = OlgramSettings.max_bots_per_user_promo()
bot_count = await Bot.filter(owner__telegram_id=message.from_user.id).count()
if bot_count >= OlgramSettings.max_bots_per_user():
await message.answer("У вас уже слишком много ботов.")
if bot_count >= max_bot_count:
await message.answer(_("У вас уже слишком много ботов. Удалите какой-нибудь свой бот из Olgram"
"(/mybots -> (Выбрать бота) -> Удалить бот)"))
return
await message.answer(dedent("""
await message.answer(dedent(_("""
Чтобы подключить бот, вам нужно выполнить три действия:
1. Перейдите в бот @BotFather, нажмите START и отправьте команду /newbot
@ -49,7 +55,7 @@ async def add_bot(message: types.Message, state: FSMContext):
3. После создания бота перешлите ответное сообщение в этот бот или скопируйте и пришлите token бота.
Важно: не подключайте боты, которые используются в других сервисах (Manybot, Chatfuel, Livegram и других).
"""))
""")))
await state.set_state("add_bot")
@ -61,26 +67,26 @@ async def bot_added(message: types.Message, state: FSMContext):
token = re.findall(token_pattern, message.text)
async def on_invalid_token():
await message.answer(dedent("""
await message.answer(dedent(_("""
Это не токен бота.
Токен выглядит вот так: 123456789:AAAA-abc123_AbcdEFghijKLMnopqrstu12
"""))
""")))
async def on_dummy_token():
await message.answer(dedent("""
await message.answer(dedent(_("""
Не удалось запустить этого бота: неверный токен
"""))
""")))
async def on_unknown_error():
await message.answer(dedent("""
await message.answer(dedent(_("""
Не удалось запустить этого бота: непредвиденная ошибка
"""))
""")))
async def on_duplication_bot():
await message.answer(dedent("""
await message.answer(dedent(_("""
Такой бот уже есть в базе данных
"""))
""")))
if not token:
return await on_invalid_token()
@ -98,8 +104,12 @@ async def bot_added(message: types.Message, state: FSMContext):
except TelegramAPIError:
return await on_unknown_error()
user, _ = await User.get_or_create(telegram_id=message.from_user.id)
bot = Bot(token=token, owner=user, name=test_bot_info.username, super_chat_id=message.from_user.id)
if token == BotSettings.token():
return await on_duplication_bot()
user, created = await User.get_or_create(telegram_id=message.from_user.id)
bot = Bot(token=Bot.encrypted_token(token), owner=user, name=test_bot_info.username,
super_chat_id=message.from_user.id)
try:
await bot.save()
except IntegrityError:
@ -109,5 +119,5 @@ async def bot_added(message: types.Message, state: FSMContext):
await bot.delete()
return await on_unknown_error()
await message.answer("Бот добавлен! Список ваших ботов: /mybots")
await message.answer(_("Бот добавлен! Список ваших ботов: /mybots"))
await state.reset_state()

40
olgram/commands/info.py Normal file
View File

@ -0,0 +1,40 @@
"""
Здесь метрики
"""
from aiogram import types
from aiogram.dispatcher import FSMContext
from olgram.models import models
from olgram.router import dp
from olgram.settings import OlgramSettings
from locales.locale import _
@dp.message_handler(commands=["info"], state="*")
async def info(message: types.Message, state: FSMContext):
"""
Команда /info
"""
if message.chat.id != OlgramSettings.supervisor_id():
await message.answer(_("Недостаточно прав"))
return
bots = await models.Bot.all()
bots_count = len(bots)
user_count = len(await models.User.all())
templates_count = len(await models.DefaultAnswer.all())
promo_count = len(await models.Promo.all())
olgram_text_disabled = len(await models.Bot.filter(enable_olgram_text=False))
income_messages = sum([bot.incoming_messages_count for bot in bots])
outgoing_messages = sum([bot.outgoing_messages_count for bot in bots])
await message.answer(_("Количество ботов: {0}\n").format(bots_count) +
_("Количество пользователей (у конструктора): {0}\n").format(user_count) +
_("Шаблонов ответов: {0}\n").format(templates_count) +
_("Входящих сообщений у всех ботов: {0}\n").format(income_messages) +
_("Исходящих сообщений у всех ботов: {0}\n").format(outgoing_messages) +
_("Промо-кодов выдано: {0}\n").format(promo_count) +
_("Рекламную плашку выключили: {0}\n").format(olgram_text_disabled))

View File

@ -1,12 +1,15 @@
import logging
from io import BytesIO
from olgram.router import dp
from datetime import datetime, timedelta, timezone
from aiogram import types, Bot as AioBot
from olgram.models.models import Bot, User
from olgram.models.models import Bot, User, DefaultAnswer, BotStartMessage, BotSecondMessage
from aiogram.dispatcher import FSMContext
from aiogram.utils.callback_data import CallbackData
from textwrap import dedent
from olgram.utils.mix import edit_or_create, button_text_limit
from olgram.utils.mix import edit_or_create, button_text_limit, wrap, send_stored_message
from olgram.commands import bot_actions
from locales.locale import _
import typing as ty
@ -27,11 +30,11 @@ async def send_bots_menu(chat_id: int, user_id: int, call=None):
user = await User.get_or_none(telegram_id=user_id)
bots = await Bot.filter(owner=user)
if not bots:
await AioBot.get_current().send_message(chat_id, dedent("""
await AioBot.get_current().send_message(chat_id, dedent(_("""
У вас нет добавленных ботов.
Отправьте команду /addbot, чтобы добавить бот.
"""))
""")))
return
keyboard = types.InlineKeyboardMarkup(row_width=2)
@ -42,7 +45,7 @@ async def send_bots_menu(chat_id: int, user_id: int, call=None):
chat=empty))
)
text = "Ваши боты"
text = _("Ваши боты")
if call:
await edit_or_create(call, text, keyboard)
else:
@ -63,26 +66,33 @@ async def send_chats_menu(bot: Bot, call: types.CallbackQuery):
)
if chats:
keyboard.insert(
types.InlineKeyboardButton(text="Личные сообщения",
types.InlineKeyboardButton(text=_("Личные сообщения"),
callback_data=menu_callback.new(level=3, bot_id=bot.id, operation="chat",
chat="personal"))
)
keyboard.insert(
types.InlineKeyboardButton(text=_("❗️ Выйти из всех чатов"),
callback_data=menu_callback.new(level=3, bot_id=bot.id, operation="chat",
chat="leave"))
)
keyboard.insert(
types.InlineKeyboardButton(text="<< Назад",
types.InlineKeyboardButton(text=_("<< Назад"),
callback_data=menu_callback.new(level=1, bot_id=bot.id, operation=empty,
chat=empty))
)
if not chats:
text = dedent(f"""
text = dedent(_("""
Этот бот не добавлен в чаты, поэтому все сообщения будут приходить вам в бот.
Чтобы подключить чат просто добавьте бот @{bot.name} в чат.
""")
Чтобы подключить чат добавьте бот @{0} в чат, откройте это меню ещё раз и выберите добавленный чат.
Если ваш бот состоял в групповом чате до того, как его добавили в Olgram - удалите бота из чата и добавьте
снова.
""")).format(bot.name)
else:
text = dedent(f"""
В этом разделе вы можете привязать бота @{bot.name} к чату.
text = dedent(_("""
В этом разделе вы можете привязать бота @{0} к чату.
Выберите чат, куда бот будет пересылать сообщения.
""")
""")).format(bot.name)
await edit_or_create(call, text, keyboard)
@ -91,76 +101,421 @@ async def send_bot_menu(bot: Bot, call: types.CallbackQuery):
await call.answer()
keyboard = types.InlineKeyboardMarkup(row_width=2)
keyboard.insert(
types.InlineKeyboardButton(text="Текст",
types.InlineKeyboardButton(text=_("Текст"),
callback_data=menu_callback.new(level=2, bot_id=bot.id, operation="text",
chat=empty))
)
keyboard.insert(
types.InlineKeyboardButton(text="Чат",
types.InlineKeyboardButton(text=_("Чат"),
callback_data=menu_callback.new(level=2, bot_id=bot.id, operation="chat",
chat=empty))
)
keyboard.insert(
types.InlineKeyboardButton(text="Удалить бот",
types.InlineKeyboardButton(text=_("Удалить бот"),
callback_data=menu_callback.new(level=2, bot_id=bot.id, operation="delete",
chat=empty))
)
keyboard.insert(
types.InlineKeyboardButton(text="<< Назад",
types.InlineKeyboardButton(text=_("Статистика"),
callback_data=menu_callback.new(level=2, bot_id=bot.id, operation="stat",
chat=empty))
)
keyboard.insert(
types.InlineKeyboardButton(text=_("<< Назад"),
callback_data=menu_callback.new(level=0, bot_id=empty, operation=empty, chat=empty))
)
keyboard.insert(
types.InlineKeyboardButton(text=_("Опции"),
callback_data=menu_callback.new(level=2, bot_id=bot.id, operation="settings",
chat=empty))
)
if bot.enable_mailing:
keyboard.insert(
types.InlineKeyboardButton(text=_("Рассылка"),
callback_data=menu_callback.new(level=2, bot_id=bot.id, operation="go_mailing",
chat=empty))
)
await edit_or_create(call, dedent(f"""
Управление ботом @{bot.name}.
await edit_or_create(call, dedent(_("""
Управление ботом @{0}.
Если у вас возникли вопросы по настройке бота, то посмотрите нашу справку /help или напишите нам
@civsocit_feedback_bot
"""), reply_markup=keyboard)
""")).format(bot.name), reply_markup=keyboard)
async def send_bot_delete_menu(bot: Bot, call: types.CallbackQuery):
await call.answer()
keyboard = types.InlineKeyboardMarkup(row_width=2)
keyboard.insert(
types.InlineKeyboardButton(text="Да, удалить бот",
types.InlineKeyboardButton(text=_("Да, удалить бот"),
callback_data=menu_callback.new(level=3, bot_id=bot.id, operation="delete_yes",
chat=empty))
)
keyboard.insert(
types.InlineKeyboardButton(text="<< Назад",
types.InlineKeyboardButton(text=_("<< Назад"),
callback_data=menu_callback.new(level=1, bot_id=bot.id, operation=empty, chat=empty))
)
await edit_or_create(call, dedent(f"""
Вы уверены, что хотите удалить бота @{bot.name}?
"""), reply_markup=keyboard)
await edit_or_create(call, dedent(_("""
Вы уверены, что хотите удалить бота @{0}?
""")).format(bot.name), reply_markup=keyboard)
async def send_bot_text_menu(bot: Bot, call: ty.Optional[types.CallbackQuery] = None, chat_id: ty.Optional[int] = None):
async def send_bot_settings_menu(bot: Bot, call: types.CallbackQuery):
await call.answer()
keyboard = types.InlineKeyboardMarkup(row_width=2)
keyboard.insert(
types.InlineKeyboardButton(text=_("Потоки сообщений"),
callback_data=menu_callback.new(level=3, bot_id=bot.id, operation="threads",
chat=empty))
)
keyboard.insert(
types.InlineKeyboardButton(text=_("Данные пользователя"),
callback_data=menu_callback.new(level=3, bot_id=bot.id, operation="additional_info",
chat=empty))
)
keyboard.insert(
types.InlineKeyboardButton(text=_("Антифлуд"),
callback_data=menu_callback.new(level=3, bot_id=bot.id, operation="antiflood",
chat=empty))
)
keyboard.insert(
types.InlineKeyboardButton(text=_("Автоответчик всегда"),
callback_data=menu_callback.new(level=3, bot_id=bot.id,
operation="always_second_message",
chat=empty))
)
keyboard.insert(
types.InlineKeyboardButton(text=_("Рассылка"),
callback_data=menu_callback.new(level=3, bot_id=bot.id, operation="mailing",
chat=empty))
)
keyboard.insert(
types.InlineKeyboardButton(text=_("Прерывать поток"),
callback_data=menu_callback.new(level=3, bot_id=bot.id,
operation="thread_interrupt",
chat=empty))
)
keyboard.insert(
types.InlineKeyboardButton(text=_("Теги"),
callback_data=menu_callback.new(level=3, bot_id=bot.id,
operation="tags",
chat=empty))
)
is_promo = await bot.is_promo()
if is_promo:
keyboard.insert(
types.InlineKeyboardButton(text=_("Olgram подпись"),
callback_data=menu_callback.new(level=3, bot_id=bot.id, operation="olgram_text",
chat=empty))
)
keyboard.insert(
types.InlineKeyboardButton(text=_("<< Назад"),
callback_data=menu_callback.new(level=1, bot_id=bot.id, operation=empty,
chat=empty))
)
thread_turn = _("включены") if bot.enable_threads else _("выключены")
info_turn = _("включены") if bot.enable_additional_info else _("выключены")
antiflood_turn = _("включен") if bot.enable_antiflood else _("выключен")
enable_always_second_message = _("включён") if bot.enable_always_second_message else _("выключен")
thread_interrupt = _("да") if bot.enable_thread_interrupt else _("нет")
mailing_turn = _("включена") if bot.enable_mailing else _("выключена")
tags_turn = _("включены") if bot.enable_tags else _("выключены")
text = dedent(_("""
<a href="https://olgram.readthedocs.io/ru/latest/options.html#threads">Потоки сообщений</a>: <b>{0}</b>
<a href="https://olgram.readthedocs.io/ru/latest/options.html#user-info">Данные пользователя</a>: <b>{1}</b>
<a href="https://olgram.readthedocs.io/ru/latest/options.html#antiflood">Антифлуд</a>: <b>{2}</b>
<a href="https://olgram.readthedocs.io/ru/latest/options.html#always_second_message">Автоответчик всегда</a>: <b>{3}</b>
<a href="https://olgram.readthedocs.io/ru/latest/options.html#thread_interrupt">Прерывать поток</a>: <b>{4}</b>
<a href="https://olgram.readthedocs.io/ru/latest/options.html#mailing">Рассылка</a>: <b>{5}</b>
Теги: <b>{6}</b>
""")).format(thread_turn, info_turn, antiflood_turn, enable_always_second_message, thread_interrupt,
mailing_turn, tags_turn)
if is_promo:
olgram_turn = _("включена") if bot.enable_olgram_text else _("выключена")
text += _("Olgram подпись: <b>{0}</b>").format(olgram_turn)
await edit_or_create(call, text, reply_markup=keyboard, parse_mode="HTML")
languages = {
"en": "English 🇺🇸",
"ru": "Русский 🇷🇺",
"uk": "Український 🇺🇦",
"tr": "Türkçe 🇹🇷",
"hy": "հայերեն 🇦🇲",
"ka": "ქართული ენა 🇬🇪"
}
async def send_bot_text_menu(bot: Bot, call: ty.Optional[types.CallbackQuery] = None, chat_id: ty.Optional[int] = None,
state=None):
if call:
await call.answer()
async with state.proxy() as proxy:
lang = proxy.get("lang", "none")
prepared_languages = {ln.locale: ln.text for ln in await bot.start_texts}
keyboard = types.InlineKeyboardMarkup(row_width=2)
keyboard.insert(
types.InlineKeyboardButton(text=_("<< Завершить редактирование"),
callback_data=menu_callback.new(level=1, bot_id=bot.id, operation=empty, chat=empty))
)
keyboard.insert(
types.InlineKeyboardButton(text=_("Автоответчик"),
callback_data=menu_callback.new(level=3, bot_id=bot.id, operation="next_text",
chat=empty))
)
keyboard.row(
types.InlineKeyboardButton(text=_("Сбросить текст"),
callback_data=menu_callback.new(level=3, bot_id=bot.id, operation="reset_text",
chat=empty))
)
keyboard.add(
types.InlineKeyboardButton(text=("🟢 " if lang == "none" else "") + _("[все языки]"),
callback_data=menu_callback.new(level=3, bot_id=bot.id,
operation="slang_none", chat=empty))
)
for code, name in languages.items():
prefix = ""
if code == lang:
prefix = "🟢 "
elif code in prepared_languages:
prefix = "✔️ "
keyboard.insert(
types.InlineKeyboardButton(text=prefix + name,
callback_data=menu_callback.new(level=3, bot_id=bot.id,
operation=f"slang_{code}",
chat=empty))
)
text = dedent(_("""
Сейчас вы редактируете текст, который отправляется после того, как пользователь отправит вашему боту @{0}
команду /start
Текущий текст{2}:
<pre>{1}</pre>
Отправьте сообщение, чтобы изменить текст.
"""))
text = text.format(bot.name,
prepared_languages.get(lang, bot.start_text),
_(" (для языка {0})").format(languages[lang]) if lang != "none" else "")
if call:
await edit_or_create(call, text, keyboard, parse_mode="HTML")
else:
await AioBot.get_current().send_message(chat_id, text, reply_markup=keyboard, parse_mode="HTML")
async def send_bot_mailing_menu(bot: Bot, call: ty.Optional[types.CallbackQuery] = None,
chat_id: ty.Optional[int] = None):
if call:
await call.answer()
keyboard = types.InlineKeyboardMarkup(row_width=1)
keyboard.insert(
types.InlineKeyboardButton(text=_("<< Отменить рассылку"),
callback_data=menu_callback.new(level=1, bot_id=bot.id, operation=empty, chat=empty))
)
text = dedent(_("""
Напишите сообщение, которое нужно разослать всем подписчикам вашего бота @{0}.
У сообщения будет до {1} получателей.
Учтите, что
1. Рассылается только одно сообщение за раз (в т.ч. только одна картинка)
2. Когда рассылка запущена, её нельзя отменить
"""))
text = text.format(bot.name, len(await bot.mailing_users))
if call:
await edit_or_create(call, text, keyboard, parse_mode="HTML")
else:
await AioBot.get_current().send_message(chat_id, text, reply_markup=keyboard, parse_mode="HTML")
@dp.message_handler(state="wait_mailing_text",
content_types=[types.ContentType.TEXT,
types.ContentType.LOCATION,
types.ContentType.DOCUMENT,
types.ContentType.PHOTO,
types.ContentType.AUDIO,
types.ContentType.VIDEO]) # TODO: not command
async def mailing_text_received(message: types.Message, state: FSMContext):
async with state.proxy() as proxy:
bot_id = proxy["bot_id"]
proxy["mailing_content_type"] = message.content_type
buffer = BytesIO()
if message.content_type == types.ContentType.TEXT:
proxy["mailing_text"] = message.html_text
elif message.content_type == types.ContentType.LOCATION:
proxy["mailing_location"] = message.location
elif message.content_type in (types.ContentType.PHOTO, types.ContentType.DOCUMENT, types.ContentType.AUDIO,
types.ContentType.VIDEO):
proxy["mailing_caption"] = message.caption
if message.content_type == types.ContentType.PHOTO:
obj = message.photo[-1]
elif message.content_type == types.ContentType.DOCUMENT:
obj = message.document
elif message.content_type == types.ContentType.AUDIO:
obj = message.audio
elif message.content_type == types.ContentType.VIDEO:
obj = message.video
if obj.file_size and obj.file_size > 4194304:
return await message.answer(_("Слишком большой файл (4 Мб максимум)"))
try:
await obj.download(buffer, timeout=5)
except Exception as err:
logging.error("Error downloading file")
logging.error(err, exc_info=True)
return await message.answer(_("Не удалось загрузить файл (слишком большой размер?)"))
proxy["mailing_data"] = buffer.getvalue()
proxy["mailing_file_name"] = getattr(obj, "file_name", None)
_message_id = await send_stored_message(proxy, AioBot.get_current(), message.chat.id)
keyboard = types.InlineKeyboardMarkup(row_width=1)
keyboard.insert(
types.InlineKeyboardButton(text=_("<< Отменить рассылку"),
callback_data=menu_callback.new(level=1, bot_id=bot_id, operation=empty, chat=empty))
)
keyboard.insert(
types.InlineKeyboardButton(text=_("Да, начать рассылку"),
callback_data=menu_callback.new(level=3, bot_id=bot_id, operation="go_go_mailing",
chat=empty))
)
await AioBot.get_current().send_message(message.chat.id, reply_to_message_id=_message_id,
text="Вы уверены, что хотите разослать это сообщение всем пользователям?",
reply_markup=keyboard)
async def send_bot_statistic_menu(bot: Bot, call: ty.Optional[types.CallbackQuery] = None,
chat_id: ty.Optional[int] = None):
if call:
await call.answer()
keyboard = types.InlineKeyboardMarkup(row_width=2)
keyboard.insert(
types.InlineKeyboardButton(text="Сбросить текст",
callback_data=menu_callback.new(level=3, bot_id=bot.id, operation="reset_text",
chat=empty))
)
keyboard.insert(
types.InlineKeyboardButton(text="<< Завершить редактирование",
types.InlineKeyboardButton(text=_("<< Назад"),
callback_data=menu_callback.new(level=1, bot_id=bot.id, operation=empty, chat=empty))
)
text = dedent("""
Сейчас вы редактируете текст, который отправляется после того, как пользователь отправит вашему боту {0}
команду /start
text = dedent(_("""
Статистика по боту @{0}
Входящих сообщений: <b>{1}</b>
Ответных сообщений: <b>{2}</b>
Шаблоны ответов: <b>{3}</b>
Забанено пользователей: <b>{4}</b>
""")).format(bot.name, bot.incoming_messages_count, bot.outgoing_messages_count, len(await bot.answers),
len(await bot.banned_users))
if call:
await edit_or_create(call, text, keyboard, parse_mode="HTML")
else:
await AioBot.get_current().send_message(chat_id, text, reply_markup=keyboard, parse_mode="HTML")
async def send_bot_second_text_menu(bot: Bot, call: ty.Optional[types.CallbackQuery] = None,
chat_id: ty.Optional[int] = None, state=None):
if call:
await call.answer()
async with state.proxy() as proxy:
lang = proxy.get("lang", "none")
prepared_languages = {ln.locale: ln.text for ln in await bot.second_texts}
keyboard = types.InlineKeyboardMarkup(row_width=2)
keyboard.insert(
types.InlineKeyboardButton(text=_("<< Завершить редактирование"),
callback_data=menu_callback.new(level=1, bot_id=bot.id, operation=empty, chat=empty))
)
keyboard.insert(
types.InlineKeyboardButton(text=_("Предыдущий текст"),
callback_data=menu_callback.new(level=2, bot_id=bot.id, operation="text",
chat=empty))
)
keyboard.insert(
types.InlineKeyboardButton(text=_("Шаблоны ответов..."),
callback_data=menu_callback.new(level=3, bot_id=bot.id, operation="templates",
chat=empty))
)
keyboard.insert(
types.InlineKeyboardButton(text=_("Сбросить текст"),
callback_data=menu_callback.new(level=3, bot_id=bot.id,
operation="reset_second_text", chat=empty))
)
keyboard.add(
types.InlineKeyboardButton(text=("🟢 " if lang == "none" else "") + _("[все языки]"),
callback_data=menu_callback.new(level=3, bot_id=bot.id,
operation="alang_none", chat=empty))
)
for code, name in languages.items():
prefix = ""
if code == lang:
prefix = "🟢 "
elif code in prepared_languages:
prefix = "✔️ "
keyboard.insert(
types.InlineKeyboardButton(text=prefix + name,
callback_data=menu_callback.new(level=3, bot_id=bot.id,
operation=f"alang_{code}",
chat=empty))
)
text = dedent(_("""
Сейчас вы редактируете текст автоответчика. Это сообщение отправляется в ответ на все входящие сообщения @{0} \
автоматически. По умолчанию оно отключено.
Текущий текст{2}:
<pre>{1}</pre>
Отправьте сообщение, чтобы изменить текст.
"""))
text = text.format(bot.name,
prepared_languages.get(lang, bot.second_text or _("отключено")),
_(" (для языка {0})").format(languages[lang]) if lang != "none" else "")
if call:
await edit_or_create(call, text, keyboard, parse_mode="HTML")
else:
await AioBot.get_current().send_message(chat_id, text, reply_markup=keyboard, parse_mode="HTML")
async def send_bot_templates_menu(bot: Bot, call: ty.Optional[types.CallbackQuery] = None,
chat_id: ty.Optional[int] = None):
if call:
await call.answer()
keyboard = types.InlineKeyboardMarkup(row_width=2)
keyboard.insert(
types.InlineKeyboardButton(text=_("<< Завершить редактирование"),
callback_data=menu_callback.new(level=1, bot_id=bot.id, operation=empty, chat=empty))
)
text = dedent(_("""
Сейчас вы редактируете шаблоны ответов для @{0}. Текущие шаблоны:
Текущий текст:
<pre>
{1}
</pre>
Отправьте сообщение, чтобы изменить текст.
""")
text = text.format(bot.name, bot.start_text)
Отправьте какую-нибудь фразу (например: "Ваш заказ готов, ожидайте!"), чтобы добавить её в шаблон.
Чтобы удалить шаблон из списка, отправьте его номер в списке (например, 4)
"""))
templates = await bot.answers
total_text_len = sum(len(t.text) for t in templates) + len(text) # примерная длина текста
max_len = 1000
if total_text_len > 4000:
max_len = 100
templates_text = "\n".join(f"{n}. {wrap(template.text, max_len)}" for n, template in enumerate(templates))
if not templates_text:
templates_text = _("(нет шаблонов)")
text = text.format(bot.name, templates_text)
if call:
await edit_or_create(call, text, keyboard, parse_mode="HTML")
else:
@ -171,10 +526,76 @@ async def send_bot_text_menu(bot: Bot, call: ty.Optional[types.CallbackQuery] =
async def start_text_received(message: types.Message, state: FSMContext):
async with state.proxy() as proxy:
bot_id = proxy.get("bot_id")
lang = proxy.get("lang", "none")
bot = await Bot.get_or_none(pk=bot_id)
bot.start_text = message.text
await bot.save()
await send_bot_text_menu(bot, chat_id=message.chat.id)
if lang == "none":
bot.start_text = message.html_text
await bot.save(update_fields=["start_text"])
else:
obj, created = await BotStartMessage.get_or_create(bot=bot,
locale=lang,
defaults={"text": message.html_text})
if not created:
obj.text = message.html_text
await obj.save(update_fields=["text"])
await send_bot_text_menu(bot, chat_id=message.chat.id, state=state)
@dp.message_handler(state="wait_second_text", content_types="text", regexp="^[^/].+") # Not command
async def second_text_received(message: types.Message, state: FSMContext):
async with state.proxy() as proxy:
bot_id = proxy.get("bot_id")
lang = proxy.get("lang", "none")
bot = await Bot.get_or_none(pk=bot_id)
if lang == "none":
bot.second_text = message.html_text
await bot.save(update_fields=["second_text"])
else:
obj, created = await BotSecondMessage.get_or_create(bot=bot,
locale=lang,
defaults={"text": message.html_text})
if not created:
obj.text = message.html_text
await obj.save(update_fields=["text"])
if not bot.second_text:
bot.second_text = message.html_text
await bot.save(update_fields=["second_text"])
await send_bot_second_text_menu(bot, chat_id=message.chat.id, state=state)
@dp.message_handler(state="wait_template", content_types="text", regexp="^[^/](.+)?") # Not command
async def template_received(message: types.Message, state: FSMContext):
async with state.proxy() as proxy:
bot_id = proxy.get("bot_id")
bot = await Bot.get_or_none(pk=bot_id)
if message.text.isdigit():
# Delete template
number = int(message.text)
templates = await bot.answers
if not templates:
await message.answer(_("У вас нет шаблонов, чтобы их удалять"))
if number < 0 or number >= len(templates):
await message.answer(_("Неправильное число. Чтобы удалить шаблон, введите число от 0 до {0}").format(
len(templates)))
return
await templates[number].delete()
else:
# Add template
total_templates = len(await bot.answers)
if total_templates > 30:
await message.answer(_("У вашего бота уже слишком много шаблонов"))
else:
answers = await bot.answers.filter(text=message.text)
if answers:
await message.answer(_("Такой текст уже есть в списке шаблонов"))
else:
template = DefaultAnswer(text=message.text, bot=bot)
await template.save()
await send_bot_templates_menu(bot, chat_id=message.chat.id)
@dp.callback_query_handler(menu_callback.filter(), state="*")
@ -187,10 +608,11 @@ async def callback(call: types.CallbackQuery, callback_data: dict, state: FSMCon
bot_id = callback_data.get("bot_id")
bot = await Bot.get_or_none(id=bot_id)
if not bot or (await bot.owner).telegram_id != call.from_user.id:
await call.answer("У вас нет прав на этого бота", show_alert=True)
await call.answer(_("У вас нет прав на этого бота"), show_alert=True)
return
if level == "1":
await state.reset_state()
return await send_bot_menu(bot, call)
operation = callback_data.get("operation")
@ -200,17 +622,98 @@ async def callback(call: types.CallbackQuery, callback_data: dict, state: FSMCon
return await send_chats_menu(bot, call)
if operation == "delete":
return await send_bot_delete_menu(bot, call)
if operation == "stat":
return await send_bot_statistic_menu(bot, call)
if operation == "settings":
return await send_bot_settings_menu(bot, call)
if operation == "go_mailing":
if bot.last_mailing_at and bot.last_mailing_at >= datetime.now(tz=timezone.utc) - timedelta(minutes=5):
return await call.answer(_("Рассылка была совсем недавно, подождите немного"), show_alert=True)
if not await bot.mailing_users:
return await call.answer(_("Нет пользователей для рассылки"))
await state.set_state("wait_mailing_text")
async with state.proxy() as proxy:
proxy["bot_id"] = bot.id
return await send_bot_mailing_menu(bot, call)
if operation == "text":
await state.set_state("wait_start_text")
async with state.proxy() as proxy:
proxy["bot_id"] = bot.id
return await send_bot_text_menu(bot, call)
return await send_bot_text_menu(bot, call, state=state)
if level == "3":
if operation == "delete_yes":
return await bot_actions.delete_bot(bot, call)
if operation == "mailing":
await bot_actions.mailing(bot, call)
return await send_bot_settings_menu(bot, call)
if operation == "go_go_mailing":
if (await state.get_state()) == "wait_mailing_text":
async with state.proxy() as proxy:
mailing_data = dict(proxy)
await state.reset_state()
if bot.last_mailing_at and bot.last_mailing_at >= datetime.now(tz=timezone.utc) - timedelta(minutes=5):
return await call.answer(_("Рассылка была совсем недавно, подождите немного"), show_alert=True)
if not await bot.mailing_users:
return await call.answer(_("Нет пользователей для рассылки"))
await call.answer(_("Рассылка запущена"))
count = await bot_actions.go_mailing(bot, mailing_data)
return await call.message.answer(_("Рассылка завершена, отправлено {0} сообщений").format(count))
else:
return await call.answer(_("Устарело, создайте новую рассылку"))
if operation == "chat":
return await bot_actions.select_chat(bot, call, callback_data.get("chat"))
if operation == "threads":
await bot_actions.threads(bot, call)
return await send_bot_settings_menu(bot, call)
if operation == "antiflood":
await bot_actions.antiflood(bot, call)
return await send_bot_settings_menu(bot, call)
if operation == "additional_info":
await bot_actions.additional_info(bot, call)
return await send_bot_settings_menu(bot, call)
if operation == "always_second_message":
await bot_actions.always_second_message(bot, call)
return await send_bot_settings_menu(bot, call)
if operation == "mailing":
await bot_actions.mailing(bot, call)
return await send_bot_settings_menu(bot, call)
if operation == "tags":
await bot_actions.tags(bot, call)
return await send_bot_settings_menu(bot, call)
if operation == "thread_interrupt":
await bot_actions.thread_interrupt(bot, call)
return await send_bot_settings_menu(bot, call)
if operation == "olgram_text":
await bot_actions.olgram_text(bot, call)
return await send_bot_settings_menu(bot, call)
if operation == "reset_text":
await bot_actions.reset_bot_text(bot, call)
return await send_bot_text_menu(bot, call)
await bot_actions.reset_bot_text(bot, call, state)
return await send_bot_text_menu(bot, call, state=state)
if operation.startswith("slang_"):
async with state.proxy() as proxy:
lang = operation.replace("slang_", "")
if lang == "none" or lang in languages:
proxy["lang"] = lang
return await send_bot_text_menu(bot, call, state=state)
if operation == "next_text":
await state.set_state("wait_second_text")
async with state.proxy() as proxy:
proxy["bot_id"] = bot.id
return await send_bot_second_text_menu(bot, call, state=state)
if operation.startswith("alang_"):
async with state.proxy() as proxy:
lang = operation.replace("alang_", "")
if lang == "none" or lang in languages:
proxy["lang"] = lang
return await send_bot_second_text_menu(bot, call, state=state)
if operation == "reset_second_text":
await bot_actions.reset_bot_second_text(bot, call, state)
return await send_bot_second_text_menu(bot, call, state=state)
if operation == "templates":
await state.set_state("wait_template")
async with state.proxy() as proxy:
proxy["bot_id"] = bot.id
return await send_bot_templates_menu(bot, call)

91
olgram/commands/promo.py Normal file
View File

@ -0,0 +1,91 @@
"""
Здесь промокоды
"""
from aiogram import types
from aiogram.dispatcher import FSMContext
from olgram.models import models
from uuid import UUID
from olgram.router import dp
from olgram.settings import OlgramSettings
from locales.locale import _
@dp.message_handler(commands=["newpromo"], state="*")
async def new_promo(message: types.Message, state: FSMContext):
"""
Команда /newpromo
"""
if message.chat.id != OlgramSettings.supervisor_id():
await message.answer(_("Недостаточно прав"))
return
promo = await models.Promo()
await message.answer(_("Новый промокод\n```{0}```").format(promo.code), parse_mode="Markdown")
await promo.save()
@dp.message_handler(commands=["delpromo"], state="*")
async def del_promo(message: types.Message, state: FSMContext):
"""
Команда /delpromo
"""
if message.chat.id != OlgramSettings.supervisor_id():
await message.answer(_("Недостаточно прав"))
return
try:
uuid = UUID(message.get_args().strip())
promo = await models.Promo.get_or_none(code=uuid)
except ValueError:
return await message.answer(_("Неправильный токен"))
if not promo:
return await message.answer(_("Такого кода не существует"))
user = await models.User.filter(promo=promo)
bots = await user.bots()
for bot in bots:
bot.enable_olgram_text = True
await bot.save(update_fields=["enable_olgram_text"])
await promo.delete()
await message.answer(_("Промокод отозван"))
@dp.message_handler(commands=["setpromo"], state="*")
async def setpromo(message: types.Message, state: FSMContext):
"""
Команда /setpromo
"""
arg = message.get_args()
if not arg:
return await message.answer(_("Укажите аргумент: промокод. Например: <pre>/setpromo my-promo-code</pre>"),
parse_mode="HTML")
arg = arg.strip()
try:
UUID(arg)
except ValueError:
return await message.answer(_("Промокод не найден"))
promo = await models.Promo.get_or_none(code=arg)
if not promo:
return await message.answer(_("Промокод не найден"))
if promo.owner:
return await message.answer(_("Промокод уже использован"))
user, created = await models.User.get_or_create(telegram_id=message.from_user.id)
promo.owner = user
await promo.save(update_fields=["owner_id"])
await message.answer(_("Промокод активирован! Спасибо 🙌"))

View File

@ -6,21 +6,24 @@ from aiogram import types
from aiogram.dispatcher import FSMContext
from textwrap import dedent
from olgram.settings import OlgramSettings
from olgram.utils.permissions import public
from locales.locale import _
from olgram.router import dp
@dp.message_handler(commands=["start"], state="*")
@public()
async def start(message: types.Message, state: FSMContext):
"""
Команда /start
"""
await state.reset_state()
# TODO: locale
await message.answer(dedent("""
Olgram Bot это конструктор ботов обратной связи в Telegram.
await message.answer(dedent(_("""
Olgram Bot это конструктор ботов обратной связи в Telegram. Подробнее \
<a href="https://olgram.readthedocs.io">читайте здесь</a>. Следите за обновлениями \
<a href="https://t.me/civsoc_it">здесь</a>.
Используйте эти команды, чтобы управлять этим ботом:
@ -28,20 +31,26 @@ async def start(message: types.Message, state: FSMContext):
/mybots - управление ботами
/help - помощь
"""))
""")), parse_mode="html", disable_web_page_preview=True)
@dp.message_handler(commands=["help"], state="*")
@public()
async def help(message: types.Message, state: FSMContext):
"""
Команда /help
"""
await message.answer(dedent(f"""
О проекте https://telegra.ph/Olgram-09-15
await message.answer(dedent(_("""
Читайте инструкции на нашем сайте https://olgram.readthedocs.io
Техническая поддержка: @civsocit_feedback_bot
Версия {0}
""")).format(OlgramSettings.version()))
Репозиторий https://github.com/civsocit/olgram
Поддержка: @civsocit_feedback_bot
Версия {OlgramSettings.version()}
"""))
@dp.message_handler(commands=["chatid"], state="*")
@public()
async def chat_id(message: types.Message, state: FSMContext):
"""
Команда /chatid
"""
await message.answer(message.chat.id)

View File

@ -0,0 +1,79 @@
"""Наши собственные миграции, которые нельзя описать на языке SQL и с которыми не справится TortoiseORM/Aerich"""
import aioredis
from tortoise import transactions, Tortoise
from olgram.settings import TORTOISE_ORM, ServerSettings
from olgram.models.models import MetaInfo, Bot
import logging
async def upgrade_1():
"""Шифруем токены"""
meta_info = await MetaInfo.first()
if meta_info.version != 0:
logging.info("skip")
return
async with transactions.in_transaction():
bots = await Bot.all()
for bot in bots:
bot.token = bot.encrypted_token(bot.token)
await bot.save()
meta_info.version = 1
await meta_info.save()
logging.info("done")
async def upgrade_2():
"""Отменяем малый TTL для старых сообщений"""
meta_info = await MetaInfo.first()
if meta_info.version != 1:
logging.info("skip")
return
con = await aioredis.create_connection(ServerSettings.redis_path())
client = aioredis.Redis(con)
i, keys = await client.scan()
for key in keys:
if not key.startswith(b"thread"):
await client.pexpire(key, ServerSettings.redis_timeout_ms())
meta_info.version = 2
await meta_info.save()
logging.info("done")
async def upgrade_3():
"""start_text и second_text должны быть валидными HTML"""
import html
meta_info = await MetaInfo.first()
if meta_info.version != 2:
logging.info("skip")
return
async with transactions.in_transaction():
bots = await Bot.all()
for bot in bots:
if bot.start_text:
bot.start_text = html.escape(bot.start_text)
if bot.second_text:
bot.second_text = html.escape(bot.second_text)
await bot.save(update_fields=["start_text", "second_text"])
meta_info.version = 3
await meta_info.save()
logging.info("done")
# Не забудь добавить миграцию в этот лист!
_migrations = [upgrade_1, upgrade_2, upgrade_3]
async def migrate():
logging.info("Run custom migrations...")
await Tortoise.init(config=TORTOISE_ORM)
for migration in _migrations:
logging.info(f"Migration {migration.__name__}...")
await migration()

View File

@ -0,0 +1,4 @@
-- upgrade --
ALTER TABLE "bot" ADD "enable_threads" BOOL NOT NULL DEFAULT False;
-- downgrade --
ALTER TABLE "bot" DROP COLUMN "enable_threads";

View File

@ -0,0 +1,4 @@
-- upgrade --
ALTER TABLE "bot" ADD "enable_additional_info" BOOL NOT NULL DEFAULT False;
-- downgrade --
ALTER TABLE "bot" DROP COLUMN "enable_additional_info";

View File

@ -0,0 +1,10 @@
-- upgrade --
CREATE TABLE IF NOT EXISTS "promo" (
"id" BIGSERIAL NOT NULL PRIMARY KEY,
"code" UUID NOT NULL,
"date" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"owner_id" INT REFERENCES "user" ("id") ON DELETE SET NULL
);
CREATE INDEX IF NOT EXISTS "idx_promo_code_9b981a" ON "promo" ("code");
-- downgrade --
DROP TABLE IF EXISTS "promo";

View File

@ -0,0 +1,4 @@
-- upgrade --
ALTER TABLE "bot" ADD "enable_olgram_text" BOOL NOT NULL DEFAULT True;
-- downgrade --
ALTER TABLE "bot" DROP COLUMN "enable_olgram_text";

View File

@ -0,0 +1,4 @@
-- upgrade --
ALTER TABLE "bot" ADD "enable_antiflood" BOOL NOT NULL DEFAULT False;
-- downgrade --
ALTER TABLE "bot" DROP COLUMN "enable_antiflood";

View File

@ -0,0 +1,9 @@
-- upgrade --
CREATE TABLE IF NOT EXISTS "bot_start_message" (
"id" SERIAL NOT NULL PRIMARY KEY,
"locale" VARCHAR(5) NOT NULL,
"bot_id" INT NOT NULL REFERENCES "bot" ("id") ON DELETE CASCADE,
CONSTRAINT "uid_bot_start_m_bot_id_871cd1" UNIQUE ("bot_id", "locale")
);
-- downgrade --
DROP TABLE IF EXISTS "bot_start_message";

View File

@ -0,0 +1,4 @@
-- upgrade --
ALTER TABLE "bot_start_message" ADD "text" TEXT NOT NULL;
-- downgrade --
ALTER TABLE "bot_start_message" DROP COLUMN "text";

View File

@ -0,0 +1,10 @@
-- upgrade --
CREATE TABLE IF NOT EXISTS "bot_second_message" (
"id" SERIAL NOT NULL PRIMARY KEY,
"locale" VARCHAR(5) NOT NULL,
"text" TEXT NOT NULL,
"bot_id" INT NOT NULL REFERENCES "bot" ("id") ON DELETE CASCADE,
CONSTRAINT "uid_bot_second__bot_id_432892" UNIQUE ("bot_id", "locale")
);
-- downgrade --
DROP TABLE IF EXISTS "bot_second_message";

View File

@ -0,0 +1,4 @@
-- upgrade --
ALTER TABLE "bot" ADD "enable_always_second_message" BOOL NOT NULL DEFAULT False;
-- downgrade --
ALTER TABLE "bot" DROP COLUMN "enable_always_second_message";

View File

@ -0,0 +1,4 @@
-- upgrade --
ALTER TABLE "bot" ADD "enable_thread_interrupt" BOOL NOT NULL DEFAULT True;
-- downgrade --
ALTER TABLE "bot" DROP COLUMN "enable_thread_interrupt";

View File

@ -0,0 +1,6 @@
-- upgrade --
ALTER TABLE "bot" ADD "enable_mailing" BOOL NOT NULL DEFAULT False;
ALTER TABLE "bot" ADD "last_mailing_at" TIMESTAMPTZ;
-- downgrade --
ALTER TABLE "bot" DROP COLUMN "enable_mailing";
ALTER TABLE "bot" DROP COLUMN "last_mailing_at";

View File

@ -0,0 +1,10 @@
-- upgrade --
CREATE TABLE IF NOT EXISTS "mailinguser" (
"id" BIGSERIAL NOT NULL PRIMARY KEY,
"telegram_id" BIGINT NOT NULL,
"bot_id" INT NOT NULL REFERENCES "bot" ("id") ON DELETE CASCADE,
CONSTRAINT "uid_mailinguser_bot_id_906a76" UNIQUE ("bot_id", "telegram_id")
);
CREATE INDEX IF NOT EXISTS "idx_mailinguser_telegra_55de60" ON "mailinguser" ("telegram_id");
-- downgrade --
DROP TABLE IF EXISTS "mailinguser";

View File

@ -0,0 +1,4 @@
-- upgrade --
ALTER TABLE "bot_start_message" ALTER COLUMN "locale" TYPE VARCHAR(15) USING "locale"::VARCHAR(15);
-- downgrade --
ALTER TABLE "bot_start_message" ALTER COLUMN "locale" TYPE VARCHAR(5) USING "locale"::VARCHAR(5);

View File

@ -0,0 +1,4 @@
-- upgrade --
ALTER TABLE "bot" ADD "enable_tags" BOOL NOT NULL DEFAULT False;
-- downgrade --
ALTER TABLE "bot" DROP COLUMN "enable_tags";

View File

@ -0,0 +1,4 @@
-- upgrade --
ALTER TABLE "bot" ADD "second_text" TEXT;
-- downgrade --
ALTER TABLE "bot" DROP COLUMN "second_text";

View File

@ -0,0 +1,10 @@
-- upgrade --
ALTER TABLE "bot" ALTER COLUMN "token" TYPE VARCHAR(200) USING "token"::VARCHAR(200);
CREATE TABLE IF NOT EXISTS "_custom_meta_info" (
"id" SERIAL NOT NULL PRIMARY KEY,
"version" INT NOT NULL DEFAULT 0
);;
INSERT INTO _custom_meta_info (id, version) VALUES (0, 0) ON CONFLICT DO NOTHING;
-- downgrade --
ALTER TABLE "bot" ALTER COLUMN "token" TYPE VARCHAR(50) USING "token"::VARCHAR(50);
DROP TABLE IF EXISTS "_custom_meta_info";

View File

@ -0,0 +1,11 @@
-- upgrade --
CREATE TABLE IF NOT EXISTS "bot_banned_user" (
"id" BIGSERIAL NOT NULL PRIMARY KEY,
"telegram_id" BIGINT NOT NULL,
"username" VARCHAR(100),
"bot_id" INT NOT NULL REFERENCES "bot" ("id") ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS "idx_bot_banned__telegra_915aca" ON "bot_banned_user" ("telegram_id");
-- downgrade --
DROP TABLE IF EXISTS "bot_banned_user";
DROP INDEX IF EXISTS "idx_bot_banned__telegra_915aca";

View File

@ -0,0 +1,7 @@
-- upgrade --
CREATE TABLE IF NOT EXISTS "defaultanswer" (
"id" BIGSERIAL NOT NULL PRIMARY KEY,
"bot_id" INT NOT NULL REFERENCES "bot" ("id") ON DELETE CASCADE
);
-- downgrade --
DROP TABLE IF EXISTS "defaultanswer";

View File

@ -0,0 +1,4 @@
-- upgrade --
ALTER TABLE "defaultanswer" ADD "text" TEXT NOT NULL;
-- downgrade --
ALTER TABLE "defaultanswer" DROP COLUMN "text";

View File

@ -0,0 +1,6 @@
-- upgrade --
ALTER TABLE "bot" ADD "outgoing_messages_count" BIGINT NOT NULL DEFAULT 0;
ALTER TABLE "bot" ADD "incoming_messages_count" BIGINT NOT NULL DEFAULT 0;
-- downgrade --
ALTER TABLE "bot" DROP COLUMN "outgoing_messages_count";
ALTER TABLE "bot" DROP COLUMN "incoming_messages_count";

View File

@ -2,18 +2,36 @@ from tortoise.models import Model
from tortoise import fields
from uuid import uuid4
from textwrap import dedent
from olgram.settings import DatabaseSettings
from locales.locale import _
class MetaInfo(Model):
id = fields.IntField(pk=True)
version = fields.IntField(default=0)
def __init__(self, **kwargs):
# Кажется это единственный способ сделать single-instance модель в TortoiseORM :(
if "id" in kwargs:
kwargs["id"] = 0
self.id = 0
super(MetaInfo, self).__init__(**kwargs)
class Meta:
table = '_custom_meta_info'
class Bot(Model):
id = fields.IntField(pk=True)
token = fields.CharField(max_length=50, unique=True)
token = fields.CharField(max_length=200, unique=True)
owner = fields.ForeignKeyField("models.User", related_name="bots")
name = fields.CharField(max_length=33)
code = fields.UUIDField(default=uuid4, index=True)
start_text = fields.TextField(default=dedent("""
start_text = fields.TextField(default=dedent(_("""
Здравствуйте!
Напишите ваш вопрос и мы ответим вам в ближайшее время.
"""))
""")))
second_text = fields.TextField(null=True, default=None)
group_chats = fields.ManyToManyField("models.GroupChat", related_name="bots", on_delete=fields.relational.CASCADE,
null=True)
@ -21,24 +39,87 @@ class Bot(Model):
on_delete=fields.relational.CASCADE,
null=True)
incoming_messages_count = fields.BigIntField(default=0)
outgoing_messages_count = fields.BigIntField(default=0)
enable_threads = fields.BooleanField(default=False)
enable_additional_info = fields.BooleanField(default=False)
enable_olgram_text = fields.BooleanField(default=True)
enable_antiflood = fields.BooleanField(default=False)
enable_always_second_message = fields.BooleanField(default=False)
enable_thread_interrupt = fields.BooleanField(default=True)
enable_mailing = fields.BooleanField(default=False)
enable_tags = fields.BooleanField(default=False)
last_mailing_at = fields.DatetimeField(null=True, default=None)
def decrypted_token(self):
cryptor = DatabaseSettings.cryptor()
return cryptor.decrypt(self.token)
@classmethod
def encrypted_token(cls, token: str):
cryptor = DatabaseSettings.cryptor()
return cryptor.encrypt(token)
async def super_chat_id(self):
group_chat = await self.group_chat
if group_chat:
return group_chat.chat_id
return (await self.owner).telegram_id
async def is_promo(self):
await self.fetch_related("owner")
return await self.owner.is_promo()
class Meta:
table = 'bot'
class BotStartMessage(Model):
id = fields.IntField(pk=True)
bot = fields.ForeignKeyField("models.Bot", related_name="start_texts", on_delete=fields.CASCADE)
locale = fields.CharField(max_length=15)
text = fields.TextField()
class Meta:
unique_together = ("bot", "locale")
table = 'bot_start_message'
class BotSecondMessage(Model):
id = fields.IntField(pk=True)
bot = fields.ForeignKeyField("models.Bot", related_name="second_texts", on_delete=fields.CASCADE)
locale = fields.CharField(max_length=5)
text = fields.TextField()
class Meta:
unique_together = ("bot", "locale")
table = 'bot_second_message'
class User(Model):
id = fields.IntField(pk=True)
telegram_id = fields.BigIntField(index=True, unique=True)
async def is_promo(self):
await self.fetch_related("promo")
return bool(self.promo)
class Meta:
table = 'user'
class MailingUser(Model):
id = fields.BigIntField(pk=True)
telegram_id = fields.BigIntField(index=True)
bot = fields.ForeignKeyField("models.Bot", related_name="mailing_users", on_delete=fields.relational.CASCADE)
class Meta:
table = 'mailinguser'
unique_together = (("bot", "telegram_id"), )
class GroupChat(Model):
id = fields.IntField(pk=True)
chat_id = fields.BigIntField(index=True, unique=True)
@ -46,3 +127,29 @@ class GroupChat(Model):
class Meta:
table = 'group_chat'
class BannedUser(Model):
id = fields.BigIntField(pk=True)
telegram_id = fields.BigIntField(index=True)
username = fields.CharField(max_length=100, default=None, null=True)
bot = fields.ForeignKeyField("models.Bot", related_name="banned_users", on_delete=fields.relational.CASCADE)
class Meta:
table = "bot_banned_user"
class DefaultAnswer(Model):
id = fields.BigIntField(pk=True)
bot = fields.ForeignKeyField("models.Bot", related_name="answers", on_delete=fields.relational.CASCADE)
text = fields.TextField()
class Promo(Model):
id = fields.BigIntField(pk=True)
code = fields.UUIDField(default=uuid4, index=True)
date = fields.DatetimeField(auto_now_add=True)
owner = fields.ForeignKeyField("models.User", related_name="promo", on_delete=fields.relational.SET_NULL,
null=True, default=None)

View File

@ -1,18 +1,24 @@
from dotenv import load_dotenv
from abc import ABC
import os
import logging
from functools import lru_cache
from datetime import timedelta
import typing as ty
from olgram.utils.crypto import Cryptor
load_dotenv()
# TODO: рефакторинг, использовать какой-нибудь lazy-config вместо своих костылей
class AbstractSettings(ABC):
@classmethod
def _get_env(cls, parameter: str, allow_none: bool = False) -> str:
parameter = os.getenv(parameter, None)
if not parameter and not allow_none:
parameter_v = os.getenv(parameter, None)
if not parameter_v and not allow_none:
raise ValueError(f"{parameter} not defined in ENV")
return parameter
return parameter_v
class OlgramSettings(AbstractSettings):
@ -22,11 +28,31 @@ class OlgramSettings(AbstractSettings):
Максимальное количество ботов у одного пользователя
:return: int
"""
return 5
return 10
@classmethod
def max_bots_per_user_promo(cls) -> int:
"""
Максимальное количество ботов у одного пользователя с промо-доступом
:return: int
"""
return 25
@classmethod
def version(cls):
return "0.0.3"
return "0.7.3"
@classmethod
@lru_cache
def admin_ids(cls):
_ids = cls._get_env("ADMIN_ID", True)
return set(map(int, _ids.split(","))) if _ids else None
@classmethod
@lru_cache
def supervisor_id(cls):
_id = cls._get_env("SUPERVISOR_ID", True)
return int(_id) if _id else None
class ServerSettings(AbstractSettings):
@ -38,10 +64,6 @@ class ServerSettings(AbstractSettings):
def hook_port(cls) -> int:
return int(cls._get_env("WEBHOOK_PORT"))
@classmethod
def app_host(cls) -> str:
return "olgram"
@classmethod
def app_port(cls) -> int:
return 80
@ -71,9 +93,24 @@ class ServerSettings(AbstractSettings):
def append_text(cls) -> str:
return "\n\nЭтот бот создан с помощью @OlgramBot"
@classmethod
@lru_cache
def redis_timeout_ms(cls) -> ty.Optional[int]:
return int(timedelta(days=180).total_seconds() * 1000.0)
@classmethod
@lru_cache
def thread_timeout_ms(cls) -> int:
return int(timedelta(days=1).total_seconds() * 1000.0)
logging.basicConfig(level=os.environ.get("LOGLEVEL") or "WARNING",
format='%(asctime)s %(levelname)-8s %(message)s')
class BotSettings(AbstractSettings):
@classmethod
@lru_cache
def token(cls) -> str:
"""
Токен olgram бота
@ -81,6 +118,14 @@ class BotSettings(AbstractSettings):
"""
return cls._get_env("BOT_TOKEN")
@classmethod
def language(cls) -> str:
"""
Язык
"""
lang = cls._get_env("O_LANG", allow_none=True)
return lang.lower() if lang else "ru"
class DatabaseSettings(AbstractSettings):
@classmethod
@ -99,6 +144,12 @@ class DatabaseSettings(AbstractSettings):
def host(cls) -> str:
return cls._get_env("POSTGRES_HOST")
@classmethod
@lru_cache
def cryptor(cls) -> Cryptor:
password = cls._get_env("TOKEN_ENCRYPTION_KEY")
return Cryptor(password)
TORTOISE_ORM = {
"connections": {"default": f'postgres://{DatabaseSettings.user()}:{DatabaseSettings.password()}'
@ -109,4 +160,6 @@ TORTOISE_ORM = {
"default_connection": "default",
},
},
"use_tz": False,
"timezone": "UTC"
}

16
olgram/utils/crypto.py Normal file
View File

@ -0,0 +1,16 @@
import base64
from Crypto.Cipher import AES
class Cryptor:
def __init__(self, password: str):
password = password.rjust(32)[:32]
self._cipher = AES.new(password.encode("utf-8"), AES.MODE_ECB)
def encrypt(self, data: str) -> str:
if data.startswith(" "):
raise ValueError("Data should not start with space!")
return base64.b64encode(self._cipher.encrypt(data.encode("utf-8").rjust(64))).decode("utf-8")
def decrypt(self, data: str) -> str:
return self._cipher.decrypt(base64.b64decode(data.encode("utf-8"))).decode("utf-8").lstrip()

View File

@ -1,5 +1,8 @@
import logging
from io import BytesIO
from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup
from aiogram.utils.exceptions import TelegramAPIError
from aiogram import types, Bot as AioBot
from typing import Optional
@ -22,8 +25,39 @@ async def edit_or_create(call: CallbackQuery, message: str,
parse_mode=parse_mode)
def button_text_limit(data: str) -> str:
max_len = 30
def wrap(data: str, max_len: int) -> str:
if len(data) > max_len:
data = data[:max_len-4] + "..."
return data
def button_text_limit(data: str) -> str:
return wrap(data, 30)
async def send_stored_message(storage: dict, bot: AioBot, chat_id: int):
content_type = storage["mailing_content_type"]
if content_type == types.ContentType.TEXT:
return await bot.send_message(chat_id, storage["mailing_text"], parse_mode="HTML")
if content_type == types.ContentType.LOCATION:
return await bot.send_location(chat_id, storage["mailing_location"][0], storage["mailing_location"][1])
if content_type in (types.ContentType.AUDIO, types.ContentType.VIDEO, types.ContentType.DOCUMENT,
types.ContentType.PHOTO):
caption = storage.get("mailing_caption")
if storage.get("mailing_id"):
logging.info("Mailing use file id")
obj = storage["mailing_id"]
else:
logging.info("Mailing upload file")
obj = types.InputFile(BytesIO(storage["mailing_data"]), filename=storage.get("mailing_file_name"))
if content_type == types.ContentType.AUDIO:
return (await bot.send_audio(chat_id, audio=obj, caption=caption)).audio.file_id
if content_type == types.ContentType.PHOTO:
return (await bot.send_photo(chat_id, photo=obj, caption=caption)).photo[-1].file_id
if content_type == types.ContentType.VIDEO:
return (await bot.send_video(chat_id, video=obj, caption=caption)).video.file_id
if content_type == types.ContentType.DOCUMENT:
return (await bot.send_document(chat_id, document=obj, caption=caption)).document.file_id
raise NotImplementedError("Mailing, unknown content type")

View File

@ -0,0 +1,54 @@
import aiogram.types as types
from aiogram.dispatcher.handler import CancelHandler, current_handler
from aiogram.dispatcher.middlewares import BaseMiddleware
import typing as ty
from locales.locale import _
def public():
"""
Хендлеры с этим декоратором будут обрабатываться даже если пользователь не является владельцем бота
(например, команда /help)
:return:
"""
def decorator(func):
setattr(func, "access_public", True)
return func
return decorator
class AccessMiddleware(BaseMiddleware):
def __init__(self, access_chat_ids: ty.Iterable[int]):
self._access_chat_ids = access_chat_ids
super(AccessMiddleware, self).__init__()
@classmethod
def _is_public_command(cls) -> bool:
handler = current_handler.get()
return handler and getattr(handler, "access_public", False)
async def on_process_message(self, message: types.Message, data: dict):
admin_ids = self._access_chat_ids
if not admin_ids:
return # Администраторы бота вообще не указаны
if self._is_public_command(): # Эта команда разрешена всем пользователям
return
if message.chat.id not in admin_ids:
await message.answer(_("Владелец бота ограничил доступ к этому функционалу 😞"))
raise CancelHandler()
async def on_process_callback_query(self, call: types.CallbackQuery, data: dict):
admin_ids = self._access_chat_ids
if not admin_ids:
return # Администраторы бота вообще не указаны
if self._is_public_command(): # Эта команда разрешена всем пользователям
return
if call.message.chat.id not in admin_ids:
await call.answer(_("Владелец бота ограничил доступ к этому функционалу😞"))
raise CancelHandler()

1082
poetry.lock generated Normal file

File diff suppressed because it is too large Load Diff

25
pyproject.toml Normal file
View File

@ -0,0 +1,25 @@
[tool.poetry]
name = "olgram"
version = "0.2.0"
description = ""
authors = ["Civ Soc <feedback@civsoc.it>"]
license = "CC0"
[tool.poetry.dependencies]
python = "^3.8"
aiogram = "2.13"
python-dotenv = "^0.19.2"
aiocache = "^0.11.1"
aiohttp = "^3.8.1"
pycrypto = "^2.6.1"
aioredis = "1.3"
aerich = "0.5.x"
tortoise-orm = {extras = ["asyncpg"], version = "^0.18.1"}
python-gettext = "^4.0"
[tool.poetry.dev-dependencies]
flake8 = "^4.0.1"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

1
refresh_lang.sh Normal file
View File

@ -0,0 +1 @@
/usr/lib/python3.11/Tools/i18n/pygettext.py -d chinese -o locales/olgram.pot olgram/ server/

View File

@ -1,7 +0,0 @@
aiogram~=2.13
tortoise-orm[asyncpg]
aerich==0.5.4
python-dotenv~=0.17.1
aioredis==1.3.1
aiocache
aiohttp

View File

@ -1,3 +1,5 @@
import asyncio
from aiogram import Bot as AioBot, Dispatcher
from aiogram.dispatcher.webhook import WebhookRequestHandler
from aiogram.dispatcher.webhook import SendMessage
@ -7,20 +9,28 @@ from contextvars import ContextVar
from aiohttp.web_exceptions import HTTPNotFound
from aioredis.commands import create_redis_pool
from aioredis import Redis
from tortoise.expressions import F
import logging
import typing as ty
from olgram.settings import ServerSettings
from olgram.models.models import Bot, GroupChat
from olgram.models.models import Bot, GroupChat, BannedUser, BotStartMessage, BotSecondMessage, MailingUser
from locales.locale import _, translators
from server.inlines import inline_handler
logging.basicConfig(level=logging.INFO)
_logger = logging.getLogger(__name__)
_logger.setLevel(logging.INFO)
db_bot_instance: ContextVar[Bot] = ContextVar('db_bot_instance')
_redis: ty.Optional[Redis] = None
def _get_translator(message: types.Message) -> ty.Callable:
if not message.from_user.locale:
return _
return translators.get(message.from_user.locale.language, _)
async def init_redis():
global _redis
_redis = await create_redis_pool(ServerSettings.redis_path())
@ -30,41 +40,280 @@ def _message_unique_id(bot_id: int, message_id: int) -> str:
return f"{bot_id}_{message_id}"
async def message_handler(message, *args, **kwargs):
_logger.info("message handler")
def _tag_uid(bot_id: int, user_id: int) -> str:
return f"tag_{bot_id}_{user_id}"
def _thread_unique_id(bot_id: int, chat_id: int) -> str:
return f"thread_{bot_id}_{chat_id}"
def _last_message_uid(bot_id: int, chat_id: int) -> str:
return f"lm_{bot_id}_{chat_id}"
def _antiflood_marker_uid(bot_id: int, chat_id: int) -> str:
return f"af_{bot_id}_{chat_id}"
def _on_security_policy(message: types.Message, bot):
_ = _get_translator(message)
text = _("<b>Политика конфиденциальности</b>\n\n"
"Этот бот не хранит ваши сообщения, имя пользователя и @username. При отправке сообщения (кроме команд "
"/start и /security_policy) ваш идентификатор пользователя записывается в кеш на некоторое время и потом "
"удаляется из кеша. Этот идентификатор используется для общения с оператором.\n\n")
if bot.enable_additional_info:
text += _("При отправке сообщения (кроме команд /start и /security_policy) оператор <b>видит</b> ваши имя "
"пользователя, @username и идентификатор пользователя в силу настроек, которые оператор указал при "
"создании бота.\n\n")
else:
text += _("В зависимости от ваших настроек конфиденциальности Telegram, оператор может видеть ваш username, "
"имя пользователя и другую информацию.\n\n")
if bot.enable_mailing:
text += _("В этом боте включена массовая рассылка в силу настроек, которые оператор указал при создании бота. "
"Ваш идентификатор пользователя может быть записан в базу данных на долгое время")
else:
text += _("В этом боте нет массовой рассылки сообщений")
return SendMessage(chat_id=message.chat.id,
text=text,
parse_mode="HTML")
async def send_user_message(message: types.Message, super_chat_id: int, bot, tag: str = ""):
"""Переслать сообщение от пользователя, добавлять к нему user info при необходимости"""
if bot.enable_additional_info:
user_info = _("Сообщение от пользователя ")
user_info += message.from_user.full_name
if message.from_user.username:
user_info += " | @" + message.from_user.username
user_info += f" | #ID{message.from_user.id}"
if message.from_user.locale:
user_info += f" | lang: {message.from_user.locale}"
if message.forward_sender_name:
user_info += f" | fwd: {message.forward_sender_name}"
tag = await _redis.get(_tag_uid(bot.pk, message.from_user.id), encoding="utf-8")
if tag:
user_info += f" | tag: {tag}"
# Добавлять информацию в конец текста
if message.content_type == types.ContentType.TEXT \
and len(message.text) + len(user_info) < 4093: # noqa:E721
new_message = await message.bot.send_message(super_chat_id, message.text + "\n\n" + user_info)
else: # Не добавлять информацию в конец текста, информация отдельным сообщением
new_message = await message.bot.send_message(super_chat_id, text=user_info)
new_message_2 = await message.copy_to(super_chat_id, reply_to_message_id=new_message.message_id)
await _redis.set(_message_unique_id(bot.pk, new_message_2.message_id), message.chat.id,
pexpire=ServerSettings.redis_timeout_ms())
elif tag:
# добавлять тег в конец текста
if message.content_type == types.ContentType.TEXT and len(message.text) + len(tag) < 4093:
new_message = await message.bot.send_message(super_chat_id, message.text + "\n\n" + tag)
else:
new_message = await message.bot.send_message(super_chat_id, text=tag)
new_message_2 = await message.copy_to(super_chat_id, reply_to_message_id=new_message.message_id)
await _redis.set(_message_unique_id(bot.pk, new_message_2.message_id), message.chat.id,
pexpire=ServerSettings.redis_timeout_ms())
else:
try:
new_message = await message.forward(super_chat_id)
except exceptions.MessageCantBeForwarded:
new_message = await message.copy_to(super_chat_id)
await _redis.set(_message_unique_id(bot.pk, new_message.message_id), message.chat.id,
pexpire=ServerSettings.redis_timeout_ms())
return new_message
async def send_to_superchat(is_super_group: bool, message: types.Message, super_chat_id: int, bot):
"""Пересылка сообщения от пользователя оператору (логика потоков сообщений)"""
if bot.enable_tags:
tag = await _redis.get(_tag_uid(bot.pk, message.chat.id), encoding="utf-8")
else:
tag = ""
if tag:
tag = str(tag)
if is_super_group and bot.enable_threads:
if bot.enable_thread_interrupt:
thread_timeout = ServerSettings.thread_timeout_ms()
else:
thread_timeout = ServerSettings.redis_timeout_ms()
thread_first_message = await _redis.get(_thread_unique_id(bot.pk, message.chat.id))
if thread_first_message:
# переслать в супер-чат, отвечая на предыдущее сообщение
try:
if tag:
if message.content_type == types.ContentType.TEXT and len(message.text) + len(tag) < 4093:
new_message = await message.bot.send_message(
super_chat_id,
message.text + "\n\n" + tag,
reply_to_message_id=int(thread_first_message))
else:
new_message = await message.copy_to(super_chat_id,
reply_to_message_id=int(thread_first_message))
new_message_2 = await message.bot.send_message(
super_chat_id, reply_to_message_id=new_message.message_id, text=tag)
await _redis.set(_message_unique_id(bot.pk, new_message_2.message_id), message.chat.id,
pexpire=thread_timeout)
else:
new_message = await message.copy_to(super_chat_id, reply_to_message_id=int(thread_first_message))
await _redis.set(_message_unique_id(bot.pk, new_message.message_id), message.chat.id,
pexpire=thread_timeout)
except exceptions.BadRequest:
new_message = await send_user_message(message, super_chat_id, bot, tag)
await _redis.set(
_thread_unique_id(bot.pk, message.chat.id), new_message.message_id, pexpire=thread_timeout)
else:
# переслать супер-чат
new_message = await send_user_message(message, super_chat_id, bot, tag)
await _redis.set(_thread_unique_id(bot.pk, message.chat.id), new_message.message_id,
pexpire=thread_timeout)
else: # личные сообщения не поддерживают потоки сообщений: просто отправляем сообщение
await send_user_message(message, super_chat_id, bot, tag)
async def _increase_count(_bot):
_bot.incoming_messages_count = F("incoming_messages_count") + 1
await _bot.save(update_fields=["incoming_messages_count"])
async def handle_user_message(message: types.Message, super_chat_id: int, bot):
"""Обычный пользователь прислал сообщение в бот, нужно переслать его операторам"""
_ = _get_translator(message)
is_super_group = super_chat_id < 0
if bot.enable_mailing:
asyncio.create_task(MailingUser.get_or_create(telegram_id=message.chat.id, bot=bot))
# Проверить, не забанен ли пользователь
banned = await bot.banned_users.filter(telegram_id=message.chat.id)
if banned:
return SendMessage(chat_id=message.chat.id,
text=_("Вы заблокированы в этом боте"))
# Проверить анти-флуд
if bot.enable_antiflood:
if await _redis.get(_antiflood_marker_uid(bot.pk, message.chat.id)):
return SendMessage(chat_id=message.chat.id,
text=_("Слишком много сообщений, подождите одну минуту"))
await _redis.setex(_antiflood_marker_uid(bot.pk, message.chat.id), 60, 1)
# Пересылаем сообщение в супер-чат
try:
await send_to_superchat(is_super_group, message, super_chat_id, bot)
except (exceptions.Unauthorized, exceptions.ChatNotFound):
return SendMessage(chat_id=message.chat.id, text=_("Не удаётся связаться с владельцем бота"))
except exceptions.RetryAfter:
return SendMessage(chat_id=message.chat.id, text=_("Слишком много сообщений, подождите одну минуту"),
reply_to_message_id=message.message_id)
except exceptions.TelegramAPIError as err:
_logger.error(f"(exception on forwarding) {err}")
return
asyncio.create_task(_increase_count(bot))
# И отправить пользователю специальный текст, если он указан и если давно не отправляли
if bot.second_text:
send_auto = not await _redis.get(_last_message_uid(bot.pk, message.chat.id))
await _redis.setex(_last_message_uid(bot.pk, message.chat.id), 60 * 60 * 3, 1)
if send_auto or bot.enable_always_second_message:
text_obj = await BotSecondMessage.get_or_none(bot=bot, locale=str(message.from_user.locale))
return SendMessage(chat_id=message.chat.id, text=text_obj.text if text_obj else bot.second_text,
parse_mode="HTML")
async def handle_operator_message(message: types.Message, super_chat_id: int, bot):
"""Оператор написал что-то, нужно переслать сообщение обратно пользователю, или забанить его и т.д."""
_ = _get_translator(message)
if message.reply_to_message:
if message.reply_to_message.from_user.id != message.bot.id:
return # нас интересуют только ответы на сообщения бота
# В супер-чате кто-то ответил на сообщение пользователя, нужно переслать тому пользователю
chat_id = await _redis.get(_message_unique_id(bot.pk, message.reply_to_message.message_id))
if not chat_id:
chat_id = message.reply_to_message.forward_from_chat
if not chat_id:
return SendMessage(chat_id=message.chat.id,
text=_("<i>Невозможно переслать сообщение: автор не найден (сообщение слишком "
"старое?)</i>"),
parse_mode="HTML")
chat_id = int(chat_id)
if message.text == "/ban":
user, create = await BannedUser.get_or_create(telegram_id=chat_id, bot=bot)
await user.save()
return SendMessage(chat_id=message.chat.id, text=_("Пользователь заблокирован"))
if message.text == "/unban":
banned_user = await bot.banned_users.filter(telegram_id=chat_id).first()
if not banned_user:
return SendMessage(chat_id=message.chat.id, text=_("Пользователь не был забанен"))
else:
await banned_user.delete()
return SendMessage(chat_id=message.chat.id, text=_("Пользователь разбанен"))
if bot.enable_tags:
if message.text and message.text.startswith("/tag "):
tag = message.text.replace("/tag ", "")[:20].strip()
if tag:
await _redis.set(_tag_uid(bot.pk, chat_id), tag, pexpire=ServerSettings.redis_timeout_ms())
return SendMessage(chat_id=message.chat.id, text=_("Тег выставлен"))
else:
await _redis.delete(_tag_uid(bot.pk, chat_id))
return SendMessage(chat_id=message.chat.id, text=_("Тег убран"))
try:
await message.copy_to(chat_id)
except (exceptions.MessageError, exceptions.Unauthorized):
await message.reply(_("<i>Невозможно переслать сообщение (автор заблокировал бота?)</i>"),
parse_mode="HTML")
return
bot.outgoing_messages_count = F("outgoing_messages_count") + 1
await bot.save(update_fields=["outgoing_messages_count"])
elif super_chat_id > 0:
# в супер-чате кто-то пишет сообщение сам себе, только для личных сообщений
if bot.enable_mailing:
asyncio.create_task(MailingUser.get_or_create(telegram_id=message.chat.id, bot=bot))
await message.forward(super_chat_id)
# И отправить пользователю специальный текст, если он указан
if bot.second_text:
return SendMessage(chat_id=message.chat.id, text=bot.second_text, parse_mode="HTML")
async def message_handler(message: types.Message, *args, **kwargs):
_ = _get_translator(message)
bot = db_bot_instance.get()
if message.text and message.text.startswith("/start"):
if message.text and message.text == "/start":
# На команду start нужно ответить, не пересылая сообщение никуда
return SendMessage(chat_id=message.chat.id,
text=bot.start_text + ServerSettings.append_text())
text_obj = await BotStartMessage.get_or_none(bot=bot, locale=str(message.from_user.locale))
text = text_obj.text if text_obj else bot.start_text
if bot.enable_olgram_text:
text += _(ServerSettings.append_text())
return SendMessage(chat_id=message.chat.id, text=text, parse_mode="HTML")
if message.text and message.text == "/security_policy":
# На команду security_policy нужно ответить, не пересылая сообщение никуда
return _on_security_policy(message, bot)
super_chat_id = await bot.super_chat_id()
if message.chat.id != super_chat_id:
# Это обычный чат: сообщение нужно переслать в супер-чат
new_message = await message.forward(super_chat_id)
await _redis.set(_message_unique_id(bot.pk, new_message.message_id), message.chat.id)
# Это обычный чат
return await handle_user_message(message, super_chat_id, bot)
else:
# Это супер-чат
if message.reply_to_message:
# Ответ из супер-чата переслать тому пользователю,
chat_id = await _redis.get(_message_unique_id(bot.pk, message.reply_to_message.message_id))
if not chat_id:
chat_id = message.reply_to_message.forward_from_chat
if not chat_id:
return SendMessage(chat_id=message.chat.id,
text="<i>Невозможно переслать сообщение: автор не найден</i>",
parse_mode="HTML")
chat_id = int(chat_id)
try:
await message.copy_to(chat_id)
except (exceptions.MessageError, exceptions.BotBlocked):
await message.reply("<i>Невозможно переслать сообщение (автор заблокировал бота?)</i>",
parse_mode="HTML")
return
else:
await message.forward(super_chat_id)
return await handle_operator_message(message, super_chat_id, bot)
async def edited_message_handler(message: types.Message, *args, **kwargs):
return await message_handler(message, *args, **kwargs, is_edited=True)
async def receive_invite(message: types.Message):
@ -81,6 +330,18 @@ async def receive_invite(message: types.Message):
break
async def receive_group_create(message: types.Message):
bot = db_bot_instance.get()
chat, _ = await GroupChat.get_or_create(chat_id=message.chat.id,
defaults={"name": message.chat.full_name})
chat.name = message.chat.full_name
await chat.save()
if chat not in await bot.group_chats.all():
await bot.group_chats.add(chat)
await bot.save()
async def receive_left(message: types.Message):
bot = db_bot_instance.get()
if message.left_chat_member.id == message.bot.id:
@ -93,6 +354,23 @@ async def receive_left(message: types.Message):
await bot.save()
async def receive_inline(inline_query):
_logger.info("inline handler")
bot = db_bot_instance.get()
return await inline_handler(inline_query, bot)
async def receive_migrate(message: types.Message):
bot = db_bot_instance.get()
from_id = message.chat.id
to_id = message.migrate_to_chat_id
chats = await bot.group_chats.filter(chat_id=from_id)
for chat in chats:
chat.chat_id = to_id
await chat.save(update_fields=["chat_id"])
class CustomRequestHandler(WebhookRequestHandler):
def __init__(self, *args, **kwargs):
@ -106,19 +384,26 @@ class CustomRequestHandler(WebhookRequestHandler):
if not bot:
return None
db_bot_instance.set(bot)
dp = Dispatcher(AioBot(bot.token))
dp = Dispatcher(AioBot(bot.decrypted_token()))
supported_messages = [types.ContentType.TEXT,
types.ContentType.CONTACT,
types.ContentType.ANIMATION,
types.ContentType.AUDIO,
types.ContentType.DOCUMENT,
types.ContentType.PHOTO,
types.ContentType.STICKER,
types.ContentType.VIDEO,
types.ContentType.VOICE,
types.ContentType.LOCATION]
dp.register_message_handler(message_handler, content_types=supported_messages)
dp.register_edited_message_handler(edited_message_handler, content_types=supported_messages)
dp.register_message_handler(message_handler, content_types=[types.ContentType.TEXT,
types.ContentType.CONTACT,
types.ContentType.ANIMATION,
types.ContentType.AUDIO,
types.ContentType.DOCUMENT,
types.ContentType.PHOTO,
types.ContentType.STICKER,
types.ContentType.VIDEO,
types.ContentType.VOICE])
dp.register_message_handler(receive_invite, content_types=[types.ContentType.NEW_CHAT_MEMBERS])
dp.register_message_handler(receive_left, content_types=[types.ContentType.LEFT_CHAT_MEMBER])
dp.register_message_handler(receive_migrate, content_types=[types.ContentType.MIGRATE_TO_CHAT_ID])
dp.register_message_handler(receive_group_create, content_types=[types.ContentType.GROUP_CHAT_CREATED])
dp.register_inline_handler(receive_inline)
return dp

56
server/inlines.py Normal file
View File

@ -0,0 +1,56 @@
from aiocache import cached
import hashlib
from aiogram.types import InlineQuery, InputTextMessageContent, InlineQueryResultArticle
from aiogram.bot import Bot as AioBot
from olgram.models.models import Bot
import typing as ty
@cached(ttl=60)
async def get_phrases(bot: Bot) -> ty.List:
objects = await bot.answers
return [obj.text for obj in objects]
async def check_chat_member(chat_id: int, user_id: int, bot: AioBot) -> bool:
member = await bot.get_chat_member(chat_id, user_id)
return member.is_chat_member()
@cached(ttl=60)
async def check_permissions(inline_query: InlineQuery, bot: Bot):
user_id = inline_query.from_user.id
super_chat_id = await bot.super_chat_id()
if super_chat_id == user_id:
return True
if super_chat_id < 0: # Group chat
is_member = await check_chat_member(super_chat_id, user_id, inline_query.bot)
return is_member
return False
async def inline_handler(inline_query: InlineQuery, bot: Bot):
# Check permissions at first
allow = await check_permissions(inline_query, bot)
if not allow:
return await inline_query.answer([]) # forbidden
all_phrases = await get_phrases(bot)
phrases = [phrase for phrase in all_phrases if inline_query.query.lower() in phrase.lower()]
items = []
for phrase in phrases:
input_content = InputTextMessageContent(phrase)
result_id: str = hashlib.md5(phrase.encode()).hexdigest()
item = InlineQueryResultArticle(
id=result_id,
title=phrase,
input_message_content=input_content,
)
items.append(item)
await inline_query.answer(results=items)

View File

@ -1,9 +1,11 @@
from aiogram import Bot as AioBot
from aiogram.types import BotCommand
from olgram.models.models import Bot
from aiohttp import web
from asyncio import get_event_loop
import ssl
from olgram.settings import ServerSettings
from locales.locale import _
from .custom import CustomRequestHandler
import logging
@ -26,14 +28,20 @@ async def register_token(bot: Bot) -> bool:
:param bot: Бот
:return: получилось ли
"""
await unregister_token(bot.token)
await unregister_token(bot.decrypted_token())
a_bot = AioBot(bot.token)
a_bot = AioBot(bot.decrypted_token())
certificate = None
if ServerSettings.use_custom_cert():
certificate = open(ServerSettings.public_path(), 'rb')
res = await a_bot.set_webhook(url_for_bot(bot), certificate=certificate, drop_pending_updates=True)
res = await a_bot.set_webhook(url_for_bot(bot), certificate=certificate, drop_pending_updates=True,
max_connections=10)
await a_bot.set_my_commands([
BotCommand("/start", _("(Пере)запустить бота")),
BotCommand("/security_policy", _("Политика конфиденциальности"))
])
await a_bot.session.close()
del a_bot
return res
@ -65,5 +73,5 @@ def main():
runner = web.AppRunner(app)
loop.run_until_complete(runner.setup())
logger.info("Server initialization done")
site = web.TCPSite(runner, host=ServerSettings.app_host(), port=ServerSettings.app_port(), ssl_context=context)
site = web.TCPSite(runner, port=ServerSettings.app_port(), ssl_context=context)
return site