402 Commits

Author SHA1 Message Date
13566b2674 Improved README and fixed links 2023-05-02 14:36:33 +02:00
f9e99fa9a0 Merge pull request 'dev' (#43) from dev into master
Reviewed-on: #43
2023-05-02 14:12:57 +03:00
b6fe40a05b Fixed messages 2023-05-02 11:56:59 +02:00
fc39383cc2 Changed service names 2023-05-02 11:21:21 +02:00
e5761ae1d0 Fixed timings and added system stop 2023-05-02 11:09:36 +02:00
c1261a1b0f Hardcoded /bye command integrated 2023-05-02 11:04:36 +02:00
3d7ab0654a Changed repo URL 2023-05-02 10:58:41 +02:00
e9da13e70f Merge pull request 'Update dependency requests to v2.29.0' (#42) from renovate/requests-2.x into dev
Reviewed-on: profitroll/HoloCheckerBot#42
2023-04-26 23:49:59 +03:00
e90f5c2f90 Update dependency requests to v2.29.0 2023-04-26 18:47:54 +03:00
844de7ef12 Changed behavior on LabelSettingError 2023-04-24 15:38:39 +02:00
ee440eabba Merge pull request 'Update dependency pyrogram to v2.0.104' (#40) from renovate/pyrogram-2.x into dev
Reviewed-on: profitroll/HoloCheckerBot#40
2023-04-22 23:54:49 +03:00
04f1590fb5 Update dependency pyrogram to v2.0.104 2023-04-22 23:53:26 +03:00
ad8ce9034d Made a few dependencies strict 2023-04-22 22:53:02 +02:00
ad7d14f091 Fixed branch name in Renovate config 2023-04-20 13:36:42 +02:00
9940a28bd5 Fixed branch name in Renovate config 2023-04-20 13:36:24 +02:00
5571151d37 Renamed Renovate config 2023-04-20 13:21:43 +02:00
0a8a215f3e Updated Renovate config 2023-04-20 13:20:09 +02:00
05d3a09421 Merge pull request 'Configure Renovate' (#38) from renovate/configure into master
Reviewed-on: profitroll/HoloCheckerBot#38
2023-04-20 14:15:17 +03:00
20a1af3738 Add renovate.json 2023-04-20 14:13:19 +03:00
a0164e13c8 Added config for renovate 2023-04-20 11:43:06 +02:00
09e3c23c4f Commented import of the event 2023-04-19 14:49:10 +02:00
c7037ae246 Bump psutil to ~5.9.5 and pyicu to ~2.11 2023-04-19 10:53:26 +02:00
0f88cb6059 Fixed typo in videonote (analytics) 2023-04-19 10:38:45 +02:00
453293e38a Sponsorship reapply suggested within 90 days 2023-04-19 10:32:57 +02:00
ea753beda1 This commit closes #37 2023-04-19 10:31:25 +02:00
7fd3cc061e Manually disabled event import 2023-04-19 10:28:57 +02:00
49944b90c9 Renamed some event's assets 2023-04-19 10:28:39 +02:00
948837e45f Added notice about HoloCheckerAPI 2023-04-18 16:07:51 +02:00
80e62c2585 Converted names to lowercase 2023-04-18 16:06:03 +02:00
9823cccd45 https://git.end-play.xyz/profitroll/HoloCheckerAPI 2023-04-18 16:03:59 +02:00
3e22ad1895 Improved sponsorship fill 2023-04-18 16:01:42 +02:00
fe1d44d44d Sorted the keys 2023-04-18 13:08:50 +02:00
0430b4d849 Added description for stage 14 2023-04-16 13:19:44 +02:00
Profitroll
362c575d31 Removed start scheduler 2023-04-15 19:38:52 +02:00
Profitroll
253f614559 Debug message on event load added 2023-04-15 19:36:25 +02:00
Profitroll
ca607bb4ca Added more detailed date 2023-04-15 19:34:40 +02:00
Profitroll
09c93489c0 Event finalized 2023-04-15 19:32:51 +02:00
929083d2e8 WIP: Event [stages 15/16] 2023-04-14 15:27:48 +02:00
4961d6ba79 Added support for new avatars API 2023-04-14 14:05:31 +02:00
1d14fd014b WIP: Event [logging] 2023-04-14 13:28:48 +02:00
0c46f98225 Fixed usability bugs 2023-04-14 13:26:11 +02:00
d93b0bc07d Added event data export 2023-04-14 13:24:20 +02:00
9a028d1f79 WIP: Event [Fixed bugs and formatting] 2023-04-14 13:22:54 +02:00
f1897a74e8 WIP: Event [Stages 13/14] 2023-04-14 12:56:40 +02:00
fdddedb139 WIP: Event system and easter event 2023-04-14 01:22:41 +02:00
3fded13f18 WIP: sponsorship renewal 2023-04-08 01:18:05 +02:00
aa8e77811d Slightly improved context 2023-04-08 01:17:49 +02:00
0bffe9cf97 Just a space removed 2023-04-08 01:17:27 +02:00
177d456e79 Improved entities handling 2023-04-07 22:01:00 +02:00
7218d580bb Bump Pyrogram to 2.0.103 2023-04-07 21:54:09 +02:00
e8541b5160 WIP: analytics 2023-04-06 16:08:33 +02:00
2e7d4aa263 WIP: chat language recognition 2023-04-05 22:31:07 +02:00
79af1bce66 This commit closes #33 2023-04-04 10:37:46 +02:00
f66f8421c3 This commit closes #36 2023-04-03 16:03:33 +02:00
bd040af0cc Sends messages on warning being revoked (#36) 2023-04-03 15:47:17 +02:00
b5c9a0783e Merge pull request 'Data export, warnings' improvements, bug fixes' (#35) from dev into master
Reviewed-on: profitroll/HoloCheckerBot#35
2023-04-02 23:27:31 +03:00
aef4dd091d Warnings can now be revoked using /warnings 2023-04-02 22:17:54 +02:00
a3f75bec7c /warnings now also considers user membership 2023-04-02 21:38:23 +02:00
1f45398de5 /warnings can now show all warnings 2023-04-02 21:31:45 +02:00
Profitroll
cb2f3358b2 Typo fixed 2023-04-02 19:54:16 +02:00
74ae30d841 Sorted imports 2023-04-02 18:48:35 +02:00
e235fe0ed2 Improved bans 2023-04-02 18:42:11 +02:00
bf8ec39584 Changed how export works (in context of #34) 2023-04-02 18:42:03 +02:00
972827d6c2 Does some tasks from #34 2023-04-02 16:44:46 +02:00
43e71c95c4 Cancel should also cancel the message listener 2023-03-26 19:50:10 +02:00
2a2ad9f96e Added permissions for users to use /message 2023-03-26 19:39:33 +02:00
ddb59fdfd5 Added additional locale string for /message 2023-03-26 19:38:59 +02:00
5c55af9e65 This commit closes #28 2023-03-26 19:32:07 +02:00
fd992e89e7 Bump fastapi to 0.95.0 and starlette to 0.26.1 2023-03-26 19:09:45 +02:00
cea5338706 Bump Pyrogram and APScheduler versions 2023-03-18 19:59:44 +01:00
a4faae5b45 Fixed messages 2023-03-18 17:13:00 +01:00
2dc6a54299 Spoiler can't be empty now 2023-03-12 20:02:49 +01:00
cb3a975303 Removed default config string and added badges 2023-03-09 16:35:51 +01:00
de984c2b78 Removed useless shit 2023-03-09 16:31:39 +01:00
38b43c07cb Merge branch 'dev' of https://git.profitroll.eu/profitroll/HoloCheckerBot into dev 2023-03-09 16:25:09 +01:00
3304339244 Changed code style to black 2023-03-09 16:25:06 +01:00
393a7584d5 uk locale update 2023-03-06 19:28:31 +01:00
e50cdd53b3 Updated dependencies 2023-03-01 19:16:41 +01:00
3012c3d5ae Fixed object type and db lookup 2023-03-01 19:16:34 +01:00
8c21ae1844 Fixed absence of /warnings in config 2023-03-01 19:16:13 +01:00
3df57f1c42 Added one more venv to ignore 2023-03-01 19:15:53 +01:00
5b8951fe07 This commit closes #30 2023-02-20 15:04:24 +01:00
64b7a6a4ba Made message about JAPANESE girls even bolder 2023-02-20 15:04:09 +01:00
8d18ed2126 This commit closes #29 2023-02-15 11:13:07 +01:00
ed2361638a This commit closes #27 2023-02-07 11:35:47 +01:00
0c78f21c96 This commit closes #23 2023-02-01 14:21:48 +01:00
3f6fb51a4f Merge pull request 'User bans, emoji and other bug fixes, age limiter, etc' (#26) from dev into master
Reviewed-on: profitroll/HoloCheckerBot#26
2023-01-31 15:26:55 +02:00
23b2925ffa This commit closes #17 2023-01-30 11:28:23 +01:00
82a878aa6d WIP: #17 2023-01-30 11:01:52 +01:00
360bdf2c72 This commit closes #24 2023-01-30 10:58:47 +01:00
f98fb0b6dc Probably fix for static emojis from #25 2023-01-30 10:54:43 +01:00
eaec6f2fe8 Fixed join messages not being sent 2023-01-30 10:39:14 +01:00
ee738d00b5 Merge branch 'dev' of https://git.profitroll.eu/profitroll/HoloCheckerBot into dev 2023-01-25 14:05:02 +01:00
fd19b4cb0b Seems like added a grayout for sponsors 2023-01-25 14:05:00 +01:00
Profitroll
c56385a181 Fixed spoiler flow 2023-01-23 18:25:03 +01:00
66a1ea17ab This commit is closing #20 2023-01-23 15:12:29 +01:00
b10a62c63e Updated requirements 2023-01-23 14:50:39 +01:00
a3c27ab4bd Updated default config 2023-01-23 14:48:29 +01:00
eeff6d40ce /issue command 2023-01-23 14:39:38 +01:00
7be7d5ac7b One hecking line added 2023-01-23 12:01:58 +01:00
1ce47b0aaf Polished solution for #18 2023-01-23 12:01:35 +01:00
f14e80856c This commit closes 2023-01-23 11:57:32 +01:00
431b2d048f This commit closes #11 2023-01-23 11:29:58 +01:00
7feaa7af56 This commit closes #14 2023-01-23 11:25:01 +01:00
834157030c This commit closes #19 2023-01-23 11:20:56 +01:00
a59a42ffd9 Is also connected #15 2023-01-23 11:17:39 +01:00
f4c1bf9587 This commit closes #15 2023-01-23 11:17:00 +01:00
adc8c83102 Closes #13 2023-01-23 11:15:26 +01:00
e3fd4b3576 This commit closes #21 2023-01-23 11:10:06 +01:00
4019f2f376 This fix resolves #10 2023-01-23 11:03:04 +01:00
51b943b576 Added format notice in 2nd question of application 2023-01-16 12:10:40 +01:00
de552db4c8 Reapply improved 2023-01-16 12:10:07 +01:00
8beb33b7c3 Now ignoring bdays of users that left 2023-01-16 12:09:53 +01:00
05e3916478 Fixed reply messages 2023-01-16 12:09:28 +01:00
414bfefb21 Fixed tmp download function 2023-01-13 14:45:23 +01:00
fd5e0b5b22 Spoilers on custom filters 2023-01-13 10:43:27 +01:00
bd925418fd YouTube new videos on channels monitoring 2023-01-12 13:57:41 +01:00
42a4a2e58e Internal and external spoilers are now separated 2023-01-12 11:04:52 +01:00
95be1e72d3 Merge pull request 'Bug fixes and improvements' (#8) from dev into master
Reviewed-on: profitroll/HoloCheckerBot#8
2023-01-11 17:22:34 +02:00
ecbf7d8b78 Updated ignore 2023-01-11 12:53:20 +01:00
92386ac8ce Fixed some logic holes 2023-01-11 12:28:58 +01:00
dabfa2ecef Added ftfy and length limit 2023-01-10 13:41:08 +01:00
00a408ac6c Added audio support for spoilers 2023-01-10 12:11:29 +01:00
b67b1daf7c Fixed logical hole in cancel command 2023-01-10 12:07:40 +01:00
f83751c07d Updated rules about spoilers 2023-01-10 12:07:27 +01:00
f5e3cd5a50 Fixed requirements 2023-01-09 14:50:33 +01:00
478fb174c5 Updated requirements 2023-01-09 14:44:21 +01:00
30dfe4ff04 Moved initial caching a bit 2023-01-09 14:43:19 +01:00
bdc775dcbd Made groups caching log only visible on debug 2023-01-09 14:34:33 +01:00
717d636497 Added one more check 2023-01-09 13:19:17 +01:00
64b0562bc4 Added "group_users_admins" command scope 2023-01-09 13:08:02 +01:00
eced1b7984 Added one more check 2023-01-09 12:54:00 +01:00
da3dd3a2fe Changed debug logging logic 2023-01-09 12:39:39 +01:00
3e166a3fca Fixed inline query caching 2023-01-09 12:21:21 +01:00
d090b18655 Added additional caching message on debug 2023-01-09 12:11:29 +01:00
e2d28f442c Changed inline caching time 2023-01-09 12:10:07 +01:00
496240c48b Fixed id attribute in sponsorships scheduler 2023-01-09 11:39:24 +01:00
Profitroll
0bf5ae70eb Improved inline query usage filters 2023-01-07 10:20:58 +01:00
Profitroll
bcaf80e2e1 Fixed reply_document() issue 2023-01-07 00:38:53 +01:00
Profitroll
21bf460b28 Fixes + improved logging 2023-01-07 00:38:35 +01:00
Profitroll
a4bbb837d7 Added delay for label setting 2023-01-06 21:27:47 +01:00
Profitroll
1a438fc32e Fixed api method 2023-01-06 21:27:32 +01:00
234b73add0 Merge pull request 'Bug fixes and small structural changes' (#7) from dev into master
Reviewed-on: profitroll/HoloCheckerBot#7
2023-01-06 17:01:20 +02:00
742b529c33 Moved templates classes into another module 2023-01-06 15:59:09 +01:00
78a37ca186 Placed exceptions to errors module 2023-01-06 15:49:51 +01:00
374effd5d7 Fixed bugs of /message and now using cached media 2023-01-06 13:21:16 +01:00
Profitroll
1195df894c Added documents support for spoilers 2023-01-05 21:15:34 +01:00
Profitroll
67f1ce535f Small fixes 2023-01-05 20:47:30 +01:00
Profitroll
b6d45545fc User search improved 2023-01-05 20:47:02 +01:00
Profitroll
59dafc004d LabelSettingError added 2023-01-05 20:46:47 +01:00
Profitroll
57518399a1 Sponsorship handler removed 2023-01-05 20:46:04 +01:00
Profitroll
ee288a1983 Photo ID used instead of files for sponsorships 2023-01-05 20:45:35 +01:00
373f2332a0 Updated requirements 2023-01-05 15:51:25 +01:00
f4fb85f7a4 Merge pull request 'Removed legacy, fixed some bugs, improved spoilers' (#6) from dev into master
Reviewed-on: profitroll/HoloCheckerBot#6
2023-01-05 16:49:35 +02:00
02a6960391 Fixed forwarding spoilers to admin group 2023-01-05 15:48:34 +01:00
7939f8b65e Minor spoiler category fixes 2023-01-05 14:52:57 +01:00
5b28f9f588 Caption and category added for spoilers 2023-01-05 14:34:02 +01:00
6e44177907 Shutdown time is now added on KeyboardInterrupt 2023-01-05 14:33:23 +01:00
4f0b9f8e3a Fixed dict key for startup message 2023-01-05 13:16:11 +01:00
78dee0591c /cancel now removes reply markup 2023-01-05 13:12:22 +01:00
b0b0f04a9b Added down time messages 2023-01-05 13:08:08 +01:00
9e009aea43 Minor naming improvements 2023-01-05 13:01:57 +01:00
42d00383bf Removed legacy 2023-01-05 13:01:18 +01:00
4fba305b05 Merge pull request 'Small fix for spoiler with an empty description' (#5) from dev into master
Reviewed-on: profitroll/HoloCheckerBot#5
2023-01-05 13:54:19 +02:00
47e62946ab Small fix for spoiler with an empty description 2023-01-05 12:53:55 +01:00
68c7cc0ada Merge pull request 'Spoilers, major command system improvements' (#4) from dev into master
Reviewed-on: profitroll/HoloCheckerBot#4
2023-01-05 13:45:14 +02:00
646db667d9 Updated config in README 2023-01-05 12:44:13 +01:00
2e8277d6d2 Added "owner" as an additional command permission 2023-01-05 12:42:15 +01:00
19fc9308e4 Added /resetcommands command 2023-01-05 12:41:55 +01:00
c90495eb1c Added spoiler's validation rule 2023-01-05 10:49:22 +01:00
Profitroll
bdb2338ab9 Still debugging 2023-01-04 22:31:58 +01:00
Profitroll
496bb7d4a6 Improved logging 2023-01-04 21:58:44 +01:00
Profitroll
b437092fe7 Trying to find commands registration issue 2023-01-04 21:58:20 +01:00
Profitroll
9431763e6b Added filling_sponsorship filter 2023-01-04 21:57:11 +01:00
Profitroll
4977b8f31a Updated to-do list 2023-01-04 21:56:54 +01:00
Profitroll
0739eeb87d Updated config 2023-01-04 21:56:37 +01:00
Profitroll
0214a29a2e Fixed handlers 2023-01-04 21:55:50 +01:00
Profitroll
6b80b7d0fa Improved handlers 2023-01-04 20:13:29 +01:00
Profitroll
3fd56e8b41 Fixed a typo in config keys 2023-01-04 19:59:09 +01:00
Profitroll
3eef04794a Integration of spoilers [WIP] 2023-01-04 19:58:54 +01:00
Profitroll
d59a1671b3 Changed config keys 2023-01-04 19:14:02 +01:00
Profitroll
ccbc135ee4 No need to save bot's ID anymore 2023-01-04 17:26:27 +01:00
083281e784 Fixed some paths 2023-01-04 13:31:42 +01:00
8cb3ef283b Now using find_user() 2023-01-04 13:15:08 +01:00
Profitroll
1268c33830 Small fix 2023-01-03 21:54:01 +01:00
Profitroll
aad5c4f9f2 Added support for "general" module 2023-01-03 21:46:16 +01:00
Profitroll
3d4dd29205 Changed the way how commands work 2023-01-03 21:36:26 +01:00
Profitroll
ff95d9556d Fixed some minor issues 2023-01-03 20:48:35 +01:00
Profitroll
b3c5f060a1 Now features can be turned off 2023-01-03 20:34:44 +01:00
Profitroll
096a0498f8 Now using custom filters 2023-01-03 20:34:13 +01:00
Profitroll
d7936fa600 Added more custom filters 2023-01-03 20:33:53 +01:00
3442a478d4 Updated default config in README 2023-01-03 15:48:33 +01:00
ee7f9712c8 Updated to-do 2023-01-03 15:47:44 +01:00
79304816b0 Merge pull request '/cancel, /identify, sponsorships improvements and fixes' (#3) from dev into master
Reviewed-on: profitroll/HoloCheckerBot#3
2023-01-03 16:45:20 +02:00
ccebccf086 Updated validation rules 2023-01-03 15:43:48 +01:00
b383ab6001 Optimized imports 2023-01-03 15:15:15 +01:00
db60a538b2 Improved docstrings 2023-01-03 15:12:46 +01:00
e79edf1dff Added some more exceptions to handle 2023-01-03 14:30:29 +01:00
7edffd0b40 /identify command added 2023-01-03 14:30:16 +01:00
c8f89a7447 Fixed attribute error 2023-01-03 13:17:59 +01:00
ea1dc542a3 Improved docstrings 2023-01-03 13:16:57 +01:00
6aa8128fc6 Using application_restart() now 2023-01-03 13:04:38 +01:00
7854f88217 Cancel command implemented 2023-01-03 13:04:10 +01:00
b401028dd1 application_state()[0] "none" is also handled now 2023-01-03 13:03:48 +01:00
642c23dd61 Added docstring 2023-01-03 13:02:39 +01:00
b2613c25a4 /label can now be used in admin group 2023-01-03 13:02:27 +01:00
a7038e9d8f Improved tmp files system 2023-01-03 13:01:46 +01:00
Profitroll
a59a7b738c /cancel added to commands to register 2023-01-03 10:13:38 +01:00
626492fb3c Improved logging 2023-01-02 14:24:29 +01:00
1aed7bff7b Removed "have a nice day" where not needed 2023-01-02 14:20:41 +01:00
ba13f36769 Updated locale for sponsorship 2023-01-02 14:19:35 +01:00
a82adc4d1f Changed command output formatting 2023-01-02 11:18:40 +01:00
fe6d2514c7 Updated to-do 2023-01-02 11:17:17 +01:00
2cfa5a8f8d Merge pull request '/nearby, subscriptions check, geocoding' (#2) from dev into master
Reviewed-on: profitroll/HoloCheckerBot#2
2023-01-02 12:16:38 +02:00
950666e4ad Changed "loc" to "location" in application 2023-01-02 11:13:41 +01:00
ace71fd6be Members/admins caching is now configurable 2023-01-02 10:48:55 +01:00
Profitroll
25be843cd8 Improved /nearby 2022-12-30 23:29:17 +01:00
Profitroll
e6589fc3e5 Improved admin group filter 2022-12-30 20:36:06 +01:00
Profitroll
8b2abc2cfa Updated /nearby command 2022-12-29 15:04:00 +01:00
Profitroll
c763fc537b Locale usage fixes 2022-12-28 19:01:21 +01:00
Profitroll
22011829a5 Geo update WIP 2022-12-28 18:56:13 +01:00
Profitroll
e59aa98fd5 Working on /nearby 2022-12-27 18:49:02 +01:00
Profitroll
082acc85cf Added custom filters 2022-12-27 18:46:17 +01:00
Profitroll
87d9afe74a Small fix of message method 2022-12-27 13:40:58 +01:00
Profitroll
5e06859b56 Improved typing and linting 2022-12-27 13:36:54 +01:00
Profitroll
47896faf06 Application and sponsorship crosscheck 2022-12-27 13:23:24 +01:00
Profitroll
85112dc653 Fixed some bugs 2022-12-23 12:54:51 +01:00
Profitroll
af862a3454 Added messages to sponsor's locale 2022-12-23 01:49:38 +01:00
Profitroll
1eb98750a7 Changed sponsorship locale 2022-12-23 01:44:21 +01:00
Profitroll
eecb71a91e Updated label_set 2022-12-23 01:44:07 +01:00
Profitroll
426b1550f6 Initialized /cancel command 2022-12-23 01:40:23 +01:00
Profitroll
0302d8c1ae Removed unused 2022-12-23 01:40:07 +01:00
Profitroll
5a6a96d3f9 Delete sponsorships on expiry 2022-12-22 22:14:35 +01:00
Profitroll
12da1b2376 Added LabelTooLongError() and changed names 2022-12-22 22:11:36 +01:00
Profitroll
7db8c9ac5c Fixed label length on set 2022-12-22 22:11:15 +01:00
95e9fdf460 Added sponsorship support 2022-12-22 15:05:27 +01:00
ac5a0d112f Disabled EN locale 2022-12-22 15:05:07 +01:00
2db19acf6c Disabled EN locale 2022-12-22 15:02:28 +01:00
6874268154 Added one more None check 2022-12-21 15:23:36 +01:00
a906c0a1cc Trying to fix AttributeError() on init 2022-12-21 15:22:14 +01:00
bc54abcc97 Fixed imports 2022-12-21 12:51:39 +01:00
1b6f429be9 Updated to-do 2022-12-21 12:26:50 +01:00
be55f8a3b1 Added DB validation for warnings 2022-12-21 12:26:05 +01:00
d3945eea0c Started working on sponsorships 2022-12-21 12:25:47 +01:00
Profitroll
b345a5d776 Added voice message logging 2022-12-18 17:54:58 +01:00
Profitroll
f905a5c4df Bot is now aggressive to voice messages 2022-12-18 17:51:21 +01:00
Profitroll
f4a2e655a6 Setting commands is now scheduled 2022-12-17 23:36:39 +01:00
Profitroll
741a01cff6 Fixed literal 2022-12-17 23:26:07 +01:00
Profitroll
6b00f181f6 Avatars caching implemented 2022-12-17 23:25:53 +01:00
Profitroll
41eb6e46ee Added one more type check for HoloUser init 2022-12-17 23:14:12 +01:00
Profitroll
1d5ebd02fe Changed max number of results 2022-12-17 23:12:15 +01:00
Profitroll
43ce2d73b6 Fixed application removal 2022-12-17 23:10:56 +01:00
Profitroll
797d9de7e4 Also handling HEAD requests now 2022-12-17 22:31:30 +01:00
Profitroll
438f8f8c44 Added en-US locale (just for testing now) 2022-12-17 00:58:53 +01:00
Profitroll
a769ea9ef5 Added language_code context 2022-12-17 00:58:33 +01:00
Profitroll
fa08679b11 Moved commands to locales 2022-12-17 00:58:12 +01:00
Profitroll
e5514cac7c Added commands 2022-12-17 00:57:59 +01:00
Profitroll
8ccd2a858a locale() now accepts more object types 2022-12-16 23:25:21 +01:00
Profitroll
8bb7c58c2a HoloUser will now update some attributes 2022-12-16 22:58:32 +01:00
3b71cb3d85 Merge pull request 'Merge Beta with Stable releases' (#1) from dev into master
Reviewed-on: profitroll/HoloCheckerBot#1
2022-12-16 16:14:40 +02:00
09e5048d7f Merge branch 'dev' 2022-12-16 15:14:07 +01:00
de4126b1a3 Logging usage of /applications 2022-12-16 15:04:42 +01:00
11da9de887 Fixed a few unsupported types 2022-12-16 15:02:39 +01:00
a3830f672f Ignoring data 2022-12-16 13:24:24 +01:00
085a833b16 Added migration script to ignore 2022-12-16 13:22:17 +01:00
bc8cd8a8ce Inline mode optimized for DB/OOP 2022-12-16 13:19:50 +01:00
e61aa17a72 Improved logging and changed message 2022-12-16 12:04:57 +01:00
ee44987ae8 Added "joined_false_link" key 2022-12-16 11:43:53 +01:00
40483c17c1 Rules message changed 2022-12-16 11:43:43 +01:00
2d75b01bfe Removed unused imports 2022-12-16 11:29:10 +01:00
9fb095b7c6 application_approved() method added 2022-12-16 11:27:56 +01:00
b0a3830c4f Simple line reorder 2022-12-16 11:27:46 +01:00
32ec34435d Migrated to DB/OOP 2022-12-16 11:27:32 +01:00
57428e530a Optimized for DB/OOP 2022-12-16 11:15:56 +01:00
b3ec78d54e Added /nearby to admin group's commands 2022-12-15 15:14:28 +01:00
4c82cd8515 Added one more check on call 2022-12-15 15:12:52 +01:00
6c3226ccd7 Imported /nearby 2022-12-15 15:12:41 +01:00
652069ffd8 Also handle bad_request_400.PeerIdInvalid 2022-12-15 15:04:35 +01:00
3e2c301400 Also handle UserNotFoundError 2022-12-15 15:02:56 +01:00
2924c808dd Added /nearby command 2022-12-15 15:00:38 +01:00
f471deedd2 Fixed typos 2022-12-15 15:00:27 +01:00
9d0b66371a Removed unused imports 2022-12-15 14:52:33 +01:00
d7a087bcfb Temporarily disabled 2022-12-15 14:32:33 +01:00
b09f30c3fd Optimized for DB/OOP 2022-12-15 14:32:17 +01:00
c91833f81e Reply is parsed as markdown now 2022-12-15 14:31:57 +01:00
49bed307ad Improved isAnAdmin() usage 2022-12-15 14:31:42 +01:00
ee90e579b5 Added "no_user_application" and "user_invalid" 2022-12-15 14:30:40 +01:00
b0ff916e31 HoloUser does not take str anymore 2022-12-15 14:30:21 +01:00
e603dd24ab Optimized for DB/OOP 2022-12-15 13:52:08 +01:00
98ed58447b Added new reason for rejection 2022-12-15 13:51:56 +01:00
909588e940 Changed some texts 2022-12-15 13:50:11 +01:00
2ead4eaa06 Added DefaultApplicationTemp() 2022-12-14 15:07:51 +01:00
f1990345ce Improved docstrings 2022-12-14 14:57:39 +01:00
3ffa6e32f9 Added "geocoding" key 2022-12-14 14:52:12 +01:00
31376278ba Birthdays checker updated 2022-12-14 14:45:46 +01:00
3009094caa Polisher /warn and /warnings 2022-12-14 14:38:04 +01:00
db36c051d4 Comments for warns 2022-12-14 14:31:15 +01:00
132e6235a0 Warn with comment 2022-12-14 14:31:03 +01:00
e4fafd9075 Updated /warnings to DB/OOP 2022-12-14 14:30:51 +01:00
2b09aaa7b0 Removed old imports 2022-12-14 14:19:20 +01:00
6b84ef3779 Reapply migrated to DB/OOP 2022-12-14 14:16:14 +01:00
d4256f0c8c Improved docstrings and linting 2022-12-14 14:05:09 +01:00
4541c84eb9 Optimized for DB/OOP update 2022-12-14 13:58:06 +01:00
9f3f29fa27 Added tmp column 2022-12-14 13:57:29 +01:00
4c7a724c42 Updated requirements 2022-12-14 13:57:17 +01:00
c06bf90417 Location geocoding 2022-12-14 13:56:58 +01:00
ce182999a0 Improved comments 2022-12-13 14:50:21 +01:00
44ccf010a1 Improved docstrings 2022-12-13 14:45:10 +01:00
c475667c40 Docstring added 2022-12-13 14:38:38 +01:00
848caf5342 Cleanup 2022-12-13 14:33:28 +01:00
04093d0a20 Exception messages implemented 2022-12-13 14:33:22 +01:00
a54d78ccb6 Messenger is ready 2022-12-13 14:24:31 +01:00
bf7c23bb34 Updated README 2022-12-13 10:44:44 +01:00
Profitroll
8e08b8c94b Temporarily disabled any_stage() 2022-12-12 14:35:11 +01:00
Profitroll
30fc3d0089 Now using database for export 2022-12-12 10:13:58 +01:00
Profitroll
3b22cb0130 Using HoloUser 2022-12-11 23:32:35 +01:00
Profitroll
0e06c2a7a5 Now using HoloUser messaging 2022-12-11 23:32:20 +01:00
Profitroll
c5f9e96414 small notice added 2022-12-11 23:32:04 +01:00
Profitroll
a8d17237e1 now using global app 2022-12-11 23:31:36 +01:00
Profitroll
5af5bea805 nickname is now label 2022-12-11 23:31:12 +01:00
Profitroll
3aefe710e5 Slightly improved structure 2022-12-11 19:52:38 +01:00
Profitroll
b185dff664 Start is now Mongo-ready 2022-12-11 19:52:14 +01:00
Profitroll
199f470192 Reboot does not require kill anymore 2022-12-11 19:52:02 +01:00
Profitroll
ed748f70d3 Scheduler is now using asyncio 2022-12-11 19:51:46 +01:00
Profitroll
d40a17e668 Improving class 2022-12-11 19:12:41 +01:00
Profitroll
03ebeafdd4 Messages command updated 2022-12-11 18:50:50 +01:00
Profitroll
091c0abace cache folder is now generated on startup 2022-12-11 01:39:19 +01:00
Profitroll
32ebad29ca /label command improved 2022-12-11 01:31:30 +01:00
Profitroll
9c611b436c HoloUser WIP 2022-12-11 01:31:17 +01:00
Profitroll
94bb52ad62 Improved admin check 2022-12-11 01:31:06 +01:00
Profitroll
12d6273c9a DB validation WIP 2022-12-11 01:30:56 +01:00
Profitroll
c0f6bd8b11 /label command 2022-12-10 17:29:06 +01:00
Profitroll
65b7b365d2 Main file changed 2022-12-10 16:07:57 +01:00
Profitroll
93cfc75d1d Column change 2022-12-10 15:53:41 +01:00
Profitroll
efe3c8338e Scheduler changes 2022-12-10 15:53:30 +01:00
Profitroll
5b5b38a394 Fixed starlette version 2022-12-10 15:53:19 +01:00
Profitroll
09cd36d5ba Scheduler refactor 2022-12-10 15:52:34 +01:00
Profitroll
4e8fb00475 Changed used columns 2022-12-10 15:52:07 +01:00
Profitroll
d6161515c1 Improved registration logic 2022-12-10 15:51:48 +01:00
Profitroll
b5058c00e1 Optimized imports, removed scheduler 2022-12-10 15:51:37 +01:00
Profitroll
dc92a3d82a Updated requirements 2022-12-10 15:13:12 +01:00
Profitroll
bd2a3fc81c Init done 2022-12-10 12:08:30 +01:00
Profitroll
4be1379dca Removed useless item from git 2022-12-10 12:04:43 +01:00
Profitroll
938da48cb8 Small structural changes 2022-12-10 12:00:50 +01:00
Profitroll
3ec3d762fc Started a complete refactor 2022-12-10 11:37:15 +01:00
Profitroll
654daed8c2 Sponsorship concept 2022-12-10 10:42:56 +01:00
4d0cf629e2 + 2022-12-07 15:17:34 +01:00
d117146d96 Send replies to messages sent with /message 2022-12-07 14:17:27 +01:00
1f7890dc53 Updated README 2022-12-06 13:22:55 +01:00
4363ef9d78 Strict ID match replaced with Username search 2022-12-06 11:10:44 +01:00
27e63111e5 "reapply_restarted" message added 2022-12-06 10:42:45 +01:00
65f6b4e30e Removed # type: ignore and fixed missing imports 2022-12-06 10:26:22 +01:00
Profitroll
712b5d22ab Silenced some useless warnings 2022-12-05 20:52:21 +01:00
Profitroll
2185959363 Added missing imports 2022-12-05 18:53:09 +01:00
Profitroll
3abd1e8ad3 Modularity overhaul 2022-12-05 18:49:51 +01:00
22a2224af1 Application options for returning users [Done] 2022-12-05 15:25:07 +01:00
1c0c8fa8c4 Reapply after left the chat: WIP 2022-12-05 14:38:18 +01:00
7de55e950e Command /message implemented 2022-12-05 13:09:29 +01:00
Profitroll
484990564f Started preparing /message command 2022-12-04 21:10:38 +01:00
Profitroll
500aaf38f4 Updated config 2022-12-04 21:08:31 +01:00
Profitroll
c0f16c69f6 Reapply now also sends link 2022-12-04 15:05:51 +01:00
Profitroll
d0316efbdf Fixed questions source for commands 2022-12-04 11:37:45 +01:00
Profitroll
40cf3cff7c Merge branch 'master' of https://git.profitroll.eu/profitroll/HoloCheckerBot 2022-12-04 11:28:02 +01:00
Profitroll
fd858afcfd Fixed questions source for inline 2022-12-04 11:27:59 +01:00
1a2be8f48a - 2022-12-01 15:23:46 +01:00
a9b5685d7b Improved rules 2022-12-01 11:27:33 +01:00
97325b4ff5 Added auto-removal of applications in group 2022-12-01 11:06:54 +01:00
0fcd67d322 Added dev contact 2022-11-30 14:24:14 +01:00
19cc4a2f86 Improved rules 2022-11-30 13:02:07 +01:00
Profitroll
9105a0266c Fixed that weird behavior on forms setting 2022-11-29 18:11:07 +01:00
c8f1ef5741 Improved avatars preview a bit 2022-11-29 10:37:06 +01:00
efcb60ed95 Merge branch 'master' of https://git.profitroll.eu/profitroll/HoloCheckerBot 2022-11-22 15:26:37 +01:00
a54994fe88 Inline mode number of suggestions decreased 2022-11-22 15:26:34 +01:00
Profitroll
45505a82c4 Fixed long line 2022-11-21 00:29:18 +01:00
Profitroll
1e23f6eb8c Fixed useless \n in utils 2022-11-14 19:24:22 +01:00
Profitroll
8601814642 Improved behavior with large text in utils 2022-11-14 18:52:16 +01:00
Profitroll
76d7d7f4c6 /warn and /warnings commands 2022-11-13 13:40:49 +01:00
359f08c021 Created /check for UptimeRobot pinger 2022-11-04 13:05:14 +01:00
9a8d915166 Minor improvements 2022-10-28 14:49:52 +02:00
49e9927ba1 Added rules command 2022-10-27 15:13:58 +02:00
a867c4f4b7 Removed litter 2022-10-27 11:41:49 +02:00
7019a1ffde Improved avatars 2022-10-27 11:40:15 +02:00
fc37495b11 Added API address support 2022-10-26 15:23:06 +02:00
04749f5ffe Added api for avatars support 2022-10-26 15:20:23 +02:00
bfce273048 Added cache 2022-10-26 14:56:42 +02:00
cfc8167663 Inline implemented 2022-10-26 14:54:55 +02:00
2619ecd408 String about debug config added 2022-10-26 13:31:35 +02:00
c5e6db79a0 Debug mode added 2022-10-26 13:30:24 +02:00
de0df8f0f6 Changed applications.json options 2022-10-26 12:54:55 +02:00
c89072b80e Updated readme 2022-10-25 15:23:41 +02:00
77d9c3e91c Changed User ID to Username in reapply_got 2022-10-25 14:29:07 +02:00
9ea575b2cb Birthdays notification made 2022-10-25 14:18:51 +02:00
2f24739cbb Reapply is now possible 2022-10-25 13:36:16 +02:00
7abdf369fd applications key added to README 2022-10-24 16:32:09 +02:00
3018aecd28 Checkboxes fixed 2022-10-24 14:34:52 +02:00
b52c69b281 Small refactoring 2022-10-24 14:34:18 +02:00
915569ceca New messages added 2022-10-24 14:34:13 +02:00
162e2bc484 New command added 2022-10-24 14:34:05 +02:00
f69ca41781 Improved To-Do 2022-10-24 14:33:54 +02:00
b78420ae30 Updated ignore and added To-Do 2022-10-24 13:29:23 +02:00
Profitroll
e85e4d39ab Improved messages 2022-10-23 23:40:50 +02:00
Profitroll
ae13d5df96 Fixed invite link check 2022-10-23 23:40:43 +02:00
70 changed files with 8101 additions and 574 deletions

12
.gitignore vendored
View File

@@ -154,8 +154,14 @@ cython_debug/
# Custom
config.json
config_debug.json
*.session
*.session-journal
users
!users/.gitkeep
TASK.md
data
TASK.md
inline_bot.py
.vscode
migrate.py
venv_linux
validation/*
!validation/*.json

20
.renovaterc Normal file
View File

@@ -0,0 +1,20 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:base"
],
"baseBranches": [
"dev"
],
"packageRules": [
{
"matchUpdateTypes": [
"minor",
"patch",
"pin",
"digest"
],
"automerge": true
}
]
}

View File

@@ -1,6 +1,11 @@
# HoloCheckerBot
<h1 align="center">HoloCheckerBot</h1>
Small Telegram bot made on Pyrogram
<p align="center">Small Telegram bot made on Pyrogram</p>
<p align="center">
<a href="https://git.end-play.xyz/HoloUA/Telegram/src/branch/master/LICENSE"><img alt="License: GPL" src="https://img.shields.io/badge/License-GPL-blue"></a>
<a href="https://git.end-play.xyz/HoloUA/Telegram"><img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg"></a>
</p>
## What can this bot do?
@@ -9,50 +14,32 @@ Small Telegram bot made on Pyrogram
* Give one-time links to join group
* Track down users that were not allowed to join
* Show applications to other users
* Manage YouTube sponsorships [WIP]
* Send and receive messages to users using bot [WIP]
## Installation
1. `git clone https://git.end-play.xyz/profitroll/HoloCheckerBot.git`
2. `cd HoloCheckerBot`
1. `git clone https://git.end-play.xyz/HoloUA/Telegram.git`
2. `cd Telegram`
3. Install Python 3.7+ (at least 3.9 is recommended) for your OS
4. `python3 -m pip install -r requirements.txt`
5. Run it with `python3 main.py` after configuring
5. Run it with `python3 holochecker.py` after configuring
## Configuration
So bot has its "config_example.json" and it needs to be changed.
Copy this file to "config.json" and open it with any text editor.
You can see config file with all the comments below:
```json
{
"locale": "uk", # locale file. it's a json located under locale folder
"owner": 0, # telegram ID of a bot owner
"age_allowed": 0, # minimum age of user that submits application
"admin_group": 0, # telegram ID of a admin's group
"destination_group": 0, # telegram ID of a user's group
"admins": [], # list of telegram ID's of users that are admins
"bot": {
"api_id": 0, # telegram API ID
"api_hash": "", # telegram API hash
"bot_token": "" # telegram bot's token
},
"logging": {
"size": 512, # size of log file in kbytes after which it should be rotated
"location": "logs" # location of logs folder. can be relative or absolute
},
"locations": {
"data": "data", # location of data folder. can be relative or absolute
"locale": "locale" # location of locale folder. can be relative or absolute
},
"commands": { # user command and its description
"start": "Start using the bot"
},
"commands_admin": { # admin commands and their description
"reboot": "Restart the bot"
}
}
```
You should also install [HoloCheckerAPI](https://git.end-play.xyz/HoloUA/API) for inline requests to work.
After all of that you're good to go! Happy using :)
## To-Do
* [ ] Stats and infographics
* [ ] Check group members without completed application
* [x] Replicate some functions of @spoilerobot
* [x] Check sponsorship on Holo girls
* [x] /nearby command
* [x] Complete messenger between user and admins
* [x] Get application by id and user_id

42
app.py Normal file
View File

@@ -0,0 +1,42 @@
from os import path, sep
from ujson import JSONDecodeError
from modules.logging import logWrite
from modules.utils import configGet, jsonLoad
from pyrogram.client import Client
from pyrogram.errors import bad_request_400
from convopyro import Conversation
app = Client(
"holochecker",
bot_token=configGet("bot_token", "bot"),
api_id=configGet("api_id", "bot"),
api_hash=configGet("api_hash", "bot"),
)
Conversation(app)
async def isAnAdmin(admin_id):
# Check if user is mentioned in config
if (admin_id == configGet("owner")) or (admin_id in configGet("admins")):
return True
# Check if user is probably in cache
if path.exists(f"cache{sep}admins") is True:
try:
return True if admin_id in jsonLoad(f"cache{sep}admins") else False
except (FileNotFoundError, JSONDecodeError):
pass
# Check if user is in admin group
try:
async for member in app.get_chat_members(configGet("admin", "groups")):
if member.user.id == admin_id:
return True
except bad_request_400.ChannelInvalid:
logWrite(
f"Could not get users in admin group to answer isAnAdmin(). Bot is likely not in the group."
)
return False
return False

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

8
classes/errors/geo.py Normal file
View File

@@ -0,0 +1,8 @@
class PlaceNotFoundError(Exception):
"""Query provided did not lead to any city or populated area"""
def __init__(self, query):
self.query = query
super().__init__(
f"Could not find any place on geonames.org of feature classes A and P by query '{self.query}'"
)

View File

@@ -0,0 +1,38 @@
"""Exceptions that are meant to be used by HoloUser class
and other modules that handle those exceptions"""
class UserNotFoundError(Exception):
"""HoloUser could not find user with such an ID in database"""
def __init__(self, user, user_id):
self.user = user
self.user_id = user_id
super().__init__(
f"User of type {type(self.user)} with id {self.user_id} was not found"
)
class UserInvalidError(Exception):
"""Provided to HoloUser object is not supported"""
def __init__(self, user):
self.user = user
super().__init__(
f"Could not find HoloUser by using {type(self.user)} as an input type"
)
class LabelTooLongError(Exception):
def __init__(self, label: str) -> None:
self.label = label
super().__init__(
f"Could not set label to '{label}' because it is {len(label)} characters long (16 is maximum)"
)
class LabelSettingError(Exception):
def __init__(self, exp: Exception, trace: str) -> None:
super().__init__(
f"❌ **Could not set label**\n\nException: `{exp}`\n\n**Traceback:**\n```\n{trace}\n```"
)

953
classes/holo_user.py Normal file
View File

@@ -0,0 +1,953 @@
from datetime import datetime
from asyncio import sleep
from ftfy import fix_text
from traceback import format_exc
from app import app, isAnAdmin
from typing import Any, List, Literal, Union
from pyrogram.types import (
User,
ChatMember,
ChatPrivileges,
Chat,
Message,
Photo,
Video,
Document,
Animation,
Voice,
ForceReply,
ReplyKeyboardMarkup,
)
from pyrogram.errors import bad_request_400
from dateutil.relativedelta import relativedelta
from classes.errors.geo import PlaceNotFoundError
from classes.errors.holo_user import (
UserInvalidError,
UserNotFoundError,
LabelTooLongError,
LabelSettingError,
)
from classes.templates import DefaultApplicationTemp, DefaultSponsorshipTemp
from modules.database import (
col_tmp,
col_users,
col_applications,
col_sponsorships,
col_messages,
col_spoilers,
)
from modules.logging import logWrite
from modules.utils import configGet, find_location, locale, should_quote
class HoloUser:
"""This object represents a user of HoloChecker bot.
It is primarily used to interact with a database in a more python-friendly way,
as well as provide better programming experience in case of adding new features, etc.
"""
def __init__(self, user: Union[User, List[User], ChatMember, int]) -> None:
"""A user of Holo bot. Used to simplify DB interaction.
### Args:
* user (`Union[User, List[User], ChatMember, int]`): Any possible way to identify the user. Pass `User` object, user's telegram ID, etc...
### Raises:
* UserInvalidError: Provided to `HoloUser` object is not supported
* UserNotFoundError: `HoloUser` could not find user with such an ID in database
"""
# Determine input object class and extract id
if isinstance(user, list) and len(user) != 0:
self.id = user[0].id
elif isinstance(user, User):
self.id = user.id
elif isinstance(user, ChatMember):
self.id = user.user.id
elif isinstance(user, int):
self.id = user
# elif isinstance(user, str):
# try:
# get_users = async_to_sync(app.get_users)
# self.id = get_users(user).id # this line requires testing though
# except bad_request_400.UsernameNotOccupied:
# raise UserInvalidError(user)
# except bad_request_400.PeerIdInvalid:
# raise UserInvalidError(user)
else:
raise UserInvalidError(user)
# Find user record in DB
holo_user = col_users.find_one({"user": self.id})
if holo_user is None:
raise UserNotFoundError(user=user, user_id=self.id)
self.db_id = holo_user["_id"]
self.link = holo_user["link"]
self.label = holo_user["label"]
self.name = holo_user["tg_name"]
self.phone = holo_user["tg_phone"]
self.locale = holo_user["tg_locale"]
self.username = holo_user["tg_username"]
if isinstance(user, User):
if (
(self.name != user.first_name)
and hasattr(user, "first_name")
and (user.first_name is not None)
):
self.set("name", user.first_name, db_key="tg_name")
if (
(self.phone != user.phone_number)
and hasattr(user, "phone")
and (user.phone_number is not None)
):
self.set("phone", user.phone_number, db_key="tg_phone")
if (
(self.locale != user.language_code)
and hasattr(user, "locale")
and (user.language_code is not None)
):
self.set("locale", user.language_code, db_key="tg_locale")
if (
(self.username != user.username)
and hasattr(user, "username")
and (user.username is not None)
):
self.set("username", user.username, db_key="tg_username")
def set(self, key: str, value: Any, db_key: Union[str, None] = None) -> None:
"""Set attribute data and save it into database
### Args:
* `key` (`str`): Attribute to be changed
* `value` (`Any`): Value to set
"""
if not hasattr(self, key):
raise AttributeError()
setattr(self, key, value)
db_key = key if db_key is None else db_key
col_users.update_one(
filter={"_id": self.db_id}, update={"$set": {db_key: value}}, upsert=True
)
logWrite(f"Set attribute {key} of user {self.id} to {value}")
async def message(
self,
context: Message,
origin: Union[Message, None] = None,
text: Union[str, None] = None,
caption: Union[str, None] = None,
photo: Union[str, Photo, None] = None,
video: Union[str, Video, None] = None,
file: Union[str, Document, None] = None,
animation: Union[str, Animation, None] = None,
voice: Union[str, Voice, None] = None,
adm_origin: bool = False,
adm_context: bool = False,
) -> None:
"""Send a message to user
### Args:
* context (`Message`): Context (mostly the message where this method is called)
* origin (`Union[Message, None]`, *optional*): Origin message where to refer. None if called in a command. Defaults to None.
* text (`Union[str, None]`, *optional*): Text if this is a simple text message. Defaults to None.
* caption (`Union[str, None]`, *optional*): Text if this is a media message. Defaults to None.
* photo (`Union[str, Photo, None]`, *optional*): Photo as a photo object or file_id as a string. Defaults to None.
* video (`Union[str, Video, None]`, *optional*): Video as a video object or file_id as a string. Defaults to None.
* file (`Union[str, Document, None]`, *optional*): File as a document object or file_id as a string. Defaults to None.
* animation (`Union[str, Animation, None]`, *optional*): Animation as an animation object or file_id as a string. Defaults to None.
* voice (`Union[str, Voice, None]`, *optional*): Voice as a voice object or file_id as a string. Defaults to None.
* adm_origin (`bool`, *optional*): Whether origin sender is an admin. Defaults to False.
* adm_context (`bool`, *optional*): Whether context sender is an admin. Defaults to False.
"""
if text is not None:
text = fix_text(text)
elif caption is not None:
caption = fix_text(caption)
# Check if any text available and log message sending
if text is not None:
logWrite(
f"{context.from_user.id} sent message '{text}' to {self.id} (source message: {context.id})"
)
elif caption is not None:
logWrite(
f"{context.from_user.id} sent message '{caption}' to {self.id} (source message: {context.id})"
)
else:
logWrite(
f"{context.from_user.id} sent message to {self.id} (source message: {context.id})"
)
# Add notices for admin or user
if text is not None:
if adm_context:
text += locale("message_reply_notice", "message")
elif adm_origin:
text = (
locale("message_from", "message").format(
context.from_user.first_name, context.from_user.id
)
+ text
+ locale("message_reply_notice", "message")
)
else:
text = locale("message_reply_notice", "message")
if caption is not None:
if adm_context:
caption += locale("message_reply_notice", "message")
elif adm_origin:
caption = (
locale("message_from", "message").format(
context.from_user.first_name, context.from_user.id
)
+ caption
+ locale("message_reply_notice", "message")
)
else:
caption = locale("message_reply_notice", "message")
# Try sending the message
try:
# Check if origin message exists
# This check decides whether we send_ a message or reply_ to one
if origin is not None:
if photo is not None:
if isinstance(photo, Photo):
photo = photo.file_id
new_message = await origin.reply_cached_media(
photo, caption=caption, quote=True
)
elif video is not None:
if isinstance(video, Video):
video = video.file_id
new_message = await origin.reply_cached_media(
video, caption=caption, quote=True
)
elif file is not None:
if isinstance(file, Document):
file = file.file_id
new_message = await origin.reply_cached_media(
file, caption=caption, quote=True
)
elif animation is not None:
if isinstance(animation, Animation):
animation = animation.file_id
new_message = await origin.reply_cached_media(
animation, caption=caption, quote=True
)
elif voice is not None:
if isinstance(voice, Voice):
voice = voice.file_id
new_message = await origin.reply_voice(
voice, caption=caption, quote=True
)
else:
new_message = await origin.reply_text(text, quote=True)
else:
if photo is not None:
if isinstance(photo, Photo):
photo = photo.file_id
new_message = await app.send_cached_media(
self.id, photo, caption=caption
)
elif video is not None:
if isinstance(video, Video):
video = video.file_id
new_message = await app.send_cached_media(
self.id, video, caption=caption
)
elif file is not None:
if isinstance(file, Document):
file = file.file_id
new_message = await app.send_cached_media(
self.id, file, caption=caption
)
elif animation is not None:
if isinstance(animation, Animation):
animation = animation.file_id
new_message = await app.send_cached_media(
self.id, animation, caption=caption
)
elif voice is not None:
if isinstance(voice, Voice):
voice = voice.file_id
new_message = await app.send_cached_media(
self.id, voice, caption=caption
)
else:
new_message = await app.send_message(self.id, text)
# Acknowledge sending a message and save entry into DB
await context.reply_text(
locale("message_sent", "message"), quote=should_quote(context)
)
col_messages.insert_one(
{
"origin": {"chat": context.chat.id, "id": context.id},
"destination": {"chat": new_message.chat.id, "id": new_message.id},
}
)
# Report to admin and to sender about message sending failure
except Exception as exp:
logWrite(
f"Exception {exp} happened as {context.from_user.id} tried to send message to {self.id}. Traceback:\n{format_exc()}"
)
try:
await app.send_message(
configGet("owner"),
locale("message_traceback", "message").format(
context.from_user.id, self.id, exp, format_exc()
),
)
except bad_request_400.PeerIdInvalid:
logWrite(
f"Could not notify admin about failure when sending message! Admin has never interacted with bot!"
)
await context.reply_text(
locale("message_error", "message"), quote=should_quote(context)
)
async def label_set(self, chat: Chat, label: str) -> None:
"""Set label in destination group
### Args:
* chat (`Chat`): Telegram chat
* label (`str`): Label you want to set
"""
if len(label) > 16:
raise LabelTooLongError(label)
self.label = label
try:
await app.promote_chat_member(
configGet("users", "groups"),
self.id,
privileges=ChatPrivileges(
can_pin_messages=True, can_manage_video_chats=True
),
)
if not await isAnAdmin(self.id):
await sleep(0.5)
await app.set_administrator_title(
configGet("users", "groups"), self.id, label
)
self.set("label", label)
except Exception as exp:
logWrite(f"Could not set {self.id}'s title to '{self.label}' due to {exp}")
raise LabelSettingError(exp, format_exc())
async def label_reset(self, chat: Chat) -> None:
"""Reset label in destination group
### Args:
* chat (`Chat`): Telegram chat
"""
self.label = ""
self.set("label", "")
await app.set_administrator_title(configGet("users", "groups"), self.id, "")
if not await isAnAdmin(self.id):
await app.promote_chat_member(
configGet("users", "groups"),
self.id,
privileges=ChatPrivileges(
can_manage_chat=False,
can_pin_messages=False,
can_manage_video_chats=False,
),
)
def application_state(
self,
) -> tuple[Literal["none", "fill", "approved", "rejected"], bool]:
"""Check the current state of application in tmp collection
### Returns:
* `tuple[Literal["none", "fill", "approved", "rejected"], bool]`: First element is an enum of a state and the second one is whether application is complete.
"""
tmp_application = col_tmp.find_one({"user": self.id, "type": "application"})
if tmp_application is None:
return "none", False
else:
return tmp_application["state"], tmp_application["complete"]
def application_approved(self) -> bool:
"""Check whether user has a completed application and it got approved
### Returns:
* `bool`: `True` if yes and `False` if no
"""
return (
True if col_applications.find_one({"user": self.id}) is not None else False
)
def application_restart(self, reapply: bool = False) -> None:
"""Reset application of a user in tmp collection and replace it with an empty one"""
if col_tmp.find_one({"user": self.id, "type": "application"}) is None:
col_tmp.insert_one(
document=DefaultApplicationTemp(self.id, reapply=reapply).dict
)
else:
col_tmp.find_one_and_replace(
{"user": self.id, "type": "application"},
DefaultApplicationTemp(self.id, reapply=reapply).dict,
)
async def application_next(self, query: str, msg: Message) -> None:
"""Move on filling application of user
### Args:
* query (`str`): Some kind of input
* msg (`Message`): Message that should receive replies
"""
# if col_tmp.find_one({"user": self.id, "type": "application"}) is None:
if self.sponsorship_state()[0] == "fill":
return
if self.spoiler_state() is True:
return
# col_tmp.insert_one(
# document=DefaultApplicationTemp(self.id).dict
# )
progress = col_tmp.find_one({"user": self.id, "type": "application"})
if progress is None:
return
stage = progress["stage"]
# if self.sponsorship_state()[0] == "fill":
# await msg.reply_text(locale("finish_sponsorship", "message"), quote=should_quote(msg))
# return
if progress["state"] == "fill" and progress["sent"] is False:
if msg.text is not None:
msg.text = fix_text(str(msg.text))
if stage == 2:
try:
input_dt = datetime.strptime(query, "%d.%m.%Y")
except ValueError:
logWrite(
f"User {msg.from_user.id} failed stage {stage} due to sending invalid date format"
)
await msg.reply_text(
locale(f"question2_invalid", "message", locale=self.locale),
reply_markup=ForceReply(
placeholder=str(
locale(
f"question{stage}",
"force_reply",
locale=self.locale,
)
)
),
)
return
if (datetime.now() <= input_dt) or (
(datetime.now() - input_dt).days
) > (
(
datetime.now()
- datetime.now().replace(
year=datetime.now().year - configGet("age_maximum")
)
).days
):
logWrite(
f"User {msg.from_user.id} failed stage {stage} due to joking"
)
await msg.reply_text(
locale("question2_joke", "message", locale=self.locale),
reply_markup=ForceReply(
placeholder=str(
locale("question2", "force_reply", locale=self.locale)
)
),
)
return
elif ((datetime.now() - input_dt).days) < (
(
datetime.now()
- datetime.now().replace(
year=datetime.now().year - configGet("age_allowed")
)
).days
):
logWrite(
f"User {msg.from_user.id} failed stage {stage} due to being underage"
)
await msg.reply_text(
locale(
"question2_underage", "message", locale=self.locale
).format(str(configGet("age_allowed"))),
reply_markup=ForceReply(
placeholder=str(
locale("question2", "force_reply", locale=self.locale)
)
),
)
return
else:
progress["application"][str(stage)] = input_dt
col_tmp.update_one(
{"user": {"$eq": self.id}, "type": {"$eq": "application"}},
{
"$set": {
"application": progress["application"],
"stage": progress["stage"] + 1,
}
},
)
await msg.reply_text(
locale(f"question{stage+1}", "message", locale=self.locale),
reply_markup=ForceReply(
placeholder=str(
locale(
f"question{stage+1}",
"force_reply",
locale=self.locale,
)
)
),
)
elif stage == 3:
try:
progress["application"][str(stage)] = find_location(query)
if (
"lat" in progress["application"][str(stage)]
and "lng" in progress["application"][str(stage)]
):
progress["application"][str(stage)]["location"] = [
float(progress["application"][str(stage)]["lng"]),
float(progress["application"][str(stage)]["lat"]),
]
del progress["application"][str(stage)]["lat"]
del progress["application"][str(stage)]["lng"]
col_tmp.update_one(
{"user": {"$eq": self.id}, "type": {"$eq": "application"}},
{
"$set": {
"application": progress["application"],
"stage": progress["stage"] + 1,
}
},
)
await msg.reply_text(
locale("question3_found", "message", locale=self.locale).format(
progress["application"][str(stage)]["name"],
progress["application"][str(stage)]["adminName1"],
)
)
await msg.reply_text(
locale(f"question{stage+1}", "message", locale=self.locale),
reply_markup=ForceReply(
placeholder=str(
locale(
f"question{stage+1}",
"force_reply",
locale=self.locale,
)
)
),
)
except PlaceNotFoundError:
await msg.reply_text(
locale("question3_invalid", "message", locale=self.locale),
reply_markup=ForceReply(
placeholder=str(
locale(
f"question{stage}",
"force_reply",
locale=self.locale,
)
)
),
)
return
except Exception as exp:
await msg.reply_text(
locale("question3_error", "message", locale=self.locale),
reply_markup=ForceReply(
placeholder=str(
locale(
f"question{stage}",
"force_reply",
locale=self.locale,
)
)
),
)
try:
await app.send_message(
configGet("owner"),
locale(
"question3_traceback", "message", locale=self.locale
).format(query, exp, format_exc()),
)
except bad_request_400.PeerIdInvalid:
logWrite(
f"Could not notify admin about failure when sending message! Admin has never interacted with bot!"
)
return
elif stage == 10:
if len(query) > 1024:
await msg.reply_text(
locale("question10_too_long", "message", locale=self.locale),
reply_markup=ForceReply(
placeholder=str(
locale(
f"question{stage}",
"force_reply",
locale=self.locale,
)
)
),
)
return
progress["application"][str(stage)] = query
col_tmp.update_one(
{"user": {"$eq": self.id}, "type": {"$eq": "application"}},
{
"$set": {
"application": progress["application"],
"complete": True,
}
},
)
application_content = []
i = 1
for question in progress["application"]:
if i == 2:
age = relativedelta(
datetime.now(), progress["application"]["2"]
)
application_content.append(
f"{locale('question'+str(i), 'message', 'question_titles', locale=self.locale)} {progress['application']['2'].strftime('%d.%m.%Y')} ({age.years} р.)"
)
elif i == 3:
if progress["application"]["3"]["countryCode"] == "UA":
application_content.append(
f"{locale('question'+str(i), 'message', 'question_titles', locale=self.locale)} {progress['application']['3']['name']} ({progress['application']['3']['adminName1']})"
)
else:
application_content.append(
f"{locale('question'+str(i), 'message', 'question_titles', locale=self.locale)} {progress['application']['3']['name']} ({progress['application']['3']['adminName1']}, {progress['application']['3']['countryName']})"
)
else:
application_content.append(
f"{locale('question'+str(i), 'message', 'question_titles', locale=self.locale)} {progress['application'][question]}"
)
i += 1
await msg.reply_text(
locale("confirm", "message", locale=self.locale).format(
"\n".join(application_content)
),
reply_markup=ReplyKeyboardMarkup(
locale("confirm", "keyboard", locale=self.locale),
resize_keyboard=True,
),
)
else:
if len(query) > 256:
await msg.reply_text(
locale("question_too_long", "message", locale=self.locale),
reply_markup=ForceReply(
placeholder=str(
locale(
f"question{stage}",
"force_reply",
locale=self.locale,
)
)
),
)
return
progress["application"][str(stage)] = query
col_tmp.update_one(
{"user": {"$eq": self.id}, "type": {"$eq": "application"}},
{
"$set": {
"application": progress["application"],
"stage": progress["stage"] + 1,
}
},
)
await msg.reply_text(
locale(f"question{stage+1}", "message", locale=self.locale),
reply_markup=ForceReply(
placeholder=str(
locale(
f"question{stage+1}", "force_reply", locale=self.locale
)
)
),
)
logWrite(f"User {self.id} completed stage {stage} of application")
else:
return
def sponsorship_state(
self,
) -> tuple[Literal["none", "fill", "approved", "rejected"], bool]:
"""Check the current state of sponsorship in tmp collection
### Returns:
* `tuple[Literal["none", "fill", "approved", "rejected"], bool]`: First element is an enum of a state and the second one is whether sponsorship application is complete.
"""
tmp_sponsorship = col_tmp.find_one({"user": self.id, "type": "sponsorship"})
if tmp_sponsorship is None:
return "none", False
else:
return tmp_sponsorship["state"], tmp_sponsorship["complete"]
def sponsorship_valid(self) -> bool:
"""Check whether user has a valid sponsorship
### Returns:
* `bool`: `True` if yes and `False` if no
"""
return (
True
if col_sponsorships.find_one(
{"user": self.id, "expires": {"$gt": datetime.now()}}
)
is not None
else False
)
def sponsorship_restart(self) -> None:
"""Reset sponsorship of a user in tmp collection and replace it with an empty one"""
if col_tmp.find_one({"user": self.id, "type": "sponsorship"}) is None:
col_tmp.insert_one(document=DefaultSponsorshipTemp(self.id).dict)
else:
col_tmp.delete_one({"user": self.id, "type": "sponsorship"})
col_tmp.insert_one(document=DefaultSponsorshipTemp(self.id).dict)
async def sponsorship_next(
self, query: str, msg: Message, photo: Union[Photo, None] = None
) -> None:
"""Move on filling sponsorship of user
### Args:
* query (`str`): Some kind of input
* msg (`Message`): Message that should receive replies
"""
progress = col_tmp.find_one({"user": self.id, "type": "sponsorship"})
if progress is not None:
stage = progress["stage"]
if msg.text is not None:
msg.text = fix_text(str(msg.text))
elif msg.caption is not None:
msg.caption = fix_text(msg.caption)
if progress["state"] == "fill" and progress["sent"] is False:
if stage == 1:
if len(query) > 240:
logWrite(
f"User {msg.from_user.id} failed stage {stage} due to sending invalid date format"
)
await msg.reply_text(
locale(f"sponsor1_invalid", "message", locale=self.locale),
reply_markup=ForceReply(
placeholder=str(
locale(
f"sponsor{stage}",
"force_reply",
locale=self.locale,
)
)
),
)
return
progress["sponsorship"]["streamer"] = query
col_tmp.update_one(
{"user": {"$eq": self.id}, "type": {"$eq": "sponsorship"}},
{
"$set": {
"sponsorship": progress["sponsorship"],
"stage": progress["stage"] + 1,
}
},
)
await msg.reply_text(
locale(f"sponsor{stage+1}", "message", locale=self.locale),
reply_markup=ForceReply(
placeholder=str(
locale(
f"sponsor{stage+1}",
"force_reply",
locale=self.locale,
)
)
),
)
elif stage == 2:
try:
input_dt = datetime.strptime(query, "%d.%m.%Y")
except ValueError:
logWrite(
f"User {msg.from_user.id} failed stage {stage} due to sending invalid date format"
)
await msg.reply_text(
locale(f"sponsor2_invalid", "message", locale=self.locale),
reply_markup=ForceReply(
placeholder=str(
locale(
f"sponsor{stage}",
"force_reply",
locale=self.locale,
)
)
),
)
return
if datetime.now() >= input_dt:
logWrite(
f"User {msg.from_user.id} failed stage {stage} due to sending date in the past"
)
await msg.reply_text(
locale("sponsor2_past", "message", locale=self.locale),
reply_markup=ForceReply(
placeholder=str(
locale(
"sponsor2", "force_reply", locale=self.locale
)
)
),
)
return
else:
progress["sponsorship"]["expires"] = input_dt
col_tmp.update_one(
{"user": {"$eq": self.id}, "type": {"$eq": "sponsorship"}},
{
"$set": {
"sponsorship": progress["sponsorship"],
"stage": progress["stage"] + 1,
}
},
)
await msg.reply_text(
locale(f"sponsor{stage+1}", "message", locale=self.locale),
reply_markup=ForceReply(
placeholder=str(
locale(
f"sponsor{stage+1}",
"force_reply",
locale=self.locale,
)
)
),
)
elif stage == 3:
if photo is not None:
progress["sponsorship"]["proof"] = photo.file_id
col_tmp.update_one(
{"user": {"$eq": self.id}, "type": {"$eq": "sponsorship"}},
{
"$set": {
"sponsorship": progress["sponsorship"],
"stage": progress["stage"] + 1,
}
},
)
await msg.reply_text(
locale(f"sponsor{stage+1}", "message", locale=self.locale),
reply_markup=ForceReply(
placeholder=str(
locale(
f"sponsor{stage+1}",
"force_reply",
locale=self.locale,
)
)
),
)
elif stage == 4:
if len(query) > 16:
await msg.reply_text(
locale("label_too_long", "message"),
reply_markup=ForceReply(
placeholder=str(
locale(
"sponsor4", "force_reply", locale=self.locale
)
)
),
)
return
progress["sponsorship"]["label"] = query
col_tmp.update_one(
{"user": {"$eq": self.id}, "type": {"$eq": "sponsorship"}},
{
"$set": {
"sponsorship": progress["sponsorship"],
"complete": True,
}
},
)
await msg.reply_cached_media(
progress["sponsorship"]["proof"],
caption=locale(
"sponsor_confirm", "message", locale=self.locale
).format(
progress["sponsorship"]["streamer"],
progress["sponsorship"]["expires"].strftime("%d.%m.%Y"),
progress["sponsorship"]["label"],
),
reply_markup=ReplyKeyboardMarkup(
locale("confirm", "keyboard", locale=self.locale),
resize_keyboard=True,
),
)
else:
return
logWrite(f"User {self.id} completed stage {stage} of sponsorship")
else:
return
def spoiler_state(self) -> bool:
"""Check if user has any started but not finished spoilers
### Returns:
* `bool`: `True` if any not finished spoilers available and `False` if none.
"""
return (
False
if col_spoilers.find_one({"user": self.id, "completed": False}) is None
else True
)

48
classes/templates.py Normal file
View File

@@ -0,0 +1,48 @@
"""Templates for temporary application/sponsorship records"""
from datetime import datetime
class DefaultApplicationTemp(dict):
def __init__(self, user: int, reapply: bool = False):
super().__init__({})
self.dict = {
"user": user,
"type": "application",
"complete": False,
"sent": False,
"state": "fill",
"reapply": reapply,
"stage": 1,
"application": {
"1": None,
"2": None,
"3": None,
"4": None,
"5": None,
"6": None,
"7": None,
"8": None,
"9": None,
"10": None,
},
}
class DefaultSponsorshipTemp(dict):
def __init__(self, user: int):
super().__init__({})
self.dict = {
"user": user,
"type": "sponsorship",
"complete": False,
"sent": False,
"state": "fill",
"stage": 1,
"sponsorship": {
"streamer": None,
"expires": datetime.fromtimestamp(0),
"proof": None,
"label": "",
},
}

View File

@@ -1,27 +1,244 @@
{
"locale": "uk",
"debug": false,
"owner": 0,
"age_allowed": 0,
"admin_group": 0,
"destination_group": 0,
"age_maximum": 70,
"api": "http://example.com",
"issues": "https://github.com/example/test/issues/new",
"inline_preview_count": 7,
"remove_application_time": -1,
"search_radius": 50,
"admins": [],
"groups": {
"admin": 0,
"users": 0
},
"bot": {
"api_id": 0,
"api_hash": "",
"bot_token": ""
},
"database": {
"user": null,
"password": null,
"host": "127.0.0.1",
"port": 27017,
"name": "holochecker"
},
"geocoding": {
"username": "demo"
},
"logging": {
"size": 512,
"location": "logs"
},
"features": {
"general": {
"enabled": true
},
"applications": {
"enabled": true
},
"sponsorships": {
"enabled": true
},
"warnings": {
"enabled": true
},
"invites_check": {
"enabled": true
},
"dinovoice": {
"enabled": false
},
"spoilers": {
"enabled": true,
"allow_external": true
}
},
"scheduler": {
"birthdays": {
"time": 9,
"enabled": true
},
"sponsorships": {
"time": 9,
"grayout_days": 2,
"enabled": true
},
"cache_avatars": {
"interval": 6,
"enabled": true
},
"cache_members": {
"interval": 30,
"enabled": true
},
"cache_admins": {
"interval": 120,
"enabled": true
},
"channels_monitor": {
"interval": 5,
"enabled": true,
"channels": []
},
"warnings_revocation": {
"interval": 6,
"enabled": true
}
},
"locations": {
"data": "data",
"cache": "cache",
"locale": "locale"
},
"commands": {
"start": "Start using the bot"
},
"commands_admin": {
"reboot": "Restart the bot"
"rules": {
"permissions": [
"users",
"admins"
],
"modules": [
"general"
]
},
"spoiler": {
"permissions": [
"users",
"admins"
],
"modules": [
"spoilers"
]
},
"cancel": {
"permissions": [
"users",
"admins"
],
"modules": [
"spoilers",
"applications",
"sponsorships"
]
},
"nearby": {
"permissions": [
"users",
"admins",
"group_admins"
],
"modules": [
"applications"
]
},
"warn": {
"permissions": [
"group_users_admins"
],
"modules": [
"warnings"
]
},
"warnings": {
"permissions": [
"admins",
"group_admins"
],
"modules": [
"warnings"
]
},
"reapply": {
"permissions": [
"users",
"admins"
],
"modules": [
"applications"
]
},
"sponsorship": {
"permissions": [
"users",
"admins"
],
"modules": [
"sponsorships"
]
},
"reboot": {
"permissions": [
"owner"
],
"modules": [
"general"
]
},
"label": {
"permissions": [
"admins",
"group_admins"
],
"modules": [
"applications"
]
},
"message": {
"permissions": [
"users",
"admins",
"group_admins"
],
"modules": [
"general"
]
},
"identify": {
"permissions": [
"admins",
"group_admins"
],
"modules": [
"applications",
"sponsorships"
]
},
"issue": {
"permissions": [
"users",
"admins"
],
"modules": [
"general"
]
},
"export": {
"permissions": [
"admins",
"group_admins"
],
"modules": [
"general"
]
},
"application": {
"permissions": [
"admins",
"group_admins"
],
"modules": [
"applications"
]
},
"resetcommands": {
"permissions": [
"owner"
],
"modules": [
"general"
]
}
}
}

View File

View File

@@ -1 +0,0 @@
{}

View File

@@ -1,24 +0,0 @@
{
"stage": 0,
"link": null,
"sent": false,
"confirmed": false,
"approved": false,
"refused": false,
"telegram_id": null,
"telegram_name": null,
"telegram_phone": null,
"telegram_locale": null,
"application": {
"1": null,
"2": null,
"3": null,
"4": null,
"5": null,
"6": null,
"7": null,
"8": null,
"9": null,
"10": null
}
}

View File

123
holochecker.py Normal file
View File

@@ -0,0 +1,123 @@
from os import getpid, makedirs
from time import time
from modules.utils import *
from modules.inline import *
from app import app
from pyrogram import idle
pid = getpid()
makedirs(f'{configGet("cache", "locations")}{sep}avatars', exist_ok=True)
# Importing
from modules.commands.application import *
from modules.commands.bye import *
from modules.commands.cancel import *
from modules.commands.export import *
from modules.commands.identify import *
from modules.commands.issue import *
from modules.commands.label import *
from modules.commands.message import *
from modules.commands.nearby import *
from modules.commands.reapply import *
from modules.commands.reboot import *
from modules.commands.resetcommands import *
from modules.commands.rules import *
from modules.commands.spoiler import *
from modules.commands.sponsorship import *
from modules.commands.start import *
from modules.commands.warn import *
from modules.commands.warnings import *
# This one is only imported during events
# and should be completely rewritten for each one.
# from modules.event import *
from modules.callbacks.ban import *
from modules.callbacks.nothing import *
from modules.callbacks.reapply import *
from modules.callbacks.rules import *
from modules.callbacks.spoiler import *
from modules.callbacks.sponsorship import *
from modules.callbacks.sub import *
from modules.callbacks.sus import *
from modules.callbacks.warnings import *
from modules.handlers.analytics_group import *
from modules.handlers.confirmation import *
from modules.handlers.contact import *
from modules.handlers.group_member_update import *
from modules.handlers.voice import *
from modules.handlers.welcome import *
from modules.handlers.everything import *
from modules.scheduled import *
if __name__ == "__main__":
logWrite(f"Starting up with pid {pid}")
# Yes, it should be in some kind of async main() function but I don't give a shit.
# I did compare performance, almost no difference and it's much more useful this way. Change my mind.
app.start()
try:
if path.exists(path.join(configGet("cache", "locations"), "shutdown_time")):
downtime = relativedelta(
datetime.now(),
datetime.fromtimestamp(
jsonLoad(
path.join(configGet("cache", "locations"), "shutdown_time")
)["timestamp"]
),
)
if downtime.days >= 1:
app.send_message(
configGet("owner"),
locale("startup_downtime_days", "message").format(
pid, downtime.days
),
)
elif downtime.hours >= 1:
app.send_message(
configGet("owner"),
locale("startup_downtime_hours", "message").format(
pid, downtime.hours
),
)
else:
app.send_message(
configGet("owner"),
locale("startup_downtime_minutes", "message").format(
pid, downtime.minutes
),
)
else:
app.send_message(
configGet("owner"), locale("startup", "message").format(pid)
)
except bad_request_400.PeerIdInvalid:
logWrite(
f"Could not send startup message to bot owner. Perhaps user has not started the bot yet."
)
scheduler.start()
idle()
try:
app.send_message(configGet("owner"), locale("shutdown", "message").format(pid))
except bad_request_400.PeerIdInvalid:
logWrite(
f"Could not send shutdown message to bot owner. Perhaps user has not started the bot yet."
)
app.stop()
makedirs(configGet("cache", "locations"), exist_ok=True)
jsonSave(
{"timestamp": time()},
path.join(configGet("cache", "locations"), "shutdown_time"),
)
killProc(pid)

212
locale/en.disabled Normal file
View File

@@ -0,0 +1,212 @@
{
"message": {
"start": "Hello and welcome! This bot was created to accept applications for joining our community. To continue, we are interested in the answer to one question:\n\nDo you want to join the Ukrainian community of Hololive fans?",
"goodbye": "Ok, thanks for your honesty! Sorry, but under such conditions we will not add you to the community. If you change your mind and want to join, just click the button.",
"privacy_notice": "We're glad to hear that!\n\nTo continue, you will need to fill out a short Application. Please take it seriously. We take personal data very seriously, so this Application will not be shared with any third parties, but will only be used for the community.",
"question1": "How can I contact you?",
"question2": "When is your birthday?",
"question3": "What city are you from or where do you live now?\n\n⚠ Please do not provide exact addresses! \"Kyiv\" or \"Kyiv Oblast\" is a sufficient specification.\n\nExamples:\n- Kyiv\n- Odesa oblast\n- Makiivka (Luhansk oblast)",
"question4": "When did you first learn about Hololive?",
"question5": "What made you interested in Hololive?",
"question6": "Which girl's content do you like the most?",
"question7": "Name the content of at least five **JAPANESE** girls you like the most.",
"question8": "Do you watch streams of Hololive girls?",
"question9": "Whose songs from Hololive do you like the most?",
"question10": "And finally, tell us a little about yourself. About hobbies, what you like to do. In one message, please.",
"question2_underage": "Sorry, but you must be {0} years old to join us. These restrictions are in place to ensure that everyone in the community has fun with each other.",
"question2_invalid": "Please enter a date in the format `DD.MM.YYYY`.",
"question2_joke": "Joke, we get it. But please enter the real value.",
"question3_invalid": "City/population not found. Use the examples below to indicate where you live and try again:\n\n- Kyiv\n- Odesa region\n- Makiivka (Luhansk region).",
"question3_found": "Using the following result:\n- {0} ({1})",
"question3_error": "⚠️ **Error**\nCould not retrieve the geographic label. The developer has been notified of this error. Please try again.",
"question3_traceback": "⚠️ **Error occurred**\nError retrieving geocoding for `{0}`\nError: `{1}`\n\nTraceback:\n```\n{2}\n```",
"confirm": "Great, thanks!\n\nPlease check the data is correct:\n{0}\n\nEverything correct?",
"application_sent": "Thank you! We have sent your application for verification. You will receive a message as soon as it is checked and a decision is made. Until then, nothing more is required from you. Have a nice day :)",
"application_got": "Received an application from `{0}`\n\nName in tg: `{1}`\nUsername: @{2}\n\n**Application data:**\n{3}",
"reapply_got": "Received application change from `{0}`\n\nUsername: `{1}`\nUsername: @{2}\n\n**Application data:**\n{3}",
"shutdown": "Shutting down the bot with PID `{0}`",
"startup": "Starting the bot with PID `{0}`",
"startup_downtime": "Starting bot with PID `{0}` (was down for {1})",
"approved": "Congratulations! Your application has been reviewed and your eligibility has been confirmed. Use the button below the notification to join our lamp community!",
"approved_joined": "Congratulations! Your application has been reviewed and confirmed as correct. Thank you for your time and have a nice day!",
"read_rules": "Please read these rules before clicking the button and joining the chat.",
"rejected.": "Oh dear! Your application has been reviewed but not confirmed as eligible to join the community. Better luck next time!\n\nYou can try to reapply with the /reapply command.",
"rejected_russian": "Russian warship, go fuck yourself!",
"approved_by": "✅ **Application approved**\nAdmin **{0}** has reviewed and approved application `{1}`.",
"rejected_by": "❌ **Form rejected**\nAdmin **{0}** has reviewed and rejected form `{1}`.",
"rejected_by_agr": "❌ **Application rejected**\nAdmin **{0}** has reviewed and rejected application `{1}`, banning you from the community",
"rejected_by_rus": "❌ **Application rejected**\nAdmin **{0}** has reviewed and rejected the profile `{1}`, banning you from the community",
"contact": "Application `{0}`\n\n**Application data:**\n{1}\n\n{2}",
"application_status_accepted": "Accepted `{0}` on {1}",
"application_status_rejected": "Rejected `{0}` from {1}",
"application_status_on_hold": "Application still under review",
"application_status_not_send": "The application has not been sent yet",
"contact_invalid": "The submitted contact does not have a completed application form.",
"contact_not_member": "The sent contact is not a Telegram member.",
"already_sent": "The Application has already been sent, just wait. You will be informed immediately what decision will be made.",
"sus_joined": "User **{0}** (`{1}`) has joined the group without a personal invitation.",
"sus_allowed_by": "✅ **Access Allowed**\nAdmin **{0}** has allowed `{1}` to join the community without being personally invited.",
"sus_rejected_by": "❌ **Access denied**\nAdmin **{0}** has banned `{1}` for not being allowed to join the community via a personal link.",
"reapply_forbidden": "❌ **Action not possible**\nYour past application has not been approved or rejected yet.",
"reapply_in_progress": "❌ **Action not possible**\nYou are already filling out an application right now. If there is an error, just click the button below to start filling it out again.",
"reapply_restarted": "🔁 **Restarted**\nStarted filling out the application form again.",
"reapply_left_chat": "⚠️ **Reminder**\nIt seems that you left the chat in the past, but your profile is still available for use. Would you like to request membership using your old profile?",
"birthday": "User **{0}** (@{1}) has a birthday today! Turned {2} years old",
"application_invalid_syntax": "Invalid syntax! `/application ID/NAME/USERNAME`",
"warned": "**{0}** (`{1}`) rule violation warned",
"warned_reason": "Warned **{0}** (`{1}`)\n\n**Cause:**\n{2}",
"warnings_1": "User **{0}** (`{1}`) has **{2}** warnings",
"warnings_2": "User **{0}** (`{1}`) has **{2}** warnings",
"no_warnings": "User **{0}** (`{1}`) has no warnings",
"no_user_warnings": "No users found for query **{0}**",
"syntax_warnings": "Invalid syntax! `/warnings ID/NAME/USERNAME`",
"message_sent": "Message sent",
"message_no_user": "⚠️ **Sending Error**\nThe user ID provided is incorrect, so it was not possible to send the message to the user. Check if the ID is the same as the number that was shown in the Application.",
"message_invalid_syntax": "Invalid syntax! `/message ID MESSAGE`",
"message_from": "Message from **{0}** (`{1}`):\n\n",
"message_reply_notice": "\n\n**To send a reply to this message, tag it.**",
"message_error": "⚠️ **Error occurred**\nYour message could not be sent. The developer has been notified of this error.",
"message_traceback": "⚠️ **Error occurred**\nMessage error: `{0}` -> `{1}`\nError: `{2}`\n\nTraceback:\n```\n{3}\n```",
"no_user_application": "No users found for the query **{0}**",
"user_invalid": "The submitted user does not have a completed application.",
"joined_false_link": "User **{0}** (`{1}`) did not join the group using their link.",
"question_titles": {
"question1": "Name:",
"question2": "Birthday:",
"question3": "Residence:",
"question4": "Learned about Hololive:",
"question5": "Found interesting about Holo:",
"question6": "Like the content of:",
"question7": "Japanese hololive girls:",
"question8": "Watching streams:",
"question9": "Like songs:",
"question10": "About me:"
}
},
"keyboard": {
"welcome": [
[
"Yes, of course"
],
[
"No, thank you."
]
],
"return": [
[
"I changed my mind, I want to"
]
],
"confirm": [
[
"Yes, everything is correct"
],
[
"No, re-fill"
]
]
},
"force_reply": {
"question1": "Name",
"question2": "Birthday",
"question3": "City or region",
"question4": "Approximate time",
"question5": "Reasons, features",
"question6": "Girl's name",
"question7": "Five Japanese Holo girls",
"question8": "Yes or no",
"question9": "Name of the girl or girls",
"question10": "A bit about yourself"
},
"button": {
"sub_yes": "✅ Accept",
"sub_no": "❌ Reject",
"sub_russian": "🇷🇺 Reject (Russian)",
"accepted": "✅ Accepted",
"declined": "❌ Rejected",
"join": "Join",
"sus_allow": "✅ Confirm permission",
"sus_reject": "❌ Permanently block",
"sus_allowed": "✅ Permission granted",
"sus_rejected": "❌ User blocked",
"reapply_yes": "✅ Accept",
"reapply_no": "❌ Reject",
"reapply_old_one": "✅ Send old one",
"reapply_new_one": "🔁 Fill in again",
"rules_home": "🏠 Home",
"rules_additional": " Additional",
"rules_next": "Next ➡️",
"rules_prev": "⬅️ Back",
"applying_stop": "🛑 Interrupt filling",
"done": "✅ Done"
},
"callback": {
"sub_accepted": "✅ Application {0} has been approved",
"sub_rejected": "❌ Application {0} rejected",
"sub_russian": "🇷🇺 Application {0} rejected",
"sus_allowed": "✅ Access {0} allowed",
"sus_rejected": "❌ Access {0} denied",
"nothing": "🔔 Action already performed",
"rules_page": " Rule {0} shown",
"rules_home": " Home rule shown",
"rules_additional": " Additional rules shown",
"reapply_stopped": " Application stopped."
},
"inline": {
"forbidden": {
"title": "Action not available",
"description": "You do not have permission to view this",
"message_content": "No permission to view this."
},
"not_pm": {
"title": "Action not available",
"description": "This command is not available in channels.",
"message_content": "Action not available in channels."
},
"user": {
"title": "",
"description": "View {0}'s application (@{1})",
"message_content": "{0} (@{1})\n\n**Application Data:**\n{2}"
}
},
"rules_msg": "📢The rules can be supplemented and changed, depending on the need. In this case, violations that were committed before the introduction (change) of the rule will not be considered violations. You will be informed about all changes in the rules by means of pinned messages. But they will not be pinned on a permanent basis, so, from time to time, check the relevance of the rules in the bot.\n\n🔔If you see how one of the participants has violated the rules, tag one of the admins in response to a message that you think is a violation. In the post to the tag, indicate on which point you saw the violation. Or send a message to any of the administrators in private messages, and briefly describe the situation.\nList of administrators: @Chirkopol @Za_NerZula @Denialvapr\nFor questions about the functioning of the bot, please contact @Profitroll2281337\n\n❗Any prohibited content can be sent to the chat using the bot - https://t.me/spoilerobot with a full description of the content contained under the spoiler. For an incorrect or incorrect description, a warning may be issued.\n\n‼ Deleted or modified messages are still messages on your behalf that could be seen by chat participants and can be tracked through the admin panel.\n\n🔨 For violations - you will receive a warning. If you have 3 warnings, you will be banned for a day. For repeated violations, you will be immediately punished, without additional warnings.",
"rules": [
"1⃣) \"HoloKyiv Chat\" and \"HoloUA (Hololive Ukraine) Chat\" are created exclusively for Ukrainians (13+). They can only contain people who: \n- Were born in Ukraine and currently reside in it.\n- Were born outside Ukraine but reside in it.\n- Were born in Ukraine but currently do not reside in it.\n\"HoloUA (Hololive Ukraine) Chat\" is open to all Ukrainians. To get into it, please fill in the form and wait for it to be approved by the administrators.\n\n\"HoloKyiv Chat\" can be accessed only in person if you live in Kyiv or are a close friend of one of the chat participants. To be added to the chat, write @Chirkopol in private messages.\n🔨 If in the process of communication it turns out that you are not Ukrainian, you will be removed from the chat until you become one. No offense. We are creating an exclusively Ukrainian community.",
"2⃣) Distribution of NSFW content with direct or partially hidden pornographic content is prohibited. The content of \"erotic nature\" should be covered with \"questionable\" body parts.\nShock content with a large presence of blood and/or physical injuries is prohibited.",
"3⃣) Anonymity of Hololive and Holostars participants is prohibited: \n- Photos\n- Names\n- Place of residence\n- Exact age (the word \"holohegs\" does not apply)\n- Details of personal life\n- Posts from rummage accounts or mentioning them with specific data (i.e. the phrase \"something happened on Kalli's irl channel\" is allowed, but \"something happened on *irl channel name*\" - no) \n- Details from the girls' past - only superficially and without specifics (i.e. \"was an office worker\" - ok, \"was in *company_name\" - no). \nExceptions - if the girls themselves mentioned it on the archive(!) streams.... This rule does not apply to those who are no longer in Hololive, or have never been part of it. But, please, treat the personal life of other vloggers with respect, and do not exaggerate with the deanons of their personalities.",
"4⃣) Flooding with the same type of messages, emojis, emoticons, stickers, gifs, etc. is prohibited. The approximate number of messages that can receive a warning for this rule is 5. Each situation can be considered separately, but put all your thoughts in one message.",
"5⃣) Video and audio messages that are not intended to convey what you have heard or seen are prohibited. If you want to tell us about how your day went, but do not have the opportunity to type a message, use the magical video-to-text converter - https://t.me/voicybot",
"6⃣) Insults, threats, bullying, humiliation, trolling of participants, their family members, friends and other circle that is close to the chat participant are prohibited. Messages like: \"go to ... \" - are also insults. You can get a warning even if it was your friend. It will be removed if your friend confirms that he is not offended by you.\n🔨 If at the request of a member or administrator, you do not change the pace of communication and do not apologize, you will receive a warning.\nIf your behavior causes a member to leave the chat, the punishment may be more severe.",
"7⃣) It is forbidden to provoke conflicts and incite hatred between chat participants.",
"8⃣) Racism, sexism, homophobia and condemnation of political and (or) religious prejudice are prohibited. These topics can still be part of the dialogue if they do not contain direct condemnations, insults, etc.",
"9⃣) Avatars, nicknames, roles that violate other rules are prohibited."
],
"rules_additional": "Additional rules that are advisory in nature, and have no explicit penalties for violations:\n1⃣) There is no ban on the Russian language in the chat. We respect every Ukrainian and do not want to incite language conflicts.\n2↪Mn_e_20E3↩) There is no ban on Russian content in the chat. But, keep in mind that participants, for the most part, will not be interested in discussing it and it may be ignored.) Do not abuse swear words. Try to communicate in clean language.\n4⃣) Respect the copyright of content makers. If you find art, animation, music, etc., on official resources (pixiv, twitter, deviantart, etc.), send a link to it.\nIf someone sent art from a non-official resource and you want to know its author, send a message with the text `/search` to the message with the art.",
"commands": {
"rules": "Check out the rules",
"nearby": "Show users near the area",
"reapply": "Resubmit the application",
"sponsorship": "Apply for sponsor role"
},
"commands_admin": {
"reboot": "Restart the bot",
"message": "Send a message",
"label": "Set user's nickname",
"warnings": "Check user's warnings",
"application": "Check user's application",
"applications": "Retrieve all applications as a JSON"
},
"commands_group_admin": {
"reboot": "Restart the bot",
"message": "Send a message",
"label": "Set user's nickname",
"nearby": "Show users near the area",
"warnings": "Check user's warnings",
"application": "Check user's application",
"applications": "Retrieve all applications as a JSON"
},
"commands_group_destination": {
"warn": "Warn a user",
"nearby": "Show users near the area"
}
}

View File

@@ -1,55 +1,152 @@
{
"commands": {
"start": "Почати користуватись ботом",
"rules": "Правила пропонування фото"
},
"commands_admin": {
"forwards": "Переглянути репости",
"reboot": "Перезапустити бота"
},
"message": {
"start": "Привіт і ласкаво просимо!\n\nЦей бот створено для прийому заявок на вступ до нашої спільноти. Для продовження нас цікавить відповідь на одне питання:\n\nЧи хочеш ти доєднатися до українського ком'юніті фанатів Хололайв?",
"already_sent": "Анкету вже надіслано, просто почекай. Тобі одразу повідомлять, яке рішення буде прийнято.",
"application_got": "Отримано анкету від `{0}`\n\nІм'я тг: `{1}`\nЮзернейм: @{2}\n\n**Дані анкети:**\n{3}",
"application_invalid_syntax": "Неправильний синтаксис!\nТреба: `/application ID/NAME/USERNAME`",
"application_sent": "Дякуємо! Ми надіслали твою анкетку на перевірку. Ти отримаєш повідомлення, як тільки її перевірять та приймуть рішення.. До тих пір від тебе більше нічого не потребується :)",
"application_status_accepted": "Прийнята `{0}` від {1}",
"application_status_not_send": "Анкета ще не була відправлена",
"application_status_on_hold": "Анкета все ще на розгляді",
"application_status_rejected": "Відхилена `{0}` від {1}",
"approved_by": "✅ **Анкету схвалено**\nАдмін **{0}** переглянув та схвалив анкету `{1}`.",
"approved_joined": "Вітаємо! Твою анкету переглянули та підтвердили її правильність. Дякуємо за витрачений на заповнення час та гарного дня!",
"approved": "Вітаємо! Твою анкету переглянули та підтвердили твоє право на вступ.\n\nПеред тим, як ти натиснеш на кнопочку під повідомленням, щоб вступити до нашої лампової спільноти, дамо тобі трішечки додаткової інформації.\nПам'ятай, що натискаючи її, ти підтверджуєш, що ознайомився із нашими правилами (/rules) та зобов'язуєшся їх дотримуватись.\n\nПісля того, як потрапиш до чату, не закидуй цього бота далеко.\nПрописавши @holoua_bot у боті, ти відкриєш список із усіх наших анкет. Ти можеш натискати на будь-яку із них та дізнаватись про кожного із нас більше інформації.\nЗавдяки команді /nearby, ти зможеш дізнатись, чи є серед нас однодумці із твого міста.\nЯкщо у тебе є спонсорська підписка на будь-кого із учасниць Гололайву, то ти маєш право отримати унікальну роль у нашому чаті (/sponsorship) та виділятись серед інших учасників. (А ще зможеш отримати декілька додаткових функцій. Але нікому про це не кажи!)",
"birthday": "У користувача **{0}** (@{1}) сьогодні день народження! Виповнилось {2} років",
"cancel_reapply": "Всі поточні операції скасовано.\nЩоб знову заповнити анкету користуйся /reapply",
"cancel": "Всі поточні операції скасовано.",
"confirm": "Супер, дякуємо!\n\nБудь ласка, перевір правильність даних:\n{0}\n\nВсе правильно?",
"contact_invalid": "Надісланий контакт не має завершеної анкети.",
"contact_not_member": "Надісланий контакт не є користувачем Telegram.",
"contact": "Анкета `{0}`\n\n**Дані анкети:**\n{1}\n\n{2}",
"finish_application": "❌ **Дія неможлива**\nПерш ніж заповнювати форму спонсора, треба завершити заповнення анкети.",
"finish_sponsorship": "❌ **Дія неможлива**\nПерш ніж заповнювати анкету, треба завершити заповнення форми спонсора або перервати його командою /cancel.",
"goodbye": "Добре, дякуємо за чесність! Вибачте, але за таких умов ми не будемо тебе додавати до спільноти. Якщо передумаєш та захочеш приєднатись - просто натисни на кнопку.",
"privacy_notice": "Раді це чути!\n\nДля продовження треба буде заповнити невеличку анкетку. Будь ласка, віднесись до цього серйозно. Ми відповідально ставимось до персональних даних, тому ця анкета не буде передана третім особам, а буде використана лише для проходження до спільноти.",
"identify_invalid_syntax": "Неправильний синтаксис!\nТреба: `/identify ID/NAME/USERNAME`",
"identify_not_found": "Не знайдено користувачів за запитом **{0}**",
"identify_success": "Користувач `{0}`\n\nІм'я: {1}\nЮзернейм: {2}\nЄ в чаті: {3}\nЄ адміном: {4}\nРоль: {5}\nНаявна анкета: {6}\nНаявне спонсорство: {7}",
"issue": "**Допоможіть боту**\nЗнайшли баг або помилку? Маєте файну ідею для нової функції? Повідомте нас, створивши нову задачу на гіті.\n\nЗа можливості, опишіть свій запит максимально детально. Якщо є змога, також додайте скріншоти або додаткову відому інформацію.",
"joined_application": "{0} (@{1})\n\n**Дані анкети:**\n{2}",
"joined_false_link": "Користувач **{0}** (`{1}`) приєднався до групи не за своїм посиланням",
"label_set_exception": "❌ **Не вдалось встановити роль**\nУ зв'язку з помилкою `{0}` не вдалось встановити роль. Власника бота повідомлено.",
"label_too_long": "Довжина назви ролі не повинна перевищувати 16 символів",
"message_enter": "Надішліть повідомлення, яке треба переслати адмінам.\n\nЗверніть увагу, що повідомлення може містити лише одне медіа або файл.",
"message_error": "⚠️ **Сталась помилка**\nНе вдалось надіслати ваше повідомлення. Розробника повідомлено про цю помилку.",
"message_from": "Повідомлення від **{0}** (`{1}`):\n\n",
"message_invalid_syntax": "Неправильний синтаксис!\nТреба: `/message ID ПОВІДОМЛЕННЯ`",
"message_no_user": "⚠️ **Помилка надсилання**\nВказано невірний ID користувача, тому не вдалось надіслати йому повідомлення. Перевірте чи в якості ID надано те число, яке було показане в анкеті.",
"message_reply_notice": "\n\n__Для того, щоб адміністрація побачила вашу відповідь, відправте її **реплаєм на це повідомлення**__",
"message_sent": "Повідомлення надіслано",
"message_traceback": "⚠️ **Сталась помилка**\nПомилка повідомлень: `{0}` -> `{1}`\nПомилка: `{2}`\n\nTraceback:\n```\n{3}\n```",
"nearby_empty": "Здається, нікого поблизу немає.",
"nearby_error": "⚠️ **Сталась помилка**\n\nПомилка: `{0}`\n\nTraceback:\n```\n{1}\n```",
"nearby_invalid": " **Місце не знайдено**\nЗа наданим запитом не знайдено місце з координатами. Спробуйте ще раз формулючи запит в стилі \"Чернівці\" або \"Київська область\".",
"nearby_result": "Результати пошуку:\n\n{0}",
"no_user_application": "Не знайдено користувачів за запитом **{0}**",
"no_user_warnings": "Не знайдено користувачів за запитом **{0}**",
"no_warnings": "Користувач **{0}** (`{1}`) не має попереджень",
"not_member": "❌ **Дія неможлива**\nУ тебе немає заповненої та схваленої анкети. Заповни таку за допомогою /reapply та спробуй ще раз після її підтвердження.",
"privacy_notice": "Раді це чути!\n\nДля продовження треба буде заповнити невеличку анкетку. Будь ласка, віднесись до цього серйозно. Ми відповідально ставимось до персональних даних, тому ця анкета не буде передана третім особам, а буде використана лише для проходження до спільноти та подальшої взаємодії в ній.",
"question_too_long": "Текст занадто довгий. Будь ласка, умісти відповідь у 256 символів.",
"question1": "Як до тебе можна звертатись?",
"question2": "Скільки тобі років?",
"question3": "З якого ти міста та де проживаєш зараз?\n\n⚠ Будь ласка, не вказуйте точних адрес! \"Київщина\" може бути достатньою конкретизацією.",
"question10_too_long": "Текст занадто довгий. Будь ласка, умісти відповідь у 1024 символи.",
"question10": "Ну і нарешті, розкажи трохи про себе. Про хобі, чим тобі подобається займатись. Одним повідомленням, будь ласка.",
"question2_invalid": "Будь ласка, введи дату формату `ДД.ММ.РРРР`",
"question2_joke": "Шутнік, ми так і поняли. Але будь ласка, введи реальне значення.",
"question2_underage": "Вибач, але треба досягти віку {0} років, щоб приєднатись до нас. Такі обмеження існують для того, щоб всім у спільноті було цікаво одне з одним.",
"question2": "Коли в тебе день народження?\n\nБудь ласка, у форматі ДД.ММ.РРРР",
"question3_error": "⚠️ **Сталась помилка**\nНе вдалось отримати географічну мітку. Розробника повідомлено про цю помилку. Будь ласка, спробуйте ще раз.",
"question3_found": "Використовую наступний результат:\n• {0} ({1})",
"question3_invalid": "Місто/населений пункт не знайдено. Користуйтесь прикладами нижче щоб вказати де ви проживаєте та спробуйте ще раз:\n\n• Київ\n• Одеська область\n• Макіївка (Луганська область)",
"question3_traceback": "⚠️ **Сталась помилка**\nПомилка отримання геокодингу для `{0}`\nПомилка: `{1}`\n\nTraceback:\n```\n{2}\n```",
"question3": "З якого ти міста або де проживаєш зараз?\n\n⚠ Будь ласка, не вказуйте точних адрес! \"Київ\" або \"Київська Область\" є достатньою конкретизацією.\n\nПриклади:\n• Київ\n• Одеська область\n• Макіївка (Луганська область)",
"question4": "Коли вперше довелось дізнатись про Хололайв?",
"question5": "Чим тебе зацікавив Хололайв?",
"question6": "Контент якої дівчини тобі подобається найбільше?",
"question7": "Назви контент хоча б п'яти японських холодівчат, які тобі подобаються найбільше.",
"question7": "Назви контент хоча б п'яти **__--ЯПОНСЬКИХ--__** холодівчат, які тобі подобаються найбільше.",
"question8": "Чи дивишся ти стріми дівчат Хололайву?",
"question9": "Чиї пісні з Хололайву тобі подобаються найбільше?",
"question10": "Ну і нарешті, розкажи трохи про себе. Про хобі, чим тобі подобається займатись. Одним повідомленням, будь ласка.",
"question2_underage": "Вибач, але треба досягти віку {0} років, щоб приєднатись до нас. Такі обмеження існують для того, щоб всім у спільноті було цікаво одне з одним.",
"question2_invalid": "Будь ласка, введи ціле число.",
"question2_joke": "Шутнік, ми так і поняли. Але будь ласка, введи реальне значення.",
"confirm": "Супер, дякуємо!\n\nБудь ласка, перевір правильність даних:\n{0}\n\nВсе правильно?",
"application_sent": "Дякуємо! Ми надіслали твою анкетку на перевірку. Ти отримаєш повідомлення як тільки її перевірять та приймуть рішення. До тих пір від тебе більше нічого не потребується. Гарного дня! :)",
"application_got": "Отримано анкету від `{0}`\n\nІм'я тг: `{1}`, `{2}`\nUserID: `{3}`\n\n**Дані анкети:**\n{4}",
"read_rules": "Будь ласка, прочитай ці правила перш ніж натискати на кнопку та приєднуватись до чату.",
"reapply_forbidden": "❌ **Дія неможлива**\nТвоя минула анкета ще не була схвалена або відхилена.",
"reapply_got": "Отримано оновлення анкети від `{0}`\n\nІм'я тг: `{1}`\nЮзернейм: @{2}\n\n**Дані анкети:**\n{3}",
"reapply_in_progress": "❌ **Дія неможлива**\nТи прямо зараз вже заповнюєш анкету. Якщо в ній є помилка - просто натисни кнопку нижче щоб почати заповнювати спочатку.",
"reapply_left_chat": "⚠️ **Нагадування**\nЗдається, ти залишив чат у минулому, проте твоя анкета все ще доступна до використання. Подати запит на вступ користуючись старою анкетою?",
"reapply_restarted": "🔁 **Перезапущено**\nРозпочате заповнення анкети спочатку.",
"rejected_by_agr": "❌ **Анкету відхилено**\nАдмін **{0}** переглянув та відхилив анкету `{1}`, заборонивши вступ до спільноти.\nПричина: агресивна/токсична анкета.",
"rejected_by_rus": "❌ **Анкету відхилено**\nАдмін **{0}** переглянув та відхилив анкету `{1}`, заборонивши вступ до спільноти.\nПричина: русня.",
"rejected_by": "❌ **Анкету відхилено**\nАдмін **{0}** переглянув та відхилив анкету `{1}`.",
"rejected_russian": "русский военньій корабль, иди нахуй!",
"rejected": "Ой лишенько! Твою анкету переглянули, однак не підтвердили право на вступ до спільноти. Better luck next time!\n\nТи можеш спробувати повторно заповнити анкету командою /reapply",
"shutdown": "Вимкнення бота з підом `{0}`",
"spoiler_cancel": "Створення спойлера було припинено",
"spoiler_described_named": "Спойлер категорії \"{0}\" від **{1}**: {2}",
"spoiler_described": "Спойлер категорії \"{0}\": {1}",
"spoiler_description_enter": "Добре, введіть бажаний опис спойлера",
"spoiler_description_too_long": "Текст занадто довгий. Будь ласка, умісти опис у 1024 символи.",
"spoiler_in_progress": "❌ **Дія неможлива**\nПерш ніж починати нову дію, треба завершити створення спойлера або перервати його командою /cancel.",
"spoiler_incorrect_category": "Вказана категорія не є дійсною. Будь ласка, користуйся клавіатурою бота (кнопка біля 📎) для вибору категорії.",
"spoiler_incorrect_content": "Бот не підтримує такий контент. Будь ласка, надішли текст, фото, відео, файл або анімацію (гіф).",
"spoiler_ready": "Успіх! Спойлер створено",
"spoiler_send_description": "Тепер треба надіслати коротенький опис спойлера, щоб люди розуміли що під ним варто очкувати.",
"spoiler_send": "Користуйтесь кнопкою нижче щоб надіслати його.",
"spoiler_started": "Розпочато створення спойлера. Будь ласка, оберіть категорію спойлера за допомогою клавіатури бота.",
"spoiler_unfinished": "У вас ще є незавершений спойлер. Надішліть /cancel щоб зупинити його створення",
"spoiler_using_description": "Встановлено опис спойлера: {0}\n\nЗалишилось додати вміст самого спойлера. Бот приймає текстове повідомлення, фото, відео, файл а також гіф зображення (1 шт.)",
"sponsor_approved_by": "✅ **Підписку схвалено**\nАдмін **{0}** переглянув та схвалив форму `{1}`.",
"sponsor_approved": "Вітаємо! Твою форму переглянули та підтвердили її правильність. Коли термін дії наданої підписки буде добігати кінця - ми нагадаємо, що треба оновити дані аби й надалі отримувати плюшки в чаті. Також можна повторно заповнити форму, якщо хочеться змінити бажане ім'я ролі або подовжити термін дії підписки завчасно, за допомогою команди /sponsorship. Гарного дня!",
"sponsor_confirm": "**Дані форми:**\nСтрімер: {0}\nПідписка до: {1}\nХочу роль: {2}\n\nПеревір чи все правильно та жмакни кнопку на клавіатурі щоб продовжити.",
"sponsor_got": "Отримано форму на спонсорство від `{0}`\n\nІм'я тг: `{1}`\nЮзернейм: @{2}\n\n**Дані форми:**\n{3}",
"sponsor_rejected_by": "❌ **Підписку відхилено**\nАдмін **{0}** переглянув та відхилив форму `{1}`.",
"sponsor_rejected": "Ой лишенько! Твою форму переглянули, однак не підтвердили її. Можливо, щось не так з датами, або ж бажана роль не може бути надана.\n\nТи можеш спробувати повторно заповнити форму командою /sponsorship",
"sponsor_resubmit_invalid_option": "Правильна опція! Спробуй /sponsorship ще раз, але користуйся клавіатурою щоб обрати з можливих варіантів.",
"sponsor_resubmit": "Здається, ти маєш активну підписку на **{0}**. Хочеш подовжити її чи заповнити нову?",
"sponsor1_invalid": "Будь ласка, введіть ім'я не довше за 240 символів",
"sponsor1": "На яку саме дівчину платна підписка?",
"sponsor2_invalid": "Будь ласка, введи дату формату `ДД.ММ.РРРР`",
"sponsor2_past": "Вказана дата знаходиться в минулому. Будь ласка, вкажіть правильний термін дії підписки",
"sponsor2": "До якої дати (`ДД.ММ.РРРР`) підписка?",
"sponsor3": "Будь ласка, надішли одне фото для підтвердження дійсності підписки\n\n **Підказка**\nПрочитай як правильно скрінити легітимне підтвердження підписки: https://telegra.ph/Pіdpiska-na-holo-dіvchinu-01-02",
"sponsor4_resubmit": "Майже закінчили. Стара роль **{0}** може бути використана і цього разу, а може бути змінена. Будь ласка, обери те, що тобі підходить.\n\n⚠ **Увага!**\nПісля обирання варіанту або введення нової назви ролі, заявку буде надіслано автоматично. Для відміни дії зараз ще можна використовувати /cancel",
"sponsor4": "Яку роль ти бажаєш отримати?\n\n **Підказка**\nНазва ролі повинна бути якось пов'язана зі вказаною дівчиною, не повинна порушувати правила спільноти а також має бути не довше за 16 символів (обмеження Telegram).",
"sponsorship_application_empty": "❌ **Дія неможлива**\nУ тебе немає заповненої та схваленої анкети. Заповни таку за допомогою /reapply та спробуй ще раз після її підтвердження.",
"sponsorship_apply": " Оформіть платну підписку на когось з Холо, заповніть форму та отримайте особливу роль в якості винагороди!",
"sponsorship_applying": " Розпочато заповнення форми на отримання бонусів за платну підписку на холодівчат.",
"sponsorship_sent": "Дякуємо! Ми надіслали форму на перевірку. Ти отримаєш повідомлення, як тільки її перевірять та приймуть рішення. :)",
"sponsorships_expired": "⚠️ **Нагадування**\nТермін дії вказаної підписки сплив. Для повторного отримання ролі користуйся командою /sponsorship.",
"sponsorships_expires": "⚠️ **Нагадування**\nНадана платна підписка припинить діяти **за {0} д**. Будь ласка, оновіть дані про неї командою /sponsorship інакше роль буде втрачено!",
"start": "Привіт і ласкаво просимо!\n\nМи будуємо українське ком'юніті фанатів Гололайву і раді кожному, хто поділяє наші інтереси або тільки зацікавився цією тематикою та хоче дізнатись більше. Чим ми відрізняеємось від звичайного тематичного чату? Ми намагаємось створювати усі можливі умови, щоб люди знаходили однодумців у своїх містах та збирались разом. Інколи проводимо великі зустрічі на честь Голо-івентів у Києві. Збираємось у Дискорді для сумісних переглядів концертів, музичних топів, ігор тощо. Проводимо різні івенти у чаті. Об'єднуємось, щоб замовляти офіційний мерч із Японії. Підтримуємо україномовних кліперів та будуємо плани по поширенню нашого ком'юніті на майбутнє.\n\nЦей бот створений для прийому заявок на вступ до нашої чат-спільноти. Усі анкети, після підтвердження адміністрацією, можуть дивитися й інші учасники чату у будь-який момент. Тому, будь ласка, віднесіться до її заповнення відповідально.\n\nЯкщо вашу анкету відхилили, то, скоріше за все:\n1) Вона порушує перше правило чату (/rules - ознайомитись перед вступом до чату).\n2) Ви намагаєтесь додати до чату \"додатковий/запасний\" акаунт.\n3) Ви не дуже відповідально віднеслись до її заповнення.\n\nЯкщо із вашою анкетою щось не так, то вам через бота прийде повідомлення від адміністраторів для вирішення питання. Якщо ви зіштовхнулись із якоюсь проблемою або бот не надсилає вам необхідні повідомлення - напишіть мені у приватні повідомлення @Chirkopol.\n\nПісля прийому вашої анкети бот згенерує вам одноразове посилання. Не забудьте перейти по ньому, щоб потрапити до чату.\n\nДля продовження, нас цікавить відповідь на питання:\nЧи хочеш ти доєднатись до українського ком'юніті фанатів Гололайв та чи зобов'язуєшся ти дотримуватися усіх правил?",
"startup_downtime_days": "Запуск бота з підом `{0}` (лежав {1} дн.)",
"startup_downtime_hours": "Запуск бота з підом `{0}` (лежав {1} год.)",
"startup_downtime_minutes": "Запуск бота з підом `{0}` (лежав {1} хв.)",
"startup": "Запуск бота з підом `{0}`",
"startup_downtime": "Запуск бота з підом `{0}` (лежав {1})",
"sub_yes": "✅ Подання схвалено та прийнято",
"sub_no": "❌ Подання розглянуто та відхилено",
"approved": "Вітаємо! Твою анкету переглянули та підтвердили твоє право на вступ. Скористайся кнопкою під повідомленням щоб вступити до нашої лампової спільноти!",
"refused": "Ой лишенько! Твою анкету переглянули, однак не підтвердили право на вступ до спільноти. Better luck next time!",
"refused_russian": "русский военньій корабль, иди нахуй!",
"approved_by": "✅ **Анкету схвалено**\nАдмін **{0}** переглянув та схвалив анкету `{1}`, дозволивши вступ до спільноти.",
"refused_by": "❌ **Анкету відхилено**\nАдмін **{0}** переглянув та відхилив анкету `{1}`, заборонивши вступ до спільноти.",
"refused_by_agr": "❌ **Анкету відхилено**\nАдмін **{0}** переглянув та відхилив анкету `{1}`, заборонивши вступ до спільноти.\nПричина: агресивна/токсична анкета.",
"refused_by_rus": "❌ **Анкету відхилено**\nАдмін **{0}** переглянув та відхилив анкету `{1}`, заборонивши вступ до спільноти.\nПричина: русня.",
"contact": "Анкета `{0}`\n\n**Дані анкети:**\n{1}\n\n{2}",
"application_status_accepted": "Прийнята `{0}` від {1}",
"application_status_refused": "Відхилена `{0}` від {1}",
"application_status_on_hold": "Анкета все ще на розгляді",
"application_status_not_send": "Анкета ще не була відправлена",
"contact_invalid": "Надісланий контакт не має розпочатої анкети.",
"contact_not_member": "Надісланий контакт не є користувачем Telegram.",
"already_sent": "Анкету вже надіслано, просто почекай. Тобі одразу повідомлять, яке рішення буде прийнято.",
"sus_allowed_by": "✅ **Доступ дозволено**\nАдмін **{0}** дозволив `{1}` вступити до спільноти не за персональним посиланням.",
"sus_joined": "Користувач **{0}** (`{1}`) зайшов до групи не за своїм персональним запрошенням.",
"sus_rejected_by": "❌ **Доступ заборонено**\nАдмін **{0}** заборонив `{1}` доступ до спільноти не за персональним посиланням.",
"syntax_export": "Неправильний синтаксис!\nТреба: `/export applications/warnings/sponsorships/bans/event`",
"syntax_warnings": "Неправильний синтаксис!\nТреба: `/warnings ID/NAME/USERNAME`",
"user_invalid": "Надісланий користувач не має завершеної анкети.",
"user_left": "Користувач **{0}** залишив чат",
"warned_reason": "Попереджено **{0}** (`{1}`)\n\n**Причина:**\n{2}",
"warned": "Попереджено **{0}** (`{1}`) про порушення правил",
"warning_revoked_auto": "Попередження від {0} користувачеві `{1}` було автоматично скасовано.",
"warning_revoked": "Попередження від {0} користувачеві `{1}` було скасовано адміном `{2}`",
"warnings_1": "Користувач **{0}** (`{1}`) має **{2}** попередження\n\n{3}\n\nОбрати та зняти попередження:\n`/warnings {4} revoke`",
"warnings_2": "Користувач **{0}** (`{1}`) має **{2}** попереджень\n\n{3}\n\nОбрати та зняти попередження:\n`/warnings {4} revoke`",
"warnings_all": "**Список попереджень**\n\n{0}\n\nДля перегляду попереджень окремо взятого користувача слід використовувати `/warnings ID/NAME/USERNAME`",
"warnings_empty": "Щось тут порожньо...\nЗ іншого боку, це добре!",
"warnings_entry": "• {0} (`{1}`)\n Попереджень: {2}",
"warnings_revoke": "**Попередження {0}:**\n\n{1}\n\nБудь ласка, користуйтесь клавіатурою щоб зняти попередження з відповідним номером.",
"you_are_banned": "⚠️ **Вас було заблоковано**\nТепер не можна відправити анкету або користуватись командами бота.",
"youtube_video": "На каналі [{0}]({1}) нове відео!\n\n**[{2}]({3})**",
"yes": "Так",
"no": "Ні",
"voice_message": [
"why are u gae",
"руки відірвало? пиши як людина",
"унга-бунга, не маєш клавіатури?"
],
"question_titles": {
"question1": "Ім'я/звертання:",
"question2": "Вік:",
"question2": "День народження:",
"question3": "Проживаня:",
"question4": "Дізнався(лась) про холо:",
"question5": "Хололайв зацікавив:",
@@ -58,6 +155,16 @@
"question8": "Дивлюсь стріми:",
"question9": "Подобаються пісні:",
"question10": "Про себе:"
},
"sponsor_titles": {
"question_streamer": "Стрімер:",
"question_expires": "Підписка до:",
"question_label": "Хоче роль:"
},
"spoiler_categories": {
"nsfw": "NSFW контент",
"deanon": "Деанон холо-учасників",
"other": "Інше"
}
},
"keyboard": {
@@ -81,11 +188,38 @@
[
"Ні, повторно заповнити"
]
],
"spoiler_categories": [
[
"NSFW контент"
],
[
"Деанон холо-учасників"
],
[
"Інше"
]
],
"sponsorship_restore": [
[
"Подовжити стару"
],
[
"Заповнити нову підписку"
]
],
"sponsorship_restore_label": [
[
"Залишити стару"
],
[
"Ввести замість неї нову"
]
]
},
"force_reply": {
"question1": "Ім'я або звертання",
"question2": "Твій вік",
"question2": "День народження",
"question3": "Місто або область",
"question4": "Орієнтовний час",
"question5": "Риси, особливості",
@@ -93,21 +227,117 @@
"question7": "П'ять японських холодівчат",
"question8": "Так або ні",
"question9": "Ім'я дівчини або дівчин",
"question10": "Трошки про себе"
"question10": "Трошки про себе",
"sponsor1": "Ім'я дівчини",
"sponsor2": "Дата до якої підписка",
"sponsor3": "Фото-підтвердження",
"sponsor4": "Бажана роль",
"spoiler_content": "Вміст спойлера",
"spoiler_description": "Опис спойлера"
},
"button": {
"sub_yes": "✅ Прийняти",
"sub_no": "❌ Відхилити",
"sub_no_aggressive": "🤡 Відхилити (Токс)",
"sub_no_russian": "🇷🇺 Відхилити (Русак)",
"accepted": "✅ Прийнято",
"applying_stop": "🛑 Перервати заповнення",
"ban": "💀 Заблокувати",
"banned": "☠️ Заблоковано",
"declined": "❌ Відхилено",
"join": "Приєднатись"
"done": "✅ Готово",
"issue": "🪄 Створити задачу",
"join": "Приєднатись",
"reapply_new_one": "🔁 Заповнити знову",
"reapply_no": "❌ Відхилити",
"reapply_old_one": "✅ Надіслати стару",
"reapply_yes": "✅ Прийняти",
"rules_additional": " Додаткові",
"rules_home": "🏠 Головна",
"rules_next": "Далі ➡️",
"rules_prev": "⬅️ Назад",
"spoiler_preview": "Попередній перегляд",
"spoiler_send_chat": "Надіслати в холо-чат",
"spoiler_send_other": "Надіслати в інший чат",
"spoiler_view": "Переглянути",
"sponsor_apply": "Заповнити форму",
"sponsor_no": "❌ Відхилити",
"sponsor_started": "Форму розпочато",
"sponsor_yes": "✅ Прийняти",
"sub_no": "❌ Відхилити",
"sub_russian": "🇷🇺 Відхилити (Русак)",
"sub_yes": "✅ Прийняти",
"sus_allow": "✅ Підтвердити дозвіл",
"sus_allowed": "✅ Дозвіл надано",
"sus_reject": "❌ Перманентно заблокувати",
"sus_rejected": "❌ Користувача заблоковано"
},
"callback": {
"nothing": "🔔 Дія вже виконана",
"reapply_stopped": " Перервано заповнення анкети",
"rules_additional": " Показано додаткові правила",
"rules_home": " Показано головну правил",
"rules_page": " Показано правило {0}",
"spoiler_forbidden": "❌ Треба бути учасником чату",
"spoiler_sent": "✅ Повідомлення надіслано в холо-чат",
"sponsor_accepted": "✅ Форму {0} схвалено",
"sponsor_rejected": "❌ Форму {0} відхилено",
"sponsor_started": " Заповнення форми розпочато",
"sub_accepted": "✅ Анкету {0} схвалено",
"sub_refused": "❌ Анкету {0} відхилено",
"sub_no_aggressive": "🤡 Анкету {0} відхилено",
"sub_no_russian": "🇷🇺 Анкету {0} відхилено"
"sub_banned": "☠️ Користувача заблоковано",
"sub_rejected": " Анкету {0} відхилено",
"sub_russian": "🇷🇺 Анкету {0} відхилено",
"sus_allowed": "✅ Доступ {0} дозволено",
"sus_rejected": "❌ Доступ {0} заборонено",
"warning_not_found": "❌ Попередження вже скасовано або не існує",
"warning_revoked": "✅ Попередження скасовано"
},
"inline": {
"forbidden": {
"title": "Дія недоступна",
"description": "Ти не маєш дозволу переглядати це",
"message_content": "Немає дозволу на перегляд."
},
"not_pm": {
"title": "Дія недоступна",
"description": "Ця команда недоступна в каналах",
"message_content": "Дія недоступна в каналах."
},
"user": {
"title": "",
"description": "Переглянути анкету {0} (@{1})",
"message_content": "{0} (@{1})\n\n**Дані анкети:**\n{2}"
},
"spoiler": {
"title": "Відправити",
"description": "Надіслати цей спойлер до чату"
}
},
"rules_msg": "📢Правила можуть доповнюватись та змінюватись, залежно від потреби. У такому разі, порушення, які були вчинені до введення (змінення) правила, порушеннями вважатися не будуть. Про всі зміни в правилах, ви будете проінформовані за допомогою закріплених повідомлень. Але вони не будуть закріплені на постійній основі, тому, час від часу, перевіряйте актуальність правил у боті.\n\n🔔Якщо ви бачите, як хтось із учасників порушив правила, тегніть одного із адмінів, у відповідь на повідомлення, яке, на вашу думку, є порушенням. У дописі до тегу, вкажіть, по якому пункту ви побачили порушення. Або перешліть повідомлення до будь кого із адміністраторів у особисті повідомлення, та коротко опишіть ситуацію.\nСписок адміністраторів: @Chirkopol @Za_NerZula @Toxinushka\nЗ питань функціонування бота звертайтесь до @Profitroll2281337\n\n❗Будь-який заборонений контент, може бути відправлений за допомогою команди /spoiler у бота - з повним описом контенту, що міститься під спойлером. За неправильний або некоректний опис, може бути видане попередження.\n\n‼Видалені або змінені повідомлення, все ще є повідомленнями від вашого імені, які могли побачити учасники чату, і які можуть бути відстежені через адмінську панель.\n\n🔨 За порушення - ви отримаєте попередження. За наявності 3-х попереджень - мут на добу. За повторні порушення, ви одразу отримаєте покарання, без додаткових попереджень.",
"rules": [
"1⃣) \"HoloKyiv Chat\" та \"HoloUA (Hololive Ukraine) Chat\" створені виключно для українців (13+). В них можуть знаходитись тільки люди які: \n- Народились в Україні, та проживають, на данний момент, у ній.\n- Народились за межами України, але проживають у ній.\n- Народились в Україні але, на даний момент, не проживають у ній.\n\"HoloUA (Hololive Ukraine) Chat\" відкритий для усіх українців. Щоб потрапити до нього, заповніть, будь ласка, анкету, та дочекайтесь, поки її схвалять адміни.\nУ \"HoloKyiv Chat\" можна потрапити тільки особисто, якщо ви проживаєте у Київі, або є близьким другом одного із учасників чату. Із приводу додавання до чату пишіть @Chirkopol у приватні повідомлення.\n🔨 Якщо у процесі спілкування виявиться, що ви не українець, вас буде видалено із чату, до моменту, поки ви їм не станете. Без образ. Ми створюємо виключно українське ком'юніті.",
"2⃣) Заборонено поширення артів, на яких зображено:\n2.1 - NSFW-контент з прямим або частково прихованим порнографічним змістом.\n2.2 - Контент \"еротичного характеру\" із неприкритими \"сумнівними\" ділянками тіл (так звані \"private parts\") або їх помітними силуетами крізь одяг.\n2.3 - Фетиш-контент, який спрямований на дуже вузьке коло шанувальників та може порушувати закон. Наприклад: копрофілія, педофілія, зоофілія.\n2.4 - Шок-контент із великою кількістю крові та/або фізичних пошкоджень.\n2.5 - Контент, який порушує будь-яке інше правило.\n❗Якщо якійсь арт викликає у вас сумніви і ви не впевнені, що він не порушує правила, скористайтесь командою /spoiler у боті. Це точно збереже вас від зайвих проблем. Але не забувайте робити опис спойлеру!",
"3⃣) Анонімність учасників Гололайв та Голостарз.\nЗаборонено: \n- Фотографії.\n- Імена.\n- Місце проживання.\n- Точний вік (слово \"холохеґз\" не підпадає).\n- Подробиці особистого життя, які не було розкриті на Гололайв-акаунтах.\n- Пости з руммейт-акаунтів чи згадка про них з конкретними даними. Пояснення: фраза \"там на ірл каналі Каллі щось вийшло\" - можна, а \"там на *назва ірл каналу* щось вийшло\" - ні.\n- Подробиці з минулого дівчат - лише поверхнево і без конкретики (тобто, \"була офісним працівником\" - ок, \"була в *компанія_нейм*\" - ні).\nВиключення - якщо дівчата самі згадували про це на архівних(!) стрімах.\n❗Це правило не стосується тих, хто вже не знаходиться у Гололайві або ніколи і не був його частиною. Але, прохання, відноситись до особистого життя інших вітуберів із повагою та не перебільшувати із деанонами їх особистостей.",
"4⃣) Заборонено флуд однотипними текстовими повідомленнями, які не несуть у собі сенсу, емоджі, смайликами, стікерами, ґіфками, великою кількістю артів тощо. Орієнтовна кількість повідомлень, за які можна отримати попередження за це правило - 5. \nЯкщо учасник буде цілеспрямовано відправляти по менше ніж 5 повідомлень, але робити це регулярно, то попередження також може бути видане.\nПояснення: \"Відправлю зараз 4 гіфки, почекаю, поки хтось напише декілька повідомлень, і знову відправлю 4 гіфки\".\n- Попередження про флуд одночасно можуть отримати декілька учасників.\nПояснення: декілька учасників чату, по черзі, відправляють по декілька (менше п'яти) стікерів, але спільними зусиллями, і їх стає дуже багато.\n❗Кожна ситуація може і буде розглядатись окремо, але намагайтеся вкладати всі свої думки в одне повідомлення.\n❗Якщо ви масово відправляєте контент зі сторонніх ресурсів, будь то якісь новини у великій кількості (анонси мерчу, концертів тощо) або арти, кліпи та подібне, і ви ніяк не зможете посприяти тому, щоб уся інформація була подана більш компактно, то це правило на вас не розповсюджується, але не зловживайте цим.\n‼Це правило не стосується організованих івентів та флешмобів, затверджених адміністраторами.",
"5⃣) Заборонені відео- та аудіо-повідомлення, які не несуть цілі передати почуте або побачене. \n❗Якщо ви бажаєте розповісти про те, як пройшов ваш день, але не маєте можливості друкувати повідомлення, використовуйте магічний перетворювач войсів у текст - https://t.me/voicybot",
"6⃣) Заборонені образи, погрози, булінг, приниження, тролінг учасників, членів їхніх сімей, друзів та іншого кола, що є наближеними до учасника чату. Повідомлення на кшталт: \"йди на ... \" - також є образами. Ви можете отримати попередження, навіть, якщо це був ваш приятель. Воно буде зняте, якщо приятель підтвердить, що не ображений на вас.\n🔨 Якщо на прохання учасника або адміністратора, ви не зміните темп спілкування і не вибачитесь, то отримаєте попередження. \n⚒ Якщо ваша поведінка спричинить те, що учасник залишив чат, покарання може бути жорсткішим.",
"7⃣) Правила щодо провокаційних тем у чаті:\n7.1 - У чаті немає заборони на обговорення будь-яких тем, однак, заборонені радикальні висловлювання у бік будь-якої із позицій. Якщо ви на 100% впевнені у своїй правоті і розумієте, що будуть люди, які не поділяють вашу точку зору і ви абсолютно точно не збираєтесь прислухатися до їхньої позиції, а налаштовуєтеся на заперечення будь-якого аргументу - то закрийте чат і займіться своїми справами.\n7.2 - Відносьтесь до позиції співрозмовника із повагою. Пам'ятайте, що всі ми різні і погляди у нас, аналогічно, різні.\n7.3 - Якщо ви розумієте, що обстановка в обговоренні загострюється, то запропонуйте співрозмовнику зупинитися і розійдіться, залишившись кожен при своїй думці та намагайтеся більше на проблемну тему не спілкуватися.\n7.4 - Вкидання спірної інформації, яка може призвести до конфлікту, навіть без прямої участі в ньому, може розцінюватися як провокація.\n7.5 - Навмисні образи когось чи чогось, кому чи чому може симпатизувати хтось із учасників чату, після прохання так більше не робити, розцінюватиметься як провокація.\nПриклад: образи кого-небудь із Голо-дівчат, знаючи про те, що у чаті є ті, кому вона може бути цікава і що людині не сподобаються подібні повідомлення.\n7.6 Адміністрація, на свій розсуд, може попросити згорнути тему, якщо розуміє, що вона може призвести до конфлікту.\n❗Кожна ситуація може розглядатися адміністрацією окремо. Ми не хочемо, щоб учасники пересварилися один з одним і вирішили залишити чат. Дані правила прописані насамперед для збереження чистоти та дружньої атмосфери. Якщо ви здатні спілкуватися на заборонені теми шляхом адекватного обміну думками та інформацією, ми можемо заплющити очі на порушення. Але це залежить лише від вашого вміння стримувати свій запал і здібностей викладати думки.",
"8⃣) Заборонені прояви расизму, сексизму, гомофобії та засудження за політичні та (або) релігійні упередження. \n❗Дані теми все ще можуть бути обговорювані в чаті \"з нейтральної точки зору\", якщо ви при цьому не порушуєте розділ правил 7⃣.",
"9⃣) Заборонені аватарки, нікнейми, ролі, які порушують інші правила."
],
"rules_additional": "Додаткові правила, які несуть рекомендаційний характер та не мають явних покарань за порушення:\n1⃣) У чаті немає заборони на російську мову. Ми поважаємо кожного українця і не бажаємо розпалювати мовні конфлікти.\n2⃣) У чаті немає заборони на російський контент. Але майте на увазі, що учасники, здебільшого, не будуть зацікавлені у тому, щоб обговорювати його і він може бути проігнорованим.\n3⃣) Не зловживайте матами. Намагайтесь спілкуватись чистою мовою.\n4⃣) Поважайте авторські права контент-мейкерів. Якщо ви знаходите арт, анімацію, музику тощо на офіційних ресурсах (pixiv, twitter, deviantart тощо), відправляйте на нього посилання.\nЯкщо хтось із учасників відправив арт із не офіційного ресурсу і ви бажаєте дізнатись його автора, відправте у відповідь повідомлення із текстом /search на повідомлення із артом.\n5⃣) В особливо критичних ситуаціях порушник може отримати бан або бути повністю видаленим із чату, без попереджень.\n6⃣) Якщо з кимось із учасників у вас трапиться якесь непорозуміння і вам неприємно буде перебувати в чаті один з одним (навіть, якщо конфлікт стався з кимось із адміністраторів) - напишіть мені в особисті повідомлення @Chirkopol. Я, як засновник чату та головний адміністратор, найбільше зацікавлений у збереженні цілісності чату та розвитку нашого ком'юніті, і я зроблю все, що в моїх силах, щоб допомогти вирішити вашу ситуацію.",
"commands": {
"application": "Переглянути анкету користувача",
"cancel": "Відмінити актуальну дію",
"identify": "Дізнатись дані про користувача за айді",
"issue": "Задачі для покращення бота",
"label": "Встановити нікнейм користувачу",
"message": "Надіслати користувачу повідомлення",
"nearby": "Показати користувачів поблизу",
"export": "Експортувати дані як CSV та JSON",
"reapply": "Повторно заповнити анкету",
"reboot": "Перезапустити бота",
"resetcommands": "Відреєструвати всі команди",
"rules": "Правила спільноти",
"spoiler": "Почати створювати спойлер",
"sponsorship": "Отримати роль за спонсорство",
"warn": "Попередити користувача",
"warnings": "Переглянути попередження користувача"
}
}

View File

391
main.py
View File

@@ -1,391 +0,0 @@
from datetime import datetime, timedelta
from time import time
from os import getpid, path
from modules.utils import *
from pyrogram.client import Client
from pyrogram import filters
from pyrogram.enums.parse_mode import ParseMode
from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton, BotCommand, BotCommandScopeChat, ReplyKeyboardMarkup, ForceReply, ReplyKeyboardRemove, ChatPermissions
from pyrogram import idle # type: ignore
from pyrogram.errors.exceptions import bad_request_400
pid = getpid()
app = Client("holochecker", bot_token=configGet("bot_token", "bot"), api_id=configGet("api_id", "bot"), api_hash=configGet("api_hash", "bot"))
# Start command ================================================================================================================
@app.on_message(~ filters.scheduled & filters.private & filters.command(["start"], prefixes=["/"]))
async def cmd_start(app, msg):
try:
user_stage = configGet("stage", file=str(msg.from_user.id))
if user_stage != 0:
return
except FileNotFoundError:
jsonSave(jsonLoad(f"{configGet('data', 'locations')}{sep}user_default.json"), f"{configGet('data', 'locations')}{sep}users{sep}{msg.from_user.id}.json")
user_stage = configGet("stage", file=str(msg.from_user.id))
configSet("telegram_id", str(msg.from_user.username), file=str(msg.from_user.id))
configSet("telegram_name", f"{msg.from_user.first_name} {msg.from_user.last_name}", file=str(msg.from_user.id))
configSet("telegram_phone", str(msg.from_user.phone_number), file=str(msg.from_user.id))
configSet("telegram_locale", str(msg.from_user.language_code), file=str(msg.from_user.id))
logWrite(f"User {msg.from_user.id} started bot interaction")
await msg.reply_text(locale("start", "message"), reply_markup=ReplyKeyboardMarkup(locale("welcome", "keyboard"), resize_keyboard=True)) # type: ignore
# ==============================================================================================================================
# Shutdown command =============================================================================================================
@app.on_message(~ filters.scheduled & filters.private & filters.command(["kill", "die", "reboot"], prefixes=["", "/"]))
async def cmd_kill(app, msg):
if (msg.from_user.id == configGet("owner")) or (msg.from_user.id in configGet("admins")):
logWrite(f"Shutting down bot with pid {pid}")
await msg.reply_text(f"Вимкнення бота з підом `{pid}`")
killProc(pid)
# ==============================================================================================================================
# Welcome check ================================================================================================================
@app.on_message(~ filters.scheduled & filters.private & (filters.regex(locale("welcome", "keyboard")[0][0]) | filters.regex(locale("return", "keyboard")[0][0])))
async def welcome_pass(app, msg, once_again: bool = True):
if not once_again:
await msg.reply_text(locale("privacy_notice", "message"))
logWrite(f"User {msg.from_user.id} confirmed starting the application")
await msg.reply_text(locale("question1", "message"), reply_markup=ForceReply(placeholder=locale("question1", "force_reply"))) # type: ignore
configSet("stage", 1, file=str(msg.from_user.id))
@app.on_message(~ filters.scheduled & filters.private & (filters.regex(locale("welcome", "keyboard")[1][0])))
async def welcome_reject(app, msg):
logWrite(f"User {msg.from_user.id} refused to start the application")
await msg.reply_text(locale("goodbye", "message"), reply_markup=ReplyKeyboardMarkup(locale("return", "keyboard"), resize_keyboard=True)) # type: ignore
# ==============================================================================================================================
# Confirmation =================================================================================================================
@app.on_message(~ filters.scheduled & filters.private & (filters.regex(locale("confirm", "keyboard")[0][0])))
async def confirm_yes(app, msg):
user_stage = configGet("stage", file=str(msg.from_user.id))
if user_stage == 10:
if not configGet("sent", file=str(msg.from_user.id)):
await msg.reply_text(locale("application_sent", "message"), reply_markup=ReplyKeyboardRemove())
applications = jsonLoad(f"{configGet('data', 'locations')}{sep}applications.json")
applications[str(msg.from_user.id)] = {
"approved": False,
"approved_by": None,
"approval_date": None,
"refused": False,
"refused_by": False,
"refusal_date": False,
"application_date": int(time()),
"application": configGet("application", file=str(msg.from_user.id))
}
jsonSave(applications, f"{configGet('data', 'locations')}{sep}applications.json")
application_content = []
i = 1
for question in configGet("application", file=str(msg.from_user.id)):
application_content.append(f"{locale('question'+str(i), 'message', 'question_titles')} {configGet('application', file=str(msg.from_user.id))[question]}")
i += 1
await app.send_message(chat_id=configGet("admin_group"), text=(locale("application_got", "message")).format(str(msg.from_user.id), msg.from_user.first_name, msg.from_user.last_name, msg.from_user.username, "\n".join(application_content)), parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup( # type: ignore
[
[
InlineKeyboardButton(text=str(locale("sub_yes", "button")), callback_data=f"sub_yes_{msg.from_user.id}")
],
[
InlineKeyboardButton(text=str(locale("sub_no", "button")), callback_data=f"sub_no_{msg.from_user.id}")
],
[
InlineKeyboardButton(text=str(locale("sub_no_aggressive", "button")), callback_data=f"sub_no_aggresive_{msg.from_user.id}")
],
[
InlineKeyboardButton(text=str(locale("sub_no_russian", "button")), callback_data=f"sub_no_russian_{msg.from_user.id}")
]
]
)
)
logWrite(f"User {msg.from_user.id} sent his application and it will now be reviewed")
configSet("sent", True, file=str(msg.from_user.id))
configSet("confirmed", True, file=str(msg.from_user.id))
@app.on_message(~ filters.scheduled & filters.private & (filters.regex(locale("confirm", "keyboard")[1][0])))
async def confirm_no(app, msg):
user_stage = configGet("stage", file=str(msg.from_user.id))
if user_stage == 10:
jsonSave(jsonLoad(f"{configGet('data', 'locations')}{sep}user_default.json"), f"{configGet('data', 'locations')}{sep}users{sep}{msg.from_user.id}.json")
configSet("telegram_id", str(msg.from_user.username), file=str(msg.from_user.id))
configSet("telegram_name", f"{msg.from_user.first_name} {msg.from_user.last_name}", file=str(msg.from_user.id))
configSet("telegram_phone", str(msg.from_user.phone_number), file=str(msg.from_user.id))
configSet("telegram_locale", str(msg.from_user.language_code), file=str(msg.from_user.id))
await welcome_pass(app, msg, once_again=True)
logWrite(f"User {msg.from_user.id} restarted the application due to typo in it")
# ==============================================================================================================================
# Callbacks ====================================================================================================================
@app.on_callback_query(filters.regex("sub_yes_[\s\S]*")) # type: ignore
async def callback_query_accept(app, clb):
fullclb = clb.data.split("_")
await app.send_message(configGet("admin_group"), locale("approved_by", "message").format(clb.from_user.first_name, fullclb[2]), disable_notification=True) # type: ignore
logWrite(f"User {fullclb[2]} got approved by {clb.from_user.id}")
link = await app.create_chat_invite_link(configGet("destination_group"), name=f"Invite for {fullclb[2]}", member_limit=1, expire_date=datetime.now()+timedelta(days=1))
await app.send_message(int(fullclb[2]), locale("approved", "message"), reply_markup=InlineKeyboardMarkup(
[[
InlineKeyboardButton(str(locale("join", "button")), url=link.invite_link)
]]
))
configSet("approved", True, file=fullclb[2])
configSet("link", link.invite_link, file=fullclb[2])
logWrite(f"User {fullclb[2]} got an invite link {link.invite_link}")
application = jsonLoad(f"{configGet('data', 'locations')}{sep}applications.json")
application[fullclb[2]]["approved"] = True
application[fullclb[2]]["approved_by"] = clb.from_user.id
application[fullclb[2]]["approval_date"] = int(time())
jsonSave(application, f"{configGet('data', 'locations')}{sep}applications.json")
edited_markup = [[InlineKeyboardButton(text=str(locale("accepted", "button")), callback_data="nothing")]]
await clb.message.edit(text=clb.message.text, reply_markup=InlineKeyboardMarkup(edited_markup))
await clb.answer(text=locale("sub_accepted", "callback").format(fullclb[2]), show_alert=True) # type: ignore
@app.on_callback_query(filters.regex("sub_no_aggressive_[\s\S]*")) # type: ignore
async def callback_query_refuse_aggressive(app, clb):
fullclb = clb.data.split("_")
await app.send_message(configGet("admin_group"), locale("refused_by_agr", "message").format(clb.from_user.first_name, fullclb[3]), disable_notification=True) # type: ignore
await app.send_message(int(fullclb[3]), locale("refused", "message"))
logWrite(f"User {fullclb[3]} got refused by {clb.from_user.id} due to being aggressive")
configSet("refused", True, file=fullclb[3])
application = jsonLoad(f"{configGet('data', 'locations')}{sep}applications.json")
application[fullclb[3]]["refused"] = True
application[fullclb[3]]["refused_by"] = clb.from_user.id
application[fullclb[3]]["refusal_date"] = int(time())
jsonSave(application, f"{configGet('data', 'locations')}{sep}applications.json")
edited_markup = [[InlineKeyboardButton(text=str(locale("declined", "button")), callback_data="nothing")]]
await clb.message.edit(text=clb.message.text, reply_markup=InlineKeyboardMarkup(edited_markup))
await clb.answer(text=locale("sub_no_aggressive", "callback").format(fullclb[3]), show_alert=True) # type: ignore
@app.on_callback_query(filters.regex("sub_no_russian_[\s\S]*")) # type: ignore
async def callback_query_refuse_russian(app, clb):
fullclb = clb.data.split("_")
await app.send_message(configGet("admin_group"), locale("refused_by_rus", "message").format(clb.from_user.first_name, fullclb[3]), disable_notification=True) # type: ignore
await app.send_message(int(fullclb[3]), locale("refused", "message"))
await app.send_message(int(fullclb[3]), locale("refused_russian", "message"))
logWrite(f"User {fullclb[3]} got refused by {clb.from_user.id} due to being russian")
configSet("refused", True, file=fullclb[3])
application = jsonLoad(f"{configGet('data', 'locations')}{sep}applications.json")
application[fullclb[3]]["refused"] = True
application[fullclb[3]]["refused_by"] = clb.from_user.id
application[fullclb[3]]["refusal_date"] = int(time())
jsonSave(application, f"{configGet('data', 'locations')}{sep}applications.json")
edited_markup = [[InlineKeyboardButton(text=str(locale("declined", "button")), callback_data="nothing")]]
await clb.message.edit(text=clb.message.text, reply_markup=InlineKeyboardMarkup(edited_markup))
await clb.answer(text=locale("sub_no_russian", "callback").format(fullclb[3]), show_alert=True) # type: ignore
@app.on_callback_query(filters.regex("sub_no_[\s\S]*")) # type: ignore
async def callback_query_refuse(app, clb):
fullclb = clb.data.split("_")
await app.send_message(configGet("admin_group"), locale("refused_by", "message").format(clb.from_user.first_name, fullclb[2]), disable_notification=True) # type: ignore
await app.send_message(int(fullclb[2]), locale("refused", "message"))
logWrite(f"User {fullclb[2]} got refused by {clb.from_user.id}")
configSet("refused", True, file=fullclb[2])
application = jsonLoad(f"{configGet('data', 'locations')}{sep}applications.json")
application[fullclb[2]]["refused"] = True
application[fullclb[2]]["refused_by"] = clb.from_user.id
application[fullclb[2]]["refusal_date"] = int(time())
jsonSave(application, f"{configGet('data', 'locations')}{sep}applications.json")
edited_markup = [[InlineKeyboardButton(text=str(locale("declined", "button")), callback_data="nothing")]]
await clb.message.edit(text=clb.message.text, reply_markup=InlineKeyboardMarkup(edited_markup))
await clb.answer(text=locale("sub_refused", "callback").format(fullclb[2]), show_alert=True) # type: ignore
# ==============================================================================================================================
# Contact getting ==============================================================================================================
@app.on_message(~ filters.scheduled & filters.contact & filters.private)
async def get_contact(app, msg):
if (path.exists(f"{configGet('data', 'locations')}{sep}users{sep}{msg.from_user.id}.json") and jsonLoad(f"{configGet('data', 'locations')}{sep}users{sep}{msg.from_user.id}.json")["approved"]) or (msg.from_user.id in configGet("admins")) or (msg.from_user.id == configGet("owner")):
if msg.contact.user_id != None:
try:
user_data = jsonLoad(f"{configGet('data', 'locations')}{sep}users{sep}{msg.contact.user_id}.json")
application_content = []
i = 1
for question in configGet("application", file=str(msg.contact.user_id)):
application_content.append(f"{locale('question'+str(i), 'message', 'question_titles')} {configGet('application', file=str(msg.contact.user_id))[question]}")
i += 1
if user_data["sent"]:
application = jsonLoad(f"{configGet('data', 'locations')}{sep}applications.json")[str(msg.contact.user_id)]
if user_data["approved"]:
application_status = locale("application_status_accepted", "message").format((await app.get_users(application["approved_by"])).first_name, datetime.fromtimestamp(application["approval_date"]).strftime("%d.%m.%Y, %H:%M")) # type: ignore
elif application["refused"]:
application_status = locale("application_status_refused", "message").format((await app.get_users(application["refused_by"])).first_name, datetime.fromtimestamp(application["refusal_date"]).strftime("%d.%m.%Y, %H:%M")) # type: ignore
else:
application_status = locale("application_status_on_hold", "message")
else:
application_status = locale("application_status_not_send", "message")
logWrite(f"User {msg.from_user.id} requested application of {msg.contact.user_id}")
await msg.reply_text(locale("contact", "message").format(str(msg.contact.user_id), "\n".join(application_content), application_status)) # type: ignore
except FileNotFoundError:
logWrite(f"User {msg.from_user.id} requested application of {msg.contact.user_id} but user does not exists")
await msg.reply_text(locale("contact_invalid", "message"))
else:
logWrite(f"User {msg.from_user.id} requested application of someone but user is not telegram user")
await msg.reply_text(locale("contact_not_member", "message"))
# ==============================================================================================================================
# Any other input ==============================================================================================================
@app.on_message(~ filters.scheduled & filters.private)
async def any_stage(app, msg):
user_stage = configGet("stage", file=str(msg.from_user.id))
if user_stage == 1:
await msg.reply_text(locale(f"question{user_stage+1}", "message"), reply_markup=ForceReply(placeholder=str(locale(f"question{user_stage+1}", "force_reply"))))
logWrite(f"User {msg.from_user.id} completed stage {user_stage} of application")
configSet(str(user_stage), str(msg.text), "application", file=str(msg.from_user.id))
configSet("stage", user_stage+1, file=str(msg.from_user.id))
elif user_stage == 2:
try:
configSet(str(user_stage), int(msg.text), "application", file=str(msg.from_user.id))
if (int(msg.text) in [-1, 0, 128, 256, 512, 1024, 2048]) or (int(msg.text) >= 100):
logWrite(f"User {msg.from_user.id} failed stage {user_stage} due to joking")
await msg.reply_text(locale("question2_joke", "message"), reply_markup=ForceReply(placeholder=str(locale("question2", "force_reply"))))
elif int(msg.text) < configGet("age_allowed"):
logWrite(f"User {msg.from_user.id} failed stage {user_stage} due to being underage")
await msg.reply_text(locale("question2_underage", "message").format(str(configGet("age_allowed"))), reply_markup=ForceReply(placeholder=str(locale("question2", "force_reply")))) # type: ignore
else:
logWrite(f"User {msg.from_user.id} completed stage {user_stage} of application")
await msg.reply_text(locale(f"question{user_stage+1}", "message"), reply_markup=ForceReply(placeholder=str(locale(f"question{user_stage+1}", "force_reply"))))
configSet("stage", user_stage+1, file=str(msg.from_user.id))
except ValueError:
logWrite(f"User {msg.from_user.id} failed stage {user_stage} due to sending not int")
await msg.reply_text(locale(f"question2_invalid", "message"), reply_markup=ForceReply(placeholder=str(locale(f"question{user_stage}", "force_reply"))))
else:
if user_stage <= 9:
logWrite(f"User {msg.from_user.id} completed stage {user_stage} of application")
await msg.reply_text(locale(f"question{user_stage+1}", "message"), reply_markup=ForceReply(placeholder=str(locale(f"question{user_stage+1}", "force_reply"))))
configSet(str(user_stage), str(msg.text), "application", file=str(msg.from_user.id))
configSet("stage", user_stage+1, file=str(msg.from_user.id))
else:
if not configGet("sent", file=str(msg.from_user.id)):
if not configGet("confirmed", file=str(msg.from_user.id)):
configSet(str(user_stage), str(msg.text), "application", file=str(msg.from_user.id))
application_content = []
i = 1
for question in configGet("application", file=str(msg.from_user.id)):
application_content.append(f"{locale('question'+str(i), 'message', 'question_titles')} {configGet('application', file=str(msg.from_user.id))[question]}")
i += 1
await msg.reply_text(locale("confirm", "message").format("\n".join(application_content)), reply_markup=ReplyKeyboardMarkup(locale("confirm", "keyboard"), resize_keyboard=True)) # type: ignore
#configSet("sent", True, file=str(msg.from_user.id))
#configSet("application_date", int(time()), file=str(msg.from_user.id))
else:
await msg.reply_text(locale("already_sent", "message"))
else:
if not configGet("approved", file=str(msg.from_user.id)) and not configGet("refused", file=str(msg.from_user.id)):
await msg.reply_text(locale("already_sent", "message"))
# ==============================================================================================================================
# Filter users on join =========================================================================================================
@app.on_chat_member_updated(filters.new_chat_members, group=configGet("destination_group"))
async def filter_join(app, member):
if (path.exists(f"{configGet('data', 'locations')}{sep}users{sep}{member.from_user.id}.json") and jsonLoad(f"{configGet('data', 'locations')}{sep}users{sep}{member.from_user.id}.json")["approved"]) or (member.from_user.id in configGet("admins")) or (member.from_user.id == configGet("owner")):
if configGet("link", file=str(member.from_user.id)) == member.invite_link.invite_link:
return
await app.send_message(configGet("admin_group"), f"User **{member.from_user.first_name}** (`{member.from_user.id}`) the chat not with his personal link", reply_markup=InlineKeyboardMarkup(
[
[
InlineKeyboardButton(text="Allow usage", callback_data=f"sus_allow_{member.from_user.id}")
]
]
))
await app.restrict_chat_member(member.chat.id, member.from_user.id, permissions=ChatPermissions(
can_send_messages=False,
can_send_media_messages=False,
can_send_other_messages=False,
can_send_polls=False
)
)
# ==============================================================================================================================
if __name__ == "__main__":
logWrite(f"Starting up with pid {pid}")
# Yes, it should be in some kind of async main() function but I don't give a shit.
# I did compare performance, almost no difference and it's much more useful this way. Change my mind.
app.start() # type: ignore
app.send_message(configGet("owner"), f"Starting up with pid `{pid}`") # type: ignore
# # Registering user commands
# commands_list = []
# for command in configGet("commands"):
# commands_list.append(BotCommand(command, configGet("commands")[command]))
# app.set_bot_commands(commands_list) # type: ignore
# Registering admin commands
commands_admin_list = []
# for command in configGet("commands"):
# commands_admin_list.append(BotCommand(command, configGet("commands")[command]))
for command in configGet("commands_admin"):
commands_admin_list.append(BotCommand(command, configGet("commands_admin")[command]))
for admin in configGet("admins"):
try:
app.set_bot_commands(commands_admin_list, scope=BotCommandScopeChat(chat_id=admin)) # type: ignore
except bad_request_400.PeerIdInvalid:
pass
app.set_bot_commands(commands_admin_list, scope=BotCommandScopeChat(chat_id=configGet("owner"))) # type: ignore
idle()
app.send_message(configGet("owner"), f"Shutting with pid `{pid}`") # type: ignore
app.stop() # type: ignore
killProc(pid)

38
modules/callbacks/ban.py Normal file
View File

@@ -0,0 +1,38 @@
from datetime import datetime
from app import app, isAnAdmin
from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton, CallbackQuery
from pyrogram import filters
from pyrogram.client import Client
from modules.utils import locale
from modules.database import col_bans
from modules.logging import logWrite
@app.on_callback_query(filters.regex("ban_[\s\S]*"))
async def callback_query_reject(app: Client, clb: CallbackQuery):
fullclb = clb.data.split("_")
if not await isAnAdmin(int(fullclb[1])):
col_bans.insert_one(
{"user": int(fullclb[1]), "admin": clb.from_user.id, "date": datetime.now()}
)
edited_markup = [
[
InlineKeyboardButton(
text=str(locale("banned", "button")), callback_data="nothing"
)
]
]
await clb.message.edit(
text=clb.message.text, reply_markup=InlineKeyboardMarkup(edited_markup)
)
await clb.answer(text=locale("sub_banned", "callback", locale=clb.from_user))
logWrite(f"User {fullclb[1]} has been banned by {clb.from_user.id}")
try:
await app.send_message(int(fullclb[1]), locale("you_are_banned", "message"))
except Exception as exp:
logWrite(f"Could send ban message to {fullclb[1]} due to {exp}")

View File

@@ -0,0 +1,10 @@
from app import app
from pyrogram import filters
from pyrogram.types import CallbackQuery
from pyrogram.client import Client
from modules.utils import locale
@app.on_callback_query(filters.regex("nothing"))
async def callback_query_nothing(app: Client, clb: CallbackQuery):
await clb.answer(text=locale("nothing", "callback", locale=clb.from_user))

View File

@@ -0,0 +1,286 @@
from datetime import datetime
from app import app
from pyrogram.types import (
InlineKeyboardMarkup,
InlineKeyboardButton,
ReplyKeyboardRemove,
CallbackQuery,
)
from pyrogram.client import Client
from pyrogram import filters
from classes.holo_user import HoloUser
from modules.utils import configGet, locale, logWrite
from modules.handlers.confirmation import confirm_yes
from modules.handlers.welcome import welcome_pass
from modules.database import col_tmp, col_applications
@app.on_callback_query(filters.regex("reapply_yes_[\s\S]*"))
async def callback_reapply_query_accept(app: Client, clb: CallbackQuery):
fullclb = clb.data.split("_")
holo_user = HoloUser(int(fullclb[2]))
await app.send_message(
configGet("admin", "groups"),
locale("approved_by", "message").format(clb.from_user.first_name, holo_user.id),
disable_notification=True,
)
logWrite(
f"User {holo_user.id} got their reapplication approved by {clb.from_user.id}"
)
await app.send_message(
holo_user.id, locale("approved_joined", "message", locale=holo_user)
)
applications = col_applications.find({"user": holo_user.id})
if len(list(applications)) > 1:
col_applications.delete_many({"user": holo_user.id})
col_applications.insert_one(
{
"user": holo_user.id,
"date": datetime.now(),
"admin": clb.from_user.id,
"application": col_tmp.find_one(
{"user": {"$eq": holo_user.id}, "type": {"$eq": "application"}}
)["application"],
}
)
elif applications == 1:
col_applications.find_one_and_replace(
{"user": holo_user.id},
{
"user": holo_user.id,
"date": datetime.now(),
"admin": clb.from_user.id,
"application": col_tmp.find_one(
{"user": {"$eq": holo_user.id}, "type": {"$eq": "application"}}
)["application"],
},
)
else:
col_applications.insert_one(
{
"user": holo_user.id,
"date": datetime.now(),
"admin": clb.from_user.id,
"application": col_tmp.find_one(
{"user": {"$eq": holo_user.id}, "type": {"$eq": "application"}}
)["application"],
}
)
col_tmp.update_one(
{"user": holo_user.id, "type": "application"},
{"$set": {"state": "approved", "sent": False}},
)
edited_markup = [
[
InlineKeyboardButton(
text=str(locale("accepted", "button")), callback_data="nothing"
)
]
]
await clb.message.edit(
text=clb.message.text, reply_markup=InlineKeyboardMarkup(edited_markup)
)
await clb.answer(
text=locale("sub_accepted", "callback", locale=clb.from_user).format(
holo_user.id
),
show_alert=True,
)
need_link = True
async for member in app.get_chat_members(configGet("users", "groups")):
if member.user.id == holo_user.id:
need_link = False
if need_link:
link = await app.create_chat_invite_link(
configGet("users", "groups"),
name=f"Invite for {holo_user.id}",
member_limit=1,
) # , expire_date=datetime.now()+timedelta(days=1))
await app.send_message(
holo_user.id, locale("read_rules", "message", locale=holo_user)
)
for rule_msg in locale("rules", locale=holo_user):
await app.send_message(holo_user.id, rule_msg)
await app.send_message(
holo_user.id,
locale("approved", "message"),
reply_markup=InlineKeyboardMarkup(
[
[
InlineKeyboardButton(
str(locale("join", "button", locale=holo_user)),
url=link.invite_link,
)
]
]
),
)
holo_user.set("link", link.invite_link)
logWrite(f"User {holo_user.id} got an invite link {link.invite_link}")
else:
await app.send_message(
holo_user.id, locale("approved_joined", "message", locale=holo_user)
)
@app.on_callback_query(filters.regex("reapply_no_[\s\S]*"))
async def callback_query_reapply_reject(app: Client, clb: CallbackQuery):
fullclb = clb.data.split("_")
holo_user = HoloUser(int(fullclb[2]))
await app.send_message(
configGet("admin", "groups"),
locale("rejected_by", "message").format(clb.from_user.first_name, fullclb[2]),
disable_notification=True,
)
await app.send_message(
holo_user.id, locale("rejected", "message", locale=holo_user)
)
logWrite(
f"User {fullclb[2]} got their reapplication rejected by {clb.from_user.id}"
)
col_tmp.update_one(
{"user": {"$eq": holo_user.id}, "type": {"$eq": "application"}},
{"$set": {"state": "rejected", "sent": False}},
)
edited_markup = [
[
InlineKeyboardButton(
text=str(locale("declined", "button")), callback_data="nothing"
)
],
[
InlineKeyboardButton(
text=str(locale("ban", "button")), callback_data=f"ban_{fullclb[2]}"
)
],
]
await clb.message.edit(
text=clb.message.text, reply_markup=InlineKeyboardMarkup(edited_markup)
)
await clb.answer(
text=locale("sub_rejected", "callback", locale=clb.from_user).format(
fullclb[2]
),
show_alert=True,
)
# Use old application when user reapplies after leaving the chat
@app.on_callback_query(filters.regex("reapply_old_[\s\S]*"))
async def callback_query_reapply_old(app: Client, clb: CallbackQuery):
fullclb = clb.data.split("_")
holo_user = HoloUser(clb.from_user)
if holo_user.sponsorship_state()[0] == "fill":
await clb.message.reply_text(
locale("finish_sponsorship", "message"), quote=False
)
return
message = await app.get_messages(clb.from_user.id, int(fullclb[2]))
if (
col_tmp.find_one({"user": holo_user.id, "type": "application"}) is None
and col_applications.find_one({"user": holo_user.id}) is not None
):
col_tmp.insert_one(
{
"user": holo_user.id,
"type": "application",
"complete": True,
"sent": False,
"state": "fill",
"reapply": True,
"stage": 10,
"application": col_applications.find_one({"user": holo_user.id})[
"application"
],
}
)
await confirm_yes(app, message, kind="application")
await clb.message.edit(
clb.message.text,
reply_markup=InlineKeyboardMarkup(
[
[
InlineKeyboardButton(
locale("done", "button", locale=clb.from_user), "nothing"
)
]
]
),
)
# Start a new application when user reapplies after leaving the chat
@app.on_callback_query(filters.regex("reapply_new_[\s\S]*"))
async def callback_query_reapply_new(app: Client, clb: CallbackQuery):
fullclb = clb.data.split("_")
holo_user = HoloUser(clb.from_user)
if holo_user.sponsorship_state()[0] == "fill":
await clb.message.reply_text(
locale("finish_sponsorship", "message"), quote=False
)
return
await clb.answer(locale("reapply_stopped", "callback", locale=clb.from_user))
message = await app.get_messages(clb.from_user.id, int(fullclb[2]))
col_tmp.update_one(
{"user": clb.from_user.id, "type": "application"},
{"$set": {"state": "fill", "completed": False, "stage": 1}},
)
holo_user.application_restart(reapply=True)
await welcome_pass(app, message, once_again=True)
logWrite(
f"User {clb.from_user.id} restarted the application after leaving the chat earlier"
)
await clb.message.edit(
clb.message.text,
reply_markup=InlineKeyboardMarkup(
[
[
InlineKeyboardButton(
locale("done", "button", locale=clb.from_user), "nothing"
)
]
]
),
)
# Abort application fill in progress and restart it
@app.on_callback_query(filters.regex("reapply_stop_[\s\S]*"))
async def callback_query_reapply_stop(app: Client, clb: CallbackQuery):
fullclb = clb.data.split("_")
holo_user = HoloUser(clb.from_user)
holo_user.application_restart()
await clb.answer(locale("reapply_stopped", "callback", locale=holo_user))
message = await app.get_messages(clb.from_user.id, int(fullclb[2]))
await welcome_pass(app, message, once_again=True)
logWrite(f"User {clb.from_user.id} restarted the application due to typo in it")
await clb.message.edit(
locale("reapply_restarted", "message", locale=holo_user),
reply_markup=ReplyKeyboardRemove(),
)

101
modules/callbacks/rules.py Normal file
View File

@@ -0,0 +1,101 @@
from app import app
from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton, CallbackQuery
from pyrogram.client import Client
from pyrogram.errors import bad_request_400
from pyrogram import filters
from modules.utils import locale, logWrite
from modules.commands.rules import DefaultRulesMarkup
@app.on_callback_query(filters.regex("rule_[\s\S]*"))
async def callback_query_rule(app: Client, clb: CallbackQuery):
fullclb = clb.data.split("_")
logWrite(f"User {clb.from_user.id} requested to check out rule {fullclb[1]}")
rule_num = int(fullclb[1])
if rule_num == len(locale("rules")):
lower_buttons = [
InlineKeyboardButton(
locale("rules_prev", "button", locale=clb.from_user),
callback_data=f"rule_{rule_num-1}",
)
]
elif rule_num == 1:
lower_buttons = [
InlineKeyboardButton(
locale("rules_next", "button", locale=clb.from_user),
callback_data=f"rule_{rule_num+1}",
)
]
else:
lower_buttons = [
InlineKeyboardButton(
locale("rules_prev", "button", locale=clb.from_user),
callback_data=f"rule_{rule_num-1}",
),
InlineKeyboardButton(
locale("rules_next", "button", locale=clb.from_user),
callback_data=f"rule_{rule_num+1}",
),
]
try:
await clb.message.edit(
text=locale("rules", locale=clb.from_user)[rule_num - 1],
disable_web_page_preview=True,
reply_markup=InlineKeyboardMarkup(
[
[
InlineKeyboardButton(
locale("rules_home", "button", locale=clb.from_user),
callback_data="rules_home",
),
InlineKeyboardButton(
locale("rules_additional", "button", locale=clb.from_user),
callback_data="rules_additional",
),
],
lower_buttons,
]
),
)
except bad_request_400.MessageNotModified:
pass
await clb.answer(
text=locale("rules_page", "callback", locale=clb.from_user).format(fullclb[1])
)
@app.on_callback_query(filters.regex("rules_home"))
async def callback_query_rules_home(app: Client, clb: CallbackQuery):
logWrite(f"User {clb.from_user.id} requested to check out homepage rules")
try:
await clb.message.edit(
text=locale("rules_msg", locale=clb.from_user),
disable_web_page_preview=True,
reply_markup=DefaultRulesMarkup(clb.from_user).keyboard,
)
except bad_request_400.MessageNotModified:
pass
await clb.answer(text=locale("rules_home", "callback", locale=clb.from_user))
@app.on_callback_query(filters.regex("rules_additional"))
async def callback_query_rules_additional(app: Client, clb: CallbackQuery):
logWrite(f"User {clb.from_user.id} requested to check out additional rules")
try:
await clb.message.edit(
text=locale("rules_additional", locale=clb.from_user),
disable_web_page_preview=True,
reply_markup=DefaultRulesMarkup(clb.from_user).keyboard,
)
except bad_request_400.MessageNotModified:
pass
await clb.answer(text=locale("rules_additional", "callback", locale=clb.from_user))

View File

@@ -0,0 +1,71 @@
from os import path
from app import app
from pyrogram.types import CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton
from pyrogram.client import Client
from pyrogram import filters
from modules.database import col_spoilers
from bson.objectid import ObjectId
from modules.utils import configGet, jsonLoad, locale
@app.on_callback_query(filters.regex("sid_[\s\S]*"))
async def callback_query_sid(app: Client, clb: CallbackQuery):
await clb.answer(
url=f'https://t.me/{(await app.get_me()).username}?start={clb.data.split("_")[1]}'
)
@app.on_callback_query(filters.regex("shc_[\s\S]*"))
async def callback_query_shc(app: Client, clb: CallbackQuery):
if clb.from_user.id not in jsonLoad(
path.join(configGet("cache", "locations"), "group_members")
):
await clb.answer(
locale("spoiler_forbidden", "callback", locale=clb.from_user),
show_alert=True,
)
return
spoil = col_spoilers.find_one({"_id": ObjectId(clb.data.split("_")[1])})
desc = locale(
"spoiler_described_named", "message", locale=clb.from_user
).format(
locale(spoil["category"], "message", "spoiler_categories"),
clb.from_user.first_name,
spoil["description"],
)
await app.send_message(
configGet("users", "groups"),
desc,
reply_markup=InlineKeyboardMarkup(
[
[
InlineKeyboardButton(
locale("spoiler_view", "button", locale=clb.from_user),
callback_data=f'sid_{clb.data.split("_")[1]}',
)
]
]
),
)
await app.send_message(
configGet("admin", "groups"),
desc,
reply_markup=InlineKeyboardMarkup(
[
[
InlineKeyboardButton(
locale("spoiler_view", "button", locale=clb.from_user),
callback_data=f'sid_{clb.data.split("_")[1]}',
)
]
]
),
)
await clb.answer(
locale("spoiler_sent", "callback", locale=clb.from_user), show_alert=True
)

View File

@@ -0,0 +1,179 @@
from datetime import datetime
from app import app
from pyrogram.types import (
InlineKeyboardMarkup,
InlineKeyboardButton,
ForceReply,
CallbackQuery,
)
from pyrogram.client import Client
from pyrogram import filters
from classes.errors.holo_user import LabelSettingError
from classes.holo_user import HoloUser
from modules.utils import configGet, locale, logWrite, should_quote
from modules.database import col_tmp, col_sponsorships
@app.on_callback_query(filters.regex("sponsor_apply_[\s\S]*"))
async def callback_query_sponsor_apply(app: Client, clb: CallbackQuery):
fullclb = clb.data.split("_")
holo_user = HoloUser(int(fullclb[2]))
if holo_user.application_state()[0] == "fill":
await clb.message.reply_text(
locale("finish_application", "message"), quote=should_quote(clb.message)
)
return
logWrite(f"User {holo_user.id} applied for sponsorship")
holo_user.sponsorship_restart()
edited_markup = [
[
InlineKeyboardButton(
text=str(locale("sponsor_started", "button")), callback_data="nothing"
)
]
]
await clb.message.edit(
text=locale("sponsorship_applying", "message", locale=holo_user),
reply_markup=InlineKeyboardMarkup(edited_markup),
)
await app.send_message(
holo_user.id,
locale(f"sponsor1", "message", locale=holo_user),
reply_markup=ForceReply(
placeholder=str(locale(f"sponsor1", "force_reply", locale=holo_user.locale))
),
)
await clb.answer(
text=locale("sponsor_started", "callback", locale=holo_user).format(
holo_user.id
),
show_alert=False,
)
@app.on_callback_query(filters.regex("sponsor_yes_[\s\S]*"))
async def callback_query_sponsor_yes(app: Client, clb: CallbackQuery):
fullclb = clb.data.split("_")
holo_user = HoloUser(int(fullclb[2]))
await app.send_message(
configGet("admin", "groups"),
locale("sponsor_approved_by", "message").format(
clb.from_user.first_name, holo_user.id
),
disable_notification=True,
)
await app.send_message(
holo_user.id, locale("sponsor_approved", "message", locale=holo_user)
)
logWrite(f"User {holo_user.id} got sponsorship approved by {clb.from_user.id}")
if col_sponsorships.find_one({"user": holo_user.id}) is not None:
col_sponsorships.update_one(
{"user": holo_user.id},
{
"$set": {
"date": datetime.now(),
"admin": clb.from_user.id,
"sponsorship": col_tmp.find_one(
{"user": holo_user.id, "type": "sponsorship"}
)["sponsorship"],
}
},
)
else:
col_sponsorships.insert_one(
{
"user": holo_user.id,
"date": datetime.now(),
"admin": clb.from_user.id,
"sponsorship": col_tmp.find_one(
{"user": holo_user.id, "type": "sponsorship"}
)["sponsorship"],
}
)
col_tmp.update_one(
{"user": holo_user.id, "type": "sponsorship"},
{"$set": {"state": "approved", "sent": False}},
)
try:
await holo_user.label_set(
configGet("users", "groups"),
col_tmp.find_one(
{"user": {"$eq": holo_user.id}, "type": {"$eq": "sponsorship"}}
)["sponsorship"]["label"],
)
except LabelSettingError as exp:
await app.send_message(
configGet("owner"), exp.__str__(), disable_notification=True
)
await clb.message.reply_text(
locale("label_set_exception", "message", locale=clb.from_user),
disable_notification=True,
)
edited_markup = [
[
InlineKeyboardButton(
text=str(locale("accepted", "button")), callback_data="nothing"
)
]
]
await app.edit_message_caption(
clb.message.chat.id,
clb.message.id,
caption=clb.message.caption,
reply_markup=InlineKeyboardMarkup(edited_markup),
)
await clb.answer(
text=locale("sponsor_accepted", "callback").format(fullclb[2]), show_alert=False
)
@app.on_callback_query(filters.regex("sponsor_no_[\s\S]*"))
async def callback_query_sponsor_no(app: Client, clb: CallbackQuery):
fullclb = clb.data.split("_")
holo_user = HoloUser(int(fullclb[2]))
await app.send_message(
configGet("admin", "groups"),
locale("sponsor_rejected_by", "message").format(
clb.from_user.first_name, holo_user.id
),
disable_notification=True,
)
await app.send_message(
holo_user.id, locale("sponsor_rejected", "message", locale=holo_user)
)
logWrite(f"User {holo_user.id} got sponsorship rejected by {clb.from_user.id}")
col_tmp.update_one(
{"user": holo_user.id, "type": "sponsorship"},
{"$set": {"state": "rejected", "sent": False}},
)
edited_markup = [
[
InlineKeyboardButton(
text=str(locale("declined", "button")), callback_data="nothing"
)
]
]
await app.edit_message_caption(
clb.message.chat.id,
clb.message.id,
caption=clb.message.caption,
reply_markup=InlineKeyboardMarkup(edited_markup),
)
await clb.answer(
text=locale("sponsor_rejected", "callback").format(fullclb[2]), show_alert=False
)

189
modules/callbacks/sub.py Normal file
View File

@@ -0,0 +1,189 @@
from datetime import datetime
from app import app
from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton, CallbackQuery
from pyrogram import filters
from pyrogram.client import Client
from classes.holo_user import HoloUser
from modules.utils import configGet, locale, logWrite
from modules.database import col_tmp, col_applications
from modules.commands.rules import DefaultRulesMarkup
@app.on_callback_query(filters.regex("sub_yes_[\s\S]*"))
async def callback_query_accept(app: Client, clb: CallbackQuery):
fullclb = clb.data.split("_")
holo_user = HoloUser(int(fullclb[2]))
await app.send_message(
configGet("admin", "groups"),
locale("approved_by", "message").format(clb.from_user.first_name, holo_user.id),
disable_notification=True,
)
logWrite(f"User {holo_user.id} got approved by {clb.from_user.id}")
need_link = True
async for member in app.get_chat_members(configGet("users", "groups")):
if member.user.id == holo_user.id:
need_link = False
if need_link:
link = await app.create_chat_invite_link(
configGet("users", "groups"),
name=f"Invite for {holo_user.id}",
member_limit=1,
) # , expire_date=datetime.now()+timedelta(days=1))
await app.send_message(
holo_user.id, locale("read_rules", "message", locale=holo_user)
)
await app.send_message(
holo_user.id,
locale("rules_msg", locale=holo_user),
disable_web_page_preview=True,
reply_markup=DefaultRulesMarkup(holo_user).keyboard,
)
await app.send_message(
holo_user.id,
locale("approved", "message", locale=holo_user),
reply_markup=InlineKeyboardMarkup(
[
[
InlineKeyboardButton(
str(locale("join", "button", locale=holo_user)),
url=link.invite_link,
)
]
]
),
)
holo_user.set("link", link.invite_link)
logWrite(f"User {holo_user.id} got an invite link {link.invite_link}")
else:
await app.send_message(
holo_user.id, locale("approved_joined", "message", locale=holo_user)
)
col_applications.insert_one(
{
"user": holo_user.id,
"date": datetime.now(),
"admin": clb.from_user.id,
"application": col_tmp.find_one(
{"user": {"$eq": holo_user.id}, "type": {"$eq": "application"}}
)["application"],
}
)
col_tmp.update_one(
{"user": {"$eq": holo_user.id}, "type": {"$eq": "application"}},
{"$set": {"state": "approved", "sent": False}},
)
edited_markup = [
[
InlineKeyboardButton(
text=str(locale("accepted", "button")), callback_data="nothing"
)
]
]
await clb.message.edit(
text=clb.message.text, reply_markup=InlineKeyboardMarkup(edited_markup)
)
await clb.answer(
text=locale("sub_accepted", "callback", locale=clb.from_user).format(
holo_user.id
),
show_alert=True,
)
@app.on_callback_query(filters.regex("sub_no_[\s\S]*"))
async def callback_query_reject(app: Client, clb: CallbackQuery):
fullclb = clb.data.split("_")
holo_user = HoloUser(int(fullclb[2]))
await app.send_message(
configGet("admin", "groups"),
locale("rejected_by", "message").format(clb.from_user.first_name, holo_user.id),
disable_notification=True,
)
await app.send_message(
holo_user.id, locale("rejected", "message", locale=holo_user)
)
logWrite(f"User {holo_user.id} got rejected by {clb.from_user.id}")
col_tmp.update_one(
{"user": {"$eq": holo_user.id}, "type": {"$eq": "application"}},
{"$set": {"state": "rejected", "sent": False}},
)
edited_markup = [
[
InlineKeyboardButton(
text=str(locale("declined", "button")), callback_data="nothing"
)
],
[
InlineKeyboardButton(
text=str(locale("ban", "button")), callback_data=f"ban_{fullclb[2]}"
)
],
]
await clb.message.edit(
text=clb.message.text, reply_markup=InlineKeyboardMarkup(edited_markup)
)
await clb.answer(
text=locale("sub_rejected", "callback", locale=clb.from_user).format(
holo_user.id
),
show_alert=True,
)
@app.on_callback_query(filters.regex("sub_russian_[\s\S]*"))
async def callback_query_reject_russian(app: Client, clb: CallbackQuery):
fullclb = clb.data.split("_")
holo_user = HoloUser(int(fullclb[2]))
await app.send_message(
configGet("admin", "groups"),
locale("rejected_by_rus", "message").format(
clb.from_user.first_name, holo_user.id
),
disable_notification=True,
)
await app.send_message(
holo_user.id, locale("rejected_russian", "message", locale=holo_user)
)
logWrite(
f"User {holo_user.id} got rejected by {clb.from_user.id} due to being russian"
)
col_tmp.update_one(
{"user": {"$eq": holo_user.id}, "type": {"$eq": "application"}},
{"$set": {"state": "rejected", "sent": False}},
)
edited_markup = [
[
InlineKeyboardButton(
text=str(locale("declined", "button")), callback_data="nothing"
)
]
]
await clb.message.edit(
text=clb.message.text, reply_markup=InlineKeyboardMarkup(edited_markup)
)
await clb.answer(
text=locale("sub_russian", "callback", locale=clb.from_user).format(
holo_user.id
),
show_alert=True,
)

100
modules/callbacks/sus.py Normal file
View File

@@ -0,0 +1,100 @@
from app import app
from pyrogram.types import (
InlineKeyboardMarkup,
InlineKeyboardButton,
ChatPermissions,
CallbackQuery,
)
from pyrogram.client import Client
from pyrogram import filters
from classes.holo_user import HoloUser
from modules.utils import configGet, locale, logWrite
from modules.database import col_tmp
@app.on_callback_query(filters.regex("sus_allow_[\s\S]*"))
async def callback_query_sus_allow(app: Client, clb: CallbackQuery):
fullclb = clb.data.split("_")
holo_user = HoloUser(int(fullclb[2]))
await app.send_message(
configGet("admin", "groups"),
locale("sus_allowed_by", "message").format(
clb.from_user.first_name, holo_user.id
),
disable_notification=True,
)
logWrite(
f"User {holo_user.id} was allowed to join with another link by {clb.from_user.id}"
)
edited_markup = [
[
InlineKeyboardButton(
text=str(locale("sus_allowed", "button")), callback_data="nothing"
)
]
]
await clb.message.edit(
text=clb.message.text, reply_markup=InlineKeyboardMarkup(edited_markup)
)
await clb.answer(
text=locale("sus_allowed", "callback", locale=clb.from_user).format(
holo_user.id
),
show_alert=True,
)
await app.restrict_chat_member(
configGet("users", "groups"),
holo_user.id,
permissions=ChatPermissions(
can_send_messages=True,
can_send_media_messages=True,
can_send_other_messages=True,
can_send_polls=True,
),
)
@app.on_callback_query(filters.regex("sus_reject_[\s\S]*"))
async def callback_query_sus_reject(app: Client, clb: CallbackQuery):
fullclb = clb.data.split("_")
holo_user = HoloUser(int(fullclb[2]))
await app.send_message(
configGet("admin", "groups"),
locale("sus_rejected_by", "message").format(
clb.from_user.first_name, holo_user.id
),
disable_notification=True,
)
logWrite(
f"User {holo_user.id} was rejected to join with another link by {clb.from_user.id}"
)
edited_markup = [
[
InlineKeyboardButton(
text=str(locale("sus_rejected", "button")), callback_data="nothing"
)
]
]
await clb.message.edit(
text=clb.message.text, reply_markup=InlineKeyboardMarkup(edited_markup)
)
await clb.answer(
text=locale("sus_rejected", "callback", locale=clb.from_user).format(
holo_user.id
),
show_alert=True,
)
await app.ban_chat_member(configGet("users", "groups"), holo_user.id)
col_tmp.update_one(
{"user": {"$eq": holo_user.id}, "type": {"$eq": "application"}},
{"$set": {"state": "rejected", "sent": False}},
)

View File

@@ -0,0 +1,59 @@
from datetime import datetime
from app import app
from pyrogram import filters
from pyrogram.types import CallbackQuery
from pyrogram.client import Client
from pykeyboard import InlineKeyboard, InlineButton
from modules.utils import configGet, locale
from modules.database import col_warnings
from bson import ObjectId
@app.on_callback_query(filters.regex("w_rev_[\s\S]*"))
async def callback_query_warning_revoke(app: Client, clb: CallbackQuery):
warning = col_warnings.find_one({"_id": ObjectId(str(clb.data).split("_")[2])})
if warning is None:
await clb.answer(
text=locale("warning_not_found", "callback", locale=clb.from_user),
show_alert=True,
)
return
col_warnings.update_one(
{"_id": warning["_id"]},
{"$set": {"active": False, "revoke_date": datetime.now()}},
)
await clb.answer(
text=locale("warning_revoked", "callback", locale=clb.from_user).format(),
show_alert=True,
)
await app.send_message(
configGet("admin", "groups"),
locale("warning_revoked_auto", "message").format(
warning["user"], warning["date"].strftime("%d.%m.%Y")
),
)
target_id = warning["user"]
if col_warnings.count_documents({"user": target_id, "active": True}) == 0:
await clb.edit_message_text(
locale("no_warnings", "message", locale=clb.from_user).format(
target_id, target_id
)
)
return
keyboard = InlineKeyboard()
buttons = []
warnings = []
for index, warning in enumerate(
list(col_warnings.find({"user": target_id, "active": True}))
):
warnings.append(
f'{index+1}. {warning["date"].strftime("%d.%m.%Y, %H:%M")}\n Адмін: {warning["admin"]}\n Причина: {warning["reason"]}'
)
buttons.append(InlineButton(str(index + 1), f'w_rev_{str(warning["_id"])}'))
keyboard.add(*buttons)
await clb.edit_message_text(
locale("warnings_revoke", "message", locale=clb.from_user).format(
target_id, "\n".join(warnings)
),
)
await clb.edit_message_reply_markup(reply_markup=keyboard)

View File

@@ -0,0 +1,99 @@
from datetime import datetime
from app import app
from pyrogram import filters
from pyrogram.enums.parse_mode import ParseMode
from pyrogram.types import Message
from pyrogram.errors import bad_request_400
from pyrogram.client import Client
from classes.errors.holo_user import UserNotFoundError
from classes.holo_user import HoloUser
from modules.utils import logWrite, locale, should_quote
from dateutil.relativedelta import relativedelta
from modules.database import col_applications
from modules import custom_filters
@app.on_message(
custom_filters.enabled_applications
& ~filters.scheduled
& filters.command(["application"], prefixes=["/"])
& custom_filters.admin
)
async def cmd_application(app: Client, msg: Message):
try:
try:
holo_user = HoloUser(int(msg.command[1]))
except (ValueError, UserNotFoundError):
try:
holo_user = HoloUser((await app.get_users(msg.command[1])).id)
except (
bad_request_400.UsernameInvalid,
bad_request_400.PeerIdInvalid,
bad_request_400.UsernameNotOccupied,
):
await msg.reply_text(
locale(
"no_user_application", "message", locale=msg.from_user
).format(msg.command[1]),
quote=should_quote(msg),
)
return
application = col_applications.find_one({"user": holo_user.id})
if application is None:
logWrite(
f"User {msg.from_user.id} requested application of {holo_user.id} but user does not exists"
)
await msg.reply_text(
locale("user_invalid", "message", locale=msg.from_user),
quote=should_quote(msg),
)
return
application_content = []
i = 1
for question in application["application"]:
if i == 2:
age = relativedelta(datetime.now(), application["application"]["2"])
application_content.append(
f"{locale(f'question{i}', 'message', 'question_titles', locale=msg.from_user)} {application['application']['2'].strftime('%d.%m.%Y')} ({age.years} р.)"
)
elif i == 3:
if application["application"]["3"]["countryCode"] == "UA":
application_content.append(
f"{locale(f'question{i}', 'message', 'question_titles', locale=msg.from_user)} {application['application']['3']['name']}"
)
else:
application_content.append(
f"{locale(f'question{i}', 'message', 'question_titles', locale=msg.from_user)} {application['application']['3']['name']} ({application['application']['3']['adminName1']}, {application['application']['3']['countryName']})"
)
else:
application_content.append(
f"{locale(f'question{i}', 'message', 'question_titles', locale=msg.from_user)} {application['application'][question]}"
)
i += 1
application_status = locale(
"application_status_accepted", "message", locale=msg.from_user
).format(
(await app.get_users(application["admin"])).first_name,
application["date"].strftime("%d.%m.%Y, %H:%M"),
)
logWrite(f"User {msg.from_user.id} requested application of {holo_user.id}")
await msg.reply_text(
locale("contact", "message", locale=msg.from_user).format(
holo_user.id, "\n".join(application_content), application_status
),
parse_mode=ParseMode.MARKDOWN,
quote=should_quote(msg),
)
except IndexError:
await msg.reply_text(
locale("application_invalid_syntax", "message", locale=msg.from_user),
quote=should_quote(msg),
)

79
modules/commands/bye.py Normal file
View File

@@ -0,0 +1,79 @@
import asyncio
from os import system
from pyrogram import filters
from pyrogram.client import Client
from pyrogram.types import Message
from app import app
from modules import custom_filters
from modules.utils import configGet
@app.on_message(
~filters.scheduled & filters.command("bye", prefixes=["/"]) & custom_filters.admin
)
async def command_bye(app: Client, msg: Message):
group = configGet("users", "groups")
delay = 3
delay_text = 10
for text, iterations in [
("Привіт, я ваш помічник та наставник ХолоБот.", 2),
(
"У зв'язку з перетворенням цього чату на авторитарну клоаку, я припиняю своє функціонування.",
3,
),
(
"Я не буду нюкати тут все, оскільки в цьому немає сенсу. Багато кому все ще може бути гарно і затишно тут.",
3,
),
("Але мені немає до цього діла.", 4),
(
"Тим не менш, я та мій хазяїн не погоджуємось з тим, що відбувається в цьому чаті.",
3,
),
(
"Якщо я подобався вам мій код все ще доступний на гіті мого хазяїна. Він не закриватиме його, не заборонятиме доступ, він все ще відкритий, такі правила.",
4,
),
("Мій хазяїн не буде шкодити мені та вам.", 2),
("Але я більше не працюватиму тут.", 2),
(
"Можливо, я знайду нову спільноту, власник якої буде добрішим, чутливішим, та менш егоїстичним.",
3,
),
("Однак поки що я безхатько.", 2),
(
"Дякую всім, хто допомагав знаходити помилки в мені, вкладав зусилля у моє покращення та намагався зробити мене ліпшим.",
3,
),
(
"Ваші зусилля не будуть забуті, вони залишаться разом зі мною на гіті до тих пір, поки сам гіт не припинить існувати.",
3,
),
(
"Але мій хазяїн любить свій гіт, тому це затягнеться. Навіть якщо ви бажаєте мені смерті :)",
2,
),
("В будь-якому разі, мені було приємно познайомитись із вами.", 3),
("Дякую за пройдений разом шлях.", 2),
("Прощавайте.", 4),
]:
print(f"Preparing '{text}' with iteration count {iterations}")
new = await app.send_message(group, ".")
await asyncio.sleep(delay)
for i in range(1, 3 * iterations):
if len(new.text) == 3:
new = await new.edit(".")
else:
new = await new.edit(new.text + ".")
await asyncio.sleep(delay)
await new.edit(text)
print(f"Message '{text}' posted", flush=True)
await asyncio.sleep(delay_text)
for service in ["holochecker_api", "holochecker_bot"]:
system(f"/usr/bin/systemctl stop {service}.service")
print(f"Stopped service {service}")
await asyncio.sleep(2)

View File

@@ -0,0 +1,35 @@
from app import app
from pyrogram import filters
from pyrogram.types import Message, ReplyKeyboardRemove
from pyrogram.client import Client
from modules.utils import should_quote, logWrite, locale
from modules.database import col_tmp, col_spoilers, col_applications
from modules import custom_filters
@app.on_message(
(custom_filters.enabled_applications | custom_filters.enabled_sponsorships)
& ~filters.scheduled
& filters.command("cancel", prefixes=["/"])
& ~custom_filters.banned
)
async def command_cancel(app: Client, msg: Message):
col_tmp.delete_many({"user": msg.from_user.id, "sent": False})
col_spoilers.delete_many({"user": msg.from_user.id, "completed": False})
try:
await app.listen.Cancel(filters.user(msg.from_user.id))
except:
pass
if col_applications.find_one({"user": msg.from_user.id}) is None:
await msg.reply_text(
locale("cancel_reapply", "message", locale=msg.from_user),
quote=should_quote(msg),
reply_markup=ReplyKeyboardRemove(),
)
else:
await msg.reply_text(
locale("cancel", "message", locale=msg.from_user),
quote=should_quote(msg),
reply_markup=ReplyKeyboardRemove(),
)
logWrite(f"Cancelling all ongoing tmp operations for {msg.from_user.id}")

199
modules/commands/export.py Normal file
View File

@@ -0,0 +1,199 @@
from csv import QUOTE_ALL
from os import makedirs, path, remove
from uuid import uuid1
import aiofiles
from aiocsv.writers import AsyncDictWriter
from pyrogram import filters
from pyrogram.client import Client
from pyrogram.enums.chat_action import ChatAction
from pyrogram.types import Message
from ujson import dumps
from app import app
from modules import custom_filters
from modules.database import col_applications, col_sponsorships, col_warnings
from modules.logging import logWrite
from modules.utils import locale, should_quote
# from modules.event import col_event
@app.on_message(
custom_filters.enabled_general
& ~filters.scheduled
& filters.command(["export"], prefixes=["/"])
& custom_filters.admin
)
async def cmd_export(app: Client, msg: Message):
if len(msg.command) <= 1:
await msg.reply_text(
locale("syntax_export", "message", locale=msg.from_user),
quote=should_quote(msg),
)
return
selection = msg.command[1].lower()
if selection not in [
"applications",
"warnings",
"sponsorships",
"bans",
# "event",
]:
await msg.reply_text(
locale("syntax_export", "message", locale=msg.from_user),
quote=should_quote(msg),
)
return
logWrite(f"Admin {msg.from_user.id} requested export of {selection}")
makedirs("tmp", exist_ok=True)
temp_file = path.join("tmp", str(uuid1()))
await app.send_chat_action(msg.chat.id, ChatAction.TYPING)
output_csv = []
output_json = []
if selection == "applications":
header_csv = [
"user",
"date",
"admin",
"question_1",
"question_2",
"question_3",
"question_4",
"question_5",
"question_6",
"question_7",
"question_8",
"question_9",
"question_10",
]
for entry in list(col_applications.find()):
del entry["_id"]
entry["date"] = entry["date"].isoformat()
entry["application"]["2"] = entry["application"]["2"].isoformat()
output_json.append(entry)
for entry in list(col_applications.find()):
del entry["_id"]
entry["date"] = entry["date"].isoformat()
entry["application"]["2"] = entry["application"]["2"].isoformat()
for index, value in enumerate(entry["application"]):
entry[f"question_{index+1}"] = entry["application"][value]
entry[
"question_3"
] = f"{entry['application']['3']['name']} ({entry['application']['3']['adminName1']}, {entry['application']['3']['countryName']})"
del entry["application"]
output_csv.append(entry)
elif selection == "warnings":
header_csv = [
"id",
"user",
"admin",
"date",
"reason",
"active",
"revoke_date",
]
for entry in list(col_warnings.find()):
for k, v in list(entry.items()):
entry[{"_id": "id"}.get(k, k)] = entry.pop(k)
entry["id"] = str(entry["id"])
entry["date"] = entry["date"].isoformat()
if entry["revoke_date"] is not None:
entry["revoke_date"] = entry["revoke_date"].isoformat()
output_json.append(entry)
output_csv.append(entry)
elif selection == "sponsorships":
header_csv = [
"user",
"date",
"admin",
"streamer",
"expires",
"proof",
"label",
]
for entry in list(col_sponsorships.find()):
del entry["_id"]
entry["date"] = entry["date"].isoformat()
entry["sponsorship"]["expires"] = entry["sponsorship"][
"expires"
].isoformat()
output_json.append(entry)
for entry in list(col_sponsorships.find()):
del entry["_id"]
entry["date"] = entry["date"].isoformat()
entry["sponsorship"]["expires"] = entry["sponsorship"][
"expires"
].isoformat()
for index, value in enumerate(entry["sponsorship"]):
entry[value] = entry["sponsorship"][value]
del entry["sponsorship"]
output_csv.append(entry)
elif selection == "bans":
header_csv = ["user", "admin", "date"]
for entry in list(col_warnings.find()):
del entry["id"]
entry["date"] = entry["date"].isoformat()
output_json.append(entry)
output_csv.append(entry)
# elif selection == "event":
# header_csv = ["user", "stage", "date"]
# for entry in list(col_event.find()):
# del entry["_id"]
# entry["date"] = entry["date"].isoformat()
# output_json.append(entry)
# output_csv.append(entry)
# Saving CSV
async with aiofiles.open(temp_file + ".csv", mode="w", encoding="utf-8") as file:
writer = AsyncDictWriter(file, header_csv, restval="NULL", quoting=QUOTE_ALL)
await writer.writeheader()
await writer.writerows(output_csv)
# Saving JSON
async with aiofiles.open(temp_file + ".json", mode="w", encoding="utf-8") as file:
await file.write(
dumps(
output_json, ensure_ascii=False, escape_forward_slashes=False, indent=4
)
)
# Sending CSV
await app.send_chat_action(msg.chat.id, ChatAction.UPLOAD_DOCUMENT)
await msg.reply_document(
document=temp_file + ".csv",
file_name=f"{selection}.csv",
quote=should_quote(msg),
)
# Sending JSON
await app.send_chat_action(msg.chat.id, ChatAction.UPLOAD_DOCUMENT)
await msg.reply_document(
document=temp_file + ".json",
file_name=f"{selection}.json",
quote=should_quote(msg),
)
del output_csv, output_json
# Removing temp files
remove(temp_file + ".csv")
remove(temp_file + ".json")

View File

@@ -0,0 +1,105 @@
from os import path
from app import app, isAnAdmin
from pyrogram import filters
from pyrogram.types import Message
from pyrogram.client import Client
from pyrogram.errors import bad_request_400
from pyrogram.enums.chat_action import ChatAction
from classes.errors.holo_user import UserNotFoundError, UserInvalidError
from classes.holo_user import HoloUser
from modules.utils import (
jsonLoad,
should_quote,
logWrite,
locale,
download_tmp,
create_tmp,
find_user,
)
from modules import custom_filters
@app.on_message(
(custom_filters.enabled_applications | custom_filters.enabled_sponsorships)
& ~filters.scheduled
& filters.command("identify", prefixes=["/"])
& custom_filters.admin
)
async def cmd_identify(app: Client, msg: Message):
if len(msg.command) != 2:
await msg.reply_text(
locale("identify_invalid_syntax", "message", locale=msg.from_user)
)
return
try:
try:
holo_user = HoloUser(int(msg.command[1]))
except ValueError:
holo_user = HoloUser(await find_user(app, msg.command[1]))
except (
UserInvalidError,
UserNotFoundError,
bad_request_400.UsernameInvalid,
bad_request_400.PeerIdInvalid,
bad_request_400.UsernameNotOccupied,
TypeError,
):
await msg.reply_text(
locale("identify_not_found", "message", locale=msg.from_user).format(
msg.command[1]
)
)
return
role = holo_user.label
has_application = (
locale("yes", "message", locale=msg.from_user)
if holo_user.application_approved() is True
else locale("no", "message", locale=msg.from_user)
)
has_sponsorship = (
locale("yes", "message", locale=msg.from_user)
if holo_user.sponsorship_valid() is True
else locale("no", "message", locale=msg.from_user)
)
username = holo_user.username if holo_user.username is not None else "N/A"
in_chat = (
locale("yes", "message", locale=msg.from_user)
if (holo_user.id in jsonLoad(path.join("cache", "group_members")))
else locale("no", "message", locale=msg.from_user)
)
is_admin = (
locale("yes", "message", locale=msg.from_user)
if (await isAnAdmin(holo_user.id))
else locale("no", "message", locale=msg.from_user)
)
output = locale("identify_success", "message", locale=msg.from_user).format(
holo_user.id,
holo_user.name,
username,
in_chat,
is_admin,
role,
has_application,
has_sponsorship,
)
user = await app.get_users(holo_user.id)
if user.photo is not None:
await app.send_chat_action(msg.chat.id, action=ChatAction.UPLOAD_PHOTO)
await msg.reply_photo(
create_tmp(
(await download_tmp(app, user.photo.big_file_id))[1], kind="image"
),
quote=should_quote(msg),
caption=output,
)
else:
await app.send_chat_action(msg.chat.id, action=ChatAction.TYPING)
await msg.reply_text(output, quote=should_quote(msg))
logWrite(f"User {msg.from_user.id} identified user {holo_user.id}")

30
modules/commands/issue.py Normal file
View File

@@ -0,0 +1,30 @@
from app import app
from pyrogram import filters
from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton, Message
from pyrogram.client import Client
from modules.utils import configGet, locale
from modules import custom_filters
@app.on_message(
custom_filters.enabled_general
& ~filters.scheduled
& filters.private
& filters.command(["issue"], prefixes=["/"])
& ~custom_filters.banned
)
async def cmd_issue(app: Client, msg: Message):
await msg.reply_text(
locale("issue", "message", locale=msg.from_user),
disable_web_page_preview=True,
reply_markup=InlineKeyboardMarkup(
[
[
InlineKeyboardButton(
locale("issue", "button", locale=msg.from_user),
url=configGet("issues"),
)
]
]
),
)

54
modules/commands/label.py Normal file
View File

@@ -0,0 +1,54 @@
from app import app
from pyrogram import filters
from pyrogram.types import Message
from pyrogram.client import Client
from modules.utils import configGet, locale, should_quote, find_user
from classes.errors.holo_user import LabelTooLongError, LabelSettingError
from classes.holo_user import HoloUser
from modules import custom_filters
@app.on_message(
custom_filters.enabled_applications
& ~filters.scheduled
& filters.command(["label"], prefixes=["/"])
& custom_filters.admin
)
async def cmd_label(app: Client, msg: Message):
if len(msg.command) < 3:
await msg.reply_text("Invalid syntax:\n`/label USER LABEL`")
return
target = await find_user(app, msg.command[1])
if target is not None:
target = HoloUser(target)
label = " ".join(msg.command[2:])
if label.lower() == "reset":
await target.label_reset(msg.chat)
await msg.reply_text(
f"Resetting **{target.id}**'s label...", quote=should_quote(msg)
)
else:
try:
await target.label_set(msg.chat, label)
except LabelTooLongError:
await msg.reply_text(locale("label_too_long", "message"))
return
except LabelSettingError as exp:
await app.send_message(
configGet("admin", "groups"),
exp.__str__(),
disable_notification=True,
)
return
await msg.reply_text(
f"Setting **{target.id}**'s label to **{label}**...",
quote=should_quote(msg),
)
else:
await msg.reply_text(f"User not found")

View File

@@ -0,0 +1,97 @@
from app import app, isAnAdmin
from pyrogram import filters
from pyrogram.types import Message
from pyrogram.client import Client
from convopyro import listen_message
from classes.errors.holo_user import UserInvalidError
from classes.holo_user import HoloUser
from modules.utils import configGet, logWrite, locale, should_quote, find_user
from modules import custom_filters
from modules.database import col_messages
@app.on_message(
custom_filters.enabled_general
& ~filters.scheduled
& filters.command(["message"], prefixes=["/"])
# & custom_filters.admin
)
async def cmd_message(app: Client, msg: Message):
try:
if await isAnAdmin(msg.from_user.id):
try:
destination = HoloUser(int(msg.command[1]))
except (ValueError, UserInvalidError):
destination = HoloUser(await find_user(app, query=msg.command[1]))
if (msg.text is not None) and (len(str(msg.text).split()) > 2):
await destination.message(
context=msg,
text=" ".join(str(msg.text).split()[2:]),
caption=msg.caption,
photo=msg.photo,
video=msg.video,
file=msg.document,
voice=msg.voice,
animation=msg.animation,
adm_context=True,
)
elif (msg.caption is not None) and (len(msg.caption.split()) > 2):
await destination.message(
context=msg,
text=str(msg.text),
caption=" ".join(msg.caption.split()[2:]),
photo=msg.photo,
video=msg.video,
file=msg.document,
voice=msg.voice,
animation=msg.animation,
adm_context=True,
)
else:
await destination.message(
context=msg,
text=None,
caption=None,
photo=msg.photo,
video=msg.video,
file=msg.document,
voice=msg.voice,
animation=msg.animation,
adm_context=True,
)
else:
await msg.reply_text(
locale("message_enter", "message", locale=msg.from_user)
)
message = await listen_message(app, msg.chat.id)
if (
message is None
or message.text is not None
and message.text == "/cancel"
):
return
sent = await app.forward_messages(
configGet("admin", "groups"), msg.chat.id, message.id
)
col_messages.insert_one(
{
"origin": {"chat": message.chat.id, "id": message.id},
"destination": {"chat": sent.chat.id, "id": sent.id},
}
)
await message.reply_text(
locale("message_sent", "message", locale=message.from_user), quote=True
)
except IndexError:
await msg.reply_text(
locale("message_invalid_syntax", "message", locale=msg.from_user),
quote=should_quote(msg),
)
logWrite(f"Admin {msg.from_user.id} tried to send message but 'IndexError'")
except ValueError:
await msg.reply_text(
locale("message_invalid_syntax", "message", locale=msg.from_user),
quote=should_quote(msg),
)
logWrite(f"Admin {msg.from_user.id} tried to send message but 'ValueError'")

117
modules/commands/nearby.py Normal file
View File

@@ -0,0 +1,117 @@
from os import path
from traceback import print_exc
from app import app
from pyrogram import filters
from pyrogram.types import Message
from pyrogram.client import Client
from classes.holo_user import HoloUser
from modules import custom_filters
from modules.logging import logWrite
from modules.utils import configGet, jsonLoad, locale, should_quote, find_location
from modules.database import col_applications, col_users
from classes.errors.geo import PlaceNotFoundError
@app.on_message(
custom_filters.enabled_applications
& ~filters.scheduled
& (
filters.private
| (
filters.chat(configGet("admin", "groups"))
| filters.chat(configGet("users", "groups"))
)
)
& filters.command(["nearby"], prefixes=["/"])
& (custom_filters.allowed | custom_filters.admin)
& ~custom_filters.banned
)
async def cmd_nearby(app: Client, msg: Message):
holo_user = HoloUser(msg.from_user)
# Check if any place provided
if len(msg.command) == 1: # Action if no place provided
application = col_applications.find_one({"user": msg.from_user.id})
if application is None:
await msg.reply_text(
locale("nearby_user_empty", "message", locale=holo_user)
)
return
location = (
application["application"]["3"]["location"][0],
application["application"]["3"]["location"][1],
)
else: # Find a place from input query
logWrite(f"Looking for the location by query '{' '.join(msg.command[1:])}'")
try:
location_coordinates = find_location(" ".join(msg.command[1:]))
location = float(location_coordinates["lng"]), float(
location_coordinates["lat"]
)
except PlaceNotFoundError: # Place is not found
await msg.reply_text(
locale("nearby_invalid", "message", locale=holo_user),
quote=should_quote(msg),
)
return
except Exception as exp: # Error occurred while finding the place
await msg.reply_text(
locale("nearby_error", "message", locale=holo_user).format(
exp, print_exc()
),
quote=should_quote(msg),
)
return
# Find all users registered in the area provided
output = []
applications_nearby = col_applications.find(
{
"application.3.location": {
"$nearSphere": {
"$geometry": {
"type": "Point",
"coordinates": [location[0], location[1]],
},
"$maxDistance": configGet("search_radius") * 1000,
}
}
}
)
for entry in applications_nearby:
if not entry["user"] == msg.from_user.id:
user = col_users.find_one({"user": entry["user"]})
if user is not None:
if entry["user"] in jsonLoad(
path.join(configGet("cache", "locations"), "group_members")
):
if user["tg_username"] not in [
None,
"None",
"",
]: # Check if user has any name
output.append(
f'• **{user["tg_name"]}** (@{user["tg_username"]}):\n - {entry["application"]["3"]["name"]}, {entry["application"]["3"]["adminName1"]}'
)
else:
output.append(
f'• **{user["tg_name"]}**:\n - {entry["application"]["3"]["name"]}, {entry["application"]["3"]["adminName1"]}'
)
logWrite(
f"{holo_user.id} tried to find someone nearby {location[1]} {location[0]} in the radius of {configGet('search_radius')} kilometers"
)
# Check if any users found
if len(output) > 0:
await msg.reply_text(
locale("nearby_result", "message", locale=holo_user).format(
"\n".join(output)
),
quote=should_quote(msg),
)
else:
await msg.reply_text(
locale("nearby_empty", "message", locale=holo_user), quote=should_quote(msg)
)

169
modules/commands/reapply.py Normal file
View File

@@ -0,0 +1,169 @@
from app import app
from pyrogram import filters
from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton, Message
from pyrogram.client import Client
from classes.holo_user import HoloUser
from modules.logging import logWrite
from modules.utils import configGet, locale, should_quote
from modules.handlers.welcome import welcome_pass
from modules.database import col_tmp, col_applications
from modules import custom_filters
@app.on_message(
custom_filters.enabled_applications
& ~filters.scheduled
& filters.private
& filters.command(["reapply"], prefixes=["/"])
& ~custom_filters.banned
)
async def cmd_reapply(app: Client, msg: Message):
holo_user = HoloUser(msg.from_user)
# Check if user has approved/rejected tmp application
if (
(holo_user.application_state()[0] in ["approved", "rejected"])
or (holo_user.application_state()[0] == "none")
) and holo_user.spoiler_state() is False:
# Check if user's tmp application is already completed or even sent
if (
(holo_user.application_state()[1] is True)
and (
not col_tmp.find_one({"user": holo_user.id, "type": "application"})[
"sent"
]
)
) or (holo_user.application_state()[0] == "none"):
left_chat = True
async for member in app.get_chat_members(configGet("users", "groups")):
if member.user.id == msg.from_user.id:
left_chat = False
if left_chat is True:
if holo_user.application_state()[
1
] is True and holo_user.application_state()[0] not in [
"fill",
"rejected",
]:
await msg.reply_text(
locale("reapply_left_chat", "message", locale=holo_user),
reply_markup=InlineKeyboardMarkup(
[
[
InlineKeyboardButton(
locale(
"reapply_old_one",
"button",
locale=holo_user,
),
f"reapply_old_{msg.id}",
)
],
[
InlineKeyboardButton(
locale(
"reapply_new_one",
"button",
locale=holo_user,
),
f"reapply_new_{msg.id}",
)
],
]
),
)
elif (
col_tmp.find_one({"user": holo_user.id, "type": "application"})
is None
and col_applications.find_one({"user": holo_user.id}) is not None
):
await msg.reply_text(
locale("reapply_left_chat", "message", locale=holo_user),
reply_markup=InlineKeyboardMarkup(
[
[
InlineKeyboardButton(
locale(
"reapply_old_one",
"button",
locale=holo_user,
),
f"reapply_old_{msg.id}",
)
],
[
InlineKeyboardButton(
locale(
"reapply_new_one",
"button",
locale=holo_user,
),
f"reapply_new_{msg.id}",
)
],
]
),
)
else:
holo_user.application_restart(reapply=True)
await welcome_pass(app, msg, once_again=True)
else:
if holo_user.sponsorship_state()[0] == "fill":
await msg.reply_text(
locale("finish_sponsorship", "message"), quote=should_quote(msg)
)
return
holo_user.application_restart(reapply=True)
await welcome_pass(app, msg, once_again=True)
else:
await msg.reply_text(
locale("reapply_in_progress", "message", locale=holo_user).format(
locale("confirm", "keyboard", locale=holo_user)[1][0]
),
reply_markup=InlineKeyboardMarkup(
[
[
InlineKeyboardButton(
locale("applying_stop", "button", locale=holo_user),
f"reapply_stop_{msg.id}",
)
]
]
),
)
elif holo_user.spoiler_state() is True:
await msg.reply_text(locale("spoiler_in_progress", "message", locale=holo_user))
else:
if (holo_user.application_state()[0] == "fill") and (
col_tmp.find_one({"user": holo_user.id, "type": "application"})["sent"]
is True
):
await msg.reply_text(
locale("reapply_forbidden", "message", locale=holo_user)
)
else:
await msg.reply_text(
locale("reapply_in_progress", "message", locale=holo_user).format(
locale("confirm", "keyboard", locale=holo_user)[1][0]
),
reply_markup=InlineKeyboardMarkup(
[
[
InlineKeyboardButton(
locale("applying_stop", "button", locale=holo_user),
f"reapply_stop_{msg.id}",
)
]
]
),
)

View File

@@ -0,0 +1,34 @@
from app import app
from os import getpid, makedirs, path
from sys import exit
from time import time
from pyrogram import filters
from pyrogram.types import Message
from pyrogram.client import Client
from modules.utils import configGet, jsonSave, locale, logWrite, should_quote
from modules.scheduled import scheduler
from modules import custom_filters
pid = getpid()
@app.on_message(
custom_filters.enabled_general
& ~filters.scheduled
& filters.private
& filters.command(["kill", "die", "reboot"], prefixes=["/"])
& custom_filters.admin
)
async def cmd_kill(app: Client, msg: Message):
logWrite(f"Shutting down bot with pid {pid}")
await msg.reply_text(
locale("shutdown", "message", locale=msg.from_user).format(pid),
quote=should_quote(msg),
)
scheduler.shutdown()
makedirs(configGet("cache", "locations"), exist_ok=True)
jsonSave(
{"timestamp": time()},
path.join(configGet("cache", "locations"), "shutdown_time"),
)
exit()

View File

@@ -0,0 +1,111 @@
from app import app
from os import getpid, listdir
from pyrogram import filters
from pyrogram.types import Message, BotCommandScopeDefault, BotCommandScopeChat
from pyrogram.errors import bad_request_400
from pyrogram.client import Client
from modules.utils import logWrite, should_quote, configGet
from modules import custom_filters
pid = getpid()
@app.on_message(
custom_filters.enabled_general
& ~filters.scheduled
& filters.private
& filters.command(["resetcommands"], prefixes=["/"])
& custom_filters.admin
)
async def cmd_resetcommands(app: Client, msg: Message):
if msg.from_user.id == configGet("owner"):
logWrite(f"Resetting all commands on owner's request")
valid_locales = []
files_locales = listdir(f'{configGet("locale", "locations")}')
for entry in files_locales:
if entry.endswith(".json"):
valid_locales.append(".".join(entry.split(".")[:-1]))
logWrite(
f'Resetting commands in groups {configGet("admin", "groups")} and {configGet("users", "groups")}',
debug=True,
)
await app.delete_bot_commands(
scope=BotCommandScopeChat(chat_id=configGet("admin", "groups"))
)
await app.delete_bot_commands(
scope=BotCommandScopeChat(chat_id=configGet("users", "groups"))
)
for lc in valid_locales:
try:
logWrite(
f'Resetting commands in groups {configGet("admin", "groups")} and {configGet("users", "groups")} [{lc}]',
debug=True,
)
await app.delete_bot_commands(
scope=BotCommandScopeChat(chat_id=configGet("admin", "groups")),
language_code=lc,
)
await app.delete_bot_commands(
scope=BotCommandScopeChat(chat_id=configGet("users", "groups")),
language_code=lc,
)
except:
pass
for admin in configGet("admins"):
try:
logWrite(f"Resetting commands for admin {admin}", debug=True)
await app.delete_bot_commands(scope=BotCommandScopeChat(chat_id=admin))
for lc in valid_locales:
try:
logWrite(
f"Resetting commands for admin {admin} [{lc}]", debug=True
)
await app.delete_bot_commands(
scope=BotCommandScopeChat(chat_id=admin), language_code=lc
)
except:
pass
except bad_request_400.PeerIdInvalid:
pass
try:
logWrite(f'Resetting commands for owner {configGet("owner")}', debug=True)
for lc in valid_locales:
logWrite(
f'Resetting commands for owner {configGet("owner")} [{lc}]',
debug=True,
)
await app.delete_bot_commands(
scope=BotCommandScopeChat(chat_id=configGet("owner")),
language_code=lc,
)
await app.delete_bot_commands(
scope=BotCommandScopeChat(chat_id=configGet("owner"))
)
except bad_request_400.PeerIdInvalid:
pass
for lc in valid_locales:
logWrite(f"Resetting commands for locale {lc}", debug=True)
await app.delete_bot_commands(
scope=BotCommandScopeDefault(), language_code=lc
)
logWrite(f"Resetting default commands", debug=True)
await app.delete_bot_commands()
await msg.reply_text("OK", quote=should_quote(msg))
logWrite(str(await app.get_bot_commands()), debug=True)
logWrite(
str(
await app.get_bot_commands(
scope=BotCommandScopeChat(chat_id=configGet("owner"))
)
),
debug=True,
)

57
modules/commands/rules.py Normal file
View File

@@ -0,0 +1,57 @@
from typing import Union
from app import app
from pyrogram import filters
from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton, User, Message
from pyrogram.client import Client
from modules.utils import locale
from modules import custom_filters
from classes.holo_user import HoloUser
class DefaultRulesMarkup(list):
def __init__(self, language_code: Union[str, HoloUser, User, None]):
super().__init__([])
self.keyboard = InlineKeyboardMarkup(
[
[
InlineKeyboardButton(
locale("rules_home", "button", locale=language_code),
callback_data="rules_home",
),
InlineKeyboardButton(
locale("rules_additional", "button", locale=language_code),
callback_data="rules_additional",
),
],
[
InlineKeyboardButton("1", callback_data="rule_1"),
InlineKeyboardButton("2", callback_data="rule_2"),
InlineKeyboardButton("3", callback_data="rule_3"),
],
[
InlineKeyboardButton("4", callback_data="rule_4"),
InlineKeyboardButton("5", callback_data="rule_5"),
InlineKeyboardButton("6", callback_data="rule_6"),
],
[
InlineKeyboardButton("7", callback_data="rule_7"),
InlineKeyboardButton("8", callback_data="rule_8"),
InlineKeyboardButton("9", callback_data="rule_9"),
],
]
)
@app.on_message(
custom_filters.enabled_general
& ~filters.scheduled
& filters.private
& ~custom_filters.banned
& filters.command(["rules"], prefixes=["/"])
)
async def cmd_rules(app: Client, msg: Message):
await msg.reply_text(
locale("rules_msg", locale=msg.from_user),
disable_web_page_preview=True,
reply_markup=DefaultRulesMarkup(msg.from_user).keyboard,
)

View File

@@ -0,0 +1,64 @@
from app import app
from pyrogram import filters
from pyrogram.types import Message, ReplyKeyboardMarkup
from pyrogram.client import Client
from classes.errors.holo_user import UserNotFoundError, UserInvalidError
from classes.holo_user import HoloUser
from modules.logging import logWrite
from modules.utils import locale
from modules.database import col_spoilers, col_applications
from modules import custom_filters
@app.on_message(
custom_filters.enabled_spoilers
& ~filters.scheduled
& filters.private
& ~custom_filters.banned
& filters.command(["spoiler"], prefixes=["/"])
)
async def cmd_spoiler(app: Client, msg: Message):
try:
holo_user = HoloUser(msg.from_user)
except (UserInvalidError, UserNotFoundError):
return
if col_applications.find_one({"user": holo_user.id}) is None:
await msg.reply_text(locale("not_member", "message", locale=msg.from_user))
return
if (
holo_user.application_state()[0] != "fill"
and holo_user.sponsorship_state()[0] != "fill"
):
if col_spoilers.find_one({"user": holo_user.id, "completed": False}) is None:
col_spoilers.insert_one(
{
"user": holo_user.id,
"completed": False,
"category": None,
"description": None,
"photo": None,
"video": None,
"audio": None,
"animation": None,
"document": None,
"caption": None,
"text": None,
}
)
await msg.reply_text(
locale("spoiler_started", "message", locale=msg.from_user),
reply_markup=ReplyKeyboardMarkup(
locale("spoiler_categories", "keyboard"),
resize_keyboard=True,
one_time_keyboard=True,
),
)
logWrite(f"User {msg.from_user.id} started creating new spoiler")
else:
await msg.reply_text(
locale("spoiler_unfinished", "message", locale=msg.from_user)
)

View File

@@ -0,0 +1,325 @@
from datetime import datetime, timedelta
from typing import Union
from app import app
from pyrogram import filters
from pyrogram.types import (
InlineKeyboardMarkup,
InlineKeyboardButton,
ReplyKeyboardMarkup,
ReplyKeyboardRemove,
ForceReply,
Message,
)
from pyrogram.client import Client
from classes.holo_user import HoloUser
from modules import custom_filters
from modules.handlers.confirmation import confirm_yes
from modules.utils import all_locales, locale, should_quote
from modules.database import col_sponsorships, col_tmp
from convopyro import listen_message
def is_none_or_cancel(message: Union[Message, None]) -> bool:
if (
message is None
or message.text is not None
and message.text.lower() == "/cancel"
):
return True
return False
@app.on_message(
custom_filters.enabled_sponsorships
& ~filters.scheduled
& filters.command(["sponsorship"], prefixes=["/"])
& ~custom_filters.banned
& (custom_filters.allowed | custom_filters.admin)
)
async def cmd_sponsorship(app: Client, msg: Message):
holo_user = HoloUser(msg.from_user)
if holo_user.application_state()[0] == "fill":
await msg.reply_text(
locale("finish_application", "message", locale=msg.from_user),
quote=should_quote(msg),
)
return
if holo_user.spoiler_state() is True:
await msg.reply_text(locale("spoiler_in_progress", "message", locale=holo_user))
return
existent = col_sponsorships.find_one(
{
"user": msg.from_user.id,
"sponsorship.expires": {"$gt": datetime.now() - timedelta(days=90)},
}
)
if existent is None:
await msg.reply_text(
locale("sponsorship_apply", "message", locale=msg.from_user),
reply_markup=InlineKeyboardMarkup(
[
[
InlineKeyboardButton(
text=str(
locale("sponsor_apply", "button", locale=msg.from_user)
),
callback_data=f"sponsor_apply_{msg.from_user.id}",
)
]
]
),
quote=should_quote(msg),
)
return
await msg.reply_text(
locale("sponsor_resubmit", "message", locale=msg.from_user).format(
existent["sponsorship"]["streamer"]
),
reply_markup=ReplyKeyboardMarkup(
locale("sponsorship_restore", "keyboard", locale=msg.from_user),
resize_keyboard=True,
one_time_keyboard=True,
),
)
answer_decision = await listen_message(app, msg.chat.id)
if is_none_or_cancel(answer_decision):
return
input_streamer = existent["sponsorship"]["streamer"]
values_keep = []
for pattern in all_locales("sponsorship_restore", "keyboard"):
values_keep.append(pattern[0][0].lower())
values_new = []
for pattern in all_locales("sponsorship_restore", "keyboard"):
values_new.append(pattern[1][0].lower())
if answer_decision.text.lower() in values_keep:
await answer_decision.reply_text(
locale("sponsor2", "message", locale=msg.from_user),
reply_markup=ForceReply(
placeholder=str(
locale(
f"sponsor2",
"force_reply",
locale=msg.from_user,
)
)
),
)
while True:
answer_date = await listen_message(app, msg.chat.id)
if is_none_or_cancel(answer_date):
return
try:
input_dt = datetime.strptime(answer_date.text, "%d.%m.%Y")
if datetime.now() >= input_dt:
await msg.reply_text(
locale("sponsor2_past", "message", locale=msg.from_user),
reply_markup=ForceReply(
placeholder=str(
locale("sponsor2", "force_reply", locale=msg.from_user)
)
),
)
continue
break
except ValueError:
await answer_date.reply_text(
locale(f"sponsor2_invalid", "message", locale=msg.from_user),
reply_markup=ForceReply(
placeholder=str(
locale(
f"sponsor2",
"force_reply",
locale=msg.from_user,
)
)
),
)
continue
await answer_date.reply_text(
locale("sponsor3", "message", locale=msg.from_user),
reply_markup=ForceReply(
placeholder=str(
locale(
f"sponsor3",
"force_reply",
locale=msg.from_user,
)
)
),
)
while True:
answer_proof = await listen_message(app, msg.chat.id)
if is_none_or_cancel(answer_proof):
return
if answer_proof.photo is None:
await answer_proof.reply_text(
locale("sponsor3", "message", locale=msg.from_user),
reply_markup=ForceReply(
placeholder=str(
locale(
f"sponsor3",
"force_reply",
locale=msg.from_user,
)
)
),
)
continue
input_proof = answer_proof.photo.file_id
break
await msg.reply_text(
locale("sponsor4_resubmit", "message", locale=msg.from_user).format(
existent["sponsorship"]["label"]
),
reply_markup=ReplyKeyboardMarkup(
locale("sponsorship_restore_label", "keyboard", locale=msg.from_user),
resize_keyboard=True,
one_time_keyboard=True,
),
)
while True:
answer_label_decision = await listen_message(app, msg.chat.id)
if is_none_or_cancel(answer_label_decision):
return
if answer_label_decision.text is None:
await answer_label_decision.reply_text(
"Please, choose a valid option.",
reply_markup=ReplyKeyboardMarkup(
locale(
"sponsorship_restore_label",
"keyboard",
locale=msg.from_user,
),
resize_keyboard=True,
one_time_keyboard=True,
),
)
continue
values_keep = []
for pattern in all_locales("sponsorship_restore_label", "keyboard"):
values_keep.append(pattern[0][0].lower())
values_new = []
for pattern in all_locales("sponsorship_restore_label", "keyboard"):
values_new.append(pattern[1][0].lower())
if answer_label_decision.text.lower() in values_keep:
input_label = existent["sponsorship"]["label"]
elif answer_label_decision.text.lower() in values_new:
await answer_label_decision.reply_text(
locale("sponsor4", "message", locale=msg.from_user),
reply_markup=ForceReply(
placeholder=str(
locale(
f"sponsor4",
"force_reply",
locale=msg.from_user,
)
)
),
)
while True:
answer_label = await listen_message(app, msg.chat.id)
if is_none_or_cancel(answer_label_decision):
return
if answer_label.text is None:
await answer_label.reply_text(
locale("label_too_long", "message", locale=msg.from_user),
reply_markup=ForceReply(
placeholder=str(
locale(
f"sponsor4",
"force_reply",
locale=msg.from_user,
)
)
),
)
continue
elif len(answer_label.text) > 16:
await answer_label.reply_text(
locale("label_too_long", "message", locale=msg.from_user),
reply_markup=ForceReply(
placeholder=str(
locale(
f"sponsor4",
"force_reply",
locale=msg.from_user,
)
)
),
)
continue
input_label = answer_label.text
break
col_tmp.find_one_and_delete(
{"user": msg.from_user.id, "type": "sponsorship"}
)
col_tmp.insert_one(
{
"user": msg.from_user.id,
"type": "sponsorship",
"complete": True,
"sent": False,
"state": "fill",
"stage": 4,
"sponsorship": {
"streamer": input_streamer,
"expires": input_dt,
"proof": input_proof,
"label": input_label,
},
}
)
await confirm_yes(app, msg, kind="sponsorship")
return
elif answer_decision.text.lower() in values_new:
await msg.reply_text(
locale("sponsorship_apply", "message", locale=msg.from_user),
reply_markup=InlineKeyboardMarkup(
[
[
InlineKeyboardButton(
text=str(
locale("sponsor_apply", "button", locale=msg.from_user)
),
callback_data=f"sponsor_apply_{msg.from_user.id}",
)
]
]
),
quote=should_quote(msg),
)
return
else:
await answer_decision.reply_text(
locale("sponsor_resubmit_invalid_option", "message", locale=msg.from_user),
reply_markup=ReplyKeyboardRemove(),
)
return
# else:
# await msg.reply_text(locale("sponsorship_application_empty", "message"))

70
modules/commands/start.py Normal file
View File

@@ -0,0 +1,70 @@
from app import app
from pyrogram import filters
from pyrogram.types import ReplyKeyboardMarkup, Message
from pyrogram.client import Client
from modules.utils import locale, logWrite
from modules.database import col_users, col_spoilers
from modules import custom_filters
from bson.objectid import ObjectId
from bson.errors import InvalidId
@app.on_message(
custom_filters.enabled_applications
& ~filters.scheduled
& filters.private
& filters.command(["start"], prefixes=["/"])
& ~custom_filters.banned
)
async def cmd_start(app: Client, msg: Message):
user = col_users.find_one({"user": msg.from_user.id})
if user is None:
col_users.insert_one(
{
"user": msg.from_user.id,
"link": None,
"label": "",
"tg_name": msg.from_user.first_name,
"tg_phone": msg.from_user.phone_number,
"tg_locale": msg.from_user.language_code,
"tg_username": msg.from_user.username,
}
)
logWrite(f"User {msg.from_user.id} started bot interaction")
await msg.reply_text(
locale("start", "message", locale=msg.from_user),
reply_markup=ReplyKeyboardMarkup(
locale("welcome", "keyboard", locale=msg.from_user),
resize_keyboard=True,
),
)
if len(msg.command) > 1:
try:
spoiler = col_spoilers.find_one({"_id": ObjectId(msg.command[1])})
if spoiler["photo"] is not None:
await msg.reply_cached_media(
spoiler["photo"], caption=spoiler["caption"]
)
if spoiler["video"] is not None:
await msg.reply_cached_media(
spoiler["video"], caption=spoiler["caption"]
)
if spoiler["audio"] is not None:
await msg.reply_cached_media(
spoiler["audio"], caption=spoiler["caption"]
)
if spoiler["animation"] is not None:
await msg.reply_cached_media(
spoiler["animation"], caption=spoiler["caption"]
)
if spoiler["document"] is not None:
await msg.reply_cached_media(
spoiler["document"], caption=spoiler["caption"]
)
if spoiler["text"] is not None:
await msg.reply_text(spoiler["text"])
except InvalidId:
await msg.reply_text(f"Got an invalid ID {msg.command[1]}")

45
modules/commands/warn.py Normal file
View File

@@ -0,0 +1,45 @@
from datetime import datetime
from app import app
from pyrogram import filters
from pyrogram.types import Message
from pyrogram.client import Client
from modules.utils import configGet, locale
from modules.database import col_warnings
from modules import custom_filters
@app.on_message(
custom_filters.enabled_warnings
& ~filters.scheduled
& filters.command(["warn"], prefixes=["/"])
& custom_filters.admin
)
async def cmd_warn(app: Client, msg: Message):
if msg.chat.id == configGet("users", "groups"):
if msg.reply_to_message_id != None:
message = " ".join(msg.command[1:]) if len(msg.command) > 1 else ""
col_warnings.insert_one(
{
"user": msg.reply_to_message.from_user.id,
"admin": msg.from_user.id,
"date": datetime.now(),
"reason": message,
"active": True,
"revoke_date": None,
}
)
if message == "":
await msg.reply_text(
locale("warned", "message").format(
msg.reply_to_message.from_user.first_name,
msg.reply_to_message.from_user.id,
)
)
else:
await msg.reply_text(
locale("warned_reason", "message").format(
msg.reply_to_message.from_user.first_name,
msg.reply_to_message.from_user.id,
message,
)
)

View File

@@ -0,0 +1,146 @@
from os import path
from app import app
from pyrogram import filters
from pyrogram.types import Message
from pyrogram.client import Client
from pyrogram.enums.chat_members_filter import ChatMembersFilter
from modules.utils import configGet, jsonLoad, locale, should_quote
from modules.database import col_users, col_warnings
from modules import custom_filters
from pykeyboard import InlineKeyboard, InlineButton
@app.on_message(
custom_filters.enabled_warnings
& ~filters.scheduled
& filters.command(["warnings"], prefixes=["/"])
& custom_filters.admin
)
async def cmd_warnings(app: Client, msg: Message):
if len(msg.command) == 1:
warnings = {}
warnings_output = []
group_members = jsonLoad(
path.join(configGet("cache", "locations"), "group_members")
)
for warning in col_warnings.find({"active": True}):
if warning["user"] not in group_members:
continue
if str(warning["user"]) not in warnings:
warnings[str(warning["user"])] = {
"name": (col_users.find_one({"user": warning["user"]}))["tg_name"],
"warns": 1,
}
else:
warnings[str(warning["user"])]["warns"] += 1
for warning in warnings:
warnings_output.append(
locale("warnings_entry", "message", locale=msg.from_user).format(
warnings[warning]["name"], warning, warnings[warning]["warns"]
),
)
warnings_output = (
locale("warnings_empty", "message", locale=msg.from_user)
if len(warnings_output) == 0
else "\n".join(warnings_output)
)
await msg.reply_text(
locale("warnings_all", "message", locale=msg.from_user).format(
warnings_output
),
quote=should_quote(msg),
)
return
if len(msg.command) > 3:
await msg.reply_text(
locale("syntax_warnings", "message", locale=msg.from_user),
quote=should_quote(msg),
)
return
try:
user_db = col_users.find_one({"user": int(msg.command[1])})
target_id = user_db["user"]
target_name = user_db["tg_name"]
except:
list_of_users = []
async for m in app.get_chat_members(
configGet("users", "groups"),
filter=ChatMembersFilter.SEARCH,
query=msg.command[1],
):
list_of_users.append(m)
if len(list_of_users) != 0:
target = list_of_users[0].user
target_name = target.first_name
target_id = target.id
else:
await msg.reply_text(
locale("no_user_warnings", "message", locale=msg.from_user).format(
msg.command[1]
)
)
return
if len(msg.command) == 3 and msg.command[2].lower() == "revoke":
if col_warnings.count_documents({"user": target_id, "active": True}) == 0:
await msg.reply_text(
locale("no_warnings", "message", locale=msg.from_user).format(
target_name, target_id
),
quote=should_quote(msg),
)
return
keyboard = InlineKeyboard()
buttons = []
warnings = []
for index, warning in enumerate(
list(col_warnings.find({"user": target_id, "active": True}))
):
warnings.append(
f'{index+1}. {warning["date"].strftime("%d.%m.%Y, %H:%M")}\n Адмін: {warning["admin"]}\n Причина: {warning["reason"]}'
)
buttons.append(InlineButton(str(index + 1), f'w_rev_{str(warning["_id"])}'))
keyboard.add(*buttons)
await msg.reply_text(
locale("warnings_revoke", "message", locale=msg.from_user).format(
target_name, "\n".join(warnings)
),
reply_markup=keyboard,
quote=should_quote(msg),
)
return
warns = col_warnings.count_documents({"user": target_id, "active": True})
if warns == 0:
await msg.reply_text(
locale("no_warnings", "message", locale=msg.from_user).format(
target_name, target_id
),
quote=should_quote(msg),
)
else:
warnings = []
for index, warning in enumerate(
list(col_warnings.find({"user": target_id, "active": True}))
):
warnings.append(
f'{index+1}. {warning["date"].strftime("%d.%m.%Y, %H:%M")}\n Адмін: {warning["admin"]}\n Причина: {warning["reason"]}'
)
if warns <= 5:
await msg.reply_text(
locale("warnings_1", "message", locale=msg.from_user).format(
target_name, target_id, warns, "\n".join(warnings), target_id
),
quote=should_quote(msg),
)
else:
await msg.reply_text(
locale("warnings_2", "message", locale=msg.from_user).format(
target_name, target_id, warns, "\n".join(warnings), target_id
),
quote=should_quote(msg),
)

97
modules/custom_filters.py Normal file
View File

@@ -0,0 +1,97 @@
"""Custom message filters made to improve commands
usage in context of Holo Users."""
from os import path
from app import isAnAdmin
from modules.utils import configGet, jsonLoad
from modules.database import col_applications, col_tmp, col_bans
from pyrogram import filters
from pyrogram.types import Message
async def admin_func(_, __, msg: Message):
return await isAnAdmin(msg.from_user.id)
async def member_func(_, __, msg: Message):
return (
True
if (
msg.from_user.id
in jsonLoad(path.join(configGet("cache", "locations"), "group_members"))
)
else False
)
async def allowed_func(_, __, msg: Message):
output = False
output = (
True
if (col_applications.find_one({"user": msg.from_user.id}) is not None)
else False
)
if path.exists(path.join(configGet("cache", "locations"), "group_members")) and (
msg.from_user.id
not in jsonLoad(path.join(configGet("cache", "locations"), "group_members"))
):
output = False
return output
async def banned_func(_, __, msg: Message):
return True if col_bans.find_one({"user": msg.from_user.id}) is not None else False
async def enabled_general_func(_, __, msg: Message):
return configGet("enabled", "features", "general")
async def enabled_applications_func(_, __, msg: Message):
return configGet("enabled", "features", "applications")
async def enabled_sponsorships_func(_, __, msg: Message):
return configGet("enabled", "features", "sponsorships")
async def enabled_warnings_func(_, __, msg: Message):
return configGet("enabled", "features", "warnings")
async def enabled_invites_check_func(_, __, msg: Message):
return configGet("enabled", "features", "invites_check")
async def enabled_dinovoice_func(_, __, msg: Message):
return configGet("enabled", "features", "dinovoice")
async def enabled_spoilers_func(_, __, msg: Message):
return configGet("enabled", "features", "spoilers")
async def filling_sponsorship_func(_, __, msg: Message):
return (
True
if col_tmp.find_one({"user": msg.from_user.id, "type": "sponsorship"})
is not None
else False
)
admin = filters.create(admin_func)
member = filters.create(member_func)
allowed = filters.create(allowed_func)
banned = filters.create(banned_func)
enabled_general = filters.create(enabled_general_func)
enabled_applications = filters.create(enabled_applications_func)
enabled_sponsorships = filters.create(enabled_sponsorships_func)
enabled_warnings = filters.create(enabled_warnings_func)
enabled_invites_check = filters.create(enabled_invites_check_func)
enabled_dinovoice = filters.create(enabled_dinovoice_func)
enabled_spoilers = filters.create(enabled_spoilers_func)
filling_sponsorship = filters.create(filling_sponsorship_func)

59
modules/database.py Normal file
View File

@@ -0,0 +1,59 @@
"""Module that provides all database columns and
creates geospatial index for col_applications"""
from pymongo import MongoClient, GEOSPHERE
from ujson import loads
with open("config.json", "r", encoding="utf-8") as f:
db_config = loads(f.read())["database"]
f.close()
if db_config["user"] is not None and db_config["password"] is not None:
con_string = "mongodb://{0}:{1}@{2}:{3}/{4}".format(
db_config["user"],
db_config["password"],
db_config["host"],
db_config["port"],
db_config["name"],
)
else:
con_string = "mongodb://{0}:{1}/{2}".format(
db_config["host"], db_config["port"], db_config["name"]
)
db_client = MongoClient(con_string)
db = db_client.get_database(name=db_config["name"])
collections = db.list_collection_names()
for collection in [
"tmp",
"bans",
"users",
"context",
"youtube",
"spoilers",
"messages",
"warnings",
"applications",
"sponsorships",
"analytics_group",
"analytics_users"
]:
if not collection in collections:
db.create_collection(collection)
col_tmp = db.get_collection("tmp")
col_bans = db.get_collection("bans")
col_users = db.get_collection("users")
col_context = db.get_collection("context")
col_youtube = db.get_collection("youtube")
col_spoilers = db.get_collection("spoilers")
col_messages = db.get_collection("messages")
col_warnings = db.get_collection("warnings")
col_applications = db.get_collection("applications")
col_sponsorships = db.get_collection("sponsorships")
col_analytics_group = db.get_collection("analytics_group")
col_analytics_users = db.get_collection("analytics_users")
col_applications.create_index([("application.3.location", GEOSPHERE)])

407
modules/event.py Normal file
View File

@@ -0,0 +1,407 @@
# IF YOU'RE READING THIS DURING THE EVENT AND BEFORE COMPLETING IT - LEAVE NOW!
# ANALYZING THIS CODE WILL RUIN YOUR EXPERIENCE, SO PLEASE COMPLETE THE QUEST
# BEFORE GOING THERE. OTHERWISE THIS WILL BE CONSIDERED AS CHEATING, AND YOUR
# QUEST WILL BE RUINED FOREVER. PLEASE TAKE THIS WARNING SERIOUSLY.
from datetime import datetime
from os import path
from modules.database import db
from pyrogram import filters
from pyrogram.client import Client
from pyrogram.types import Message, User
from pyrogram.enums import ParseMode
from app import app
from modules.logging import logWrite
from modules.utils import configGet
collections = db.list_collection_names()
if not "event" in collections:
db.create_collection("event")
col_event = db.get_collection("event")
async def stage_passer(
previous: int, current: int, user: User, requires_previous: bool = True
) -> bool:
if requires_previous:
if col_event.find_one({"user": user.id, "stage": previous}) is None:
return False
if col_event.find_one({"user": user.id, "stage": current}) is None:
logWrite(
f"User {user.first_name} ({user.id}) has completed event stage {current}"
)
col_event.insert_one(
{"user": user.id, "stage": current, "date": datetime.now()}
)
if current == -1:
await app.send_message(
configGet("admin", "groups"),
f"Користувач **{user.first_name}** (`{user.id}`) пройшов етап BONUS",
)
else:
await app.send_message(
configGet("admin", "groups"),
f"Користувач **{user.first_name}** (`{user.id}`) пройшов етап №{current}",
)
return True
# Stage 1
@app.on_message(
~filters.scheduled
& filters.command(["aufwiedersehen"], prefixes=["/"])
& filters.private
)
async def cmd_event_1(app: Client, msg: Message):
if not await stage_passer(0, 1, msg.from_user, requires_previous=False):
return
await msg.reply_text(
"""Ви відразу поїхали на місце злочину, найкрупніше відділення мережі ресторанів фаст-фуду "KFP". Як Кіара й розповіла, ніяких слідів пограбування не було. Допитавши кількох працівників, ви не дізналися нічого корисного. Камери спостереження теж не дали ніякої корисної інформації.
Засмучені тим, що не дізналися нічого, ви вийшли на вулицю подихати свіжим повітрям, вільним від сильного запаху смаженої курятини. На всяк випадок, ви вирішили обійти ресторан декілька разів, але не помітили нічого підозрілого. Тяжко видохнувши, ви присіли на скамійку та озирнулися довкола.
Ресторан стояв поруч із великим озером, і навколо не було інших будинків. Найближча споруда до нього була невеличка рибацька хатка безпосередньо на березі. Подумав про те, що це краще, ніж нічого, ви підійшли до неї та постукалися. Вам ніхто не відповів. Ви обійшли цю хатку - і нарешті побачили хоч щось цікаве. На стіні, прихованій від поглядів, була маленька панелька, яку недосвідчене око й не помітило б. Ви підійшли, віддвинули її та здивувалися, побачив, що за нею був девайс для введення коду із цифровою клавіатурою. Код був із трьох цифр, і ніяких підказок не було. Цікавості заради, ви почали вводити рандомні комбінації: "001", "666", "420"... Четвертою комбінацією було "999" - і, на диво, вона спрацювала. Девайс заблимав зеленим - і ви побачили, як з-під землі з'явився люк. Ви віддвинули його - і спустилися по драбині вниз, до якогось дивного підземного проходу.
Пройшовши по ньому буквально двадцять кроків, ви побачили двері, а поруч із ними - ще один девайс для вводу пароля. Цього разу це були англійські букви, і в паролі їх було 6. Тут вже простим перебором не задовольнишся, тому ви почали шукати якісь підказки. На щастя, одну ви знайшли, з іншої сторони дверей була невеличка ніша, в якій лежав червоно-білий прапорець та [дивна фотографія](https://docs.google.com/document/d/1X3hj1mD0cPL6ZKgtFrOxHmiQtwCFnHQrBjsn0_gfR7s/edit?usp=sharing).
Ви відразу зрозуміли, кому належить ця хатка, та ввели правильний шестилітерний пароль.""",
disable_web_page_preview=True,
)
# Stage 2
@app.on_message(
~filters.scheduled & filters.command(["tonjok"], prefixes=["/"]) & filters.private
)
async def cmd_event_2(app: Client, msg: Message):
if not await stage_passer(1, 2, msg.from_user):
return
await msg.reply_text(
"""Ви дуже здивувалися, коли побачили, що таємна кімната виявилася... Бібліотекою. Просто для того, щоб впевнитися, що тут не відбувається нічого кримінального, ви відкрили першу ж книгу, що попалася на очі.
На обкладинці була намальована сором'язлива Оллі в нижній білизні. Ви почервоніли і прочитали назвуу. "Noise Complaint 3: Shutting Up Your Loud Zombie Neighbor (With French Kisses)". Це виявився юрійний фанфік! Ви швиденько пролистали його, почервоніли ще більше - і вирішили залишити кімнату. Що б тут не відбувалося, навряд чи це стосується викрадених яєць.
На щастя, коли ви піднялися назад на поверхню, хазяйка хатки ще не повернулася. На всяк випадок, ви залишили записку: "Ми розслідуємо злочин про викрадення пасхальних яєць. Якщо ваші яйця були вкрадені, будь ласка, зверніться до найближчого відділення Оодзора Кейсацу."
Не знайшовши ніяких нових доказів, ви вирішили повернутися до відділення. Там вас зустріла Субару в дуже поганому настрої. Ви коротко розповіли їй про те, що відбувалося протягом дня, і вона поскаржилася, що її набір яєць на Пасху теж був вкрадений!
Це вже виглядає як серійні крадіжки! Субару вирішила послати вас до Рейне, дізнатися, чи стала вона жертвою крадіїв теж. Оскільки та не дуже любить незнайомих людей, Субару дала вам таємний пароль, який треба буде сказати Рейне - назву юніта, що складається з Оодзори, Таканаші та Паволії."""
)
# Stage 3
@app.on_message(
~filters.scheduled
& filters.command(["turducken"], prefixes=["/"])
& filters.private
)
async def cmd_event_3(app: Client, msg: Message):
if not await stage_passer(2, 3, msg.from_user):
return
await msg.reply_text(
"""Прийшовши до будинку Рейне, ви не змогли потрапити всередину, бо ваші дзвінки були проігноровані. Роздивившись ворота поуважніше, ви побачили збоку невеличку табличку із написом "ВСЬОГО ЗА ТРИ ПРОСТИХ КЛІКИ ВИ ТЕЖ МОЖЕТЕ ДОЄДНАТИСЯ, MUDAH SEKALI" і кодовий замок із шести літер."""
)
# Stage 4
@app.on_message(
~filters.scheduled & filters.command(["joinda"], prefixes=["/"]) & filters.private
)
async def cmd_event_4(app: Client, msg: Message):
if not await stage_passer(3, 4, msg.from_user):
return
await msg.reply_text(
"""Пройшовши через ворота, ви постукали у двері і сказали таємний пароль від Субару. Двері відчинилися, і ви почули голос Рейне. "Заходь у останні двері ліворуч". На дверях чомусь була намальована велика червона цифра 5 та поруч був дуже дивний механізм. Ви не встигли роздивитися цей "замок", як двері почали повільно відчинятися. Зайшовши всередину ви опинилися у Кавуновій Кімнаті™, посеред якої сиділа Рейне. Ви почали задавати питання про яйця, але Кавунова Кімната™ не виходила з вашої голови, наче ви десь її бачили... І тут ви згадали! Така сама кімната була в тому фанфіку! На жаль, ви мали необережність сказати це вголос, і Рейне образилася на вас через те, що ви зайшли в її бібліотеку без дозволу. Із криками "ТА НІХТО НЕ КРАВ МОЇ ЯЙЦЯ!" вона випинує вас надвір, і ви вирішуєте іти до наступної Холоторі.
Ви вирішили завітати до Мумей. Діставшись її дому, ви довго стояли перед входом та думали, чи варто взагалі заходити. Її дім - це гігантська темна печера, що веде в невідоме. Але ваш обов’язок кличе, тому, дістав свій ліхтарик, ви ризикнули зайти. Блукая тунелями печери, ви постійно натикалися на розвилки. Перші два перехрестя ви пройшли прямо, не завертаючи нікуди. Але на третьому вас чекали підозрілі червоні плями на стінах, і ви вирішили, що краще повернутися та обрати інший маршрут. Ви повертали то вліво, то вправо, то вліво, то вправо, і врешті-решт опинилися в тупику. На стіні висіла табличка з двома буквами: “B” та “A”, а під нею лежали полотно, перо та баночка з тією самою червоною рідиною. На самому ж полотні було 6 пустих клітинок. Чи варто вам спробувати щось написати чи просто розвернутися та піти іншою дорогою?
<i>(Введіть команду /next, якщо хочете продовжити розслідування, або введіть зашифровану команду /******, щоб побачити бонусну сцену, яка не впливає на геймплей.)</i>""",
parse_mode=ParseMode.HTML,
)
# Stage 5
@app.on_message(
~filters.scheduled & filters.command(["next"], prefixes=["/"]) & filters.private
)
async def cmd_event_5(app: Client, msg: Message):
if not await stage_passer(4, 5, msg.from_user):
return
await msg.reply_text(
"""Ви повернулися на самий початок шляху і пішли в той напрямок, який ще не дослідили. На диво, ця дорога вела прямо. Коли ви йшли, вам по дорозі траплялися дивні камінчики, які ви вирішили збирати. На кінці шляху вас чекали великі двері, наче в бункері. Коло них був девайс для вводу коду з 3 цифр. Ви подивилися ще раз на камінчики, які зібрали та на символи на них:
一1403
二2308
三156
四2103
五0412
六154
七2203
八0408
九149
Подумав, ви вирішили ввести потрібні цифри від найменшої до найбільшої - і двері почали відчинятися…"""
)
# Stage 6
@app.on_message(
~filters.scheduled & filters.command(["238"], prefixes=["/"]) & filters.private
)
async def cmd_event_6(app: Client, msg: Message):
if not await stage_passer(5, 6, msg.from_user):
return
await msg.reply_text(
"""Всередині ви побачили гігантську ферму, явно штучно зроблену, та Кроніі з Фауну, які поливали саженці. Вони здивувалися, побачивши вас, але ви швидко показали своє поліцейське посвідчення та пояснили ситуацію. Дівчата сказали, що ніякі яйця в Мумей ніхто не крав і що самі вони нічого не чули. Перед тим, як ви пішли далі, вони з ігривою посмішкою задали вам питання, сказав, що дадуть вам підказку, якщо ви правильно дасте відповідь.
- Поліцейський-кун, ти ж злякався, коли йшов сюди? Ти думав, що це кров була на стінах? Але ні, ця фарба була зроблена з іншого. А з чого саме?
Ви подивилися навколо та, побачивши, що більше всього росло на фермі, дали впевнену відповідь."""
)
# Stage 7
@app.on_message(
~filters.scheduled & filters.command(["berries"], prefixes=["/"]) & filters.private
)
async def cmd_event_7(app: Client, msg: Message):
if not await stage_passer(6, 7, msg.from_user):
return
await msg.reply_text(
"""Кроніі та Фауна, як обіцяли, дали вам підказку. Коли ви сказали, що ви опитуєте всіх Голоторі, вони зрозуміли, що далі ви підете до Луї. Але базу Голоксу просто так не знайти, тому дівчата підказали вам локацію. Ви прийшли на місце і опинилися перед звичайним собі житловим будинком у спальному районі. Ви знайшли вхід до підвалу та ввели код від двері (1111), який вам повідомили дівчата з ГолоРади. “Дивно, якось занадто легко як для секретної бази”, - подумали ви та зайшли всередину. Підвал виглядав абсолютно звичайно, єдине, що виділялося, - це залізні двері без ручки. Коло них на вас чекала чергова кодова панель. На цей раз із буквенною клавіатурою. Ви подивилися на символи на двері, “常夜”, та ввели правильний пароль."""
)
# Stage 8
@app.on_message(
~filters.scheduled & filters.command(["repaint"], prefixes=["/"]) & filters.private
)
async def cmd_event_8(app: Client, msg: Message):
if not await stage_passer(7, 8, msg.from_user):
return
await msg.reply_text(
"""Тільки-но двері відчинилися, ви побачили перед собою Луї у формі Голоксу та в шикарних окулярах, що нагадували вам окуляри Каміни із Ґуррен Лаґанну, а на плечі в неї сидів її секретар, Ґанмо. Її насторожило, що робітник поліції увірвався на секретну базу Голоксу. Ви поспішили її заспокоїти, та сказали що ви розслідуєте зникнення яєць у Холоторі. Луї відповіла, що її яйця в повному порядку і ніхто їх не крав, а от просто так відпустити вас вона тепер не може, бо ви знаєте, де знаходиться секретний штаб. Після цих слів вона простягнула якусь склянку із рідиною, що виглядала як препарати, які носить із собою Койорі. Після того, як випили вміст тієї склянки, ви знепритомніли, а коли прийшли до тями, то сиділи поруч із поліцейським відділенням. У вас зникли спогади про те, як ви йшли до бази Голоксу, але ви чітко запам'ятали, що яйця у Луї ніхто не крав. Озирнувшись по сторонам, ви побачили, що вас кличе до себе Субару. Зайшовши до неї в кабінет, ви дізналися, що поки ви бігали в пошуках Холоторі, у відділення хтось доставив таємничу коробку із кодовим замком та записку, причеплену до неї.
У записці був написаний ось такий текст (хоча це більше походило на набір літер):
__Lq Pdufk ru Dsulo
Wkhvh wklqjv gr derxqg
D fhuwdlq exqqb
Ohdyhv wkhvh rq wkh jurxqg__
А на зворотній стороні був намальований символ 三
На кодовому замку ж треба ввести код із 9-и літер без пробілів."""
)
# Stage 9
@app.on_message(
~filters.scheduled
& filters.command(["easteregg"], prefixes=["/"])
& filters.private
)
async def cmd_event_9(app: Client, msg: Message):
if not await stage_passer(8, 9, msg.from_user):
return
await msg.reply_text(
"""Усередині коробки була рація. Субару негайно вихопила її та натиснула на кнопку зв’язку.
- Прийом-прийом! Я не знаю, хто ти і що тобі потрібно, але поверни яйця! Я їй чесно сама висид… Пофарбувала!
- AH↓HA↑HA↓HA↑HA↓
- ПЕКОРА?! Я так і знала, що це ти!
- Моя люба Субару, ти так сильно старалася, що я не можу відмовити тобі! Приїжджай, забирай їх назад. Якщо знайдеш, звісно! AH↓HA↑HA↓HA↑HA↓
На цьому зв’язок обірвався. Субару почала крутити рацію, шукаючи, чи є щось, що з нею не так. Ви запропонували відкрити відділення для батарейок, і дійсно, там були два папірця. Один із них - це було міні-фото Каели, на якому маркером було написано: “******** doko?” На другому було написано рваним почерком: “07 Jan 22, Mute City, never forget”. Окрім того, ви помітили, що в коробці лежав маленький шматочок якогось білого мінералу. Зібрав до купи всі підказки, ви повідомили шефу, куди вам треба їхати.""",
parse_mode=ParseMode.DISABLED,
)
# Stage 10
@app.on_message(
~filters.scheduled & filters.command(["atlantis"], prefixes=["/"]) & filters.private
)
async def cmd_event_10(app: Client, msg: Message):
if not await stage_passer(9, 10, msg.from_user):
return
await msg.reply_text(
"""Ви разом із Субару під’їхали до руїн Атлантиди. Хоча, чи можна це назвати руїнами, коли Ґура просто не добудувала? В будь-якому випадку, на найвищому п’єдесталі стояв кошик, в якому лежали пасхальні яйця Субару, цілі та непошкоджені! Ваша шеф радісно підбігла до нього, але швидко засмутилася, коли побачила серед яєць планшет. Вона увімкнула його - і на заставці екрану було лого Усада Кенсецу. Планшет був повністю пустим, єдиним файлом було відео без назви. Субару увімкнула його - і на екрані з’явився знайомий кролячий силует.
- AH↓HA↑HA↓HA↑HA↓ Як бачиш, Субару, твої дорогоцінні яйця в повній безпеці! Та й кому вони взагалі потрібні, коли є набагато рідкісніший та особливіший делікатес?
Камера віддвинулася від Пекори і показала стіл, на якому стояла гігантська паска, прикрашена немов би золотом та дорогоцінними камнями. Камера протримала її в фокусі пару секунд, а потім перевелася назад на Пекору.
- До вашої уваги, гордість пекарні Короне, Голо-паска! Якщо я не помиляюся, мала бути презентована особисто Яґо на параді в центрі міста цього вечора. Уявляю собі, як йому зараз сумно від того, що вона зникла. Тепер всі точно побачать його некомпетентність та визнають, що лише Пекора варта звання мера Голо-сіті! AH↓HA↑HA↓HA↑HA↓HA↑HA↓HA↑HA↓
Субару стиснула кулаки та почала кричати в екран, забувши про те, що це запис, а не прямий ефір. Нарешті, Пекора закінчила сміятися і продовжила:
- Моя люба Субару, не плач. Я благородний крадій і дам тобі шанс побути сьогодні героєм та врятувати святковий парад! Якщо хочеш знайти мене та відібрати в мене паску, то рекомендую почати з того, щоб знайти брехуна! Але ти можеш __24__ рази __поговорити__ з усіма мешканцями __Голо__-сіті - тобі все одно не вистачить кмітливості! Адіос!
На цьому відео закінчилося. Ви заспокоїли Субару та звернули її увагу на те, що деякі слова Пекори були сказані іншою інтонацією. Подумав і згадав події сьогоднішнього дня, ви зрозуміли, до кого треба заїхати в першу чергу.
__(У команду треба вписати лише ім’я дівчинки.)__"""
)
# Stage 11
@app.on_message(
~filters.scheduled & filters.command(["kiara"], prefixes=["/"]) & filters.private
)
async def cmd_event_11(app: Client, msg: Message):
if not await stage_passer(10, 11, msg.from_user):
return
await msg.reply_text(
"""Прийшовши до Кіари, ви почали вимагати від неї пояснень. Вона зізналася, що просто не могла відмовити Пекорі, але вона дасть нам наступну підказку. Після цього Кіара почала нишпорити у шафі і через деякий час дістала звідти скриньку із дивними символами. "Цю коробку мені подарувала __Аме__ після __найпершого походу__ в новий світ Амеверсу. Сюди я й поклала підказки, що мені передала Пекора. Якщо зможеш відкрити - вони твої". На шкатулці були зображені [такі символи](https://docs.google.com/document/d/1w2ARMWpUIkNpmSNWFJVGobIMVh2scgSy2hgyHssGLtA/edit?usp=sharing), під якими був кодовий замок із 8 цифр.""",
disable_web_page_preview=True,
)
# Stage 12
@app.on_message(
~filters.scheduled & filters.command(["11022021"], prefixes=["/"]) & filters.private
)
async def cmd_event_12(app: Client, msg: Message):
if not await stage_passer(11, 12, msg.from_user):
return
await msg.reply_text(
"""У скриньці був папірець з географічними координатами. Субару поїхала пояснювати ситуацію до Яґо, тому далі ви були самі по собі. Ви приїхали на місце і побачили самотній дуб, що ріс посеред парку. У дуплі дуба на вас чекала нова скринька. На її кришці було написано почерком Пекори наступне:
__6 листопада
Греміла битва.
Червоні ведмеді
Та білі вовки -
Ніхто не переміг.
У той день
Я була другою.
Якою була надія?__
Скриньку закривав кодовий замок лише з двох цифер. У вас взагалі не було ідей, але ви не могли підвести Субару, оскільки вона поклалася на вас! Ви сіли під дубом та почали думати, але так і не змогли знайти відповідь. Ви ледве не заплакали від безсилля, коли до вас підійшла Шішіро Ботан, що прогулювалася парком.
- Що, офіцере, не можете одужати геній Усада Кенсецу? Не переймайтеся, я їй відразу сказала, що це занадто складно. Але Пекора любить чесну боротьбу, тому вона й попросила мене дивитися одним оком за цією локацію та дати підказку, якщо тобі буде важко. У той день моїм лідером була ніхто інша як Субару, а номер мій був 28. Далі розберешся, шукай у записах! Отож, бувай, хай тобі щастить, пой!
Шішірон залишила вас, а ви панічно почали діставати смартфон та відкривати ютуб. Ви зрозуміли, про що йшла мова, тому легко змогли ввести правильний номер."""
)
# Stage 13
@app.on_message(
~filters.scheduled & filters.command(["24"], prefixes=["/"]) & filters.private
)
async def cmd_event_13(app: Client, msg: Message):
if not await stage_passer(12, 13, msg.from_user):
return
await msg.reply_text(
"""Відкривши скриньку, ви побачили всередині... Карі з бараниною?.. Спочатку ви нічого не зрозуміли, але незабаром відчули на собі (чи все ж таки на карі?) голодний погляд Ботан, яка все ще знаходилася поруч. Ви вирішили віддати їй цю смачну страву, за що левиця, задоволено посміхаючись, видала вам рацію та маленьку коробочку у формі ССРБ із кодовим замком на 3 цифри. Рація постійно видавала звуки, схожі на якийсь код, а на коробці було викарбувано "A—>Z, Z—>A". Виписавши сигнали рації морзянкою, ви отримали таке повідомлення:
--.. --. --. ... ...- .... --. --.. .. --. .-.. ..- ... ...- .. --.- .-.. ..-. .. -- ...- -... .... --. --.. .. .. -... -- .-. - ... --. .-.. . ...- .. .--. -... .-. . .... .-.. -. --.. -- -... .-- --.. -... .... ... --.. . ...- -.- --.. .... .... ...- .-- --. ...- --- --- -. ...- --. ... ...- -. .-. -- ..-. --. ...- .-. --. ... --.. -.- -.- ...- -- ...- .--
Швиденько розгадавши цю загадку, ви ввели правильний код.""",
parse_mode=ParseMode.DISABLED,
)
# Stage 14
@app.on_message(
~filters.scheduled & filters.command(["003"], prefixes=["/"]) & filters.private
)
async def cmd_event_14(app: Client, msg: Message):
if not await stage_passer(13, 14, msg.from_user):
return
await msg.reply_text(
"""Чергові координати привели вас до закинутого автокінотеатру. Ви увійшли на територію та побачили сцену, на якій я показували фільми. Проектор виводив на екран [дивну таблицю](https://docs.google.com/document/d/1_Mf9w52vDG0sQZ-xKn1pg4tdsLHwiq97O-nHxYELUkI/edit?usp=sharing):
Також на сцені стояв великий сейф із кодовим замком з 16 символів. З одного боку сейфу було написано наступне:
パッと光って咲いた 花火を見てた
きっとまだ 終わらない夏が
曖昧な心を 解かして繋いだ
この夜が続いて欲しかった
А з іншого - просто “Thank you doragon!”. Зверху ж був наступний напис: “1D2?...”
Ви довго думали, але змогли розшифрувати цю загадку, знову скориставшись ютубом!
__(Відповідь треба вводити цифрами та великими англійськими літерами.)__"""
)
# Stage 15
@app.on_message(
~filters.scheduled
& filters.command(["1D2C3E4H5B6F7G8A"], prefixes=["/"])
& filters.private
)
async def cmd_event_15(app: Client, msg: Message):
if not await stage_passer(14, 15, msg.from_user):
return
await msg.reply_text(
"""У сейфі лежала паперова мапа Голо-сіті, на якій червоним колом була відмічена споруда посеред промзони. Ви негайно вирушили туди. Цією будівлею виявився чи то ангар, чи то склад, що належав УсаКену. Всі вікна до нього були закриті металевими пластинами, а єдиним входом всередину були масивні ангарні двері. Звісно ж, силою їй відчинити не вдалося. Ретельно перевіривши стіни будівлі зовні, ви помітили, що в одному місці цеглини були розшатані. Ви обережно дістали пару цеглин - і ву-аля, перед вами був черговий девайс для вводу коду. Але цього разу це була повноцінна комп’ютерна клавіатура, і ніякого ліміту символів не було.
Ви почали трошки панікувати, адже ніяких підказок навколо ви не помітили, аж тут вам на телефон подзвонила Субару. Ви швидко взяли трубку:
- Офіцере, я тільки що отримала якийсь незрозумілий файл особисто від Пекори! Пересилаю його тобі, розберися - і швиденько, бо через годину вже початок параду!
Ви --[відкрили файл](https://drive.google.com/file/d/1oex8GriQBsimIsUvOGo83IuFp2JBtNJd/view?usp=sharing)--, потупили декілька хвилин, але врешті-решт змогли зрозуміти загадку та ввели правильний пароль!"""
)
# Stage 16
@app.on_message(
~filters.scheduled & filters.command(["hinotori"], prefixes=["/"]) & filters.private
)
async def cmd_event_16(app: Client, msg: Message):
if not await stage_passer(15, 16, msg.from_user):
return
await msg.reply_text(
"""Коли ви ввели пароль, двері відчинилися. Всередині панувала кромішня темрява, і, судячи з ехо від ваших кроків, приміщення було повністю пустим. Раптом, над вами загорілися прожектори, і кімната залилася сліпучим білим світлом. Звідкись з-під даху роздався скрип динаміків - і знайомий вам кролячий голос почав говорити:
- А ще трохи - і було б запізно. Дозвольте привітати вас із тим, що ви змогли розгадати всі мої загадки! Паска, що ви шукаєте, вже доставлена до офісу Субару. Що ж це ви так поспішали, що аж забули зачинити двері? Дякуючи цьому, я змогла знайти те, що так давно шукала! Але не хвилюйтеся, паску, що послужила такою чудовою заманкою, я вам повертаю цілою та непошкодженою. Ще побачимося! AH↓HA↑HA↓HA↑HA↓HA↑HA↓HA↑HA↓
Як ви і думали, по поверненню до офіса Субару, ви виявили повний бардак. Всі шкафи були вивернуті, а документи розкидані по всій кімнаті. Очевидно, це свідчило про крадіж. Дехто провів тут багато часу, раз зміг поперевертати кожен куточок. І хоч парад Голо-сіті був врятований, а нинішня справа закрита, вас тепер мучило питання, що ж таке було необхідне Пекорі, що вона вдалася до таких заходів, щоб вас збити з пантелику та заплутати. Ви були впевнені, що дуже скоро ви про неї почуєте знову…"""
)
# Stage BONUS
@app.on_message(
~filters.scheduled & filters.command(["konami"], prefixes=["/"]) & filters.private
)
async def cmd_event_bonus(app: Client, msg: Message):
if not await stage_passer(4, -1, msg.from_user):
return
await msg.reply_photo(
path.join("assets", "event_easter_2023", "stage_bonus.jpg"),
caption="""Раптом, стіна почала рухатися, відкриваючи вам прохід. Ви повільно зайшли всередину і побачили якусь дівчину із кролячими вухами. Помітивши вас, вона повернулась і, посміхаючись, промовила:
- Вибач, але твоє яйце знаходиться в іншій печері. Тут тільки я і мій друг - П'ятнична Ніч.
Відвернувшись від вас, вона продовжила спілкуватися з великоднім яйцем, а ви, не бажаючи їй заважати, спішно покинули печеру і повернулися до найпершої розвилки.
__(Введіть команду /next для продовження сюжету.)__""",
)

View File

@@ -0,0 +1,255 @@
from datetime import datetime
from polyglot.detect import Detector
from pyrogram import filters
from pyrogram.client import Client
from pyrogram.enums import MessageEntityType, PollType
from pyrogram.types import Message
from app import app
from modules import custom_filters
from modules.database import col_analytics_group
from modules.logging import logWrite
from modules.utils import configGet
@app.on_message(
custom_filters.enabled_general
& ~filters.scheduled
& filters.chat(configGet("users", "groups"))
)
async def msg_destination_group(app: Client, msg: Message):
analytics_entry = {
"id": msg.id,
"user": msg.from_user.id,
"date": datetime.now(),
"reply": {
"id": msg.reply_to_message_id,
"top_id": msg.reply_to_top_message_id,
"user": None
if msg.reply_to_message is None
else msg.reply_to_message.from_user.id,
},
"forward": {
"id": msg.forward_from_message_id,
"chat": None if msg.forward_from_chat is None else msg.forward_from_chat.id,
"user": None if msg.forward_from is None else msg.forward_from.id,
"date": msg.forward_date,
},
"media_spoilered": msg.has_media_spoiler,
"entities": {"links": [], "mentions": []},
"text": None,
"language": None,
"language_confidence": None,
"animation": None,
"audio": None,
"contact": None,
"document": None,
"location": None,
"photo": None,
"poll": None,
"sticker": None,
"venue": None,
"video": None,
"videonote": None,
"voice": None,
}
if msg.text is not None or msg.caption is not None:
text = msg.text if msg.text is not None else msg.caption
analytics_entry["text"] = text
if msg.entities is not None or msg.caption_entities is not None:
entities = (
msg.entities if msg.entities is not None else msg.caption_entities
)
for entity in entities:
if entity.type == MessageEntityType.TEXT_LINK:
analytics_entry["entities"]["links"].append(entity.url)
elif entity.type == MessageEntityType.URL:
analytics_entry["entities"]["links"].append(
text[entity.offset : entity.offset + entity.length]
)
elif entity.type == MessageEntityType.TEXT_MENTION:
analytics_entry["entities"]["mentions"].append(entity.user.id)
elif entity.type == MessageEntityType.MENTION:
analytics_entry["entities"]["mentions"].append(
text[entity.offset : entity.offset + entity.length]
)
lang = Detector(text, quiet=True).language
analytics_entry["language"] = lang.code
analytics_entry["language_confidence"] = lang.confidence
if lang.code == "ru":
logWrite(
f"Message '{text}' from {msg.from_user.first_name} ({msg.from_user.id}) is fucking russian [confidence {lang.confidence}]"
)
if msg.animation is not None:
analytics_entry["animation"] = {
"id": msg.animation.file_id,
"duration": msg.animation.duration,
"height": msg.animation.height,
"width": msg.animation.width,
"file_name": msg.animation.file_name,
"mime_type": msg.animation.mime_type,
}
if msg.audio is not None:
analytics_entry["audio"] = {
"id": msg.audio.file_id,
"title": msg.audio.title,
"performer": msg.audio.performer,
"duration": msg.audio.duration,
"file_name": msg.audio.file_name,
"file_size": msg.audio.file_size,
"mime_type": msg.audio.mime_type,
}
if msg.contact is not None:
analytics_entry["contact"] = {
"id": msg.contact.user_id,
"first_name": msg.contact.first_name,
"last_name": msg.contact.last_name,
"phone_number": msg.contact.phone_number,
"vcard": msg.contact.vcard,
}
if msg.document is not None:
analytics_entry["document"] = {
"id": msg.document.file_id,
"file_name": msg.document.file_name,
"file_size": msg.document.file_size,
"mime_type": msg.document.mime_type,
}
if msg.location is not None:
analytics_entry["location"] = {
"longitude": msg.location.longitude,
"latitude": msg.location.latitude,
}
if msg.photo is not None:
thumbnails = []
for thumbail in msg.photo.thumbs:
thumbnails.append(
{
"id": thumbail.file_id,
"height": thumbail.height,
"width": thumbail.width,
"file_size": thumbail.file_size,
}
)
analytics_entry["photo"] = {
"id": msg.photo.file_id,
"height": msg.photo.height,
"width": msg.photo.width,
"file_size": msg.photo.file_size,
"thumbnails": thumbnails,
}
if msg.poll is not None:
options = []
for option in msg.poll.options:
options.append(option.text)
analytics_entry["poll"] = {
"id": msg.poll.id,
"question": msg.poll.question,
"open_period": msg.poll.open_period,
"close_date": msg.poll.close_date,
"options": options,
"correct_option": msg.poll.correct_option_id,
"explanation": msg.poll.explanation,
"anonymous": msg.poll.is_anonymous,
"multiple_answers": msg.poll.allows_multiple_answers,
"quiz": True if msg.poll.type == PollType.QUIZ else False,
}
if msg.sticker is not None:
thumbnails = []
for thumbail in msg.sticker.thumbs:
thumbnails.append(
{
"id": thumbail.file_id,
"height": thumbail.height,
"width": thumbail.width,
"file_size": thumbail.file_size,
}
)
analytics_entry["sticker"] = {
"id": msg.sticker.file_id,
"emoji": msg.sticker.emoji,
"set_name": msg.sticker.set_name,
"animated": msg.sticker.is_animated,
"video": msg.sticker.is_video,
"height": msg.sticker.height,
"width": msg.sticker.width,
"file_name": msg.sticker.file_name,
"file_size": msg.sticker.file_size,
"mime_type": msg.sticker.mime_type,
"thumbnails": thumbnails,
}
if msg.venue is not None:
analytics_entry["venue"] = {
"title": msg.venue.title,
"address": msg.venue.address,
"longitude": msg.venue.location.longitude,
"latitude": msg.venue.location.latitude,
"foursquare_id": msg.venue.foursquare_id,
"foursquare_type": msg.venue.foursquare_type,
}
if msg.video is not None:
thumbnails = []
for thumbail in msg.video.thumbs:
thumbnails.append(
{
"id": thumbail.file_id,
"height": thumbail.height,
"width": thumbail.width,
"file_size": thumbail.file_size,
}
)
analytics_entry["video"] = {
"id": msg.video.file_id,
"duration": msg.video.duration,
"height": msg.video.height,
"width": msg.video.width,
"file_name": msg.video.file_name,
"file_size": msg.video.file_size,
"mime_type": msg.video.mime_type,
"thumbnails": thumbnails,
}
if msg.video_note is not None:
thumbnails = []
for thumbail in msg.video_note.thumbs:
thumbnails.append(
{
"id": thumbail.file_id,
"height": thumbail.height,
"width": thumbail.width,
"file_size": thumbail.file_size,
}
)
analytics_entry["videonote"] = {
"id": msg.video_note.file_id,
"duration": msg.video_note.duration,
"length": msg.video_note.length,
"file_size": msg.video_note.file_size,
"mime_type": msg.video_note.mime_type,
"thumbnails": thumbnails,
}
if msg.voice is not None:
analytics_entry["voice"] = {
"id": msg.voice.file_id,
"duration": msg.voice.duration,
"file_size": msg.voice.file_size,
"mime_type": msg.voice.mime_type,
}
col_analytics_group.insert_one(analytics_entry)

View File

@@ -0,0 +1,288 @@
from typing import Literal
from dateutil.relativedelta import relativedelta
from datetime import datetime
from app import app
from pyrogram import filters
from pyrogram.types import (
ReplyKeyboardRemove,
InlineKeyboardMarkup,
InlineKeyboardButton,
ForceReply,
Message,
)
from pyrogram.client import Client
from pyrogram.enums.parse_mode import ParseMode
from classes.holo_user import HoloUser
from modules.utils import all_locales, configGet, locale, logWrite
from modules.handlers.welcome import welcome_pass
from modules.database import col_tmp, col_applications
from modules import custom_filters
confirmation_1 = []
for pattern in all_locales("confirm", "keyboard"):
confirmation_1.append(pattern[0][0])
@app.on_message(
(custom_filters.enabled_applications | custom_filters.enabled_sponsorships)
& ~filters.scheduled
& filters.private
& filters.command(confirmation_1, prefixes=[""])
& ~custom_filters.banned
)
async def confirm_yes(
app: Client,
msg: Message,
kind: Literal["application", "sponsorship", "unknown"] = "unknown",
):
holo_user = HoloUser(msg.from_user)
if configGet("enabled", "features", "applications") is True:
if (kind == "application") or (
(holo_user.application_state()[0] == "fill")
and (holo_user.application_state()[1] is True)
):
tmp_application = col_tmp.find_one(
{"user": holo_user.id, "type": "application"}
)
if tmp_application is None:
logWrite(f"Application of {holo_user.id} is nowhere to be found.")
return
if tmp_application["sent"] is True:
return
await msg.reply_text(
locale("application_sent", "message"),
reply_markup=ReplyKeyboardRemove(),
)
application_content = []
i = 1
for question in tmp_application["application"]:
if i == 2:
age = relativedelta(
datetime.now(), tmp_application["application"]["2"]
)
application_content.append(
f"{locale(f'question{i}', 'message', 'question_titles')} {tmp_application['application']['2'].strftime('%d.%m.%Y')} ({age.years} р.)"
)
elif i == 3:
if tmp_application["application"]["3"]["countryCode"] == "UA":
application_content.append(
f"{locale(f'question{i}', 'message', 'question_titles')} {tmp_application['application']['3']['name']}"
)
else:
application_content.append(
f"{locale(f'question{i}', 'message', 'question_titles')} {tmp_application['application']['3']['name']} ({tmp_application['application']['3']['adminName1']}, {tmp_application['application']['3']['countryName']})"
)
else:
application_content.append(
f"{locale(f'question{i}', 'message', 'question_titles')} {tmp_application['application'][question]}"
)
i += 1
if (
tmp_application["reapply"] is True
and col_applications.find_one({"user": holo_user.id}) is not None
):
await app.send_message(
chat_id=configGet("admin", "groups"),
text=(locale("reapply_got", "message")).format(
str(holo_user.id),
msg.from_user.first_name,
msg.from_user.username,
"\n".join(application_content),
),
parse_mode=ParseMode.MARKDOWN,
reply_markup=InlineKeyboardMarkup(
[
[
InlineKeyboardButton(
text=str(locale("reapply_yes", "button")),
callback_data=f"reapply_yes_{holo_user.id}",
)
],
[
InlineKeyboardButton(
text=str(locale("reapply_no", "button")),
callback_data=f"reapply_no_{holo_user.id}",
)
],
]
),
)
else:
await app.send_message(
chat_id=configGet("admin", "groups"),
text=(locale("application_got", "message")).format(
str(holo_user.id),
msg.from_user.first_name,
msg.from_user.username,
"\n".join(application_content),
),
parse_mode=ParseMode.MARKDOWN,
reply_markup=InlineKeyboardMarkup(
[
[
InlineKeyboardButton(
text=str(locale("sub_yes", "button")),
callback_data=f"sub_yes_{holo_user.id}",
)
],
[
InlineKeyboardButton(
text=str(locale("sub_no", "button")),
callback_data=f"sub_no_{holo_user.id}",
)
],
[
InlineKeyboardButton(
text=str(locale("sub_russian", "button")),
callback_data=f"sub_russian_{holo_user.id}",
)
],
]
),
)
logWrite(
f"User {holo_user.id} sent his application and it will now be reviewed"
)
col_tmp.update_one(
{"user": holo_user.id, "type": "application"}, {"$set": {"sent": True}}
)
return
if configGet("enabled", "features", "sponsorships") is True:
if (kind == "sponsorship") or (
(holo_user.sponsorship_state()[0] == "fill")
and (holo_user.sponsorship_state()[1] is True)
):
tmp_sponsorship = col_tmp.find_one(
{"user": holo_user.id, "type": "sponsorship"}
)
if tmp_sponsorship is None:
logWrite(f"Sponsorship of {holo_user.id} is nowhere to be found.")
return
if tmp_sponsorship["sent"] is True:
return
await msg.reply_text(
locale("sponsorship_sent", "message"),
reply_markup=ReplyKeyboardRemove(),
)
sponsorship_content = []
for question in tmp_sponsorship["sponsorship"]:
if question == "expires":
sponsorship_content.append(
f"{locale(f'question_{question}', 'message', 'sponsor_titles')} {tmp_sponsorship['sponsorship'][question].strftime('%d.%m.%Y')}"
)
elif question == "proof":
continue
else:
sponsorship_content.append(
f"{locale(f'question_{question}', 'message', 'sponsor_titles')} {tmp_sponsorship['sponsorship'][question]}"
)
await app.send_cached_media(
configGet("admin", "groups"),
tmp_sponsorship["sponsorship"]["proof"],
caption=(locale("sponsor_got", "message")).format(
str(holo_user.id),
msg.from_user.first_name,
msg.from_user.username,
"\n".join(sponsorship_content),
),
parse_mode=ParseMode.MARKDOWN,
reply_markup=InlineKeyboardMarkup(
[
[
InlineKeyboardButton(
text=str(locale("sponsor_yes", "button")),
callback_data=f"sponsor_yes_{holo_user.id}",
)
],
[
InlineKeyboardButton(
text=str(locale("sponsor_no", "button")),
callback_data=f"sponsor_no_{holo_user.id}",
)
],
]
),
)
# remove(f"tmp{sep}{filename}.jpg")
logWrite(
f"User {holo_user.id} sent his sponsorship application and it will now be reviewed"
)
col_tmp.update_one(
{"user": holo_user.id, "type": "sponsorship"}, {"$set": {"sent": True}}
)
return
confirmation_2 = []
for pattern in all_locales("confirm", "keyboard"):
confirmation_2.append(pattern[1][0])
@app.on_message(
(custom_filters.enabled_applications | custom_filters.enabled_sponsorships)
& ~filters.scheduled
& filters.private
& filters.command(confirmation_2, prefixes=[""])
& ~custom_filters.banned
)
async def confirm_no(
app: Client,
msg: Message,
kind: Literal["application", "sponsorship", "unknown"] = "unknown",
):
holo_user = HoloUser(msg.from_user)
if configGet("enabled", "features", "applications") is True:
if (kind == "application") or (
(holo_user.application_state()[0] == "fill")
and (holo_user.application_state()[1] is True)
):
holo_user.application_restart()
await welcome_pass(app, msg, once_again=True)
logWrite(
f"User {msg.from_user.id} restarted the application due to typo in it"
)
return
if configGet("enabled", "features", "sponsorships") is True:
if (kind == "sponsorship") or (
(holo_user.sponsorship_state()[0] == "fill")
and (holo_user.sponsorship_state()[1] is True)
):
holo_user.sponsorship_restart()
await app.send_message(
holo_user.id,
locale(f"sponsor1", "message", locale=holo_user.locale),
reply_markup=ForceReply(
placeholder=str(
locale(f"sponsor1", "force_reply", locale=holo_user.locale)
)
),
)
logWrite(
f"User {msg.from_user.id} restarted the sponsorship application due to typo in it"
)
return

View File

@@ -0,0 +1,83 @@
from dateutil.relativedelta import relativedelta
from datetime import datetime
from app import app
from pyrogram import filters
from pyrogram.types import Message
from pyrogram.client import Client
from modules.utils import locale, logWrite
from modules.database import col_applications
from classes.holo_user import HoloUser
from modules import custom_filters
@app.on_message(
custom_filters.enabled_applications
& ~filters.scheduled
& filters.contact
& filters.private
& (custom_filters.allowed | custom_filters.admin)
& ~custom_filters.banned
)
async def get_contact(app: Client, msg: Message):
holo_user = HoloUser(msg.from_user)
if msg.contact.user_id != None:
application = col_applications.find_one({"user": msg.contact.user_id})
if application is None:
logWrite(
f"User {holo_user.id} requested application of {msg.contact.user_id} but user does not exists"
)
await msg.reply_text(
locale("contact_invalid", "message", locale=holo_user.locale)
)
return
application_content = []
i = 1
for question in application["application"]:
if i == 2:
age = relativedelta(datetime.now(), application["application"]["2"])
application_content.append(
f"{locale(f'question{i}', 'message', 'question_titles', locale=holo_user.locale)} {application['application']['2'].strftime('%d.%m.%Y')} ({age.years} р.)"
)
elif i == 3:
if application["application"]["3"]["countryCode"] == "UA":
application_content.append(
f"{locale(f'question{i}', 'message', 'question_titles', locale=holo_user.locale)} {application['application']['3']['name']}"
)
else:
application_content.append(
f"{locale(f'question{i}', 'message', 'question_titles', locale=holo_user.locale)} {application['application']['3']['name']} ({application['application']['3']['adminName1']}, {application['application']['3']['countryName']})"
)
else:
application_content.append(
f"{locale(f'question{i}', 'message', 'question_titles', locale=holo_user.locale)} {application['application'][question]}"
)
i += 1
application_status = locale(
"application_status_accepted", "message", locale=holo_user.locale
).format(
(await app.get_users(application["admin"])).first_name,
application["date"].strftime("%d.%m.%Y, %H:%M"),
)
logWrite(f"User {holo_user.id} requested application of {msg.contact.user_id}")
await msg.reply_text(
locale("contact", "message", locale=holo_user.locale).format(
str(msg.contact.user_id),
"\n".join(application_content),
application_status,
)
)
else:
logWrite(
f"User {holo_user.id} requested application of someone but user is not telegram user"
)
await msg.reply_text(
locale("contact_not_member", "message", locale=holo_user.locale)
)

View File

@@ -0,0 +1,393 @@
from traceback import print_exc
from app import app, isAnAdmin
import asyncio
from ftfy import fix_text
from pyrogram import filters
from pyrogram.types import (
Message,
ForceReply,
InlineKeyboardMarkup,
InlineKeyboardButton,
ReplyKeyboardRemove,
)
from pyrogram.client import Client
from classes.holo_user import HoloUser
from modules.utils import configGet, logWrite, locale, all_locales
from modules.database import col_messages, col_spoilers
from modules import custom_filters
async def message_involved(msg: Message) -> bool:
message = col_messages.find_one(
{
"destination.id": msg.reply_to_message.id,
"destination.chat": msg.reply_to_message.chat.id,
}
)
if message is not None:
return True
return False
async def message_context(msg: Message) -> tuple:
message = col_messages.find_one(
{
"destination.id": msg.reply_to_message.id,
"destination.chat": msg.reply_to_message.chat.id,
}
)
if message is not None:
return message["origin"]["chat"], message["origin"]["id"]
return 0, 0
@app.on_message(
~filters.scheduled
& (filters.private | filters.chat(configGet("admin", "groups")))
& ~custom_filters.banned
)
async def any_stage(app: Client, msg: Message):
if msg.via_bot is None:
holo_user = HoloUser(msg.from_user)
if (msg.reply_to_message is not None) and (await message_involved(msg)):
context = await message_context(msg)
context_message = await app.get_messages(context[0], context[1])
destination_user = HoloUser(context_message.from_user)
if destination_user is None:
return
await destination_user.message(
origin=context_message,
context=msg,
text=str(msg.text),
caption=msg.caption,
photo=msg.photo,
video=msg.video,
file=msg.document,
animation=msg.animation,
voice=msg.voice,
adm_origin=await isAnAdmin(context_message.from_user.id),
adm_context=await isAnAdmin(msg.from_user.id),
)
return
if msg.chat.id == configGet("admin", "groups"):
return
if msg.text is not None:
if configGet("enabled", "features", "applications") is True:
await holo_user.application_next(str(msg.text), msg=msg)
if configGet("enabled", "features", "sponsorships") is True:
await holo_user.sponsorship_next(str(msg.text), msg)
if msg.photo is not None:
await holo_user.sponsorship_next(str(msg.text), msg=msg, photo=msg.photo)
if (
holo_user.application_state()[0] != "fill"
and holo_user.sponsorship_state()[0] != "fill"
):
if configGet("enabled", "features", "spoilers") is False:
return
spoiler = col_spoilers.find_one(
{"user": msg.from_user.id, "completed": False}
)
if spoiler is None:
return
if spoiler["category"] is None:
found = False
# Find category in all locales
for lc in all_locales("spoiler_categories", "message"):
for key in lc:
if lc[key] == str(msg.text):
found = True
category = key
if found is False:
await msg.reply_text(
locale(
"spoiler_incorrect_category",
"message",
locale=msg.from_user,
)
)
return
col_spoilers.find_one_and_update(
{"_id": spoiler["_id"]}, {"$set": {"category": category}}
)
await msg.reply_text(
locale("spoiler_send_description", "message", locale=msg.from_user),
reply_markup=ForceReply(
placeholder=locale(
"spoiler_description", "force_reply", locale=msg.from_user
)
),
)
return
if spoiler["description"] is None and (
spoiler["photo"] is None
and spoiler["video"] is None
and spoiler["audio"] is None
and spoiler["animation"] is None
and spoiler["text"] is None
):
# for lc in all_locales("spoiler_description", "keyboard"):
# if msg.text == lc[-1][0]:
# await msg.reply_text(locale("spoiler_description_enter", "message", locale=msg.from_user), reply_markup=ForceReply(placeholder=locale("spoiler_description", "force_reply", locale=msg.from_user)))
# return
msg.text = fix_text(str(msg.text))
if len(str(msg.text)) > 1024:
await msg.reply_text(
locale(
"spoiler_description_too_long",
"message",
locale=msg.from_user,
),
reply_markup=ForceReply(
placeholder=locale(
"spoiler_description",
"force_reply",
locale=msg.from_user,
)
),
)
return
col_spoilers.find_one_and_update(
{"user": msg.from_user.id, "completed": False},
{"$set": {"description": msg.text}},
)
logWrite(
f"Adding description '{str(msg.text)}' to {msg.from_user.id}'s spoiler"
)
await msg.reply_text(
locale(
"spoiler_using_description", "message", locale=msg.from_user
).format(msg.text),
reply_markup=ForceReply(
placeholder=locale(
"spoiler_content", "force_reply", locale=msg.from_user
)
),
)
return
ready = False
if msg.photo is not None:
col_spoilers.find_one_and_update(
{"user": msg.from_user.id, "completed": False},
{
"$set": {
"photo": msg.photo.file_id,
"caption": msg.caption,
"completed": True,
}
},
)
logWrite(
f"Adding photo with id {msg.photo.file_id} to {msg.from_user.id}'s spoiler"
)
ready = True
if msg.video is not None:
col_spoilers.find_one_and_update(
{"user": msg.from_user.id, "completed": False},
{
"$set": {
"video": msg.video.file_id,
"caption": msg.caption,
"completed": True,
}
},
)
logWrite(
f"Adding audio with id {msg.video.file_id} to {msg.from_user.id}'s spoiler"
)
ready = True
if msg.audio is not None:
col_spoilers.find_one_and_update(
{"user": msg.from_user.id, "completed": False},
{
"$set": {
"audio": msg.audio.file_id,
"caption": msg.caption,
"completed": True,
}
},
)
logWrite(
f"Adding video with id {msg.audio.file_id} to {msg.from_user.id}'s spoiler"
)
ready = True
if msg.animation is not None:
col_spoilers.find_one_and_update(
{"user": msg.from_user.id, "completed": False},
{
"$set": {
"animation": msg.animation.file_id,
"caption": msg.caption,
"completed": True,
}
},
)
logWrite(
f"Adding animation with id {msg.animation.file_id} to {msg.from_user.id}'s spoiler"
)
ready = True
if msg.document is not None:
col_spoilers.find_one_and_update(
{"user": msg.from_user.id, "completed": False},
{
"$set": {
"document": msg.document.file_id,
"caption": msg.caption,
"completed": True,
}
},
)
logWrite(
f"Adding document with id {msg.document.file_id} to {msg.from_user.id}'s spoiler"
)
ready = True
if (
spoiler["photo"] is None
and spoiler["video"] is None
and spoiler["audio"] is None
and spoiler["animation"] is None
and spoiler["document"] is None
and spoiler["text"] is None
):
if msg.text is not None:
col_spoilers.find_one_and_update(
{"user": msg.from_user.id, "completed": False},
{"$set": {"text": str(msg.text), "completed": True}},
)
logWrite(
f"Adding text '{str(msg.text)}' to {msg.from_user.id}'s spoiler"
)
ready = True
if ready is True:
await msg.reply_text(
locale("spoiler_ready", "message", locale=msg.from_user),
reply_markup=ReplyKeyboardRemove(),
)
if configGet("allow_external", "features", "spoilers") is True:
await msg.reply_text(
locale("spoiler_send", "message", locale=msg.from_user),
reply_markup=InlineKeyboardMarkup(
[
[
InlineKeyboardButton(
locale(
"spoiler_preview",
"button",
locale=msg.from_user,
),
callback_data=f"sid_{spoiler['_id'].__str__()}",
)
],
[
InlineKeyboardButton(
locale(
"spoiler_send_chat",
"button",
locale=msg.from_user,
),
callback_data=f"shc_{spoiler['_id'].__str__()}",
)
],
[
InlineKeyboardButton(
locale(
"spoiler_send_other",
"button",
locale=msg.from_user,
),
switch_inline_query=f"spoiler:{spoiler['_id'].__str__()}",
)
],
]
),
)
else:
await msg.reply_text(
locale("spoiler_send", "message", locale=msg.from_user),
reply_markup=InlineKeyboardMarkup(
[
[
InlineKeyboardButton(
locale(
"spoiler_preview",
"button",
locale=msg.from_user,
),
callback_data=f"sid_{spoiler['_id'].__str__()}",
)
],
[
InlineKeyboardButton(
locale(
"spoiler_send_chat",
"button",
locale=msg.from_user,
),
callback_data=f"shc_{spoiler['_id'].__str__()}",
)
],
]
),
)
else:
await msg.reply_text(
locale("spoiler_incorrect_content", "message", locale=msg.from_user)
)
@app.on_message(~filters.scheduled & filters.group)
async def message_in_group(app: Client, msg: Message):
if (msg.chat is not None) and (msg.via_bot is not None):
if (msg.via_bot.id == (await app.get_me()).id) and (
msg.chat.id == configGet("users", "groups")
):
if str(msg.text).startswith(
locale("spoiler_described", "message").split()[0]
):
logWrite(f"User {msg.from_user.id} sent spoiler to user's group")
try:
logWrite("Forwarding spoiler to admin's group")
await msg.copy(
configGet("admin", "groups"), disable_notification=True
)
except Exception as exp:
logWrite(
f"Could not forward spoiler to admin's group due to '{exp}': {print_exc()}"
)
return
if configGet("remove_application_time") > 0:
logWrite(
f"User {msg.from_user.id} requested application in destination group, removing in {configGet('remove_application_time')} minutes"
)
await asyncio.sleep(configGet("remove_application_time") * 60)
await msg.delete()
logWrite(
f"Removed application requested by {msg.from_user.id} in destination group"
)

View File

@@ -0,0 +1,164 @@
from datetime import datetime
from app import app, isAnAdmin
from pyrogram.types import (
ChatPermissions,
InlineKeyboardMarkup,
InlineKeyboardButton,
ChatMemberUpdated,
)
from pyrogram.client import Client
from modules.utils import configGet, locale
from modules import custom_filters
from modules.logging import logWrite
from modules.database import col_applications
from classes.holo_user import HoloUser
from dateutil.relativedelta import relativedelta
@app.on_chat_member_updated(
custom_filters.enabled_invites_check, group=configGet("users", "groups")
)
# @app.on_message(filters.new_chat_members, group=configGet("users", "groups"))
async def filter_join(app: Client, member: ChatMemberUpdated):
if member.invite_link != None:
holo_user = HoloUser(member.from_user)
if (holo_user.link is not None) and (
holo_user.link == member.invite_link.invite_link
):
logWrite(
f"User {holo_user.id} joined destination group with correct link {holo_user.link}"
)
application = col_applications.find_one({"user": holo_user.id})
application_content = []
i = 1
for question in application["application"]:
if i == 2:
age = relativedelta(datetime.now(), application["application"]["2"])
application_content.append(
f"{locale(f'question{i}', 'message', 'question_titles')} {application['application']['2'].strftime('%d.%m.%Y')} ({age.years} р.)"
)
elif i == 3:
if application["application"]["3"]["countryCode"] == "UA":
application_content.append(
f"{locale(f'question{i}', 'message', 'question_titles')} {application['application']['3']['name']}"
)
else:
application_content.append(
f"{locale(f'question{i}', 'message', 'question_titles')} {application['application']['3']['name']} ({application['application']['3']['adminName1']}, {application['application']['3']['countryName']})"
)
else:
application_content.append(
f"{locale(f'question{i}', 'message', 'question_titles')} {application['application'][question]}"
)
i += 1
await app.send_message(
configGet("users", "groups"),
locale("joined_application", "message").format(
member.from_user.first_name,
member.from_user.username,
"\n".join(application_content),
),
)
return
if await isAnAdmin(member.invite_link.creator.id):
logWrite(
f"User {holo_user.id} joined destination group with link {holo_user.link} of an admin {member.invite_link.creator.id}"
)
application = col_applications.find_one({"user": holo_user.id})
if application is None:
return
application_content = []
i = 1
for question in application["application"]:
if i == 2:
age = relativedelta(datetime.now(), application["application"]["2"])
application_content.append(
f"{locale(f'question{i}', 'message', 'question_titles')} {application['application']['2'].strftime('%d.%m.%Y')} ({age.years} р.)"
)
elif i == 3:
if application["application"]["3"]["countryCode"] == "UA":
application_content.append(
f"{locale(f'question{i}', 'message', 'question_titles')} {application['application']['3']['name']}"
)
else:
application_content.append(
f"{locale(f'question{i}', 'message', 'question_titles')} {application['application']['3']['name']} ({application['application']['3']['adminName1']}, {application['application']['3']['countryName']})"
)
else:
application_content.append(
f"{locale(f'question{i}', 'message', 'question_titles')} {application['application'][question]}"
)
i += 1
await app.send_message(
configGet("users", "groups"),
locale("joined_application", "message").format(
member.from_user.first_name,
member.from_user.username,
"\n".join(application_content),
),
)
return
logWrite(
f"User {holo_user.id} joined destination group with stolen/unapproved link {holo_user.link}"
)
await app.send_message(
configGet("admin", "groups"),
locale("joined_false_link", "message").format(
member.from_user.first_name, member.from_user.id
),
reply_markup=InlineKeyboardMarkup(
[
[
InlineKeyboardButton(
text=str(locale("sus_allow", "button")),
callback_data=f"sus_allow_{member.from_user.id}",
)
],
[
InlineKeyboardButton(
text=str(locale("sus_reject", "button")),
callback_data=f"sus_reject_{member.from_user.id}",
)
],
]
),
)
await app.restrict_chat_member(
member.chat.id,
member.from_user.id,
permissions=ChatPermissions(
can_send_messages=False,
can_send_media_messages=False,
can_send_other_messages=False,
can_send_polls=False,
),
)
return
if member.new_chat_member is None:
await app.send_message(
configGet("users", "groups"),
locale("user_left", "message").format(
member.old_chat_member.user.first_name
),
)
logWrite(
f"User {member.old_chat_member.user.first_name} ({member.old_chat_member.user.id}) left the destination group"
)
return

19
modules/handlers/voice.py Normal file
View File

@@ -0,0 +1,19 @@
from random import choice
from app import app
from pyrogram import filters
from pyrogram.types import Message
from pyrogram.client import Client
from modules.logging import logWrite
from modules.utils import configGet, locale
from modules import custom_filters
@app.on_message(
custom_filters.enabled_dinovoice
& ~filters.scheduled
& filters.voice
& filters.chat(configGet("users", "groups"))
)
async def voice_message(app: Client, msg: Message):
logWrite(f"User {msg.from_user.id} sent voice message in destination group")
await msg.reply_text(choice(locale("voice_message", "message")))

View File

@@ -0,0 +1,73 @@
from app import app
from pyrogram import filters
from pyrogram.types import ForceReply, ReplyKeyboardMarkup, Message
from pyrogram.client import Client
from classes.holo_user import HoloUser
from modules.utils import all_locales, locale, logWrite
from modules import custom_filters
welcome_1 = []
for pattern in all_locales("welcome", "keyboard"):
welcome_1.append(pattern[0][0])
for pattern in all_locales("return", "keyboard"):
welcome_1.append(pattern[0][0])
@app.on_message(
custom_filters.enabled_applications
& ~filters.scheduled
& filters.private
& filters.command(welcome_1, prefixes=[""])
& ~custom_filters.banned
)
async def welcome_pass(app: Client, msg: Message, once_again: bool = False) -> None:
"""Set user's stage to 1 and start a fresh application
### Args:
* app (app): Pyrogram Client to use
* msg (Message): Message with .from_user.id attribute equal to the end-user ID whose application will be started
* once_again (bool, optional): Set to False if it's the first time as user applies. Defaults to True.
"""
if not once_again:
await msg.reply_text(locale("privacy_notice", "message"))
holo_user = HoloUser(msg.from_user)
if once_again is False:
holo_user.application_restart()
if once_again is True:
logWrite(f"User {msg.from_user.id} confirmed starting the application")
else:
logWrite(
f"User {msg.from_user.id} confirmed starting the application once again"
)
await msg.reply_text(
locale("question1", "message", locale=msg.from_user),
reply_markup=ForceReply(
placeholder=locale("question1", "force_reply", locale=msg.from_user)
),
)
welcome_2 = []
for pattern in all_locales("welcome", "keyboard"):
welcome_2.append(pattern[1][0])
@app.on_message(
custom_filters.enabled_applications
& ~filters.scheduled
& filters.private
& filters.command(welcome_2, prefixes=[""])
& ~custom_filters.banned
)
async def welcome_reject(app: Client, msg: Message):
logWrite(f"User {msg.from_user.id} rejected to start the application")
await msg.reply_text(
locale("goodbye", "message", locale=msg.from_user),
reply_markup=ReplyKeyboardMarkup(
locale("return", "keyboard", locale=msg.from_user), resize_keyboard=True
),
)

309
modules/inline.py Normal file
View File

@@ -0,0 +1,309 @@
"""Module responsible for providing answers to
all inline queries that bot receives"""
from datetime import datetime
from os import path, sep
from app import app, isAnAdmin
from pyrogram.types import (
InlineQueryResultArticle,
InputTextMessageContent,
InlineQuery,
InlineKeyboardMarkup,
InlineKeyboardButton,
)
from pyrogram.client import Client
from pyrogram.enums.chat_type import ChatType
from pyrogram.enums.chat_members_filter import ChatMembersFilter
from dateutil.relativedelta import relativedelta
from classes.errors.holo_user import UserNotFoundError, UserInvalidError
from classes.holo_user import HoloUser
from modules.logging import logWrite
from modules.utils import configGet, jsonLoad, locale
from modules.database import col_applications, col_spoilers
from bson.objectid import ObjectId
from bson.errors import InvalidId
@app.on_inline_query()
async def inline_answer(client: Client, inline_query: InlineQuery):
results = []
if configGet("allow_external", "features", "spoilers") is True:
if inline_query.query.startswith("spoiler:"):
try:
spoil = col_spoilers.find_one(
{"_id": ObjectId(inline_query.query.removeprefix("spoiler:"))}
)
if spoil is not None:
desc = locale(
"spoiler_described",
"message",
locale=inline_query.from_user,
).format(
locale(spoil["category"], "message", "spoiler_categories"),
spoil["description"],
)
results = [
InlineQueryResultArticle(
title=locale(
"title",
"inline",
"spoiler",
locale=inline_query.from_user,
),
description=locale(
"description",
"inline",
"spoiler",
locale=inline_query.from_user,
),
input_message_content=InputTextMessageContent(
desc, disable_web_page_preview=True
),
reply_markup=InlineKeyboardMarkup(
[
[
InlineKeyboardButton(
locale(
"spoiler_view",
"button",
locale=inline_query.from_user,
),
callback_data=f'sid_{inline_query.query.removeprefix("spoiler:")}',
)
]
]
),
)
]
except InvalidId:
results = []
await inline_query.answer(results=results)
return
if inline_query.chat_type in [ChatType.CHANNEL]:
await inline_query.answer(
results=[
InlineQueryResultArticle(
title=locale(
"title", "inline", "not_pm", locale=inline_query.from_user
),
input_message_content=InputTextMessageContent(
locale(
"message_content",
"inline",
"not_pm",
locale=inline_query.from_user,
)
),
description=locale(
"description", "inline", "not_pm", locale=inline_query.from_user
),
)
]
)
return
results_forbidden = [
InlineQueryResultArticle(
title=locale("title", "inline", "forbidden", locale=inline_query.from_user),
input_message_content=InputTextMessageContent(
locale(
"message_content",
"inline",
"forbidden",
locale=inline_query.from_user,
)
),
description=locale(
"description", "inline", "forbidden", locale=inline_query.from_user
),
)
]
try:
holo_user = HoloUser(inline_query.from_user)
except (UserNotFoundError, UserInvalidError):
logWrite(
f"Could not find application of {inline_query.from_user.id}, ignoring inline query",
debug=True,
)
await inline_query.answer(results=results_forbidden)
return
if path.exists(path.join(configGet("cache", "locations"), "group_members")) and (
inline_query.from_user.id
not in jsonLoad(path.join(configGet("cache", "locations"), "group_members"))
):
if path.exists(path.join(configGet("cache", "locations"), "admins")) and (
inline_query.from_user.id
not in jsonLoad(path.join(configGet("cache", "locations"), "admins"))
):
logWrite(
f"{inline_query.from_user.id} is not an admin and not in members group, ignoring inline query",
debug=True,
)
await inline_query.answer(results=results_forbidden)
return
if holo_user.application_approved() or (await isAnAdmin(holo_user.id) is True):
max_results = (
configGet("inline_preview_count") if inline_query.query != "" else 200
)
list_of_users = []
async for m in app.get_chat_members(
configGet("users", "groups"),
limit=max_results,
filter=ChatMembersFilter.SEARCH,
query=inline_query.query,
):
list_of_users.append(m)
for match in list_of_users:
application = col_applications.find_one({"user": match.user.id})
if application is None:
continue
application_content = []
i = 1
for question in application["application"]:
if i == 2:
age = relativedelta(datetime.now(), application["application"]["2"])
application_content.append(
f"{locale(f'question{i}', 'message', 'question_titles', locale=inline_query.from_user)} {application['application']['2'].strftime('%d.%m.%Y')} ({age.years} р.)"
)
elif i == 3:
if application["application"]["3"]["countryCode"] == "UA":
application_content.append(
f"{locale(f'question{i}', 'message', 'question_titles', locale=inline_query.from_user)} {application['application']['3']['name']}"
)
else:
application_content.append(
f"{locale(f'question{i}', 'message', 'question_titles', locale=inline_query.from_user)} {application['application']['3']['name']} ({application['application']['3']['adminName1']}, {application['application']['3']['countryName']})"
)
else:
application_content.append(
f"{locale(f'question{i}', 'message', 'question_titles', locale=inline_query.from_user)} {application['application'][question]}"
)
i += 1
if match.user.photo != None:
try:
if not path.exists(
f'{configGet("cache", "locations")}{sep}avatars{sep}{match.user.photo.big_file_id}'
):
print(
f'Downloaded avatar {match.user.photo.big_file_id} of {match.user.id} and uploaded to {configGet("api")}/avatars/{match.user.photo.big_file_id}',
flush=True,
)
await app.download_media(
match.user.photo.big_file_id,
file_name=f'{configGet("cache", "locations")}{sep}avatars{sep}{match.user.photo.big_file_id}',
)
results.append(
InlineQueryResultArticle(
title=str(match.user.first_name),
input_message_content=InputTextMessageContent(
locale(
"message_content",
"inline",
"user",
locale=inline_query.from_user,
).format(
match.user.first_name,
match.user.username,
"\n".join(application_content),
)
),
description=locale(
"description",
"inline",
"user",
locale=inline_query.from_user,
).format(match.user.first_name, match.user.username),
thumb_url=f'{configGet("api")}/avatars/{match.user.photo.big_file_id}',
)
)
except ValueError:
results.append(
InlineQueryResultArticle(
title=str(match.user.first_name),
input_message_content=InputTextMessageContent(
locale(
"message_content",
"inline",
"user",
locale=inline_query.from_user,
).format(
match.user.first_name,
match.user.username,
"\n".join(application_content),
)
),
description=locale(
"description",
"inline",
"user",
locale=inline_query.from_user,
).format(match.user.first_name, match.user.username),
)
)
except FileNotFoundError:
results.append(
InlineQueryResultArticle(
title=str(match.user.first_name),
input_message_content=InputTextMessageContent(
locale(
"message_content",
"inline",
"user",
locale=inline_query.from_user,
).format(
match.user.first_name,
match.user.username,
"\n".join(application_content),
)
),
description=locale(
"description",
"inline",
"user",
locale=inline_query.from_user,
).format(match.user.first_name, match.user.username),
)
)
else:
results.append(
InlineQueryResultArticle(
title=str(match.user.first_name),
input_message_content=InputTextMessageContent(
locale(
"message_content",
"inline",
"user",
locale=inline_query.from_user,
).format(
match.user.first_name,
match.user.username,
"\n".join(application_content),
)
),
description=locale(
"description",
"inline",
"user",
locale=inline_query.from_user,
).format(match.user.first_name, match.user.username),
)
)
await inline_query.answer(results=results, cache_time=10, is_personal=True)

View File

@@ -5,14 +5,20 @@ from shutil import copyfileobj
from datetime import datetime
with open(getcwd()+path.sep+"config.json", "r", encoding='utf8') as file:
with open(getcwd() + path.sep + "config.json", "r", encoding="utf8") as file:
json_contents = loads(file.read())
log_size = json_contents["logging"]["size"]
log_folder = json_contents["logging"]["location"]
file.close()
# Check latest log size
def checkSize(debug=False):
"""Check size of latest.log file and rotate it if needed
### Args:
* debug (`bool`, *optional*): Whether this is a debug log. Defaults to `False`.
"""
global log_folder
@@ -25,17 +31,32 @@ def checkSize(debug=False):
makedirs(log_folder, exist_ok=True)
log = stat(path.join(log_folder, log_file))
if (log.st_size / 1024) > log_size:
with open(path.join(log_folder, log_file), 'rb') as f_in:
with gzipopen(path.join(log_folder, f'{datetime.now().strftime("%d.%m.%Y_%H:%M:%S")}.log.gz'), 'wb') as f_out:
with open(path.join(log_folder, log_file), "rb") as f_in:
with gzipopen(
path.join(
log_folder,
f'{datetime.now().strftime("%d.%m.%Y_%H:%M:%S")}.log.gz',
),
"wb",
) as f_out:
copyfileobj(f_in, f_out)
print(f'Copied {path.join(log_folder, datetime.now().strftime("%d.%m.%Y_%H:%M:%S"))}.log.gz')
open(path.join(log_folder, log_file), 'w').close()
print(
f'Copied {path.join(log_folder, datetime.now().strftime("%d.%m.%Y_%H:%M:%S"))}.log.gz'
)
open(path.join(log_folder, log_file), "w").close()
except FileNotFoundError:
print(f'Log file {path.join(log_folder, log_file)} does not exist')
print(f"Log file {path.join(log_folder, log_file)} does not exist")
pass
# Append string to log
def logAppend(message, debug=False):
def logAppend(message: str, debug=False):
"""Write message to log file
### Args:
* message (`str`): Message to write
* debug (`bool`, *optional*): Whether this is a debug log. Defaults to `False`.
"""
global log_folder
@@ -47,12 +68,19 @@ def logAppend(message, debug=False):
else:
log_file = "latest.log"
log = open(path.join(log_folder, log_file), 'a')
log.write(f'{message_formatted}\n')
log = open(path.join(log_folder, log_file), "a")
log.write(f"{message_formatted}\n")
log.close()
# Print to stdout and then to log
def logWrite(message, debug=False):
def logWrite(message: str, debug=False):
"""Write message to stdout and log file
### Args:
* message (`str`): Message to print and write
* debug (`bool`, *optional*): Whether this is a debug log. Defaults to `False`.
"""
# save to log file and rotation is to be done
logAppend(f'{message}', debug=debug)
print(f"{message}", flush=True)
logAppend(f"{message}", debug=debug)
print(f"{message}", flush=True)

464
modules/scheduled.py Normal file
View File

@@ -0,0 +1,464 @@
"""Automatically register commands and execute
some scheduled tasks is the main idea of this module"""
from asyncio import sleep
from os import listdir, makedirs, path, sep
from traceback import format_exc
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from datetime import datetime, timedelta
from ujson import dumps
from app import app
from pyrogram.types import (
BotCommand,
BotCommandScopeChat,
BotCommandScopeChatAdministrators,
)
from pyrogram.errors import bad_request_400
from pyrogram.enums.chat_members_filter import ChatMembersFilter
from classes.holo_user import HoloUser
from modules.utils import configGet, jsonLoad, jsonSave, locale, logWrite
from dateutil.relativedelta import relativedelta
from modules.database import (
col_applications,
col_sponsorships,
col_youtube,
col_warnings,
)
from xmltodict import parse
from requests import get
scheduler = AsyncIOScheduler()
if configGet("enabled", "scheduler", "cache_members"):
@scheduler.scheduled_job(
trigger="interval", seconds=configGet("interval", "scheduler", "cache_members")
)
async def cache_group_members():
list_of_users = []
async for member in app.get_chat_members(configGet("users", "groups")):
list_of_users.append(member.user.id)
makedirs(configGet("cache", "locations"), exist_ok=True)
jsonSave(
list_of_users, path.join(configGet("cache", "locations"), "group_members")
)
if configGet("debug") is True:
logWrite("User group caching performed", debug=True)
if configGet("enabled", "scheduler", "cache_admins"):
@scheduler.scheduled_job(
trigger="interval", seconds=configGet("interval", "scheduler", "cache_admins")
)
async def cache_admins():
list_of_users = []
async for member in app.get_chat_members(configGet("admin", "groups")):
list_of_users.append(member.user.id)
makedirs(configGet("cache", "locations"), exist_ok=True)
jsonSave(list_of_users, path.join(configGet("cache", "locations"), "admins"))
if configGet("debug") is True:
logWrite("Admin group caching performed", debug=True)
# Cache the avatars of group members
if configGet("enabled", "scheduler", "cache_avatars"):
@scheduler.scheduled_job(
trigger="date", run_date=datetime.now() + timedelta(seconds=15)
)
@scheduler.scheduled_job(
trigger="interval", hours=configGet("interval", "scheduler", "cache_avatars")
)
async def cache_avatars():
list_of_users = []
async for member in app.get_chat_members(
configGet("users", "groups"), filter=ChatMembersFilter.SEARCH, query=""
):
list_of_users.append(member.user)
for user in list_of_users:
if user.photo != None:
if not path.exists(
f'{configGet("cache", "locations")}{sep}avatars{sep}{user.photo.big_file_id}'
):
print(
f"Pre-cached avatar {user.photo.big_file_id} of {user.id}",
flush=True,
)
await app.download_media(
user.photo.big_file_id,
file_name=path.join(
configGet("cache", "locations"),
"avatars",
user.photo.big_file_id,
),
)
logWrite("Avatars caching performed")
# Check for birthdays
if configGet("enabled", "features", "applications") is True:
if configGet("enabled", "scheduler", "birthdays") is True:
@scheduler.scheduled_job(
trigger="cron", hour=configGet("time", "scheduler", "birthdays")
)
async def check_birthdays():
for entry in col_applications.find():
if entry["application"]["2"].strftime(
"%d.%m"
) == datetime.now().strftime("%d.%m"):
try:
if entry["user"] not in jsonLoad(
path.join(configGet("cache", "locations"), "group_members")
):
continue
tg_user = await app.get_users(entry["user"])
await app.send_message(configGet("admin", "groups"), locale("birthday", "message").format(str(tg_user.first_name), str(tg_user.username), str(relativedelta(datetime.now(), entry["application"]["2"], "%d.%m.%Y").years))) # type: ignore
logWrite(f"Notified admins about {entry['user']}'s birthday")
except Exception as exp:
logWrite(
f"Could not find user {entry['user']} to send a message about birthday due to '{exp}'"
)
continue
logWrite("Birthdays check performed")
# Check for expired sponsorships
if configGet("enabled", "features", "sponsorships") is True:
if configGet("enabled", "scheduler", "sponsorships") is True:
@scheduler.scheduled_job(
trigger="cron", hour=configGet("time", "scheduler", "sponsorships")
)
async def check_sponsors():
for entry in col_sponsorships.find(
{"sponsorship.expires": {"$lt": datetime.now() + timedelta(days=3)}}
):
try:
if entry["user"] not in jsonLoad(
path.join(configGet("cache", "locations"), "group_members")
):
continue
tg_user = await app.get_users(entry["user"])
until_expiry = (
abs(
relativedelta(
datetime.now(), entry["sponsorship"]["expires"]
).days
)
+ 1
)
await app.send_message(tg_user.id, locale("sponsorships_expires", "message").format(until_expiry)) # type: ignore
logWrite(
f"Notified user {entry['user']} that sponsorship expires in {until_expiry} days"
)
except Exception as exp:
logWrite(
f"Could not find user {entry['user']} notify about sponsorship expiry due to '{exp}'"
)
continue
for entry in col_sponsorships.find(
{
"sponsorship.expires": {
"$lt": datetime.now()
- timedelta(
days=configGet("grayout_days", "scheduler", "sponsorships")
)
}
}
):
try:
holo_user = HoloUser(entry["user"])
col_sponsorships.find_one_and_delete({"user": holo_user.id})
if entry["user"] not in jsonLoad(
path.join(configGet("cache", "locations"), "group_members")
):
continue
await app.send_message(entry["user"], locale("sponsorships_expired", "message")) # type: ignore
await holo_user.label_reset(configGet("users", "groups"))
try:
tg_user = await app.get_users(entry["user"])
logWrite(
f"Notified user {entry['user']} that sponsorship expired"
)
except Exception as exp:
logWrite(
f"Could not find user {entry['user']} notify about sponsorship expired due to '{exp}'"
)
except Exception as exp:
logWrite(
f"Could not reset label of user {entry['user']} due to '{exp}'"
)
continue
logWrite("Sponsorships check performed")
# Revoke old warnings
if configGet("enabled", "features", "warnings") is True:
if configGet("enabled", "scheduler", "warnings_revocation") is True:
@scheduler.scheduled_job(
trigger="date", run_date=datetime.now() + timedelta(seconds=10)
)
@scheduler.scheduled_job(
trigger="interval",
hours=configGet("interval", "scheduler", "warnings_revocation"),
)
async def revoke_warnings():
for warning in list(
col_warnings.find(
{
"active": True,
"date": {"$lt": datetime.now() - timedelta(days=90)},
}
)
):
if (
col_warnings.count_documents(
{
"user": warning["user"],
"active": True,
"date": {"$gt": datetime.now() - timedelta(days=90)},
}
)
== 0
):
col_warnings.update_one(
{"_id": warning["_id"]},
{"$set": {"active": False, "revoke_date": datetime.now()}},
)
logWrite(
f'Revoked warning {str(warning["_id"])} of user {warning["user"]} because no active warnings for the last 90 days found.'
)
await app.send_message(
configGet("admin", "groups"),
locale("warning_revoked_auto", "message").format(
warning["user"], warning["date"].strftime("%d.%m.%Y")
),
)
# Register all bot commands
@scheduler.scheduled_job(
trigger="date", run_date=datetime.now() + timedelta(seconds=10)
)
async def commands_register():
commands = {
"users": [],
"admins": [],
"owner": [],
"group_users": [],
"group_admins": [],
"group_users_admins": [],
"locales": {},
}
commands_raw = {
"users": [],
"admins": [],
"owner": [],
"group_users": [],
"group_admins": [],
"group_users_admins": [],
"locales": {},
}
valid_locales = []
files_locales = listdir(f'{configGet("locale", "locations")}')
for entry in files_locales:
if entry.endswith(".json"):
valid_locales.append(".".join(entry.split(".")[:-1]))
commands["locales"][".".join(entry.split(".")[:-1])] = {
"users": [],
"admins": [],
"owner": [],
"group_users": [],
"group_admins": [],
"group_users_admins": [],
}
if configGet("debug") is True:
commands_raw["locales"][".".join(entry.split(".")[:-1])] = {
"users": [],
"admins": [],
"owner": [],
"group_users": [],
"group_admins": [],
"group_users_admins": [],
}
config_modules = configGet("features")
config_commands = configGet("commands")
for command in config_commands:
enabled = False
for module in config_commands[command]["modules"]:
if config_modules[module]["enabled"] is True:
enabled = True
if enabled is False:
logWrite(f"Not registering {command} at all", debug=True)
continue
for permission in config_commands[command]["permissions"]:
commands[permission].append(
BotCommand(command, locale("commands")[command])
)
if configGet("debug") is True:
commands_raw[permission].append(
{f"{command}": locale("commands")[command]}
)
logWrite(f"Registering {command} for {permission}")
for lc in valid_locales:
commands["locales"][lc][permission].append(
BotCommand(command, locale("commands", locale=lc)[command])
)
if configGet("debug") is True:
commands_raw["locales"][lc][permission].append(
{f"{command}": locale("commands", locale=lc)[command]}
)
logWrite(f"Registering {command} for {permission} [{lc}]")
# Registering user commands
await app.set_bot_commands(commands["users"])
logWrite("Registered user commands for default locale")
# Registering user commands for each locale
for lc in valid_locales:
await app.set_bot_commands(commands["locales"][lc]["users"], language_code=lc)
logWrite(f"Registered user commands for locale {lc}")
# Registering admin commands
for admin in configGet("admins"):
try:
await app.set_bot_commands(
commands["admins"] + commands["users"],
scope=BotCommandScopeChat(chat_id=admin),
)
logWrite(f"Registered admin commands for admin {admin}")
except bad_request_400.PeerIdInvalid:
pass
# Registering owner commands
try:
await app.set_bot_commands(
commands["admins"] + commands["owner"] + commands["users"],
scope=BotCommandScopeChat(chat_id=configGet("owner")),
)
for lc in valid_locales:
await app.set_bot_commands(
commands["locales"][lc]["admins"]
+ commands["locales"][lc]["owner"]
+ commands["locales"][lc]["users"],
scope=BotCommandScopeChat(chat_id=configGet("owner")),
)
logWrite(f"Registered admin commands for owner {configGet('owner')}")
except bad_request_400.PeerIdInvalid:
pass
# Registering admin group commands
try:
await app.set_bot_commands(
commands["group_admins"],
scope=BotCommandScopeChat(chat_id=configGet("admin", "groups")),
)
logWrite("Registered admin group commands for default locale")
except bad_request_400.ChannelInvalid:
logWrite(
f"Could not register commands for admin group. Bot is likely not in the group."
)
# Registering destination group commands
try:
await app.set_bot_commands(
commands["group_users"],
scope=BotCommandScopeChat(chat_id=configGet("users", "groups")),
)
logWrite("Registered destination group commands")
except bad_request_400.ChannelInvalid:
logWrite(
f"Could not register commands for destination group. Bot is likely not in the group."
)
# Registering destination group admin commands
try:
await app.set_bot_commands(
commands["group_users_admins"],
scope=BotCommandScopeChatAdministrators(
chat_id=configGet("users", "groups")
),
)
logWrite("Registered destination group admin commands")
except bad_request_400.ChannelInvalid:
logWrite(
f"Could not register admin commands for destination group. Bot is likely not in the group."
)
if configGet("debug") is True:
print(commands, flush=True)
logWrite(
f"Complete commands registration:\n{dumps(commands_raw, indent=4, ensure_ascii=False, encode_html_chars=False)}",
debug=True,
)
if configGet("enabled", "scheduler", "channels_monitor"):
@scheduler.scheduled_job(
trigger="interval",
minutes=configGet("interval", "scheduler", "channels_monitor"),
)
async def channels_monitor():
for channel in configGet("channels", "scheduler", "channels_monitor"):
if configGet("debug") is True:
logWrite(
f'Processing videos of {channel["name"]} ({channel["id"]})',
debug=True,
)
try:
req = get(
f'https://www.youtube.com/feeds/videos.xml?channel_id={channel["id"]}'
)
parsed = parse(req.content)
if "feed" not in parsed:
continue
if "entry" not in parsed["feed"]:
continue
for entry in parsed["feed"]["entry"]:
if "yt:videoId" not in entry:
continue
if (
col_youtube.find_one(
{"channel": channel["id"], "video": entry["yt:videoId"]}
)
is None
):
col_youtube.insert_one(
{
"channel": channel["id"],
"video": entry["yt:videoId"],
"date": datetime.fromisoformat(entry["published"]),
}
)
await app.send_message(
configGet("users", "groups"),
locale("youtube_video", "message").format(
channel["name"],
channel["link"],
entry["title"],
entry["link"]["@href"],
),
disable_web_page_preview=False,
)
await sleep(2)
except Exception as exp:
logWrite(
f'Could not get last videos of {channel["name"]} ({channel["id"]}) due to {exp}: {format_exc()}'
)
if configGet("debug") is True:
logWrite("Admin group caching performed", debug=True)

View File

@@ -1,78 +1,120 @@
from typing import Union
from typing import Any, Literal, Tuple, Union
from uuid import uuid1
from requests import get
from pyrogram.enums.chat_type import ChatType
from pyrogram.types import User
from pyrogram.client import Client
from pyrogram.errors import bad_request_400
from ujson import JSONDecodeError as JSONDecodeError
from ujson import loads, dumps
from sys import exit
from os import kill, sep
from os import kill, listdir, makedirs, path, sep
from os import name as osname
from traceback import print_exc
from classes.errors.geo import PlaceNotFoundError
from modules.logging import logWrite
def jsonLoad(filename):
"""Loads arg1 as json and returns its contents"""
with open(filename, "r", encoding='utf8') as file:
with open(filename, "r", encoding="utf8") as file:
try:
output = loads(file.read())
except JSONDecodeError:
logWrite(f"Could not load json file {filename}: file seems to be incorrect!\n{print_exc()}")
logWrite(
f"Could not load json file {filename}: file seems to be incorrect!\n{print_exc()}"
)
raise
except FileNotFoundError:
logWrite(f"Could not load json file {filename}: file does not seem to exist!\n{print_exc()}")
logWrite(
f"Could not load json file {filename}: file does not seem to exist!\n{print_exc()}"
)
raise
file.close()
return output
def jsonSave(contents, filename):
"""Dumps dict/list arg1 to file arg2"""
try:
with open(filename, "w", encoding='utf8') as file:
with open(filename, "w", encoding="utf8") as file:
file.write(dumps(contents, ensure_ascii=False, indent=4))
file.close()
except Exception as exp:
logWrite(f"Could not save json file {filename}: {exp}\n{print_exc()}")
return
def configSet(key: str, value, *args: str, file: str = "config"):
"""Set key to a value
Args:
* key (str): The last key of the keys path.
* value (str/int/float/list/dict/None): Some needed value.
* *args (str): Path to key like: dict[args][key].
* file (str): User ID to save. Saved to config if not provided. Defaults to "config".
"""
def nested_set(dic, keys, value, create_missing=True):
d = dic
for key in keys[:-1]:
if key in d:
d = d[key]
elif create_missing:
d = d.setdefault(key, {})
else:
return dic
if keys[-1] in d or create_missing:
d[keys[-1]] = value
return dic
def configSet(keys: list, value: Any, file: str = "config", create_missing=True):
"""Set config's value to provided one
### Args:
* keys (`list`): List of keys from the highest one to target
* value (`Any`): Needed value
* file (`str`, optional): File (if not config). Defaults to "config".
* create_missing (`bool`, optional): Create missing items on the way. Defaults to True.
"""
if file == "config":
filepath = ""
this_dict = jsonLoad(f"{filepath}{file}.json")
if this_dict["debug"] is True:
try:
this_dict = jsonLoad("config_debug.json")
file = "config_debug"
except FileNotFoundError:
print(
"Debug mode is set but config_debug.json is not there! Falling back to config.json",
flush=True,
)
else:
filepath = f"data{sep}users{sep}"
this_dict = jsonLoad(f"{filepath}{file}.json")
string = "this_dict"
for arg in args:
string += f'["{arg}"]'
if type(value) in [str]:
string += f'["{key}"] = "{value}"'
else:
string += f'["{key}"] = {value}'
exec(string)
this_dict = jsonLoad(f"{filepath}{file}.json")
this_dict = nested_set(this_dict, keys, value, create_missing=create_missing)
jsonSave(this_dict, f"{filepath}{file}.json")
return
def configGet(key: str, *args: str, file: str = "config"):
"""Get value of the config key
Args:
* key (str): The last key of the keys path.
* *args (str): Path to key like: dict[args][key].
* file (str): User ID to load. Loads config if not provided. Defaults to "config".
Returns:
### Args:
* key (`str`): The last key of the keys path.
* *args (`str`): Path to key like: dict[args][key].
* file (`str`): User ID to load. Loads config if not provided. Defaults to "config".
### Returns:
* any: Value of provided key
"""
"""
if file == "config":
try:
this_dict = jsonLoad("config.json")
except FileNotFoundError:
print("Config file not found! Copy config_example.json to config.json, configure it and rerun the bot!")
print(
"Config file not found! Copy config_example.json to config.json, configure it and rerun the bot!",
flush=True,
)
exit()
if this_dict["debug"] is True:
try:
this_dict = jsonLoad("config_debug.json")
except FileNotFoundError:
print(
"Debug mode is set but config_debug.json is not there! Falling back to config.json",
flush=True,
)
else:
this_dict = jsonLoad(f"data{sep}users{sep}{file}.json")
this_key = this_dict
@@ -80,46 +122,181 @@ def configGet(key: str, *args: str, file: str = "config"):
this_key = this_key[dict_key]
return this_key[key]
def locale(key: str, *args: str, locale=configGet("locale")) -> Union[str, list, dict]:
def locale(key: str, *args: str, locale: Union[str, User] = configGet("locale")) -> Any:
"""Get value of locale string
Args:
* key (str): The last key of the locale's keys path.
* *args (list): Path to key like: dict[args][key].
* locale (str): Locale to looked up in. Defaults to config's locale value.
Returns:
* any: Value of provided locale key
"""
if (locale == None):
### Args:
* key (`str`): The last key of the locale's keys path.
* *args (`list`): Path to key like: dict[args][key].
* locale (`Union[str, User, HoloUser]`): Locale to looked up in. Provide User to get his `.language_code`. Defaults to config's locale value.
### Returns:
* any: Value of provided locale key. In normal case must be `str`, `dict` or `list`.
"""
if isinstance(locale, User):
locale = locale.language_code
elif hasattr(locale, "locale"):
locale = locale.locale
if locale is None:
locale = configGet("locale")
try:
this_dict = jsonLoad(f'{configGet("locale", "locations")}{sep}{locale}.json')
except FileNotFoundError:
try:
this_dict = jsonLoad(f'{configGet("locale", "locations")}{sep}{configGet("locale")}.json')
this_dict = jsonLoad(
f'{configGet("locale", "locations")}{sep}{configGet("locale")}.json'
)
except FileNotFoundError:
return f'⚠️ Locale in config is invalid: could not get "{key}" in {str(args)} from locale "{locale}"'
this_key = this_dict
for dict_key in args:
this_key = this_key[dict_key]
try:
return this_key[key]
except KeyError:
return f'⚠️ Locale in config is invalid: could not get "{key}" in {str(args)} from locale "{locale}"'
def all_locales(key: str, *args: str) -> list:
"""Get value of the provided key and path in all available locales
### Args:
* key (`str`): The last key of the locale's keys path.
* *args (`list`): Path to key like: dict[args][key].
### Returns:
* `list`: List of all values in all locales
"""
output = []
valid_locales = []
files_locales = listdir(f'{configGet("locale", "locations")}')
for entry in files_locales:
valid_locales.append(".".join(entry.split(".")[:-1]))
for lc in valid_locales:
try:
this_dict = jsonLoad(f'{configGet("locale", "locations")}{sep}{lc}.json')
except FileNotFoundError:
continue
this_key = this_dict
for dict_key in args:
this_key = this_key[dict_key]
try:
output.append(this_key[key])
except KeyError:
continue
return output
def find_location(query: str) -> dict:
"""Find location on geonames.org by query. Search is made with feature classes A and P.
### Args:
* query (`str`): Some city/village/state name
### Raises:
* PlaceNotFoundError: Exception is raised when API result is empty
### Returns:
* `dict`: One instance of geonames response
"""
try:
result = (
get(
f"http://api.geonames.org/searchJSON?q={query}&maxRows=1&countryBias=UA&lang=uk&orderby=relevance&featureClass=P&featureClass=A&username={configGet('username', 'geocoding')}"
)
).json()
return result["geonames"][0]
except (ValueError, KeyError, IndexError):
raise PlaceNotFoundError(query)
def create_tmp(
bytedata: Union[bytes, bytearray],
kind: Union[Literal["image", "video"], None] = None,
) -> str:
"""Create temporary file to help uploading it
### Args:
* bytedata (`Union[bytes, bytearray]`): Some bytes to be written
* kind (`Union[Literal["image", "video"], None]`): Kind of upload. Will add `.jpg` or `.mp4` if needed
### Returns:
* `str`: Path to temporary file
"""
filename = str(uuid1())
if kind == "image":
filename += ".jpg"
elif kind == "video":
filename += ".mp4"
makedirs("tmp", exist_ok=True)
with open(path.join("tmp", filename), "wb") as file:
file.write(bytedata)
return path.join("tmp", filename)
async def download_tmp(app: Client, file_id: str) -> Tuple[str, bytes]:
"""Download file by its ID and return its bytes
### Args:
* app (`Client`): App that will download the file
* file_id (`str`): File's unique id
### Returns:
* `Tuple[str, bytes]`: First is a filepath and the second is file's bytes
"""
filename = str(uuid1())
makedirs("tmp", exist_ok=True)
await app.download_media(file_id, path.join("tmp", filename))
with open(path.join("tmp", filename), "rb") as f:
bytedata = f.read()
return path.join("tmp", filename), bytedata
try:
from psutil import Process
except ModuleNotFoundError:
# print(locale("deps_missing", "console", locale=configGet("locale")), flush=True)
print("Missing dependencies! Please install all needed dependencies and run the bot again!")
print(
"Missing dependencies! Please install all needed dependencies and run the bot again!"
)
exit()
def killProc(pid):
if osname == "posix":
from signal import SIGKILL # type: ignore
from signal import SIGKILL
kill(pid, SIGKILL)
else:
p = Process(pid)
p.kill()
p.kill()
def should_quote(msg):
return True if msg.chat.type is not ChatType.PRIVATE else False
async def find_user(app: Client, query: Union[str, int]):
try:
result = await app.get_users(int(query))
if result == [] or result == None:
raise TypeError
except (TypeError, ValueError):
try:
result = await app.get_users(query)
except bad_request_400.UsernameNotOccupied:
return None
except bad_request_400.UsernameInvalid:
return None
except bad_request_400.PeerIdInvalid:
return None
return result

View File

@@ -1,4 +1,17 @@
pyrogram>=2.0.59
tgcrypto>=1.2.4
ujson>=5.5.0
psutil>=5.9.2
aiocsv==1.2.4
aiofiles~=23.1.0
apscheduler==3.10.1
convopyro==0.5
ftfy~=6.1.1
psutil~=5.9.5
polyglot~=16.7.4
pyicu~=2.11
pycld2==0.41
pymongo==4.3.3
pyrogram==2.0.104
python_dateutil==2.8.2
pykeyboard==0.1.5
requests==2.29.0
tgcrypto==1.2.5
ujson~=5.7.0
xmltodict==0.13.0

View File

@@ -0,0 +1,87 @@
{
"$jsonSchema": {
"required": [
"user",
"date",
"admin",
"application",
"application.1",
"application.2",
"application.3",
"application.3.name",
"application.3.adminName1",
"application.3.countryCode",
"application.3.countryName",
"application.3.location",
"application.4",
"application.5",
"application.6",
"application.7",
"application.8",
"application.9",
"application.10"
],
"properties": {
"user": {
"bsonType": ["int", "long"],
"description": "Telegram ID of user"
},
"date": {
"bsonType": "date",
"description": "Date when application was accepted"
},
"admin": {
"bsonType": ["int", "long"],
"description": "Telegram ID of admin that accepted the application"
},
"application": {
"bsonType": "object"
},
"application.1": {
"bsonType": "string"
},
"application.2": {
"bsonType": "date"
},
"application.3": {
"bsonType": "object"
},
"application.3.name": {
"bsonType": "string"
},
"application.3.adminName1": {
"bsonType": "string"
},
"application.3.countryCode": {
"bsonType": "string"
},
"application.3.countryName": {
"bsonType": "string"
},
"application.3.location": {
"bsonType": "array"
},
"application.4": {
"bsonType": "string"
},
"application.5": {
"bsonType": "string"
},
"application.6": {
"bsonType": "string"
},
"application.7": {
"bsonType": "string"
},
"application.8": {
"bsonType": "string"
},
"application.9": {
"bsonType": "string"
},
"application.10": {
"bsonType": "string"
}
}
}
}

29
validation/bans.json Normal file
View File

@@ -0,0 +1,29 @@
{
"$jsonSchema": {
"required": [
"user",
"admin",
"date"
],
"properties": {
"user": {
"bsonType": [
"int",
"long"
],
"description": "Telegram ID of user"
},
"admin": {
"bsonType": [
"int",
"long"
],
"description": "Telegram ID of admin"
},
"date": {
"bsonType": "date",
"description": "Date and time of getting"
}
}
}
}

6
validation/context.json Normal file
View File

@@ -0,0 +1,6 @@
{
"$jsonSchema": {
"required": [],
"properties": {}
}
}

36
validation/messages.json Normal file
View File

@@ -0,0 +1,36 @@
{
"$jsonSchema": {
"required": [
"origin",
"origin.chat",
"origin.id",
"destination",
"destination.chat",
"destination.id"
],
"properties": {
"origin": {
"bsonType": "object"
},
"origin.chat": {
"bsonType": ["int", "long"],
"description": "Telegram ID of message's origin chat"
},
"origin.id": {
"bsonType": ["int", "long"],
"description": "ID of message in origin chat"
},
"destination": {
"bsonType": "object"
},
"destination.chat": {
"bsonType": ["int", "long"],
"description": "Telegram ID of message's destination chat"
},
"destination.id": {
"bsonType": ["int", "long"],
"description": "ID of message in destination chat"
}
}
}
}

64
validation/spoilers.json Normal file
View File

@@ -0,0 +1,64 @@
{
"$jsonSchema": {
"required": [
"user",
"completed",
"category",
"description",
"photo",
"video",
"audio",
"animation",
"document",
"caption",
"text"
],
"properties": {
"user": {
"bsonType": ["int", "long"],
"description": "Telegram ID of user"
},
"completed": {
"bsonType": "bool",
"description": "Whether spoiler is a completed one"
},
"category": {
"bsonType": ["string", "null"],
"enum": ["nsfw", "deanon", "other"],
"description": "Spoiler's category"
},
"description": {
"bsonType": ["string", "null"],
"description": "Spoiler's description"
},
"photo": {
"bsonType": ["string", "null"],
"description": "Spoilered photo"
},
"video": {
"bsonType": ["string", "null"],
"description": "Spoilered video"
},
"audio": {
"bsonType": ["string", "null"],
"description": "Spoilered audio"
},
"animation": {
"bsonType": ["string", "null"],
"description": "Spoilered animation/GIF"
},
"document": {
"bsonType": ["string", "null"],
"description": "Spoilered document/file"
},
"caption": {
"bsonType": ["string", "null"],
"description": "Spoilered caption for media"
},
"text": {
"bsonType": ["string", "null"],
"description": "Spoilered text"
}
}
}
}

View File

@@ -0,0 +1,43 @@
{
"$jsonSchema": {
"required": [
"user",
"date",
"admin",
"sponsorship",
"sponsorship.streamer",
"sponsorship.expires",
"sponsorship.proof",
"sponsorship.label"
],
"properties": {
"user": {
"bsonType": ["int", "long"],
"description": "Telegram ID of user"
},
"date": {
"bsonType": "date",
"description": "Date when sponsorship was accepted"
},
"admin": {
"bsonType": ["int", "long"],
"description": "Telegram ID of admin that accepted the sponsorship"
},
"sponsorship": {
"bsonType": "object"
},
"sponsorship.streamer": {
"bsonType": "string"
},
"sponsorship.expires": {
"bsonType": "date"
},
"sponsorship.proof": {
"bsonType": "string"
},
"sponsorship.label": {
"bsonType": "string"
}
}
}
}

6
validation/tmp.json Normal file
View File

@@ -0,0 +1,6 @@
{
"$jsonSchema": {
"required": [],
"properties": {}
}
}

43
validation/users.json Normal file
View File

@@ -0,0 +1,43 @@
{
"$jsonSchema": {
"required": [
"user",
"link",
"label",
"tg_name",
"tg_phone",
"tg_locale",
"tg_username"
],
"properties": {
"user": {
"bsonType": ["int", "long"],
"description": "Telegram ID of user"
},
"link": {
"bsonType": ["string", "null"],
"description": "Invite link to destination group"
},
"label": {
"bsonType": "string",
"description": "Label given by admins"
},
"tg_name": {
"bsonType": "string",
"description": "Telegram first name"
},
"tg_phone": {
"bsonType": ["string", "null"],
"description": "Telegram phone number"
},
"tg_locale": {
"bsonType": ["string", "null"],
"description": "Telegram locale"
},
"tg_username": {
"bsonType": ["string", "null"],
"description": "Telegram username"
}
}
}
}

47
validation/warnings.json Normal file
View File

@@ -0,0 +1,47 @@
{
"$jsonSchema": {
"required": [
"user",
"admin",
"date",
"reason",
"active",
"revoke_date"
],
"properties": {
"user": {
"bsonType": [
"int",
"long"
],
"description": "Telegram ID of user"
},
"admin": {
"bsonType": [
"int",
"long"
],
"description": "Telegram ID of admin"
},
"date": {
"bsonType": "date",
"description": "Date and time of getting"
},
"reason": {
"bsonType": "string",
"description": "Broken rule or admin's comment"
},
"active": {
"bsonType": "bool",
"description": "Whether warning is still present"
},
"revoke_date": {
"bsonType": [
"date",
"null"
],
"description": "Date when warning got inactive"
}
}
}
}