Compare commits

...

231 Commits
v0.0.3 ... main

Author SHA1 Message Date
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
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
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
82 changed files with 4874 additions and 199 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

View File

@ -1,11 +1,26 @@
FROM python:3.8-buster
COPY . /app
ENV PYTHONUNBUFFERED=1 \
POETRY_VERSION=1.1.12 \
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,44 @@
Дополнительно
=============
Донаты
----------------
На рекламу проекта, аренду сервера и пиццу
Bitcoin:
``bc1qlq7cm5chc8flr3fy8ewk967aknq3dwmxtwn9hl``
Litecoin:
``LTC1QXAJSVZ0LW44AA5NYTUCH8CP2G8X7A4CDASE4Y7``
Как убрать "Этот бот создан с помощью ..."
----------------
Напишите нам на `@civsocit_feedback_bot <https://t.me/civsocit_feedback_bot>`_.
История изменений
----------------
- `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 = '2023, 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>`_.

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

@ -0,0 +1,51 @@
Опции
=============
.. _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:
Защита от флуда
---------------
При включении этой опции пользователю запрещается отправлять больше одного сообщения в минуту. Используйте её, если
не успеваете обрабатывать входящие сообщения.

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

@ -0,0 +1,110 @@
Быстрый старт
=============
Как создать бота
----------------
Перейдите по ссылке `@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
Теперь просто отправьте новый текст приветствия.
Как привязать бота к групповому чату
------------------------------------
По-умолчанию ваш бот пересылает сообщения от пользователей вам в личные сообщения. Бота можно привязать к групповому
чату. Для этого добавьте его в групповой чат. Затем откройте список ботов, как в примере выше, выберите нужного бота
и нажмите кнопку "Чат":
.. 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>`_.

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,721 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR ORGANIZATION
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: \n"
"POT-Creation-Date: 2022-09-02 05:02+0400\n"
"PO-Revision-Date: 2022-09-02 05:07+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.1\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:22
msgid "Бот удалён"
msgstr "Bot removed"
#: olgram/commands/bot_actions.py:38 olgram/commands/bot_actions.py:50
msgid "Текст сброшен"
msgstr "Text is reset"
#: olgram/commands/bot_actions.py:64
msgid "Выбран личный чат"
msgstr "Personal chat selected"
#: olgram/commands/bot_actions.py:77
msgid "Бот вышел из чатов"
msgstr "Bot leaved chats"
#: olgram/commands/bot_actions.py:83
msgid "Нельзя привязать бота к этому чату"
msgstr "You can't bind a bot to this chat room"
#: olgram/commands/bot_actions.py:87
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/menu.py:31
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:46
msgid "Ваши боты"
msgstr "Your bots"
#: olgram/commands/menu.py:67
msgid "Личные сообщения"
msgstr "Personal messages"
#: olgram/commands/menu.py:72
msgid "❗️ Выйти из всех чатов"
msgstr "❗️ Leave all chats"
#: olgram/commands/menu.py:77 olgram/commands/menu.py:122
#: olgram/commands/menu.py:148 olgram/commands/menu.py:184
#: olgram/commands/menu.py:247
msgid "<< Назад"
msgstr "<< Back"
#: olgram/commands/menu.py:83
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:90
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:102
msgid "Текст"
msgstr "Text"
#: olgram/commands/menu.py:107
msgid "Чат"
msgstr "Chat"
#: olgram/commands/menu.py:112
msgid "Удалить бот"
msgstr "Delete bot"
#: olgram/commands/menu.py:117
msgid "Статистика"
msgstr "Statistics"
#: olgram/commands/menu.py:126
msgid "Опции"
msgstr "Options"
#: olgram/commands/menu.py:131
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:143
msgid "Да, удалить бот"
msgstr "Yes, delete the bot"
#: olgram/commands/menu.py:152
msgid ""
"\n"
" Вы уверены, что хотите удалить бота @{0}?\n"
" "
msgstr ""
"\n"
" Are you sure you want to delete the bot @{0}?\n"
" "
#: olgram/commands/menu.py:161
msgid "Потоки сообщений"
msgstr "Message threads"
#: olgram/commands/menu.py:166
msgid "Данные пользователя"
msgstr "User data"
#: olgram/commands/menu.py:171
msgid "Антифлуд"
msgstr "Antiflood"
#: olgram/commands/menu.py:178
msgid "Olgram подпись"
msgstr "Olgram signature"
#: olgram/commands/menu.py:189 olgram/commands/menu.py:190
msgid "включены"
msgstr "enabled"
#: olgram/commands/menu.py:189 olgram/commands/menu.py:190
msgid "выключены"
msgstr "disabled"
#: olgram/commands/menu.py:191
#, fuzzy
#| msgid "включены"
msgid "включен"
msgstr "enabled"
#: olgram/commands/menu.py:191
#, fuzzy
#| msgid "выключены"
msgid "выключен"
msgstr "disabled"
#: olgram/commands/menu.py:192
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"
" "
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\">Antiflood</a>: <b>{2}</b>"
#: olgram/commands/menu.py:199
msgid "включена"
msgstr "enabled"
#: olgram/commands/menu.py:199
msgid "выключена"
msgstr "disabled"
#: olgram/commands/menu.py:200
msgid "Olgram подпись: <b>{0}</b>"
msgstr "Olgram signature: <b>{0}</b>"
#: olgram/commands/menu.py:210 olgram/commands/menu.py:272
#: olgram/commands/menu.py:314
msgid "<< Завершить редактирование"
msgstr "<< Finish editing"
#: olgram/commands/menu.py:214
msgid "Автоответчик"
msgstr "Autoresponder"
#: olgram/commands/menu.py:219 olgram/commands/menu.py:286
msgid "Сбросить текст"
msgstr "Reset text"
#: olgram/commands/menu.py:224
msgid ""
"\n"
" Сейчас вы редактируете текст, который отправляется после того, как "
"пользователь отправит вашему боту @{0}\n"
" команду /start\n"
"\n"
" Текущий текст:\n"
" <pre>\n"
" {1}\n"
" </pre>\n"
" Отправьте сообщение, чтобы изменить текст.\n"
" "
msgstr ""
"\n"
" You are now editing the text that is sent after the user sends your bot "
"@{0}\n"
" /start command.\n"
"\n"
" Current text:\n"
" <pre>\n"
" {1}\n"
" </pre>\n"
" Send a message to change the text.\n"
" "
#: olgram/commands/menu.py:251
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:276
msgid "Предыдущий текст"
msgstr "Previous text"
#: olgram/commands/menu.py:281
msgid "Шаблоны ответов..."
msgstr "Answer templates..."
#: olgram/commands/menu.py:291
msgid ""
"\n"
" Сейчас вы редактируете текст автоответчика. Это сообщение отправляется в "
"ответ на все входящие сообщения @{0} автоматически. По умолчанию оно "
"отключено.\n"
"\n"
" Текущий текст:\n"
" <pre>\n"
" {1}\n"
" </pre>\n"
" Отправьте сообщение, чтобы изменить текст.\n"
" "
msgstr ""
"\n"
" You are now editing the autoresponder text. This message is sent in "
"response to all incoming @{0} messages automatically. It is disabled by "
"default.\n"
"\n"
" Current text:\n"
" <pre>\n"
" {1}\n"
" </pre>\n"
" Send a message to change the text.\n"
" "
#: olgram/commands/menu.py:301
msgid "(отключено)"
msgstr "(disabled)"
#: olgram/commands/menu.py:318
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:337
msgid "(нет шаблонов)"
msgstr "(no templates)"
#: olgram/commands/menu.py:376
msgid "У вас нет шаблонов, чтобы их удалять"
msgstr "You don't have templates to delete them"
#: olgram/commands/menu.py:378
msgid "Неправильное число. Чтобы удалить шаблон, введите число от 0 до {0}"
msgstr "To delete a template, enter a number between 0 and {0}"
#: olgram/commands/menu.py:386
msgid "У вашего бота уже слишком много шаблонов"
msgstr "Your bot already has too many templates"
#: olgram/commands/menu.py:390
msgid "Такой текст уже есть в списке шаблонов"
msgstr "This text is already in the list of templates"
#: olgram/commands/menu.py:408
msgid "У вас нет прав на этого бота"
msgstr "You have no permissions to this bot"
#: 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:40
msgid "Владелец бота ограничил доступ к этому функционалу 😞"
msgstr "The bot owner has restricted access to this functionality 😞"
#: olgram/utils/permissions.py:52
msgid "Владелец бота ограничил доступ к этому функционалу😞"
msgstr "The owner of the bot has restricted access to this function😞"
#: server/custom.py:55
msgid ""
"<b>Политика конфиденциальности</b>\n"
"\n"
"Этот бот не хранит ваши сообщения, имя пользователя и @username. При "
"отправке сообщения (кроме команд /start и /security_policy) ваш "
"идентификатор пользователя записывается в кеш на некоторое время и потом "
"удаляется из кеша. Этот идентификатор используется только для общения с "
"оператором; боты Olgram не делают массовых рассылок.\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 only used for "
"communicating with the operator; Olgram bots do not do mass mailings.\n"
"\n"
#: server/custom.py:61
msgid ""
"При отправке сообщения (кроме команд /start и /security_policy) оператор "
"<b>видит</b> ваши имя пользователя, @username и идентификатор пользователя в "
"силу настроек, которые оператор указал при создании бота."
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."
#: server/custom.py:65
msgid ""
"В зависимости от ваших настроек конфиденциальности Telegram, оператор может "
"видеть ваш username, имя пользователя и другую информацию."
msgstr ""
"Depending on your Telegram privacy settings, the operator may see your "
"username, username and other information."
#: server/custom.py:76
msgid "Сообщение от пользователя "
msgstr "Message from the user "
#: server/custom.py:135
msgid "Вы заблокированы в этом боте"
msgstr "You are blocked in this bot"
#: server/custom.py:141
msgid "Слишком много сообщений, подождите одну минуту"
msgstr "Too many messages, wait one minute"
#: server/custom.py:148
msgid "Не удаётся связаться с владельцем бота"
msgstr "Cannot contact the owner of the bot"
#: server/custom.py:179
msgid ""
"<i>Невозможно переслать сообщение: автор не найден (сообщение слишком "
"старое?)</i>"
msgstr ""
"<i>Cannot forward this message: author not found (message too old?)</i>"
#: server/custom.py:187
msgid "Пользователь заблокирован"
msgstr "User is blocked"
#: server/custom.py:192
msgid "Пользователь не был забанен"
msgstr "The user was not banned"
#: server/custom.py:195
msgid "Пользователь разбанен"
msgstr "A user has been unlocked"
#: server/custom.py:200
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,733 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR ORGANIZATION
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: \n"
"POT-Creation-Date: 2022-09-02 05:07+0400\n"
"PO-Revision-Date: 2022-09-02 05:12+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.1\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:22
msgid "Бот удалён"
msgstr "Бот видалений"
#: olgram/commands/bot_actions.py:38 olgram/commands/bot_actions.py:50
msgid "Текст сброшен"
msgstr "Текст скинутий"
#: olgram/commands/bot_actions.py:64
msgid "Выбран личный чат"
msgstr "Вибраний особистий чат"
#: olgram/commands/bot_actions.py:77
msgid "Бот вышел из чатов"
msgstr "Бот вийшов із чатів"
#: olgram/commands/bot_actions.py:83
msgid "Нельзя привязать бота к этому чату"
msgstr "Не можна прив'язати робота до цього чату"
#: olgram/commands/bot_actions.py:87
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/menu.py:31
msgid ""
"\n"
" У вас нет добавленных ботов.\n"
"\n"
" Отправьте команду /addbot, чтобы добавить бот.\n"
" "
msgstr ""
"\n"
" У вас немає доданих роботів.\n"
"\n"
" Надішліть команду /addbot, щоб додати бот.\n"
" \n"
" "
#: olgram/commands/menu.py:46
msgid "Ваши боты"
msgstr "Ваші боти"
#: olgram/commands/menu.py:67
msgid "Личные сообщения"
msgstr "Особисті повідомлення"
#: olgram/commands/menu.py:72
msgid "❗️ Выйти из всех чатов"
msgstr "❗️ Вийти зі всіх чатів"
#: olgram/commands/menu.py:77 olgram/commands/menu.py:122
#: olgram/commands/menu.py:148 olgram/commands/menu.py:184
#: olgram/commands/menu.py:247
msgid "<< Назад"
msgstr "<< Назад"
#: olgram/commands/menu.py:83
msgid ""
"\n"
" Этот бот не добавлен в чаты, поэтому все сообщения будут приходить "
"вам в бот.\n"
" Чтобы подключить чат — добавьте бот @{0} в чат, откройте это меню "
"ещё раз и выберите добавленный чат.\n"
" Если ваш бот состоял в групповом чате до того, как его добавили в "
"Olgram - удалите бота из чата и добавьте\n"
" снова.\n"
" "
msgstr ""
"\n"
" Цей бот не доданий до чатів, тому всі повідомлення будуть приходити "
"вам у бот.\n"
" Щоб підключити чат — додайте бот @{0} до чату, відкрийте це меню ще "
"раз і виберіть доданий чат.\n"
" Якщо ваш бот перебував у груповому чаті до того, як його додали до "
"Olgram - видаліть бота з чату та додайте\n"
" знову.\n"
" \n"
" "
#: olgram/commands/menu.py:90
msgid ""
"\n"
" В этом разделе вы можете привязать бота @{0} к чату.\n"
" Выберите чат, куда бот будет пересылать сообщения.\n"
" "
msgstr ""
"\n"
" У цьому розділі ви можете прив'язати робота @{0} до чату.\n"
" Виберіть чат, куди бот пересилатиме повідомлення.\n"
" \n"
" "
#: 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:117
msgid "Статистика"
msgstr "Статистика"
#: olgram/commands/menu.py:126
msgid "Опции"
msgstr "Опції"
#: olgram/commands/menu.py:131
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:143
msgid "Да, удалить бот"
msgstr "Так, видалити бот"
#: olgram/commands/menu.py:152
msgid ""
"\n"
" Вы уверены, что хотите удалить бота @{0}?\n"
" "
msgstr ""
"\n"
" Ви впевнені, що хочете видалити бота @{0}?\n"
" "
#: olgram/commands/menu.py:161
msgid "Потоки сообщений"
msgstr "Потоки повідомлень"
#: olgram/commands/menu.py:166
msgid "Данные пользователя"
msgstr "Дані користувача"
#: olgram/commands/menu.py:171
msgid "Антифлуд"
msgstr "Антифлуд"
#: olgram/commands/menu.py:178
msgid "Olgram подпись"
msgstr "Olgram підпис"
#: olgram/commands/menu.py:189 olgram/commands/menu.py:190
msgid "включены"
msgstr "включені"
#: olgram/commands/menu.py:189 olgram/commands/menu.py:190
msgid "выключены"
msgstr "вимкнені"
#: olgram/commands/menu.py:191
#, fuzzy
#| msgid "включены"
msgid "включен"
msgstr "включені"
#: olgram/commands/menu.py:191
#, fuzzy
#| msgid "выключены"
msgid "выключен"
msgstr "вимкнені"
#: olgram/commands/menu.py:192
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"
" "
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"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options."
"html#antiflood\">Anti-flood</a>: <b>{2}</b>"
#: olgram/commands/menu.py:199
msgid "включена"
msgstr "включена"
#: olgram/commands/menu.py:199
msgid "выключена"
msgstr "вимкнена"
#: olgram/commands/menu.py:200
msgid "Olgram подпись: <b>{0}</b>"
msgstr "Olgram підпис: <b>{0}</b>"
#: olgram/commands/menu.py:210 olgram/commands/menu.py:272
#: olgram/commands/menu.py:314
msgid "<< Завершить редактирование"
msgstr "<< Завершити редагування"
#: olgram/commands/menu.py:214
msgid "Автоответчик"
msgstr "Автовідповідач"
#: olgram/commands/menu.py:219 olgram/commands/menu.py:286
msgid "Сбросить текст"
msgstr "Скинути текст"
#: olgram/commands/menu.py:224
msgid ""
"\n"
" Сейчас вы редактируете текст, который отправляется после того, как "
"пользователь отправит вашему боту @{0}\n"
" команду /start\n"
"\n"
" Текущий текст:\n"
" <pre>\n"
" {1}\n"
" </pre>\n"
" Отправьте сообщение, чтобы изменить текст.\n"
" "
msgstr ""
"\n"
" Зараз ви редагуєте текст, який надсилається після того, як користувач "
"надішле вашому боту @{0}\n"
" команду /start\n"
"\n"
" Поточний текст:\n"
" <pre>\n"
" {1}\n"
" </pre>\n"
" Надішліть повідомлення, щоб змінити текст.\n"
" \n"
" "
#: olgram/commands/menu.py:251
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:276
msgid "Предыдущий текст"
msgstr "Попередній текст"
#: olgram/commands/menu.py:281
msgid "Шаблоны ответов..."
msgstr "Шаблони відповідей..."
#: olgram/commands/menu.py:291
msgid ""
"\n"
" Сейчас вы редактируете текст автоответчика. Это сообщение отправляется в "
"ответ на все входящие сообщения @{0} автоматически. По умолчанию оно "
"отключено.\n"
"\n"
" Текущий текст:\n"
" <pre>\n"
" {1}\n"
" </pre>\n"
" Отправьте сообщение, чтобы изменить текст.\n"
" "
msgstr ""
"\n"
" Зараз ви редагуєте текст автовідповідача. Це повідомлення надсилається у "
"відповідь на всі вхідні повідомлення @{0} автоматично. За замовчуванням його "
"вимкнено.\n"
"\n"
" Поточний текст:\n"
" <pre>\n"
" {1}\n"
" </pre>\n"
" Надішліть повідомлення, щоб змінити текст.\n"
" "
#: olgram/commands/menu.py:301
msgid "(отключено)"
msgstr "(відключено)"
#: olgram/commands/menu.py:318
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:337
msgid "(нет шаблонов)"
msgstr "(Немає шаблонів)"
#: olgram/commands/menu.py:376
msgid "У вас нет шаблонов, чтобы их удалять"
msgstr "У вас немає шаблонів, щоб їх видаляти"
#: olgram/commands/menu.py:378
msgid "Неправильное число. Чтобы удалить шаблон, введите число от 0 до {0}"
msgstr "Неправильне число. Щоб видалити шаблон, введіть число від 0 до {0}"
#: olgram/commands/menu.py:386
msgid "У вашего бота уже слишком много шаблонов"
msgstr "У вашого бота вже дуже багато шаблонів"
#: olgram/commands/menu.py:390
msgid "Такой текст уже есть в списке шаблонов"
msgstr "Такий текст вже є у списку шаблонів"
#: olgram/commands/menu.py:408
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:40
msgid "Владелец бота ограничил доступ к этому функционалу 😞"
msgstr "Власник бота обмежив доступ до цього функціоналу 😞"
#: olgram/utils/permissions.py:52
msgid "Владелец бота ограничил доступ к этому функционалу😞"
msgstr "Власник бота обмежив доступ до цього функціоналу 😞"
#: server/custom.py:55
msgid ""
"<b>Политика конфиденциальности</b>\n"
"\n"
"Этот бот не хранит ваши сообщения, имя пользователя и @username. При "
"отправке сообщения (кроме команд /start и /security_policy) ваш "
"идентификатор пользователя записывается в кеш на некоторое время и потом "
"удаляется из кеша. Этот идентификатор используется только для общения с "
"оператором; боты Olgram не делают массовых рассылок.\n"
"\n"
msgstr ""
"<b>Політика конфіденційності</b>\n"
"\n"
"Цей бот не зберігає ваші повідомлення, ім'я користувача та @username. При "
"надсиланні повідомлення (крім команд /start та /security_policy) ваш "
"ідентифікатор користувача записується в кеш на деякий час і потім "
"видаляється з кеша. Цей ідентифікатор використовується лише для спілкування "
"з оператором; боти Olgram не роблять масових розсилок.\n"
"\n"
#: server/custom.py:61
msgid ""
"При отправке сообщения (кроме команд /start и /security_policy) оператор "
"<b>видит</b> ваши имя пользователя, @username и идентификатор пользователя в "
"силу настроек, которые оператор указал при создании бота."
msgstr ""
"При надсиланні повідомлення (крім команд /start та /security_policy) "
"оператор <b>бачить</b> ваше ім'я користувача, @username та ідентифікатор "
"користувача через налаштування, які оператор вказав при створенні бота."
#: server/custom.py:65
msgid ""
"В зависимости от ваших настроек конфиденциальности Telegram, оператор может "
"видеть ваш username, имя пользователя и другую информацию."
msgstr ""
"Залежно від ваших налаштувань конфіденційності Telegram оператор може бачити "
"ваш username, ім'я користувача та іншу інформацію."
#: server/custom.py:76
msgid "Сообщение от пользователя "
msgstr "Допис від користувача "
#: server/custom.py:135
msgid "Вы заблокированы в этом боте"
msgstr "Ви заблоковані у цьому боті"
#: server/custom.py:141
msgid "Слишком много сообщений, подождите одну минуту"
msgstr "Забагато повідомлень, зачекайте одну хвилину"
#: server/custom.py:148
msgid "Не удаётся связаться с владельцем бота"
msgstr "Не вдається зв'язатися з власником бота"
#: server/custom.py:179
msgid ""
"<i>Невозможно переслать сообщение: автор не найден (сообщение слишком "
"старое?)</i>"
msgstr ""
"<i>Неможливо надіслати повідомлення: автора не знайдено (повідомлення "
"занадто старе?)</i>"
#: server/custom.py:187
msgid "Пользователь заблокирован"
msgstr "Користувач заблоковано"
#: server/custom.py:192
msgid "Пользователь не был забанен"
msgstr "Користувач не був забанений"
#: server/custom.py:195
msgid "Пользователь разбанен"
msgstr "Користувач розбанений"
#: server/custom.py:200
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,565 @@
# 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>\n"
" {1}\n"
" </pre>\n"
" Отправьте сообщение, чтобы изменить текст.\n"
" "
msgstr ""
"\n"
" 你现在正在编辑用户向你的机器人发送@{0}之后的文本。\n"
" /start\n"
"\n"
" 目前的文本。\n"
" <pre>\n"
" {1}\n"
" </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>\n"
" {1}\n"
" </pre>\n"
" Отправьте сообщение, чтобы изменить текст.\n"
" "
msgstr ""
"\n"
" 你现在正在编辑自动回复的文本。该信息会自动响应所有收到的@{0}信息而发送。"
"默认情况下,它是禁用的。\n"
"\n"
" 目前的文本。\n"
" <pre>\n"
" {1}\n"
" </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 "

35
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.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,13 +44,20 @@ 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())
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

@ -2,18 +2,24 @@
Здесь работа с конкретным ботом
"""
from aiogram import types
from aiogram.utils.exceptions import TelegramAPIError
from aiogram.utils.exceptions import TelegramAPIError, Unauthorized
from aiogram import Bot as AioBot
from olgram.models.models import Bot
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 Unauthorized:
# Вероятно пользователь сбросил токен или удалил бот, это уже не наши проблемы
pass
await bot.delete()
await call.answer("Бот удалён")
await call.answer(_("Бот удалён"))
try:
await call.message.delete()
except TelegramAPIError:
@ -29,7 +35,19 @@ async def reset_bot_text(bot: Bot, call: types.CallbackQuery):
"""
bot.start_text = bot._meta.fields_map['start_text'].default
await bot.save()
await call.answer("Текст сброшен")
await call.answer(_("Текст сброшен"))
async def reset_bot_second_text(bot: Bot, call: types.CallbackQuery):
"""
Пользователь решил сбросить second text бота
:param bot:
:param call:
:return:
"""
bot.second_text = bot._meta.fields_map['second_text'].default
await bot.save()
await call.answer(_("Текст сброшен"))
async def select_chat(bot: Bot, call: types.CallbackQuery, chat: str):
@ -43,13 +61,48 @@ 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 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 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"])

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,13 @@
from olgram.router import dp
from aiogram import types, Bot as AioBot
from olgram.models.models import Bot, User
from olgram.models.models import Bot, User, DefaultAnswer
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
from olgram.commands import bot_actions
from locales.locale import _
import typing as ty
@ -27,11 +28,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 +43,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 +64,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="<< Назад",
types.InlineKeyboardButton(text=_("❗️ Выйти из всех чатов"),
callback_data=menu_callback.new(level=3, bot_id=bot.id, operation="chat",
chat="leave"))
)
keyboard.insert(
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,49 +99,107 @@ 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))
)
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_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))
)
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 _("выключен")
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>
""")).format(thread_turn, info_turn, antiflood_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")
async def send_bot_text_menu(bot: Bot, call: ty.Optional[types.CallbackQuery] = None, chat_id: ty.Optional[int] = None):
@ -141,17 +207,22 @@ async def send_bot_text_menu(bot: Bot, call: ty.Optional[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="reset_text",
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.insert(
types.InlineKeyboardButton(text="<< Завершить редактирование",
callback_data=menu_callback.new(level=1, bot_id=bot.id, operation=empty, chat=empty))
types.InlineKeyboardButton(text=_("Сбросить текст"),
callback_data=menu_callback.new(level=3, bot_id=bot.id, operation="reset_text",
chat=empty))
)
text = dedent("""
Сейчас вы редактируете текст, который отправляется после того, как пользователь отправит вашему боту {0}
text = dedent(_("""
Сейчас вы редактируете текст, который отправляется после того, как пользователь отправит вашему боту @{0}
команду /start
Текущий текст:
@ -159,7 +230,7 @@ async def send_bot_text_menu(bot: Bot, call: ty.Optional[types.CallbackQuery] =
{1}
</pre>
Отправьте сообщение, чтобы изменить текст.
""")
"""))
text = text.format(bot.name, bot.start_text)
if call:
await edit_or_create(call, text, keyboard, parse_mode="HTML")
@ -167,16 +238,163 @@ async def send_bot_text_menu(bot: Bot, call: ty.Optional[types.CallbackQuery] =
await AioBot.get_current().send_message(chat_id, text, reply_markup=keyboard, parse_mode="HTML")
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=1, bot_id=bot.id, operation=empty, chat=empty))
)
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):
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))
)
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))
)
text = dedent(_("""
Сейчас вы редактируете текст автоответчика. Это сообщение отправляется в ответ на все входящие сообщения @{0} \
автоматически. По умолчанию оно отключено.
Текущий текст:
<pre>
{1}
</pre>
Отправьте сообщение, чтобы изменить текст.
"""))
text = text.format(bot.name, bot.second_text if bot.second_text 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>
Отправьте какую-нибудь фразу (например: "Ваш заказ готов, ожидайте!"), чтобы добавить её в шаблон.
Чтобы удалить шаблон из списка, отправьте его номер в списке (например, 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:
await AioBot.get_current().send_message(chat_id, text, reply_markup=keyboard, parse_mode="HTML")
@dp.message_handler(state="wait_start_text", content_types="text", regexp="^[^/].+") # Not command
async def start_text_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)
bot.start_text = message.text
bot.start_text = message.html_text
await bot.save()
await send_bot_text_menu(bot, chat_id=message.chat.id)
@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")
bot = await Bot.get_or_none(pk=bot_id)
bot.second_text = message.html_text
await bot.save()
await send_bot_second_text_menu(bot, chat_id=message.chat.id)
@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="*")
async def callback(call: types.CallbackQuery, callback_data: dict, state: FSMContext):
level = callback_data.get("level")
@ -187,10 +405,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,6 +419,10 @@ 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 == "text":
await state.set_state("wait_start_text")
async with state.proxy() as proxy:
@ -211,6 +434,31 @@ async def callback(call: types.CallbackQuery, callback_data: dict, state: FSMCon
return await bot_actions.delete_bot(bot, call)
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 == "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)
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)
if operation == "reset_second_text":
await bot_actions.reset_bot_second_text(bot, call)
return await send_bot_second_text_menu(bot, call)
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,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,12 +39,33 @@ 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)
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'
@ -35,6 +74,10 @@ 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'
@ -46,3 +89,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,25 @@
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 +29,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.5.0"
@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 +65,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 +94,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 +119,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 +145,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()}'

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

@ -22,8 +22,11 @@ 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)

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()

898
poetry.lock generated Normal file
View File

@ -0,0 +1,898 @@
[[package]]
name = "aerich"
version = "0.5.7"
description = "A database migrations tool for Tortoise ORM."
category = "main"
optional = false
python-versions = ">=3.7,<4.0"
[package.dependencies]
click = "*"
ddlparse = "*"
dictdiffer = "*"
pydantic = "*"
tortoise-orm = "*"
[package.extras]
aiomysql = ["aiomysql"]
asyncpg = ["asyncpg"]
[[package]]
name = "aiocache"
version = "0.11.1"
description = "multi backend asyncio cache"
category = "main"
optional = false
python-versions = "*"
[package.extras]
dev = ["asynctest (>=0.11.0)", "codecov", "coverage", "flake8", "ipdb", "marshmallow", "pystache", "pytest", "pytest-asyncio", "pytest-mock", "sphinx", "sphinx-autobuild", "sphinx-rtd-theme", "black"]
memcached = ["aiomcache (>=0.5.2)"]
msgpack = ["msgpack (>=0.5.5)"]
redis = ["aioredis (>=0.3.3)", "aioredis (>=1.0.0)"]
[[package]]
name = "aiogram"
version = "2.13"
description = "Is a pretty simple and fully asynchronous framework for Telegram Bot API"
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
aiohttp = ">=3.7.2,<4.0.0"
Babel = ">=2.8.0"
certifi = ">=2020.6.20"
[package.extras]
fast = ["uvloop (>=0.14.0,<0.15.0)", "ujson (>=1.35)"]
proxy = ["aiohttp-socks (>=0.5.3,<0.6.0)"]
[[package]]
name = "aiohttp"
version = "3.8.1"
description = "Async http client/server framework (asyncio)"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
aiosignal = ">=1.1.2"
async-timeout = ">=4.0.0a3,<5.0"
attrs = ">=17.3.0"
charset-normalizer = ">=2.0,<3.0"
frozenlist = ">=1.1.1"
multidict = ">=4.5,<7.0"
yarl = ">=1.0,<2.0"
[package.extras]
speedups = ["aiodns", "brotli", "cchardet"]
[[package]]
name = "aioredis"
version = "1.3.0"
description = "asyncio (PEP 3156) Redis support"
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
async-timeout = "*"
hiredis = "*"
[[package]]
name = "aiosignal"
version = "1.2.0"
description = "aiosignal: a list of registered asynchronous callbacks"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
frozenlist = ">=1.1.0"
[[package]]
name = "aiosqlite"
version = "0.17.0"
description = "asyncio bridge to the standard sqlite3 module"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
typing_extensions = ">=3.7.2"
[[package]]
name = "async-timeout"
version = "4.0.2"
description = "Timeout context manager for asyncio programs"
category = "main"
optional = false
python-versions = ">=3.6"
[[package]]
name = "asyncpg"
version = "0.25.0"
description = "An asyncio PostgreSQL driver"
category = "main"
optional = false
python-versions = ">=3.6.0"
[package.extras]
dev = ["Cython (>=0.29.24,<0.30.0)", "pytest (>=6.0)", "Sphinx (>=4.1.2,<4.2.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "pycodestyle (>=2.7.0,<2.8.0)", "flake8 (>=3.9.2,<3.10.0)", "uvloop (>=0.15.3)"]
docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)"]
test = ["pycodestyle (>=2.7.0,<2.8.0)", "flake8 (>=3.9.2,<3.10.0)", "uvloop (>=0.15.3)"]
[[package]]
name = "attrs"
version = "21.4.0"
description = "Classes Without Boilerplate"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[package.extras]
dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"]
docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"]
tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"]
[[package]]
name = "babel"
version = "2.10.3"
description = "Internationalization utilities"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
pytz = ">=2015.7"
[[package]]
name = "certifi"
version = "2022.6.15"
description = "Python package for providing Mozilla's CA Bundle."
category = "main"
optional = false
python-versions = ">=3.6"
[[package]]
name = "charset-normalizer"
version = "2.1.0"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
category = "main"
optional = false
python-versions = ">=3.6.0"
[package.extras]
unicode_backport = ["unicodedata2"]
[[package]]
name = "click"
version = "8.1.3"
description = "Composable command line interface toolkit"
category = "main"
optional = false
python-versions = ">=3.7"
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "colorama"
version = "0.4.5"
description = "Cross-platform colored terminal text."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "ddlparse"
version = "1.10.0"
description = "DDL parase and Convert to BigQuery JSON schema"
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
pyparsing = "*"
[[package]]
name = "dictdiffer"
version = "0.9.0"
description = "Dictdiffer is a library that helps you to diff and patch dictionaries."
category = "main"
optional = false
python-versions = "*"
[package.extras]
all = ["Sphinx (>=3)", "sphinx-rtd-theme (>=0.2)", "check-manifest (>=0.42)", "mock (>=1.3.0)", "pytest-cov (>=2.10.1)", "pytest-isort (>=1.2.0)", "sphinx (>=3)", "tox (>=3.7.0)", "numpy (>=1.13.0)", "numpy (>=1.15.0)", "numpy (>=1.18.0)", "pytest (==5.4.3)", "pytest-pycodestyle (>=2)", "pytest-pydocstyle (>=2)", "pytest (>=6)", "pytest-pycodestyle (>=2.2.0)", "pytest-pydocstyle (>=2.2.0)", "numpy (>=1.20.0)"]
docs = ["Sphinx (>=3)", "sphinx-rtd-theme (>=0.2)"]
numpy = ["numpy (>=1.13.0)", "numpy (>=1.15.0)", "numpy (>=1.18.0)", "numpy (>=1.20.0)"]
tests = ["check-manifest (>=0.42)", "mock (>=1.3.0)", "pytest-cov (>=2.10.1)", "pytest-isort (>=1.2.0)", "sphinx (>=3)", "tox (>=3.7.0)", "pytest (==5.4.3)", "pytest-pycodestyle (>=2)", "pytest-pydocstyle (>=2)", "pytest (>=6)", "pytest-pycodestyle (>=2.2.0)", "pytest-pydocstyle (>=2.2.0)"]
[[package]]
name = "flake8"
version = "4.0.1"
description = "the modular source code checker: pep8 pyflakes and co"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
mccabe = ">=0.6.0,<0.7.0"
pycodestyle = ">=2.8.0,<2.9.0"
pyflakes = ">=2.4.0,<2.5.0"
[[package]]
name = "frozenlist"
version = "1.3.0"
description = "A list-like structure which implements collections.abc.MutableSequence"
category = "main"
optional = false
python-versions = ">=3.7"
[[package]]
name = "hiredis"
version = "2.0.0"
description = "Python wrapper for hiredis"
category = "main"
optional = false
python-versions = ">=3.6"
[[package]]
name = "idna"
version = "3.3"
description = "Internationalized Domain Names in Applications (IDNA)"
category = "main"
optional = false
python-versions = ">=3.5"
[[package]]
name = "iso8601"
version = "0.1.16"
description = "Simple module to parse ISO 8601 dates"
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "mccabe"
version = "0.6.1"
description = "McCabe checker, plugin for flake8"
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "multidict"
version = "6.0.2"
description = "multidict implementation"
category = "main"
optional = false
python-versions = ">=3.7"
[[package]]
name = "pycodestyle"
version = "2.8.0"
description = "Python style guide checker"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "pycrypto"
version = "2.6.1"
description = "Cryptographic modules for Python."
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "pydantic"
version = "1.9.1"
description = "Data validation and settings management using python type hints"
category = "main"
optional = false
python-versions = ">=3.6.1"
[package.dependencies]
typing-extensions = ">=3.7.4.3"
[package.extras]
dotenv = ["python-dotenv (>=0.10.4)"]
email = ["email-validator (>=1.0.3)"]
[[package]]
name = "pyflakes"
version = "2.4.0"
description = "passive checker of Python programs"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "pyparsing"
version = "3.0.9"
description = "pyparsing module - Classes and methods to define and execute parsing grammars"
category = "main"
optional = false
python-versions = ">=3.6.8"
[package.extras]
diagrams = ["railroad-diagrams", "jinja2"]
[[package]]
name = "pypika-tortoise"
version = "0.1.5"
description = "Forked from pypika and streamline just for tortoise-orm"
category = "main"
optional = false
python-versions = ">=3.7,<4.0"
[[package]]
name = "python-dotenv"
version = "0.19.2"
description = "Read key-value pairs from a .env file and set them as environment variables"
category = "main"
optional = false
python-versions = ">=3.5"
[package.extras]
cli = ["click (>=5.0)"]
[[package]]
name = "python-gettext"
version = "4.0"
description = "Python Gettext po to mo file compiler."
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "pytz"
version = "2022.1"
description = "World timezone definitions, modern and historical"
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "tortoise-orm"
version = "0.18.1"
description = "Easy async ORM for python, built with relations in mind"
category = "main"
optional = false
python-versions = ">=3.7,<4.0"
[package.dependencies]
aiosqlite = ">=0.16.0,<0.18.0"
asyncpg = {version = "*", optional = true, markers = "extra == \"asyncpg\""}
iso8601 = ">=0.1.13,<0.2.0"
pypika-tortoise = ">=0.1.3,<0.2.0"
pytz = "*"
[package.extras]
aiomysql = ["aiomysql"]
asyncmy = ["asyncmy"]
asyncpg = ["asyncpg"]
accel = ["ciso8601", "orjson", "uvloop"]
[[package]]
name = "typing-extensions"
version = "4.2.0"
description = "Backported and Experimental Type Hints for Python 3.7+"
category = "main"
optional = false
python-versions = ">=3.7"
[[package]]
name = "yarl"
version = "1.7.2"
description = "Yet another URL library"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
idna = ">=2.0"
multidict = ">=4.0"
[metadata]
lock-version = "1.1"
python-versions = "^3.8"
content-hash = "9151864c69f349148a36cc7551bb54fd240229eb2533e92b82a0f85a251a56f3"
[metadata.files]
aerich = [
{file = "aerich-0.5.7-py3-none-any.whl", hash = "sha256:0684eb3d631c7c6e14caf2b2c3b9dad1c15ce8ade5771773c015a302f54ff4f6"},
{file = "aerich-0.5.7.tar.gz", hash = "sha256:f9ef8796f7a13ba9965eda0aa6840033bbd42b2e4e52c24d8f0dbdb85e4a5187"},
]
aiocache = [
{file = "aiocache-0.11.1-py2.py3-none-any.whl", hash = "sha256:e55c7caaa5753794fd301c3a2e592737fa1d036db9f8d04ae154facdfb48a157"},
{file = "aiocache-0.11.1.tar.gz", hash = "sha256:f2ebe0b05cec45782e7b5ea0bb74640f157dd4bb1028b4565364dda9fe33be7f"},
]
aiogram = [
{file = "aiogram-2.13-py3-none-any.whl", hash = "sha256:e1923ad789bb8d90a7b1edc5fce7dc33df982d1fedc6528c65cc38d2d88f1ae0"},
{file = "aiogram-2.13.tar.gz", hash = "sha256:f656f57580fd8e1ad0f8fe645f59cf3ad40e9899050dd6ac999a8f0cdbf1b116"},
]
aiohttp = [
{file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1ed0b6477896559f17b9eaeb6d38e07f7f9ffe40b9f0f9627ae8b9926ae260a8"},
{file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7dadf3c307b31e0e61689cbf9e06be7a867c563d5a63ce9dca578f956609abf8"},
{file = "aiohttp-3.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a79004bb58748f31ae1cbe9fa891054baaa46fb106c2dc7af9f8e3304dc30316"},
{file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12de6add4038df8f72fac606dff775791a60f113a725c960f2bab01d8b8e6b15"},
{file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f0d5f33feb5f69ddd57a4a4bd3d56c719a141080b445cbf18f238973c5c9923"},
{file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eaba923151d9deea315be1f3e2b31cc39a6d1d2f682f942905951f4e40200922"},
{file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:099ebd2c37ac74cce10a3527d2b49af80243e2a4fa39e7bce41617fbc35fa3c1"},
{file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2e5d962cf7e1d426aa0e528a7e198658cdc8aa4fe87f781d039ad75dcd52c516"},
{file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fa0ffcace9b3aa34d205d8130f7873fcfefcb6a4dd3dd705b0dab69af6712642"},
{file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:61bfc23df345d8c9716d03717c2ed5e27374e0fe6f659ea64edcd27b4b044cf7"},
{file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:31560d268ff62143e92423ef183680b9829b1b482c011713ae941997921eebc8"},
{file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:01d7bdb774a9acc838e6b8f1d114f45303841b89b95984cbb7d80ea41172a9e3"},
{file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:97ef77eb6b044134c0b3a96e16abcb05ecce892965a2124c566af0fd60f717e2"},
{file = "aiohttp-3.8.1-cp310-cp310-win32.whl", hash = "sha256:c2aef4703f1f2ddc6df17519885dbfa3514929149d3ff900b73f45998f2532fa"},
{file = "aiohttp-3.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:713ac174a629d39b7c6a3aa757b337599798da4c1157114a314e4e391cd28e32"},
{file = "aiohttp-3.8.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:473d93d4450880fe278696549f2e7aed8cd23708c3c1997981464475f32137db"},
{file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b5eeae8e019e7aad8af8bb314fb908dd2e028b3cdaad87ec05095394cce632"},
{file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3af642b43ce56c24d063325dd2cf20ee012d2b9ba4c3c008755a301aaea720ad"},
{file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3630c3ef435c0a7c549ba170a0633a56e92629aeed0e707fec832dee313fb7a"},
{file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4a4a4e30bf1edcad13fb0804300557aedd07a92cabc74382fdd0ba6ca2661091"},
{file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6f8b01295e26c68b3a1b90efb7a89029110d3a4139270b24fda961893216c440"},
{file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a25fa703a527158aaf10dafd956f7d42ac6d30ec80e9a70846253dd13e2f067b"},
{file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:5bfde62d1d2641a1f5173b8c8c2d96ceb4854f54a44c23102e2ccc7e02f003ec"},
{file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:51467000f3647d519272392f484126aa716f747859794ac9924a7aafa86cd411"},
{file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:03a6d5349c9ee8f79ab3ff3694d6ce1cfc3ced1c9d36200cb8f08ba06bd3b782"},
{file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:102e487eeb82afac440581e5d7f8f44560b36cf0bdd11abc51a46c1cd88914d4"},
{file = "aiohttp-3.8.1-cp36-cp36m-win32.whl", hash = "sha256:4aed991a28ea3ce320dc8ce655875e1e00a11bdd29fe9444dd4f88c30d558602"},
{file = "aiohttp-3.8.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b0e20cddbd676ab8a64c774fefa0ad787cc506afd844de95da56060348021e96"},
{file = "aiohttp-3.8.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:37951ad2f4a6df6506750a23f7cbabad24c73c65f23f72e95897bb2cecbae676"},
{file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c23b1ad869653bc818e972b7a3a79852d0e494e9ab7e1a701a3decc49c20d51"},
{file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15b09b06dae900777833fe7fc4b4aa426556ce95847a3e8d7548e2d19e34edb8"},
{file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:477c3ea0ba410b2b56b7efb072c36fa91b1e6fc331761798fa3f28bb224830dd"},
{file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2f2f69dca064926e79997f45b2f34e202b320fd3782f17a91941f7eb85502ee2"},
{file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ef9612483cb35171d51d9173647eed5d0069eaa2ee812793a75373447d487aa4"},
{file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6d69f36d445c45cda7b3b26afef2fc34ef5ac0cdc75584a87ef307ee3c8c6d00"},
{file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:55c3d1072704d27401c92339144d199d9de7b52627f724a949fc7d5fc56d8b93"},
{file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b9d00268fcb9f66fbcc7cd9fe423741d90c75ee029a1d15c09b22d23253c0a44"},
{file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:07b05cd3305e8a73112103c834e91cd27ce5b4bd07850c4b4dbd1877d3f45be7"},
{file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c34dc4958b232ef6188c4318cb7b2c2d80521c9a56c52449f8f93ab7bc2a8a1c"},
{file = "aiohttp-3.8.1-cp37-cp37m-win32.whl", hash = "sha256:d2f9b69293c33aaa53d923032fe227feac867f81682f002ce33ffae978f0a9a9"},
{file = "aiohttp-3.8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:6ae828d3a003f03ae31915c31fa684b9890ea44c9c989056fea96e3d12a9fa17"},
{file = "aiohttp-3.8.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0c7ebbbde809ff4e970824b2b6cb7e4222be6b95a296e46c03cf050878fc1785"},
{file = "aiohttp-3.8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b7ef7cbd4fec9a1e811a5de813311ed4f7ac7d93e0fda233c9b3e1428f7dd7b"},
{file = "aiohttp-3.8.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c3d6a4d0619e09dcd61021debf7059955c2004fa29f48788a3dfaf9c9901a7cd"},
{file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:718626a174e7e467f0558954f94af117b7d4695d48eb980146016afa4b580b2e"},
{file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:589c72667a5febd36f1315aa6e5f56dd4aa4862df295cb51c769d16142ddd7cd"},
{file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2ed076098b171573161eb146afcb9129b5ff63308960aeca4b676d9d3c35e700"},
{file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:086f92daf51a032d062ec5f58af5ca6a44d082c35299c96376a41cbb33034675"},
{file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:11691cf4dc5b94236ccc609b70fec991234e7ef8d4c02dd0c9668d1e486f5abf"},
{file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:31d1e1c0dbf19ebccbfd62eff461518dcb1e307b195e93bba60c965a4dcf1ba0"},
{file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:11a67c0d562e07067c4e86bffc1553f2cf5b664d6111c894671b2b8712f3aba5"},
{file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:bb01ba6b0d3f6c68b89fce7305080145d4877ad3acaed424bae4d4ee75faa950"},
{file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:44db35a9e15d6fe5c40d74952e803b1d96e964f683b5a78c3cc64eb177878155"},
{file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:844a9b460871ee0a0b0b68a64890dae9c415e513db0f4a7e3cab41a0f2fedf33"},
{file = "aiohttp-3.8.1-cp38-cp38-win32.whl", hash = "sha256:7d08744e9bae2ca9c382581f7dce1273fe3c9bae94ff572c3626e8da5b193c6a"},
{file = "aiohttp-3.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:04d48b8ce6ab3cf2097b1855e1505181bdd05586ca275f2505514a6e274e8e75"},
{file = "aiohttp-3.8.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f5315a2eb0239185af1bddb1abf472d877fede3cc8d143c6cddad37678293237"},
{file = "aiohttp-3.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a996d01ca39b8dfe77440f3cd600825d05841088fd6bc0144cc6c2ec14cc5f74"},
{file = "aiohttp-3.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:13487abd2f761d4be7c8ff9080de2671e53fff69711d46de703c310c4c9317ca"},
{file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea302f34477fda3f85560a06d9ebdc7fa41e82420e892fc50b577e35fc6a50b2"},
{file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2f635ce61a89c5732537a7896b6319a8fcfa23ba09bec36e1b1ac0ab31270d2"},
{file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e999f2d0e12eea01caeecb17b653f3713d758f6dcc770417cf29ef08d3931421"},
{file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0770e2806a30e744b4e21c9d73b7bee18a1cfa3c47991ee2e5a65b887c49d5cf"},
{file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d15367ce87c8e9e09b0f989bfd72dc641bcd04ba091c68cd305312d00962addd"},
{file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6c7cefb4b0640703eb1069835c02486669312bf2f12b48a748e0a7756d0de33d"},
{file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:71927042ed6365a09a98a6377501af5c9f0a4d38083652bcd2281a06a5976724"},
{file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:28d490af82bc6b7ce53ff31337a18a10498303fe66f701ab65ef27e143c3b0ef"},
{file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:b6613280ccedf24354406caf785db748bebbddcf31408b20c0b48cb86af76866"},
{file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:81e3d8c34c623ca4e36c46524a3530e99c0bc95ed068fd6e9b55cb721d408fb2"},
{file = "aiohttp-3.8.1-cp39-cp39-win32.whl", hash = "sha256:7187a76598bdb895af0adbd2fb7474d7f6025d170bc0a1130242da817ce9e7d1"},
{file = "aiohttp-3.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:1c182cb873bc91b411e184dab7a2b664d4fea2743df0e4d57402f7f3fa644bac"},
{file = "aiohttp-3.8.1.tar.gz", hash = "sha256:fc5471e1a54de15ef71c1bc6ebe80d4dc681ea600e68bfd1cbce40427f0b7578"},
]
aioredis = [
{file = "aioredis-1.3.0-py3-none-any.whl", hash = "sha256:71302cebeb7add86f1fe660b469068760ca4364504e75ee83dd6f6b7118bfe28"},
{file = "aioredis-1.3.0.tar.gz", hash = "sha256:86da2748fb0652625a8346f413167f078ec72bdc76e217db7e605a059cd56e86"},
]
aiosignal = [
{file = "aiosignal-1.2.0-py3-none-any.whl", hash = "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a"},
{file = "aiosignal-1.2.0.tar.gz", hash = "sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2"},
]
aiosqlite = [
{file = "aiosqlite-0.17.0-py3-none-any.whl", hash = "sha256:6c49dc6d3405929b1d08eeccc72306d3677503cc5e5e43771efc1e00232e8231"},
{file = "aiosqlite-0.17.0.tar.gz", hash = "sha256:f0e6acc24bc4864149267ac82fb46dfb3be4455f99fe21df82609cc6e6baee51"},
]
async-timeout = [
{file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"},
{file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"},
]
asyncpg = [
{file = "asyncpg-0.25.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bf5e3408a14a17d480f36ebaf0401a12ff6ae5457fdf45e4e2775c51cc9517d3"},
{file = "asyncpg-0.25.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2bc197fc4aca2fd24f60241057998124012469d2e414aed3f992579db0c88e3a"},
{file = "asyncpg-0.25.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a70783f6ffa34cc7dd2de20a873181414a34fd35a4a208a1f1a7f9f695e4ec4"},
{file = "asyncpg-0.25.0-cp310-cp310-win32.whl", hash = "sha256:43cde84e996a3afe75f325a68300093425c2f47d340c0fc8912765cf24a1c095"},
{file = "asyncpg-0.25.0-cp310-cp310-win_amd64.whl", hash = "sha256:56d88d7ef4341412cd9c68efba323a4519c916979ba91b95d4c08799d2ff0c09"},
{file = "asyncpg-0.25.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a84d30e6f850bac0876990bcd207362778e2208df0bee8be8da9f1558255e634"},
{file = "asyncpg-0.25.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:beaecc52ad39614f6ca2e48c3ca15d56e24a2c15cbfdcb764a4320cc45f02fd5"},
{file = "asyncpg-0.25.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:6f8f5fc975246eda83da8031a14004b9197f510c41511018e7b1bedde6968e92"},
{file = "asyncpg-0.25.0-cp36-cp36m-win32.whl", hash = "sha256:ddb4c3263a8d63dcde3d2c4ac1c25206bfeb31fa83bd70fd539e10f87739dee4"},
{file = "asyncpg-0.25.0-cp36-cp36m-win_amd64.whl", hash = "sha256:bf6dc9b55b9113f39eaa2057337ce3f9ef7de99a053b8a16360395ce588925cd"},
{file = "asyncpg-0.25.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:acb311722352152936e58a8ee3c5b8e791b24e84cd7d777c414ff05b3530ca68"},
{file = "asyncpg-0.25.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0a61fb196ce4dae2f2fa26eb20a778db21bbee484d2e798cb3cc988de13bdd1b"},
{file = "asyncpg-0.25.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2633331cbc8429030b4f20f712f8d0fbba57fa8555ee9b2f45f981b81328b256"},
{file = "asyncpg-0.25.0-cp37-cp37m-win32.whl", hash = "sha256:863d36eba4a7caa853fd7d83fad5fd5306f050cc2fe6e54fbe10cdb30420e5e9"},
{file = "asyncpg-0.25.0-cp37-cp37m-win_amd64.whl", hash = "sha256:fe471ccd915b739ca65e2e4dbd92a11b44a5b37f2e38f70827a1c147dafe0fa8"},
{file = "asyncpg-0.25.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:72a1e12ea0cf7c1e02794b697e3ca967b2360eaa2ce5d4bfdd8604ec2d6b774b"},
{file = "asyncpg-0.25.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4327f691b1bdb222df27841938b3e04c14068166b3a97491bec2cb982f49f03e"},
{file = "asyncpg-0.25.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:739bbd7f89a2b2f6bc44cb8bf967dab12c5bc714fcbe96e68d512be45ecdf962"},
{file = "asyncpg-0.25.0-cp38-cp38-win32.whl", hash = "sha256:18d49e2d93a7139a2fdbd113e320cc47075049997268a61bfbe0dde680c55471"},
{file = "asyncpg-0.25.0-cp38-cp38-win_amd64.whl", hash = "sha256:191fe6341385b7fdea7dbdcf47fd6db3fd198827dcc1f2b228476d13c05a03c6"},
{file = "asyncpg-0.25.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:52fab7f1b2c29e187dd8781fce896249500cf055b63471ad66332e537e9b5f7e"},
{file = "asyncpg-0.25.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a738f1b2876f30d710d3dc1e7858160a0afe1603ba16bf5f391f5316eb0ed855"},
{file = "asyncpg-0.25.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4105f57ad1e8fbc8b1e535d8fcefa6ce6c71081228f08680c6dea24384ff0e"},
{file = "asyncpg-0.25.0-cp39-cp39-win32.whl", hash = "sha256:f55918ded7b85723a5eaeb34e86e7b9280d4474be67df853ab5a7fa0cc7c6bf2"},
{file = "asyncpg-0.25.0-cp39-cp39-win_amd64.whl", hash = "sha256:649e2966d98cc48d0646d9a4e29abecd8b59d38d55c256d5c857f6b27b7407ac"},
{file = "asyncpg-0.25.0.tar.gz", hash = "sha256:63f8e6a69733b285497c2855464a34de657f2cccd25aeaeeb5071872e9382540"},
]
attrs = [
{file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"},
{file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"},
]
babel = [
{file = "Babel-2.10.3-py3-none-any.whl", hash = "sha256:ff56f4892c1c4bf0d814575ea23471c230d544203c7748e8c68f0089478d48eb"},
{file = "Babel-2.10.3.tar.gz", hash = "sha256:7614553711ee97490f732126dc077f8d0ae084ebc6a96e23db1482afabdb2c51"},
]
certifi = [
{file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"},
{file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"},
]
charset-normalizer = [
{file = "charset-normalizer-2.1.0.tar.gz", hash = "sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413"},
{file = "charset_normalizer-2.1.0-py3-none-any.whl", hash = "sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5"},
]
click = [
{file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
{file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
]
colorama = [
{file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"},
{file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
]
ddlparse = [
{file = "ddlparse-1.10.0-py3-none-any.whl", hash = "sha256:71761b3457c8720853af3aeef266e2da1b6edef50936969492d586d7046a2ac2"},
{file = "ddlparse-1.10.0.tar.gz", hash = "sha256:6418681baa848eb01251ab79eb3d0ad7e140e6ab1deaae5a019353ddb3a908da"},
]
dictdiffer = [
{file = "dictdiffer-0.9.0-py2.py3-none-any.whl", hash = "sha256:442bfc693cfcadaf46674575d2eba1c53b42f5e404218ca2c2ff549f2df56595"},
{file = "dictdiffer-0.9.0.tar.gz", hash = "sha256:17bacf5fbfe613ccf1b6d512bd766e6b21fb798822a133aa86098b8ac9997578"},
]
flake8 = [
{file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"},
{file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"},
]
frozenlist = [
{file = "frozenlist-1.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d2257aaba9660f78c7b1d8fea963b68f3feffb1a9d5d05a18401ca9eb3e8d0a3"},
{file = "frozenlist-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4a44ebbf601d7bac77976d429e9bdb5a4614f9f4027777f9e54fd765196e9d3b"},
{file = "frozenlist-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:45334234ec30fc4ea677f43171b18a27505bfb2dba9aca4398a62692c0ea8868"},
{file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47be22dc27ed933d55ee55845d34a3e4e9f6fee93039e7f8ebadb0c2f60d403f"},
{file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03a7dd1bfce30216a3f51a84e6dd0e4a573d23ca50f0346634916ff105ba6e6b"},
{file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:691ddf6dc50480ce49f68441f1d16a4c3325887453837036e0fb94736eae1e58"},
{file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bde99812f237f79eaf3f04ebffd74f6718bbd216101b35ac7955c2d47c17da02"},
{file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a202458d1298ced3768f5a7d44301e7c86defac162ace0ab7434c2e961166e8"},
{file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b9e3e9e365991f8cc5f5edc1fd65b58b41d0514a6a7ad95ef5c7f34eb49b3d3e"},
{file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:04cb491c4b1c051734d41ea2552fde292f5f3a9c911363f74f39c23659c4af78"},
{file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:436496321dad302b8b27ca955364a439ed1f0999311c393dccb243e451ff66aa"},
{file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:754728d65f1acc61e0f4df784456106e35afb7bf39cfe37227ab00436fb38676"},
{file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6eb275c6385dd72594758cbe96c07cdb9bd6becf84235f4a594bdf21e3596c9d"},
{file = "frozenlist-1.3.0-cp310-cp310-win32.whl", hash = "sha256:e30b2f9683812eb30cf3f0a8e9f79f8d590a7999f731cf39f9105a7c4a39489d"},
{file = "frozenlist-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f7353ba3367473d1d616ee727945f439e027f0bb16ac1a750219a8344d1d5d3c"},
{file = "frozenlist-1.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88aafd445a233dbbf8a65a62bc3249a0acd0d81ab18f6feb461cc5a938610d24"},
{file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4406cfabef8f07b3b3af0f50f70938ec06d9f0fc26cbdeaab431cbc3ca3caeaa"},
{file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8cf829bd2e2956066dd4de43fd8ec881d87842a06708c035b37ef632930505a2"},
{file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:603b9091bd70fae7be28bdb8aa5c9990f4241aa33abb673390a7f7329296695f"},
{file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25af28b560e0c76fa41f550eacb389905633e7ac02d6eb3c09017fa1c8cdfde1"},
{file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94c7a8a9fc9383b52c410a2ec952521906d355d18fccc927fca52ab575ee8b93"},
{file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:65bc6e2fece04e2145ab6e3c47428d1bbc05aede61ae365b2c1bddd94906e478"},
{file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:3f7c935c7b58b0d78c0beea0c7358e165f95f1fd8a7e98baa40d22a05b4a8141"},
{file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd89acd1b8bb4f31b47072615d72e7f53a948d302b7c1d1455e42622de180eae"},
{file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:6983a31698490825171be44ffbafeaa930ddf590d3f051e397143a5045513b01"},
{file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:adac9700675cf99e3615eb6a0eb5e9f5a4143c7d42c05cea2e7f71c27a3d0846"},
{file = "frozenlist-1.3.0-cp37-cp37m-win32.whl", hash = "sha256:0c36e78b9509e97042ef869c0e1e6ef6429e55817c12d78245eb915e1cca7468"},
{file = "frozenlist-1.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:57f4d3f03a18facacb2a6bcd21bccd011e3b75d463dc49f838fd699d074fabd1"},
{file = "frozenlist-1.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8c905a5186d77111f02144fab5b849ab524f1e876a1e75205cd1386a9be4b00a"},
{file = "frozenlist-1.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b5009062d78a8c6890d50b4e53b0ddda31841b3935c1937e2ed8c1bda1c7fb9d"},
{file = "frozenlist-1.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2fdc3cd845e5a1f71a0c3518528bfdbfe2efaf9886d6f49eacc5ee4fd9a10953"},
{file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92e650bd09b5dda929523b9f8e7f99b24deac61240ecc1a32aeba487afcd970f"},
{file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:40dff8962b8eba91fd3848d857203f0bd704b5f1fa2b3fc9af64901a190bba08"},
{file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:768efd082074bb203c934e83a61654ed4931ef02412c2fbdecea0cff7ecd0274"},
{file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:006d3595e7d4108a12025ddf415ae0f6c9e736e726a5db0183326fd191b14c5e"},
{file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:871d42623ae15eb0b0e9df65baeee6976b2e161d0ba93155411d58ff27483ad8"},
{file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aff388be97ef2677ae185e72dc500d19ecaf31b698986800d3fc4f399a5e30a5"},
{file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9f892d6a94ec5c7b785e548e42722e6f3a52f5f32a8461e82ac3e67a3bd073f1"},
{file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:e982878792c971cbd60ee510c4ee5bf089a8246226dea1f2138aa0bb67aff148"},
{file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:c6c321dd013e8fc20735b92cb4892c115f5cdb82c817b1e5b07f6b95d952b2f0"},
{file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:30530930410855c451bea83f7b272fb1c495ed9d5cc72895ac29e91279401db3"},
{file = "frozenlist-1.3.0-cp38-cp38-win32.whl", hash = "sha256:40ec383bc194accba825fbb7d0ef3dda5736ceab2375462f1d8672d9f6b68d07"},
{file = "frozenlist-1.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:f20baa05eaa2bcd5404c445ec51aed1c268d62600362dc6cfe04fae34a424bd9"},
{file = "frozenlist-1.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0437fe763fb5d4adad1756050cbf855bbb2bf0d9385c7bb13d7a10b0dd550486"},
{file = "frozenlist-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b684c68077b84522b5c7eafc1dc735bfa5b341fb011d5552ebe0968e22ed641c"},
{file = "frozenlist-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:93641a51f89473837333b2f8100f3f89795295b858cd4c7d4a1f18e299dc0a4f"},
{file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6d32ff213aef0fd0bcf803bffe15cfa2d4fde237d1d4838e62aec242a8362fa"},
{file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31977f84828b5bb856ca1eb07bf7e3a34f33a5cddce981d880240ba06639b94d"},
{file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3c62964192a1c0c30b49f403495911298810bada64e4f03249ca35a33ca0417a"},
{file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4eda49bea3602812518765810af732229b4291d2695ed24a0a20e098c45a707b"},
{file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acb267b09a509c1df5a4ca04140da96016f40d2ed183cdc356d237286c971b51"},
{file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e1e26ac0a253a2907d654a37e390904426d5ae5483150ce3adedb35c8c06614a"},
{file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f96293d6f982c58ebebb428c50163d010c2f05de0cde99fd681bfdc18d4b2dc2"},
{file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e84cb61b0ac40a0c3e0e8b79c575161c5300d1d89e13c0e02f76193982f066ed"},
{file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:ff9310f05b9d9c5c4dd472983dc956901ee6cb2c3ec1ab116ecdde25f3ce4951"},
{file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d26b650b71fdc88065b7a21f8ace70175bcf3b5bdba5ea22df4bfd893e795a3b"},
{file = "frozenlist-1.3.0-cp39-cp39-win32.whl", hash = "sha256:01a73627448b1f2145bddb6e6c2259988bb8aee0fb361776ff8604b99616cd08"},
{file = "frozenlist-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:772965f773757a6026dea111a15e6e2678fbd6216180f82a48a40b27de1ee2ab"},
{file = "frozenlist-1.3.0.tar.gz", hash = "sha256:ce6f2ba0edb7b0c1d8976565298ad2deba6f8064d2bebb6ffce2ca896eb35b0b"},
]
hiredis = [
{file = "hiredis-2.0.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b4c8b0bc5841e578d5fb32a16e0c305359b987b850a06964bd5a62739d688048"},
{file = "hiredis-2.0.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0adea425b764a08270820531ec2218d0508f8ae15a448568109ffcae050fee26"},
{file = "hiredis-2.0.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:3d55e36715ff06cdc0ab62f9591607c4324297b6b6ce5b58cb9928b3defe30ea"},
{file = "hiredis-2.0.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:5d2a48c80cf5a338d58aae3c16872f4d452345e18350143b3bf7216d33ba7b99"},
{file = "hiredis-2.0.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:240ce6dc19835971f38caf94b5738092cb1e641f8150a9ef9251b7825506cb05"},
{file = "hiredis-2.0.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:5dc7a94bb11096bc4bffd41a3c4f2b958257085c01522aa81140c68b8bf1630a"},
{file = "hiredis-2.0.0-cp36-cp36m-win32.whl", hash = "sha256:139705ce59d94eef2ceae9fd2ad58710b02aee91e7fa0ccb485665ca0ecbec63"},
{file = "hiredis-2.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:c39c46d9e44447181cd502a35aad2bb178dbf1b1f86cf4db639d7b9614f837c6"},
{file = "hiredis-2.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:adf4dd19d8875ac147bf926c727215a0faf21490b22c053db464e0bf0deb0485"},
{file = "hiredis-2.0.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0f41827028901814c709e744060843c77e78a3aca1e0d6875d2562372fcb405a"},
{file = "hiredis-2.0.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:508999bec4422e646b05c95c598b64bdbef1edf0d2b715450a078ba21b385bcc"},
{file = "hiredis-2.0.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:0d5109337e1db373a892fdcf78eb145ffb6bbd66bb51989ec36117b9f7f9b579"},
{file = "hiredis-2.0.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:04026461eae67fdefa1949b7332e488224eac9e8f2b5c58c98b54d29af22093e"},
{file = "hiredis-2.0.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:a00514362df15af041cc06e97aebabf2895e0a7c42c83c21894be12b84402d79"},
{file = "hiredis-2.0.0-cp37-cp37m-win32.whl", hash = "sha256:09004096e953d7ebd508cded79f6b21e05dff5d7361771f59269425108e703bc"},
{file = "hiredis-2.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:f8196f739092a78e4f6b1b2172679ed3343c39c61a3e9d722ce6fcf1dac2824a"},
{file = "hiredis-2.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:294a6697dfa41a8cba4c365dd3715abc54d29a86a40ec6405d677ca853307cfb"},
{file = "hiredis-2.0.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:3dddf681284fe16d047d3ad37415b2e9ccdc6c8986c8062dbe51ab9a358b50a5"},
{file = "hiredis-2.0.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:dcef843f8de4e2ff5e35e96ec2a4abbdf403bd0f732ead127bd27e51f38ac298"},
{file = "hiredis-2.0.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:87c7c10d186f1743a8fd6a971ab6525d60abd5d5d200f31e073cd5e94d7e7a9d"},
{file = "hiredis-2.0.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:7f0055f1809b911ab347a25d786deff5e10e9cf083c3c3fd2dd04e8612e8d9db"},
{file = "hiredis-2.0.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:11d119507bb54e81f375e638225a2c057dda748f2b1deef05c2b1a5d42686048"},
{file = "hiredis-2.0.0-cp38-cp38-win32.whl", hash = "sha256:7492af15f71f75ee93d2a618ca53fea8be85e7b625e323315169977fae752426"},
{file = "hiredis-2.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:65d653df249a2f95673976e4e9dd7ce10de61cfc6e64fa7eeaa6891a9559c581"},
{file = "hiredis-2.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ae8427a5e9062ba66fc2c62fb19a72276cf12c780e8db2b0956ea909c48acff5"},
{file = "hiredis-2.0.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:3f5f7e3a4ab824e3de1e1700f05ad76ee465f5f11f5db61c4b297ec29e692b2e"},
{file = "hiredis-2.0.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:e3447d9e074abf0e3cd85aef8131e01ab93f9f0e86654db7ac8a3f73c63706ce"},
{file = "hiredis-2.0.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:8b42c0dc927b8d7c0eb59f97e6e34408e53bc489f9f90e66e568f329bff3e443"},
{file = "hiredis-2.0.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:b84f29971f0ad4adaee391c6364e6f780d5aae7e9226d41964b26b49376071d0"},
{file = "hiredis-2.0.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:0b39ec237459922c6544d071cdcf92cbb5bc6685a30e7c6d985d8a3e3a75326e"},
{file = "hiredis-2.0.0-cp39-cp39-win32.whl", hash = "sha256:a7928283143a401e72a4fad43ecc85b35c27ae699cf5d54d39e1e72d97460e1d"},
{file = "hiredis-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:a4ee8000454ad4486fb9f28b0cab7fa1cd796fc36d639882d0b34109b5b3aec9"},
{file = "hiredis-2.0.0-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1f03d4dadd595f7a69a75709bc81902673fa31964c75f93af74feac2f134cc54"},
{file = "hiredis-2.0.0-pp36-pypy36_pp73-manylinux1_x86_64.whl", hash = "sha256:04927a4c651a0e9ec11c68e4427d917e44ff101f761cd3b5bc76f86aaa431d27"},
{file = "hiredis-2.0.0-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:a39efc3ade8c1fb27c097fd112baf09d7fd70b8cb10ef1de4da6efbe066d381d"},
{file = "hiredis-2.0.0-pp36-pypy36_pp73-win32.whl", hash = "sha256:07bbf9bdcb82239f319b1f09e8ef4bdfaec50ed7d7ea51a56438f39193271163"},
{file = "hiredis-2.0.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:807b3096205c7cec861c8803a6738e33ed86c9aae76cac0e19454245a6bbbc0a"},
{file = "hiredis-2.0.0-pp37-pypy37_pp73-manylinux1_x86_64.whl", hash = "sha256:1233e303645f468e399ec906b6b48ab7cd8391aae2d08daadbb5cad6ace4bd87"},
{file = "hiredis-2.0.0-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:cb2126603091902767d96bcb74093bd8b14982f41809f85c9b96e519c7e1dc41"},
{file = "hiredis-2.0.0-pp37-pypy37_pp73-win32.whl", hash = "sha256:f52010e0a44e3d8530437e7da38d11fb822acfb0d5b12e9cd5ba655509937ca0"},
{file = "hiredis-2.0.0.tar.gz", hash = "sha256:81d6d8e39695f2c37954d1011c0480ef7cf444d4e3ae24bc5e89ee5de360139a"},
]
idna = [
{file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
{file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},
]
iso8601 = [
{file = "iso8601-0.1.16-py2.py3-none-any.whl", hash = "sha256:906714829fedbc89955d52806c903f2332e3948ed94e31e85037f9e0226b8376"},
{file = "iso8601-0.1.16.tar.gz", hash = "sha256:36532f77cc800594e8f16641edae7f1baf7932f05d8e508545b95fc53c6dc85b"},
]
mccabe = [
{file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
{file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
]
multidict = [
{file = "multidict-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b9e95a740109c6047602f4db4da9949e6c5945cefbad34a1299775ddc9a62e2"},
{file = "multidict-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac0e27844758d7177989ce406acc6a83c16ed4524ebc363c1f748cba184d89d3"},
{file = "multidict-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:041b81a5f6b38244b34dc18c7b6aba91f9cdaf854d9a39e5ff0b58e2b5773b9c"},
{file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fdda29a3c7e76a064f2477c9aab1ba96fd94e02e386f1e665bca1807fc5386f"},
{file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3368bf2398b0e0fcbf46d85795adc4c259299fec50c1416d0f77c0a843a3eed9"},
{file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4f052ee022928d34fe1f4d2bc743f32609fb79ed9c49a1710a5ad6b2198db20"},
{file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:225383a6603c086e6cef0f2f05564acb4f4d5f019a4e3e983f572b8530f70c88"},
{file = "multidict-6.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50bd442726e288e884f7be9071016c15a8742eb689a593a0cac49ea093eef0a7"},
{file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:47e6a7e923e9cada7c139531feac59448f1f47727a79076c0b1ee80274cd8eee"},
{file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0556a1d4ea2d949efe5fd76a09b4a82e3a4a30700553a6725535098d8d9fb672"},
{file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:626fe10ac87851f4cffecee161fc6f8f9853f0f6f1035b59337a51d29ff3b4f9"},
{file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:8064b7c6f0af936a741ea1efd18690bacfbae4078c0c385d7c3f611d11f0cf87"},
{file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2d36e929d7f6a16d4eb11b250719c39560dd70545356365b494249e2186bc389"},
{file = "multidict-6.0.2-cp310-cp310-win32.whl", hash = "sha256:fcb91630817aa8b9bc4a74023e4198480587269c272c58b3279875ed7235c293"},
{file = "multidict-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:8cbf0132f3de7cc6c6ce00147cc78e6439ea736cee6bca4f068bcf892b0fd658"},
{file = "multidict-6.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:05f6949d6169878a03e607a21e3b862eaf8e356590e8bdae4227eedadacf6e51"},
{file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2c2e459f7050aeb7c1b1276763364884595d47000c1cddb51764c0d8976e608"},
{file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d0509e469d48940147e1235d994cd849a8f8195e0bca65f8f5439c56e17872a3"},
{file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:514fe2b8d750d6cdb4712346a2c5084a80220821a3e91f3f71eec11cf8d28fd4"},
{file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19adcfc2a7197cdc3987044e3f415168fc5dc1f720c932eb1ef4f71a2067e08b"},
{file = "multidict-6.0.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9d153e7f1f9ba0b23ad1568b3b9e17301e23b042c23870f9ee0522dc5cc79e8"},
{file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:aef9cc3d9c7d63d924adac329c33835e0243b5052a6dfcbf7732a921c6e918ba"},
{file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4571f1beddff25f3e925eea34268422622963cd8dc395bb8778eb28418248e43"},
{file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:d48b8ee1d4068561ce8033d2c344cf5232cb29ee1a0206a7b828c79cbc5982b8"},
{file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:45183c96ddf61bf96d2684d9fbaf6f3564d86b34cb125761f9a0ef9e36c1d55b"},
{file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:75bdf08716edde767b09e76829db8c1e5ca9d8bb0a8d4bd94ae1eafe3dac5e15"},
{file = "multidict-6.0.2-cp37-cp37m-win32.whl", hash = "sha256:a45e1135cb07086833ce969555df39149680e5471c04dfd6a915abd2fc3f6dbc"},
{file = "multidict-6.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6f3cdef8a247d1eafa649085812f8a310e728bdf3900ff6c434eafb2d443b23a"},
{file = "multidict-6.0.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0327292e745a880459ef71be14e709aaea2f783f3537588fb4ed09b6c01bca60"},
{file = "multidict-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e875b6086e325bab7e680e4316d667fc0e5e174bb5611eb16b3ea121c8951b86"},
{file = "multidict-6.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:feea820722e69451743a3d56ad74948b68bf456984d63c1a92e8347b7b88452d"},
{file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc57c68cb9139c7cd6fc39f211b02198e69fb90ce4bc4a094cf5fe0d20fd8b0"},
{file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:497988d6b6ec6ed6f87030ec03280b696ca47dbf0648045e4e1d28b80346560d"},
{file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:89171b2c769e03a953d5969b2f272efa931426355b6c0cb508022976a17fd376"},
{file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:684133b1e1fe91eda8fa7447f137c9490a064c6b7f392aa857bba83a28cfb693"},
{file = "multidict-6.0.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd9fc9c4849a07f3635ccffa895d57abce554b467d611a5009ba4f39b78a8849"},
{file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e07c8e79d6e6fd37b42f3250dba122053fddb319e84b55dd3a8d6446e1a7ee49"},
{file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4070613ea2227da2bfb2c35a6041e4371b0af6b0be57f424fe2318b42a748516"},
{file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:47fbeedbf94bed6547d3aa632075d804867a352d86688c04e606971595460227"},
{file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:5774d9218d77befa7b70d836004a768fb9aa4fdb53c97498f4d8d3f67bb9cfa9"},
{file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2957489cba47c2539a8eb7ab32ff49101439ccf78eab724c828c1a54ff3ff98d"},
{file = "multidict-6.0.2-cp38-cp38-win32.whl", hash = "sha256:e5b20e9599ba74391ca0cfbd7b328fcc20976823ba19bc573983a25b32e92b57"},
{file = "multidict-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:8004dca28e15b86d1b1372515f32eb6f814bdf6f00952699bdeb541691091f96"},
{file = "multidict-6.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2e4a0785b84fb59e43c18a015ffc575ba93f7d1dbd272b4cdad9f5134b8a006c"},
{file = "multidict-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6701bf8a5d03a43375909ac91b6980aea74b0f5402fbe9428fc3f6edf5d9677e"},
{file = "multidict-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a007b1638e148c3cfb6bf0bdc4f82776cef0ac487191d093cdc316905e504071"},
{file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07a017cfa00c9890011628eab2503bee5872f27144936a52eaab449be5eaf032"},
{file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c207fff63adcdf5a485969131dc70e4b194327666b7e8a87a97fbc4fd80a53b2"},
{file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:373ba9d1d061c76462d74e7de1c0c8e267e9791ee8cfefcf6b0b2495762c370c"},
{file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfba7c6d5d7c9099ba21f84662b037a0ffd4a5e6b26ac07d19e423e6fdf965a9"},
{file = "multidict-6.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19d9bad105dfb34eb539c97b132057a4e709919ec4dd883ece5838bcbf262b80"},
{file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:de989b195c3d636ba000ee4281cd03bb1234635b124bf4cd89eeee9ca8fcb09d"},
{file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7c40b7bbece294ae3a87c1bc2abff0ff9beef41d14188cda94ada7bcea99b0fb"},
{file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:d16cce709ebfadc91278a1c005e3c17dd5f71f5098bfae1035149785ea6e9c68"},
{file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:a2c34a93e1d2aa35fbf1485e5010337c72c6791407d03aa5f4eed920343dd360"},
{file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:feba80698173761cddd814fa22e88b0661e98cb810f9f986c54aa34d281e4937"},
{file = "multidict-6.0.2-cp39-cp39-win32.whl", hash = "sha256:23b616fdc3c74c9fe01d76ce0d1ce872d2d396d8fa8e4899398ad64fb5aa214a"},
{file = "multidict-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:4bae31803d708f6f15fd98be6a6ac0b6958fcf68fda3c77a048a4f9073704aae"},
{file = "multidict-6.0.2.tar.gz", hash = "sha256:5ff3bd75f38e4c43f1f470f2df7a4d430b821c4ce22be384e1459cb57d6bb013"},
]
pycodestyle = [
{file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"},
{file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"},
]
pycrypto = [
{file = "pycrypto-2.6.1.tar.gz", hash = "sha256:f2ce1e989b272cfcb677616763e0a2e7ec659effa67a88aa92b3a65528f60a3c"},
]
pydantic = [
{file = "pydantic-1.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8098a724c2784bf03e8070993f6d46aa2eeca031f8d8a048dff277703e6e193"},
{file = "pydantic-1.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c320c64dd876e45254bdd350f0179da737463eea41c43bacbee9d8c9d1021f11"},
{file = "pydantic-1.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18f3e912f9ad1bdec27fb06b8198a2ccc32f201e24174cec1b3424dda605a310"},
{file = "pydantic-1.9.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c11951b404e08b01b151222a1cb1a9f0a860a8153ce8334149ab9199cd198131"},
{file = "pydantic-1.9.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8bc541a405423ce0e51c19f637050acdbdf8feca34150e0d17f675e72d119580"},
{file = "pydantic-1.9.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e565a785233c2d03724c4dc55464559639b1ba9ecf091288dd47ad9c629433bd"},
{file = "pydantic-1.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:a4a88dcd6ff8fd47c18b3a3709a89adb39a6373f4482e04c1b765045c7e282fd"},
{file = "pydantic-1.9.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:447d5521575f18e18240906beadc58551e97ec98142266e521c34968c76c8761"},
{file = "pydantic-1.9.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:985ceb5d0a86fcaa61e45781e567a59baa0da292d5ed2e490d612d0de5796918"},
{file = "pydantic-1.9.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059b6c1795170809103a1538255883e1983e5b831faea6558ef873d4955b4a74"},
{file = "pydantic-1.9.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:d12f96b5b64bec3f43c8e82b4aab7599d0157f11c798c9f9c528a72b9e0b339a"},
{file = "pydantic-1.9.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:ae72f8098acb368d877b210ebe02ba12585e77bd0db78ac04a1ee9b9f5dd2166"},
{file = "pydantic-1.9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:79b485767c13788ee314669008d01f9ef3bc05db9ea3298f6a50d3ef596a154b"},
{file = "pydantic-1.9.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:494f7c8537f0c02b740c229af4cb47c0d39840b829ecdcfc93d91dcbb0779892"},
{file = "pydantic-1.9.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0f047e11febe5c3198ed346b507e1d010330d56ad615a7e0a89fae604065a0e"},
{file = "pydantic-1.9.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:969dd06110cb780da01336b281f53e2e7eb3a482831df441fb65dd30403f4608"},
{file = "pydantic-1.9.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:177071dfc0df6248fd22b43036f936cfe2508077a72af0933d0c1fa269b18537"},
{file = "pydantic-1.9.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:9bcf8b6e011be08fb729d110f3e22e654a50f8a826b0575c7196616780683380"},
{file = "pydantic-1.9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a955260d47f03df08acf45689bd163ed9df82c0e0124beb4251b1290fa7ae728"},
{file = "pydantic-1.9.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9ce157d979f742a915b75f792dbd6aa63b8eccaf46a1005ba03aa8a986bde34a"},
{file = "pydantic-1.9.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0bf07cab5b279859c253d26a9194a8906e6f4a210063b84b433cf90a569de0c1"},
{file = "pydantic-1.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d93d4e95eacd313d2c765ebe40d49ca9dd2ed90e5b37d0d421c597af830c195"},
{file = "pydantic-1.9.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1542636a39c4892c4f4fa6270696902acb186a9aaeac6f6cf92ce6ae2e88564b"},
{file = "pydantic-1.9.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a9af62e9b5b9bc67b2a195ebc2c2662fdf498a822d62f902bf27cccb52dbbf49"},
{file = "pydantic-1.9.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fe4670cb32ea98ffbf5a1262f14c3e102cccd92b1869df3bb09538158ba90fe6"},
{file = "pydantic-1.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:9f659a5ee95c8baa2436d392267988fd0f43eb774e5eb8739252e5a7e9cf07e0"},
{file = "pydantic-1.9.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b83ba3825bc91dfa989d4eed76865e71aea3a6ca1388b59fc801ee04c4d8d0d6"},
{file = "pydantic-1.9.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1dd8fecbad028cd89d04a46688d2fcc14423e8a196d5b0a5c65105664901f810"},
{file = "pydantic-1.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02eefd7087268b711a3ff4db528e9916ac9aa18616da7bca69c1871d0b7a091f"},
{file = "pydantic-1.9.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7eb57ba90929bac0b6cc2af2373893d80ac559adda6933e562dcfb375029acee"},
{file = "pydantic-1.9.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:4ce9ae9e91f46c344bec3b03d6ee9612802682c1551aaf627ad24045ce090761"},
{file = "pydantic-1.9.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:72ccb318bf0c9ab97fc04c10c37683d9eea952ed526707fabf9ac5ae59b701fd"},
{file = "pydantic-1.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:61b6760b08b7c395975d893e0b814a11cf011ebb24f7d869e7118f5a339a82e1"},
{file = "pydantic-1.9.1-py3-none-any.whl", hash = "sha256:4988c0f13c42bfa9ddd2fe2f569c9d54646ce84adc5de84228cfe83396f3bd58"},
{file = "pydantic-1.9.1.tar.gz", hash = "sha256:1ed987c3ff29fff7fd8c3ea3a3ea877ad310aae2ef9889a119e22d3f2db0691a"},
]
pyflakes = [
{file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"},
{file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"},
]
pyparsing = [
{file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
{file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
]
pypika-tortoise = [
{file = "pypika-tortoise-0.1.5.tar.gz", hash = "sha256:2e2f747bfc645a25e097485651278cd7d920b709686ce54e43387ba1c9294048"},
{file = "pypika_tortoise-0.1.5-py3-none-any.whl", hash = "sha256:23d993558e3005ac7f7f2865d9add3d8168097f45246f8844fa46d6682a99d90"},
]
python-dotenv = [
{file = "python-dotenv-0.19.2.tar.gz", hash = "sha256:a5de49a31e953b45ff2d2fd434bbc2670e8db5273606c1e737cc6b93eff3655f"},
{file = "python_dotenv-0.19.2-py2.py3-none-any.whl", hash = "sha256:32b2bdc1873fd3a3c346da1c6db83d0053c3c62f28f1f38516070c4c8971b1d3"},
]
python-gettext = [
{file = "python-gettext-4.0.tar.gz", hash = "sha256:626b501a51ac892fc3460cf550e60dca121f544eaa46eb69c90ce4682fc7ec02"},
]
pytz = [
{file = "pytz-2022.1-py2.py3-none-any.whl", hash = "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c"},
{file = "pytz-2022.1.tar.gz", hash = "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7"},
]
tortoise-orm = [
{file = "tortoise-orm-0.18.1.tar.gz", hash = "sha256:537361ce2d0829741afd43afd9bc9413a314a176cb58747d88047c20ccc01db1"},
{file = "tortoise_orm-0.18.1-py3-none-any.whl", hash = "sha256:edc9f3b49635b1dd74f73de38f54e031377e4f02b3698322502047f2e031af8b"},
]
typing-extensions = [
{file = "typing_extensions-4.2.0-py3-none-any.whl", hash = "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708"},
{file = "typing_extensions-4.2.0.tar.gz", hash = "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376"},
]
yarl = [
{file = "yarl-1.7.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2a8508f7350512434e41065684076f640ecce176d262a7d54f0da41d99c5a95"},
{file = "yarl-1.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da6df107b9ccfe52d3a48165e48d72db0eca3e3029b5b8cb4fe6ee3cb870ba8b"},
{file = "yarl-1.7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a1d0894f238763717bdcfea74558c94e3bc34aeacd3351d769460c1a586a8b05"},
{file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfe4b95b7e00c6635a72e2d00b478e8a28bfb122dc76349a06e20792eb53a523"},
{file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c145ab54702334c42237a6c6c4cc08703b6aa9b94e2f227ceb3d477d20c36c63"},
{file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ca56f002eaf7998b5fcf73b2421790da9d2586331805f38acd9997743114e98"},
{file = "yarl-1.7.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1d3d5ad8ea96bd6d643d80c7b8d5977b4e2fb1bab6c9da7322616fd26203d125"},
{file = "yarl-1.7.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:167ab7f64e409e9bdd99333fe8c67b5574a1f0495dcfd905bc7454e766729b9e"},
{file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:95a1873b6c0dd1c437fb3bb4a4aaa699a48c218ac7ca1e74b0bee0ab16c7d60d"},
{file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6152224d0a1eb254f97df3997d79dadd8bb2c1a02ef283dbb34b97d4f8492d23"},
{file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:5bb7d54b8f61ba6eee541fba4b83d22b8a046b4ef4d8eb7f15a7e35db2e1e245"},
{file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:9c1f083e7e71b2dd01f7cd7434a5f88c15213194df38bc29b388ccdf1492b739"},
{file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f44477ae29025d8ea87ec308539f95963ffdc31a82f42ca9deecf2d505242e72"},
{file = "yarl-1.7.2-cp310-cp310-win32.whl", hash = "sha256:cff3ba513db55cc6a35076f32c4cdc27032bd075c9faef31fec749e64b45d26c"},
{file = "yarl-1.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:c9c6d927e098c2d360695f2e9d38870b2e92e0919be07dbe339aefa32a090265"},
{file = "yarl-1.7.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9b4c77d92d56a4c5027572752aa35082e40c561eec776048330d2907aead891d"},
{file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c01a89a44bb672c38f42b49cdb0ad667b116d731b3f4c896f72302ff77d71656"},
{file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c19324a1c5399b602f3b6e7db9478e5b1adf5cf58901996fc973fe4fccd73eed"},
{file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3abddf0b8e41445426d29f955b24aeecc83fa1072be1be4e0d194134a7d9baee"},
{file = "yarl-1.7.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6a1a9fe17621af43e9b9fcea8bd088ba682c8192d744b386ee3c47b56eaabb2c"},
{file = "yarl-1.7.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8b0915ee85150963a9504c10de4e4729ae700af11df0dc5550e6587ed7891e92"},
{file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:29e0656d5497733dcddc21797da5a2ab990c0cb9719f1f969e58a4abac66234d"},
{file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:bf19725fec28452474d9887a128e98dd67eee7b7d52e932e6949c532d820dc3b"},
{file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:d6f3d62e16c10e88d2168ba2d065aa374e3c538998ed04996cd373ff2036d64c"},
{file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:ac10bbac36cd89eac19f4e51c032ba6b412b3892b685076f4acd2de18ca990aa"},
{file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:aa32aaa97d8b2ed4e54dc65d241a0da1c627454950f7d7b1f95b13985afd6c5d"},
{file = "yarl-1.7.2-cp36-cp36m-win32.whl", hash = "sha256:87f6e082bce21464857ba58b569370e7b547d239ca22248be68ea5d6b51464a1"},
{file = "yarl-1.7.2-cp36-cp36m-win_amd64.whl", hash = "sha256:ac35ccde589ab6a1870a484ed136d49a26bcd06b6a1c6397b1967ca13ceb3913"},
{file = "yarl-1.7.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a467a431a0817a292121c13cbe637348b546e6ef47ca14a790aa2fa8cc93df63"},
{file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ab0c3274d0a846840bf6c27d2c60ba771a12e4d7586bf550eefc2df0b56b3b4"},
{file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d260d4dc495c05d6600264a197d9d6f7fc9347f21d2594926202fd08cf89a8ba"},
{file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fc4dd8b01a8112809e6b636b00f487846956402834a7fd59d46d4f4267181c41"},
{file = "yarl-1.7.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c1164a2eac148d85bbdd23e07dfcc930f2e633220f3eb3c3e2a25f6148c2819e"},
{file = "yarl-1.7.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:67e94028817defe5e705079b10a8438b8cb56e7115fa01640e9c0bb3edf67332"},
{file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:89ccbf58e6a0ab89d487c92a490cb5660d06c3a47ca08872859672f9c511fc52"},
{file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:8cce6f9fa3df25f55521fbb5c7e4a736683148bcc0c75b21863789e5185f9185"},
{file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:211fcd65c58bf250fb994b53bc45a442ddc9f441f6fec53e65de8cba48ded986"},
{file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c10ea1e80a697cf7d80d1ed414b5cb8f1eec07d618f54637067ae3c0334133c4"},
{file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:52690eb521d690ab041c3919666bea13ab9fbff80d615ec16fa81a297131276b"},
{file = "yarl-1.7.2-cp37-cp37m-win32.whl", hash = "sha256:695ba021a9e04418507fa930d5f0704edbce47076bdcfeeaba1c83683e5649d1"},
{file = "yarl-1.7.2-cp37-cp37m-win_amd64.whl", hash = "sha256:c17965ff3706beedafd458c452bf15bac693ecd146a60a06a214614dc097a271"},
{file = "yarl-1.7.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fce78593346c014d0d986b7ebc80d782b7f5e19843ca798ed62f8e3ba8728576"},
{file = "yarl-1.7.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c2a1ac41a6aa980db03d098a5531f13985edcb451bcd9d00670b03129922cd0d"},
{file = "yarl-1.7.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:39d5493c5ecd75c8093fa7700a2fb5c94fe28c839c8e40144b7ab7ccba6938c8"},
{file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1eb6480ef366d75b54c68164094a6a560c247370a68c02dddb11f20c4c6d3c9d"},
{file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ba63585a89c9885f18331a55d25fe81dc2d82b71311ff8bd378fc8004202ff6"},
{file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e39378894ee6ae9f555ae2de332d513a5763276a9265f8e7cbaeb1b1ee74623a"},
{file = "yarl-1.7.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c0910c6b6c31359d2f6184828888c983d54d09d581a4a23547a35f1d0b9484b1"},
{file = "yarl-1.7.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6feca8b6bfb9eef6ee057628e71e1734caf520a907b6ec0d62839e8293e945c0"},
{file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8300401dc88cad23f5b4e4c1226f44a5aa696436a4026e456fe0e5d2f7f486e6"},
{file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:788713c2896f426a4e166b11f4ec538b5736294ebf7d5f654ae445fd44270832"},
{file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:fd547ec596d90c8676e369dd8a581a21227fe9b4ad37d0dc7feb4ccf544c2d59"},
{file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:737e401cd0c493f7e3dd4db72aca11cfe069531c9761b8ea474926936b3c57c8"},
{file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:baf81561f2972fb895e7844882898bda1eef4b07b5b385bcd308d2098f1a767b"},
{file = "yarl-1.7.2-cp38-cp38-win32.whl", hash = "sha256:ede3b46cdb719c794427dcce9d8beb4abe8b9aa1e97526cc20de9bd6583ad1ef"},
{file = "yarl-1.7.2-cp38-cp38-win_amd64.whl", hash = "sha256:cc8b7a7254c0fc3187d43d6cb54b5032d2365efd1df0cd1749c0c4df5f0ad45f"},
{file = "yarl-1.7.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:580c1f15500e137a8c37053e4cbf6058944d4c114701fa59944607505c2fe3a0"},
{file = "yarl-1.7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3ec1d9a0d7780416e657f1e405ba35ec1ba453a4f1511eb8b9fbab81cb8b3ce1"},
{file = "yarl-1.7.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3bf8cfe8856708ede6a73907bf0501f2dc4e104085e070a41f5d88e7faf237f3"},
{file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1be4bbb3d27a4e9aa5f3df2ab61e3701ce8fcbd3e9846dbce7c033a7e8136746"},
{file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:534b047277a9a19d858cde163aba93f3e1677d5acd92f7d10ace419d478540de"},
{file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6ddcd80d79c96eb19c354d9dca95291589c5954099836b7c8d29278a7ec0bda"},
{file = "yarl-1.7.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9bfcd43c65fbb339dc7086b5315750efa42a34eefad0256ba114cd8ad3896f4b"},
{file = "yarl-1.7.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f64394bd7ceef1237cc604b5a89bf748c95982a84bcd3c4bbeb40f685c810794"},
{file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044daf3012e43d4b3538562da94a88fb12a6490652dbc29fb19adfa02cf72eac"},
{file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:368bcf400247318382cc150aaa632582d0780b28ee6053cd80268c7e72796dec"},
{file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:bab827163113177aee910adb1f48ff7af31ee0289f434f7e22d10baf624a6dfe"},
{file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0cba38120db72123db7c58322fa69e3c0efa933040ffb586c3a87c063ec7cae8"},
{file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:59218fef177296451b23214c91ea3aba7858b4ae3306dde120224cfe0f7a6ee8"},
{file = "yarl-1.7.2-cp39-cp39-win32.whl", hash = "sha256:1edc172dcca3f11b38a9d5c7505c83c1913c0addc99cd28e993efeaafdfaa18d"},
{file = "yarl-1.7.2-cp39-cp39-win_amd64.whl", hash = "sha256:797c2c412b04403d2da075fb93c123df35239cd7b4cc4e0cd9e5839b73f52c58"},
{file = "yarl-1.7.2.tar.gz", hash = "sha256:45399b46d60c253327a460e99856752009fcee5f5d3c80b2f7c0cae1c38d56dd"},
]

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.10/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

@ -7,20 +7,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
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 +38,207 @@ 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")
bot = db_bot_instance.get()
def _thread_uniqie_id(bot_id: int, chat_id: int) -> str:
return f"thread_{bot_id}_{chat_id}"
if message.text and message.text.startswith("/start"):
# На команду start нужно ответить, не пересылая сообщение никуда
return SendMessage(chat_id=message.chat.id,
text=bot.start_text + ServerSettings.append_text())
super_chat_id = await bot.super_chat_id()
def _last_message_uid(bot_id: int, chat_id: int) -> str:
return f"lm_{bot_id}_{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)
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) ваш идентификатор пользователя записывается в кеш на некоторое время и потом "
"удаляется из кеша. Этот идентификатор используется только для общения с оператором; боты Olgram "
"не делают массовых рассылок.\n\n")
if bot.enable_additional_info:
text += _("При отправке сообщения (кроме команд /start и /security_policy) оператор <b>видит</b> ваши имя "
"пользователя, @username и идентификатор пользователя в силу настроек, которые оператор указал при "
"создании бота.")
else:
# Это супер-чат
text += _("В зависимости от ваших настроек конфиденциальности Telegram, оператор может видеть ваш username, "
"имя пользователя и другую информацию.")
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):
"""Переслать сообщение от пользователя, добавлять к нему 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.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())
await _redis.set(_message_unique_id(bot.pk, new_message.message_id), message.chat.id,
pexpire=ServerSettings.redis_timeout_ms())
return new_message
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 is_super_group and bot.enable_threads:
thread_first_message = await _redis.get(_thread_uniqie_id(bot.pk, message.chat.id))
if thread_first_message:
# переслать в супер-чат, отвечая на предыдущее сообщение
try:
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=ServerSettings.redis_timeout_ms())
except exceptions.BadRequest:
new_message = await send_user_message(message, super_chat_id, bot)
await _redis.set(_thread_uniqie_id(bot.pk, message.chat.id), new_message.message_id,
pexpire=ServerSettings.thread_timeout_ms())
else:
# переслать супер-чат
new_message = await send_user_message(message, super_chat_id, bot)
await _redis.set(_thread_uniqie_id(bot.pk, message.chat.id), new_message.message_id,
pexpire=ServerSettings.thread_timeout_ms())
else: # личные сообщения не поддерживают потоки сообщений: просто отправляем сообщение
await send_user_message(message, super_chat_id, bot)
async def handle_user_message(message: types.Message, super_chat_id: int, bot):
"""Обычный пользователь прислал сообщение в бот, нужно переслать его операторам"""
_ = _get_translator(message)
is_super_group = super_chat_id < 0
# Проверить, не забанен ли пользователь
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.TelegramAPIError as err:
_logger.error(f"(exception on forwarding) {err}")
return
bot.incoming_messages_count = F("incoming_messages_count") + 1
await bot.save(update_fields=["incoming_messages_count"])
# И отправить пользователю специальный текст, если он указан и если давно не отправляли
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:
return SendMessage(chat_id=message.chat.id, text=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>",
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=_("Пользователь разбанен"))
try:
await message.copy_to(chat_id)
except (exceptions.MessageError, exceptions.BotBlocked):
await message.reply("<i>Невозможно переслать сообщение (автор заблокировал бота?)</i>",
except (exceptions.MessageError, exceptions.Unauthorized):
await message.reply(_("<i>Невозможно переслать сообщение (автор заблокировал бота?)</i>"),
parse_mode="HTML")
return
else:
bot.outgoing_messages_count = F("outgoing_messages_count") + 1
await bot.save(update_fields=["outgoing_messages_count"])
elif super_chat_id > 0:
# в супер-чате кто-то пишет сообщение сам себе, только для личных сообщений
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 == "/start":
# На команду start нужно ответить, не пересылая сообщение никуда
text = 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:
# Это обычный чат
return await handle_user_message(message, super_chat_id, bot)
else:
# Это супер-чат
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 +255,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 +279,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,9 +309,9 @@ class CustomRequestHandler(WebhookRequestHandler):
if not bot:
return None
db_bot_instance.set(bot)
dp = Dispatcher(AioBot(bot.token))
dp = Dispatcher(AioBot(bot.decrypted_token()))
dp.register_message_handler(message_handler, content_types=[types.ContentType.TEXT,
supported_messages = [types.ContentType.TEXT,
types.ContentType.CONTACT,
types.ContentType.ANIMATION,
types.ContentType.AUDIO,
@ -116,9 +319,16 @@ class CustomRequestHandler(WebhookRequestHandler):
types.ContentType.PHOTO,
types.ContentType.STICKER,
types.ContentType.VIDEO,
types.ContentType.VOICE])
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(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