413 Commits
v1.1.6 ... main

Author SHA1 Message Date
AlberLC
6c2f54592d Delete useless code 2025-10-26 02:34:47 +02:00
AlberLC
eecb488ea9 Fix FlanaServer file_name extension 2025-10-18 01:05:05 +02:00
AlberLC
a44a71f3be Remove FormData quote_fields arg 2025-10-17 08:20:18 +02:00
AlberLC
aee14449ad Upload large videos to FlanaServer 2025-10-17 03:00:47 +02:00
AlberLC
e20a5135f2 Add minor changes 2025-10-17 02:58:29 +02:00
AlberLC
c392e7bdde Update Dockerfile 2025-10-17 02:57:44 +02:00
AlberLC
c95621ca7e Update requirements.txt 2025-10-17 02:57:30 +02:00
AlberLC
8efa12ad22 Update requirements.txt 2025-05-01 23:20:04 +02:00
AlberLC
b672cbf0a1 Fix imports 2025-05-01 23:18:42 +02:00
AlberLC
d377aae6ff Update requirements.txt 2025-05-01 23:17:58 +02:00
AlberLC
789034aa9f Fix BtcOffersBot._on_stop_btc_offers_notification (private or mentioned) 2025-04-18 00:14:04 +02:00
AlberLC
647b9288aa Add BtcOffersBot.stop_all_btc_offers_notification 2025-04-17 23:46:51 +02:00
AlberLC
ad2434ab67 Update type hints 2025-04-17 23:44:16 +02:00
AlberLC
fca648f8b5 Fix 0.00 premium sign 2025-04-17 17:23:15 +02:00
AlberLC
ada357fcb8 Add minor changes 2025-04-17 16:11:56 +02:00
AlberLC
b1c07ea251 Fix BtcOffersBot._on_btc_offers (server disconnected) 2025-04-17 16:11:01 +02:00
AlberLC
5ec3c1e81b Update BtcOffersBot (manage websocket reconnections) 2025-04-17 16:08:26 +02:00
AlberLC
39aea44803 Fix imports 2025-04-17 13:28:24 +02:00
AlberLC
4dbe9e0504 Update BtcOffersBot (manage websocket reconnections) 2025-04-16 20:47:36 +02:00
AlberLC
d977d5d882 Update BtcOffersBot (notifications in dollars and premiums) 2025-04-16 20:45:23 +02:00
AlberLC
aac5fe951f Fix BtcOffersBot (validate amount) 2025-04-16 20:42:19 +02:00
AlberLC
abc4462723 Fix BtcOffersBot (notify without amount) 2025-04-16 20:41:36 +02:00
AlberLC
bdf658ee9a Update requirements.txt 2025-04-14 23:03:36 +02:00
AlberLC
6aa735ddd5 Add BtcOffersBot 2025-04-14 23:03:28 +02:00
AlberLC
9f640014a3 Fix UberEatsBot._on_ready 2025-04-14 23:00:25 +02:00
AlberLC
edebc48b1f Update SteamBot._insert_conversion_rates 2025-04-14 22:58:08 +02:00
AlberLC
2babe0fee7 Update type hints 2025-04-14 22:57:35 +02:00
AlberLC
123498b1f1 Add minor changes 2025-04-14 22:56:48 +02:00
AlberLC
7f8cbd6e54 Add PenaltyBot._check_message_spam 2025-03-17 10:09:40 +01:00
AlberLC
37db1b3590 Add PenaltyBot._check_message_spam 2025-03-17 10:05:27 +01:00
AlberLC
aa6642ea26 Add PenaltyBot._check_message_spam 2025-03-17 09:05:01 +01:00
AlberLC
7d538316ad Add PenaltyBot._check_message_spam 2025-03-17 08:31:16 +01:00
AlberLC
20ca2b3ab9 Add SteamBot 2024-07-06 06:20:36 +02:00
AlberLC
6f0cd1fcf6 Add SteamBot 2024-07-06 06:06:55 +02:00
AlberLC
39bf560407 Add SteamBot 2024-07-06 06:01:49 +02:00
AlberLC
76f9920de4 Add SteamBot 2024-07-04 20:02:49 +02:00
AlberLC
2465755bc0 Add SteamBot 2024-07-04 04:18:12 +02:00
AlberLC
46a50767af Add SteamBot 2024-07-04 04:17:21 +02:00
AlberLC
05e53bdc87 Add SteamBot 2024-07-04 04:05:02 +02:00
AlberLC
4e44cdd112 Add SteamBot 2024-07-04 03:56:22 +02:00
AlberLC
848d354da2 Add SteamBot 2024-07-04 03:40:35 +02:00
AlberLC
083356ac0b Update requirements.txt 2024-07-04 03:18:39 +02:00
AlberLC
c33c04cba4 Add SteamBot 2024-07-04 03:10:10 +02:00
AlberLC
5f027e665a Add SteamBot 2024-07-04 03:03:54 +02:00
AlberLC
71a5cb74d9 Fix heating_context (models.__init__) 2024-07-04 03:02:00 +02:00
AlberLC
3ea232b183 Update requirements.txt 2024-07-04 02:58:42 +02:00
AlberLC
2419d36bbb Fix connect_4 (bots.__init__) 2024-07-04 02:55:19 +02:00
AlberLC
778500caac Fix channel heating (level 0 channel name) 2024-06-26 10:18:42 +02:00
AlberLC
4f4a05f0ff Update instagram scraper (to yt-dlp) 2024-06-11 01:05:42 +02:00
AlberLC
d6986d8b63 Update twitter scraper (to yt-dlp) 2024-06-08 15:25:25 +02:00
AlberLC
25c5391f5a Update requirements.txt 2024-05-30 15:06:14 +02:00
AlberLC
df72d752c1 Fix channel heating (level 0 channel name) 2024-05-26 09:07:00 +02:00
AlberLC
07ab104203 Add telegram restart 2024-05-17 16:24:31 +02:00
AlberLC
564a65c5a1 Add telegram restart 2024-05-17 16:06:09 +02:00
AlberLC
ebe87decf5 Add telegram restart 2024-05-17 15:57:24 +02:00
AlberLC
86a1beed72 Update requirements.txt 2024-05-17 15:57:11 +02:00
AlberLC
e6721dab8e Update FlanaBot._on_delete (user own messages) 2024-04-02 21:46:52 +02:00
AlberLC
f1f29a089f Update callback registration 2024-03-28 10:08:18 +01:00
AlberLC
2f0ca1e012 Update FlanaBot._on_delete 2024-03-28 10:04:50 +01:00
AlberLC
c57b76bf6d Add MultiBot._on_new_message_raw whitelist/blacklist 2024-03-28 01:42:36 +01:00
AlberLC
4c06b573ea Add MultiBot._on_new_message_raw whitelist/blacklist 2024-03-27 15:27:59 +01:00
AlberLC
00dbe52d84 Add MultiBot._on_new_message_raw whitelist/blacklist 2024-03-26 03:21:46 +01:00
AlberLC
473279d74a Update requirements.txt 2024-03-18 09:45:48 +01:00
AlberLC
d0bc075e59 Update FlanaBot._on_database_messages 2024-03-07 22:39:31 +01:00
AlberLC
7a0e6ba4d8 Update FlanaBot._on_database_messages 2024-03-07 22:36:33 +01:00
AlberLC
528324253c Fix channel heating (independent servers) 2024-02-15 20:54:38 +01:00
AlberLC
a45f0e76b9 Fix channel heating (independent servers) 2024-02-15 06:10:18 +01:00
AlberLC
fd12cf198a Fix channel heating (independent servers) 2024-02-14 10:19:23 +01:00
AlberLC
b9beaf1a36 Fix FlanaBot._on_database_messages (chat chat user user) 2024-02-06 01:05:35 +01:00
AlberLC
d57eae19b5 Fix FlanaBot._on_database_messages 2024-02-06 00:43:54 +01:00
AlberLC
505ec0cf51 Update FlanaBot._on_recover_message 2024-01-27 02:42:55 +01:00
AlberLC
fb7b155368 Update _add_handlers (reset polls) 2024-01-27 02:11:59 +01:00
AlberLC
0769b16dd6 Update changeable roles 2024-01-25 21:37:15 +01:00
AlberLC
024c01b46d Update constants.KEYWORDS 2024-01-24 08:20:11 +01:00
AlberLC
94883dac1e Update poll_bot._add_handlers 2024-01-24 08:19:50 +01:00
AlberLC
401769f5cd Update poll_bot._add_handlers 2024-01-24 08:01:40 +01:00
AlberLC
55eb293e11 Update constants 2024-01-24 07:59:32 +01:00
AlberLC
fed7212a3f Update requirements.txt 2024-01-22 04:12:48 +01:00
AlberLC
21367e0fc9 Fix send_negative after guard clauses 2023-12-07 22:04:10 +01:00
AlberLC
52086be24d Fix FlanaDiscBot.restore_channel_names n_fires 2023-11-12 14:00:09 +01:00
AlberLC
f0551265c4 Add FlanaDiscBot.restore_channel_names 2023-11-08 07:48:54 +01:00
AlberLC
889a6c19dd Add FlanaDiscBot.restore_channel_names 2023-11-07 09:03:56 +01:00
AlberLC
688f5fee4e Add FlanaDiscBot.restore_channel_names 2023-11-07 08:58:12 +01:00
AlberLC
4f5427093c Update FlanaBot._on_database_messages 2023-11-07 08:29:28 +01:00
AlberLC
e415516b00 Fix poll buttons text (max) 2023-10-12 14:56:09 +02:00
AlberLC
a154653e9a Fix poll buttons text 2023-09-29 00:46:02 +02:00
AlberLC
61e60fc312 Fix FlanaBot._on_database_messages 2023-09-09 06:17:36 +02:00
AlberLC
e4f5a6bab4 Update FlanaBot._on_database_messages 2023-09-05 05:00:53 +02:00
AlberLC
bf03b1c8e7 Update FlanaBot._on_database_messages 2023-09-05 01:15:11 +02:00
AlberLC
c8cea16e7d Update FlanaBot._on_database_messages 2023-09-05 00:47:14 +02:00
AlberLC
5961e1a679 Update FlanaBot._on_database_messages 2023-09-05 00:38:27 +02:00
AlberLC
b4e7287de9 Update FlanaBot._on_database_messages 2023-09-05 00:15:48 +02:00
AlberLC
818b385f2d Update FlanaBot._on_database_messages 2023-09-05 00:06:06 +02:00
AlberLC
9813064674 Fix PollBot._on_poll_button_press 2023-08-18 06:32:05 +02:00
AlberLC
20592332a2 Fix FlanaBot._on_delete n_messages with mentions 2023-08-17 19:03:33 +02:00
AlberLC
ebf58e039b Fix tunnel chats 2023-08-17 18:44:21 +02:00
AlberLC
dd12533a48 Fix ScraperBot._search_medias (restricted age results filtered) 2023-08-15 03:08:08 +02:00
AlberLC
dad3482096 Add minor changes 2023-08-06 07:23:49 +02:00
AlberLC
69e352d5ec Add FlanaBot._on_delete_until 2023-08-05 00:55:06 +02:00
AlberLC
7adfa819df Fix scraping codec and extension with audio_only 2023-07-10 17:22:43 +02:00
AlberLC
7b03f6db63 Fix punishments accumulation 2023-07-08 19:40:59 +02:00
AlberLC
72696397ec Fix punishments accumulation 2023-07-07 22:14:10 +02:00
AlberLC
c3a2d2e9f0 Fix context manager in _get_contacts_ids for telegram userbots 2023-06-27 07:20:55 +02:00
AlberLC
bb9b5caa55 Fix ScraperBot._search_medias 2023-06-11 22:05:52 +02:00
AlberLC
ce71095060 Update requirements.txt 2023-05-28 23:23:11 +02:00
AlberLC
b54dd8d944 Update requirements.txt 2023-05-19 07:07:03 +02:00
AlberLC
376da6f072 Update requirements.txt 2023-05-19 06:56:18 +02:00
AlberLC
f396d2b232 Improve legibility 2023-05-19 06:47:16 +02:00
AlberLC
6c3a8e5e4a Fix fake instagram ban (restricted age) 2023-05-19 06:43:13 +02:00
AlberLC
d9eb01e5c0 Add minor changes 2023-05-09 05:32:59 +02:00
AlberLC
a9a79b2705 Add minor changes 2023-05-06 05:36:29 +02:00
AlberLC
29ebfd5e26 Fix ScraperBot._search_medias (fake instagram ban) 2023-04-29 01:35:41 +02:00
AlberLC
cf6b39e262 Fix _on_reset_instagram_ban (private or mentioned) 2023-04-29 01:34:28 +02:00
AlberLC
225bad07cc Update requirements.txt 2023-04-27 07:28:09 +02:00
AlberLC
336d8f8b8a Fix connect_4 message data 2023-04-25 06:44:52 +02:00
AlberLC
ad98464446 Fix replied message date 2023-04-25 06:44:32 +02:00
AlberLC
94fe902d41 Update flanautils (do_later, do_every) 2023-04-16 09:59:41 +02:00
AlberLC
b86fc98377 Update ScraperBot.send_song_info 2023-04-16 09:58:11 +02:00
AlberLC
fa67733416 Update instagram scraper 2023-04-11 11:03:12 +02:00
AlberLC
63e972a774 Update instagram scraper 2023-04-11 10:52:46 +02:00
AlberLC
a1434e54d3 Update instagram scraper 2023-04-11 08:57:57 +02:00
AlberLC
88b61cd87f Update self.owner_chat initialization 2023-04-11 08:57:11 +02:00
AlberLC
4fc52128ec Update connect 4 background color 2023-04-07 06:22:22 +02:00
AlberLC
8788904d44 Update instagram scraper 2023-04-04 05:36:01 +02:00
AlberLC
fdb02e9d65 Update instagram scraper 2023-04-04 04:17:21 +02:00
AlberLC
8594351229 Update instagram scraper 2023-04-03 12:00:20 +02:00
AlberLC
ae7fb1b9b2 Update UberEatsBot._scrape_codes 2023-04-03 11:22:07 +02:00
AlberLC
d21978b2da Update FlanaBot._on_delete 2023-03-31 09:01:26 +02:00
AlberLC
35e8ec5090 Remove UberEatsBot pyperclip 2023-03-31 07:39:15 +02:00
AlberLC
8703eca5ff Update UberEatsBot._scrape_codes 2023-03-30 02:44:51 +02:00
AlberLC
6be120da17 Remove UberEatsBot pyperclip 2023-03-30 02:05:32 +02:00
AlberLC
aaf17c6877 Add FlanaBot._on_help antiflood 2023-03-24 05:07:48 +01:00
AlberLC
68a3fde4ed Update UberEatsBot.send_ubereats_code 2023-03-24 00:44:06 +01:00
AlberLC
edad87b683 Update self.owner_chat initialization 2023-03-23 07:13:40 +01:00
AlberLC
3be1cf5c47 Add FlanaBot._on_help 2023-03-23 07:13:03 +01:00
AlberLC
ff934325c5 Add FlanaBot._on_help 2023-03-23 06:33:48 +01:00
AlberLC
11ef06b1d3 Update flana_tele_bot whitelisted decorator 2023-03-23 06:33:26 +01:00
AlberLC
d5b51e3f44 Update FlanaBot._on_database_messages 2023-03-23 06:25:49 +01:00
AlberLC
a6b0e699ea Update FlanaBot._on_database_messages 2023-03-23 06:02:26 +01:00
AlberLC
e90e081b69 Update FlanaBot._on_database_messages 2023-03-23 06:01:40 +01:00
AlberLC
8a23baf626 Update FlanaBot send methods 2023-03-23 05:09:00 +01:00
AlberLC
7f795e0b73 Update setup.cfg 2023-03-23 03:43:30 +01:00
AlberLC
9fbc16df02 Update UberEatsBot.send_ubereats_code message spaces 2023-03-23 03:27:06 +01:00
AlberLC
9da95e5e1c Update ScraperBot._on_scraping 2023-03-18 06:21:07 +01:00
AlberLC
a27f86d1b9 Update FlanaBot._on_new_message_default 2023-03-15 01:17:07 +01:00
AlberLC
26b3d714d3 Add tunnel chats 2023-03-13 23:07:37 +01:00
AlberLC
afa96325fe Add tunnel chats 2023-03-13 09:35:00 +01:00
AlberLC
72a803daec Add tunnel chats 2023-03-13 09:30:15 +01:00
AlberLC
7ce6985fa9 Add tunnel chats 2023-03-13 09:24:43 +01:00
AlberLC
12d9dd6075 Add tunnel chats 2023-03-13 09:14:40 +01:00
AlberLC
cf8ec9182e Add tunnel chats 2023-03-13 08:29:37 +01:00
AlberLC
060484396e Update scraper_bot.py 2023-03-13 08:26:03 +01:00
AlberLC
94e8f72245 Add constants.KEYWORDS['tunnel'] 2023-03-13 07:07:45 +01:00
AlberLC
d359ccb39e Update song_info keywords 2023-03-12 23:26:51 +01:00
AlberLC
44dcb3d987 Improve UberEatsBot new code obtaining 2023-03-12 05:21:47 +01:00
AlberLC
4ebcade424 Improve UberEatsBot new code obtaining 2023-03-12 05:18:41 +01:00
AlberLC
52e981fc58 Improve UberEatsBot new code obtaining 2023-03-12 05:15:02 +01:00
AlberLC
722c2ffbac Fix UberEatsBot headless and new_context args 2023-03-08 23:49:49 +01:00
AlberLC
55e3fe53c2 Add user agent to UberEatsBot 2023-03-08 02:15:09 +01:00
AlberLC
34320079eb Update Chat.ubereats 2023-03-08 02:14:47 +01:00
AlberLC
61daaa387a Add ubereats_next_execution 2023-03-06 07:34:09 +01:00
AlberLC
7fee5f382c Add ubereats_next_execution 2023-03-06 07:28:37 +01:00
AlberLC
f8be4d3e5e Add ubereats_next_execution 2023-03-06 07:12:42 +01:00
AlberLC
89207de059 Fix concurrent ubereats 2023-03-06 07:12:08 +01:00
AlberLC
14793e5eb5 Fix scraping delete original in private 2023-03-05 20:45:57 +01:00
AlberLC
488db76710 Fix quick click buttons 2023-03-05 07:07:26 +01:00
AlberLC
a1cefe437d Add UberEatsBot support for multiple cookies 2023-03-05 06:41:45 +01:00
AlberLC
506af05ebb Add minor changes 2023-03-05 01:16:14 +01:00
AlberLC
c17ffe1010 Fix UberEats task stderr 2023-03-05 01:03:02 +01:00
AlberLC
81854375d1 Fix UberEats task stderr 2023-03-04 05:12:33 +01:00
AlberLC
154f02a1b6 Update requirements.txt 2023-03-03 23:03:58 +01:00
AlberLC
786bab4e04 Fix UberEats task cancellation 2023-03-03 22:07:53 +01:00
AlberLC
d045551db9 Fix UberEats task cancellation 2023-03-03 22:06:47 +01:00
AlberLC
5021bdd7ae Add UberEatsBot 2023-03-03 05:21:10 +01:00
AlberLC
e8742eca42 Add UberEatsBot 2023-03-03 05:20:20 +01:00
AlberLC
a24021c2bd Update FlanaBot.check_old_database_actions 2023-03-03 05:19:52 +01:00
AlberLC
d145f70a42 Update BotAction 2023-03-03 05:19:18 +01:00
AlberLC
73ad75e8b2 Update channel heating 2023-02-28 11:01:51 +01:00
AlberLC
1da383d5eb Update scraper_bot.py 2023-02-28 11:01:14 +01:00
AlberLC
a3893df34b Update requirements.txt 2023-02-20 23:34:13 +01:00
AlberLC
6645bef22f Update SCRAPING_TIMEOUT_SECONDS 2023-02-20 23:33:39 +01:00
AlberLC
d02d5d4df2 Update SCRAPING_TIMEOUT_SECONDS 2023-02-17 05:32:03 +01:00
AlberLC
85b181c598 Improve legibility 2023-02-01 07:15:19 +01:00
AlberLC
db5a1974b2 Fix FlanaDiscBot._changeable_roles 2023-02-01 02:30:13 +01:00
AlberLC
5e65b5a298 Update requirements.txt 2023-01-16 21:30:32 +01:00
AlberLC
4d60281a23 Add delay to FlanaBot._on_delete 2023-01-16 03:31:41 +01:00
AlberLC
0ede68d495 Add ScraperBot._on_no_scraping 2023-01-15 20:11:06 +01:00
AlberLC
cba5a5a289 Update requirements.txt 2023-01-11 00:47:05 +01:00
AlberLC
db5d4459d7 Update README.rst 2023-01-11 00:35:43 +01:00
AlberLC
697e6e7fdf Update requirements.txt 2023-01-10 23:39:48 +01:00
AlberLC
fccb675c3d Improve legibility 2023-01-03 04:03:05 +01:00
AlberLC
4795c0c286 Improve Connect4Bot ai 2023-01-03 03:56:06 +01:00
AlberLC
409b3b4864 Refactor Message.data system 2023-01-01 02:35:11 +01:00
AlberLC
94d29dafd2 Add minor changes 2022-12-22 09:36:20 +01:00
AlberLC
bdd5bd6059 Refactor Message.data system 2022-12-22 09:29:25 +01:00
AlberLC
e2298bdb41 Fix _on_delete in private is_admin 2022-12-22 09:28:29 +01:00
AlberLC
cf6844e7a1 Refactor ScraperBot.send_medias 2022-12-21 05:25:38 +01:00
AlberLC
9344aa2d16 Update force parameter 2022-12-21 05:23:09 +01:00
AlberLC
293a5e46f1 Dont call self._manage_exceptions 2022-12-20 04:47:26 +01:00
AlberLC
1bc1e1ecde Add force_gif_download 2022-12-19 07:30:26 +01:00
AlberLC
a99a185830 Add force_gif_download 2022-12-19 06:29:11 +01:00
AlberLC
bf68225d61 Fix ScraperBot.send_medias message 2022-12-19 02:12:16 +01:00
AlberLC
fb98afed79 Fix ScraperBot.send_medias message 2022-12-18 04:31:52 +01:00
AlberLC
ca9546c567 Improve legibility 2022-12-18 04:31:22 +01:00
AlberLC
9bb1e480f7 Improve ScraperBot.send_medias message 2022-12-17 10:16:02 +01:00
AlberLC
770e599eca Update tiktok scraping 2022-12-17 06:45:47 +01:00
AlberLC
64f9ebee45 Update tiktok scraping 2022-12-17 06:26:35 +01:00
AlberLC
333f494a20 Update _on_scraping bot_mentioned 2022-12-17 04:39:34 +01:00
AlberLC
ef5acc3f26 Update _on_scraping bot_mentioned 2022-12-16 11:04:24 +01:00
AlberLC
93cf015260 Update requirements.txt 2022-12-16 10:41:18 +01:00
AlberLC
2c3fbab7a8 Update requirements.txt 2022-12-16 10:33:13 +01:00
AlberLC
070d3bf572 Improve ScraperBot.send_medias message 2022-12-16 10:10:53 +01:00
AlberLC
58cdee8ccc Improve ScraperBot.send_medias message 2022-12-16 10:10:20 +01:00
AlberLC
1cb92bf3da Update requirements.txt 2022-12-16 09:50:16 +01:00
AlberLC
d42a20ef14 improve platforms support for ScraperBot 2022-12-16 09:50:09 +01:00
AlberLC
b1f3eb9ebe Update constants.KEYWORDS 2022-12-16 06:44:34 +01:00
AlberLC
3bc40c0b64 Add new keywords to no timeout_for_media 2022-12-13 22:39:37 +01:00
AlberLC
3b6d62b9e1 Improve legibility 2022-12-12 06:31:55 +01:00
AlberLC
77a44a0f9c Move flana_disct_bot constants 2022-12-11 22:20:27 +01:00
AlberLC
572800571d Fix handlers in Telegram 2022-12-11 22:08:08 +01:00
AlberLC
16b38ae257 Update requirements.txt 2022-12-08 01:29:36 +01:00
AlberLC
98f5376670 Update setup.cfg 2022-12-07 23:16:06 +01:00
AlberLC
bd38c094a4 Update requirements.txt 2022-12-07 23:16:00 +01:00
AlberLC
5e502a7c15 Update requirements.txt 2022-12-07 23:09:00 +01:00
AlberLC
203c198a84 Update FlanaBot._on_delete 2022-12-06 03:25:13 +01:00
AlberLC
e2427fa3c8 Improve legibility 2022-12-04 09:49:06 +01:00
AlberLC
73bedd9b52 Update FlanaDiscBot.is_punished 2022-12-03 06:54:52 +01:00
AlberLC
597bab672e Update FlanaDiscBot.is_punished 2022-12-03 06:51:09 +01:00
AlberLC
9c45647793 Update multibot_constants 2022-12-03 06:44:58 +01:00
AlberLC
8c65182c39 Update flanautils function names 2022-12-03 06:44:22 +01:00
AlberLC
0275cb3f84 Update flanautils function names 2022-12-03 06:42:45 +01:00
AlberLC
430a141dd7 Update PollBot delete_message order 2022-12-03 06:26:26 +01:00
AlberLC
96d117a9fe Fix PollBot._on_poll_button_press 2022-12-03 06:25:31 +01:00
AlberLC
9925084ef4 Update PollBot._get_poll_message 2022-12-03 06:25:07 +01:00
AlberLC
4ae42f386d Update PollBot._get_poll_message 2022-12-03 06:24:27 +01:00
AlberLC
b8483b4d54 Update PollBot._get_options 2022-12-03 06:23:29 +01:00
AlberLC
1751bf37e1 Update FlanaBot.check_old_database_actions 2022-12-03 06:22:50 +01:00
AlberLC
db8e020977 Update flanautils function names 2022-12-03 06:21:39 +01:00
AlberLC
90c15950fb Update _on_recover_message registration 2022-12-03 06:20:31 +01:00
AlberLC
90c2e16d5d Update buttons data system
Move data persistence from database to primary memory.
2022-12-03 06:19:30 +01:00
AlberLC
d370909e68 Update _on_users 2022-11-30 01:48:08 +01:00
AlberLC
113dee6bd8 Use delete_many_raw 2022-11-30 01:11:03 +01:00
AlberLC
83bb9b1168 Improve legibility 2022-11-30 00:55:58 +01:00
AlberLC
5fb995201a Add loser to connect 4 2022-11-29 06:31:57 +01:00
AlberLC
7e127f55a7 Add _on_database_messages_simple 2022-11-29 05:30:27 +01:00
AlberLC
09b2c7db7e Add _on_database_messages_simple 2022-11-29 05:24:34 +01:00
AlberLC
300132fb84 Add _on_database_messages_simple 2022-11-29 05:22:32 +01:00
AlberLC
032fcf7801 Fix use BaseException 2022-11-28 00:23:51 +01:00
AlberLC
9916f2b2c9 Delete Connect4Bot user messages and buttons when finished 2022-11-26 22:07:09 +01:00
AlberLC
7f4ca8c7a5 Update config names 2022-11-26 21:29:18 +01:00
AlberLC
7f5ba7cd09 Update config names 2022-11-26 21:26:36 +01:00
AlberLC
025460828b Update config names 2022-11-26 21:23:58 +01:00
AlberLC
32b582c5d9 Update constants.KEYWORDS 2022-11-26 21:23:39 +01:00
AlberLC
008de3b43e Update constants.KEYWORDS 2022-11-26 21:22:57 +01:00
AlberLC
dc29922e7c Improve legibility 2022-11-26 20:21:56 +01:00
AlberLC
79e93c01fd Add reply to send_medias 2022-11-26 04:25:01 +01:00
AlberLC
bf3bf23285 Rename whitelisted decorator 2022-11-26 02:17:23 +01:00
AlberLC
8f395d3406 Fix connect 4 edit if not found 2022-11-26 02:03:50 +01:00
AlberLC
fce458d5ed Improve legibility 2022-11-26 01:48:21 +01:00
AlberLC
60351ab574 Improve legibility 2022-11-26 01:46:12 +01:00
AlberLC
e883909769 Move config logic to FlanaBot 2022-11-26 01:39:37 +01:00
AlberLC
04eb2ff491 Fix _on_users 2022-11-25 07:09:19 +01:00
AlberLC
7a04a699d7 Fix ScraperBot.send_song_info 2022-11-25 04:39:37 +01:00
AlberLC
3f8b58437a Rename Message.contents to data 2022-11-25 03:45:48 +01:00
AlberLC
9e94d8c770 Add Connect4Bot vs itself 2022-11-23 05:28:35 +01:00
AlberLC
f0babe7c85 Improve Connect4Bot ai 2022-11-22 00:41:08 +01:00
AlberLC
c3ab1be08e Fix Connect4Bot ai patterns 2022-11-21 22:16:31 +01:00
AlberLC
28637ad684 Update constants 2022-11-18 19:54:11 +01:00
AlberLC
9e8ec86e96 Fix connect 4 ai 2022-11-17 05:10:52 +01:00
AlberLC
9ca7ea035a Improve connect 4 ai 2022-11-17 02:26:37 +01:00
AlberLC
3bf4f16aa4 Improve connect 4 ai 2022-11-17 01:49:40 +01:00
AlberLC
86d15d6b9a Improve connect 4 frontend 2022-11-16 06:12:04 +01:00
AlberLC
95d9272e39 Fix Connect4Bot last decision 2022-11-16 05:54:41 +01:00
AlberLC
47874735ec Improve connect 4 frontend 2022-11-16 02:59:03 +01:00
AlberLC
781cb8cefa Improve connect 4 frontend 2022-11-16 02:58:39 +01:00
AlberLC
6f09869a92 Make filter_mention_ids and distribute_buttons public 2022-11-14 04:44:03 +01:00
AlberLC
9b1e9f6f2f Fix Connect4Bot ai 2022-11-13 01:36:59 +01:00
AlberLC
9d21855fa1 Update Connect4Bot type hints 2022-11-13 01:36:32 +01:00
AlberLC
86d84e8fe4 Improve legibility 2022-11-12 05:10:19 +01:00
AlberLC
77e21bb068 Improve legibility 2022-11-12 05:01:53 +01:00
AlberLC
19202391ac Add PollBot._on_delete_all_votes 2022-11-10 22:02:07 +01:00
AlberLC
1358018cb0 Add PollBot._on_delete_all_votes 2022-11-10 20:00:46 +01:00
AlberLC
97efbec6a4 Add PollBot._on_delete_all_votes 2022-11-10 19:55:20 +01:00
AlberLC
af6cab6166 Add Connect4Bot 2022-11-09 04:54:43 +01:00
AlberLC
1900ba3167 Add Connect4Bot 2022-11-09 04:29:15 +01:00
AlberLC
1adfd8febf Move _distribute_buttons to MultiBot 2022-11-09 00:40:13 +01:00
AlberLC
5507236a73 Split FlanaBot into multiple bots 2022-11-08 20:33:01 +01:00
AlberLC
569bed91ad Split FlanaBot into multiple bots 2022-11-08 04:17:27 +01:00
AlberLC
7f1e596243 Fix typing 2022-11-08 03:50:09 +01:00
AlberLC
af9d284fa1 Split FlanaBot into multiple bots 2022-11-08 03:20:31 +01:00
AlberLC
36a3592a84 Add minor changes 2022-11-07 18:41:41 +01:00
AlberLC
910074d3aa Rename RatioMatch to ScoreMatch 2022-11-07 18:41:20 +01:00
AlberLC
bb30c10867 Fix calculate time of mention id 2022-11-06 05:00:05 +01:00
AlberLC
f62d224e27 Fix calculate time of mention id 2022-11-05 08:48:48 +01:00
AlberLC
4cc610d579 Update flood checking 2022-11-03 21:09:49 +01:00
AlberLC
a967f7d594 Move logic to FlanaBot 2022-11-02 03:45:23 +01:00
AlberLC
6e3f014e24 Fix CHANGEABLE_ROLES defaultdict(list, ... 2022-10-31 19:18:08 +01:00
AlberLC
a88ca55b04 Update Chat.DEFAULT_CONFIG 2022-10-29 21:13:05 +02:00
AlberLC
496b4e0898 Refactor new models import 2022-10-29 06:15:32 +02:00
AlberLC
16a009fe9a Add bots.__init__ 2022-10-29 04:29:52 +02:00
AlberLC
113c37ee2c Refactor new models import 2022-10-29 04:23:31 +02:00
AlberLC
bdbc4d5e0d Add delete votes and voting ban 2022-10-29 03:16:54 +02:00
AlberLC
967439846f Refactor new models import 2022-10-29 03:15:43 +02:00
AlberLC
24ec450de6 Refactor new models import 2022-10-28 21:58:22 +02:00
AlberLC
d8374b7d73 Move _on_config to MultiBot 2022-10-28 21:58:03 +02:00
AlberLC
ef5b156873 Refactor penalty system 2022-10-28 08:10:39 +02:00
AlberLC
7f1f275f08 Update _on_roles 2022-10-25 09:46:04 +02:00
AlberLC
da41602456 Update config system 2022-10-25 09:44:22 +02:00
AlberLC
ca6373d490 Update database management 2022-10-25 09:42:57 +02:00
AlberLC
082ae6733b Delete useless code 2022-10-25 09:42:03 +02:00
AlberLC
18ef15d031 Update todo annotations 2022-10-25 06:42:08 +02:00
AlberLC
81c9868460 Fix _on_choose 2022-10-22 21:09:07 +02:00
AlberLC
81c8c5b6d9 Update changeable roles 2022-10-21 21:54:29 +02:00
AlberLC
7006041076 Fix multiple answer polls 2022-10-20 20:06:52 +02:00
AlberLC
3347b52bab Fix super call to _on_inline_query_raw 2022-10-17 20:20:20 +02:00
AlberLC
e291711ed4 Update config message 2022-10-13 22:34:37 +02:00
AlberLC
0819b4f183 Update config message 2022-10-13 22:32:43 +02:00
AlberLC
efaf90e217 Add multiple answer polls 2022-10-13 22:15:49 +02:00
AlberLC
b4d7541851 Fix discord limit button text 2022-10-10 02:39:06 +02:00
AlberLC
c05e52eda2 Add priority to choose 2022-10-05 20:08:00 +02:00
AlberLC
b426286a23 Add priority to polls 2022-10-05 20:02:57 +02:00
AlberLC
45be09354c Update BYE_PHRASES 2022-10-05 07:43:51 +02:00
AlberLC
1623aac3c8 Minor change 2022-09-29 19:35:16 +02:00
AlberLC
bb9cf53673 Update timeout_for_media 2022-09-22 22:22:24 +02:00
AlberLC
ff887b2cd7 Update timeout_for_media 2022-09-21 19:03:01 +02:00
AlberLC
68c0558f65 Add timeout_for_media 2022-09-21 05:10:44 +02:00
AlberLC
25a1a1b601 Fix typo 2022-09-08 20:29:18 +02:00
AlberLC
d9f4a39c98 Update punishments system 2022-09-08 19:55:47 +02:00
AlberLC
30d9393de4 Update Update fire algorithm 2022-09-08 19:55:22 +02:00
AlberLC
1a410144f4 Improve _on_config_button_press button states 2022-08-26 23:34:46 +02:00
AlberLC
4a70a77e94 Improve _on_scraping_audio keywords 2022-08-26 19:46:51 +02:00
AlberLC
3ab840dd2f Use pytz timezones in _on_audit_log 2022-08-26 05:31:13 +02:00
AlberLC
a5996fa2ca Fix _create_user_from_discord_user async 2022-08-25 21:11:11 +02:00
AlberLC
50feac35ce Update _on_roles 2022-08-25 20:20:16 +02:00
AlberLC
40ddd71bee Update _on_roles 2022-08-25 06:58:05 +02:00
AlberLC
8c49b24024 Update _on_roles 2022-08-25 06:56:15 +02:00
AlberLC
499c3162ae Update _on_roles 2022-08-25 06:30:25 +02:00
AlberLC
a86251e902 Update _on_audit_log error message 2022-08-25 06:08:09 +02:00
AlberLC
cf0837536d Add audit log 2022-08-25 05:15:33 +02:00
AlberLC
6dc8da51f5 Fix fire algorithm 2022-08-19 13:45:26 +02:00
AlberLC
44e751870a Fix fire algorithm 2022-08-17 13:46:30 +02:00
AlberLC
70b5103a41 Update fire algorithm 2022-08-16 21:04:53 +02:00
AlberLC
66d555e20f Update fire algorithm 2022-08-16 21:02:11 +02:00
AlberLC
57f94aaa4e Fix scraping with reply 2022-08-16 20:55:58 +02:00
AlberLC
613a16327c Fix channel heating 2022-08-14 22:50:57 +02:00
AlberLC
65970b78e5 Fix channel heating 2022-08-14 22:31:34 +02:00
AlberLC
700123a962 Fix channel heating 2022-08-14 20:22:44 +02:00
AlberLC
f9216e1746 Update medias with extension argument 2022-08-12 17:26:38 +02:00
AlberLC
19d22c6d74 Update requirements.txt 2022-08-12 17:08:16 +02:00
AlberLC
f0e0606479 Add support for scraping audio_only 2022-08-12 16:47:44 +02:00
AlberLC
1fbe76b733 Update scraping structure 2022-08-12 16:47:17 +02:00
AlberLC
a68b779e1a Update fire algorithm 2022-08-12 16:46:25 +02:00
AlberLC
ebdfe14667 Update requirements.txt 2022-08-10 18:50:12 +02:00
AlberLC
8f7aa3698c Fix channel fire 2022-08-10 04:19:54 +02:00
AlberLC
237a7e93b6 Fix channel fire 2022-08-09 18:49:26 +02:00
AlberLC
a38932fe81 Add channel fire 2022-08-09 17:41:33 +02:00
AlberLC
eee74684dc Improve legibility 2022-08-08 10:29:49 +02:00
AlberLC
33764e9642 Add timeout_for_media to _search_medias 2022-08-07 22:29:12 +02:00
AlberLC
9fba7fd13c Update spaces between text and emojis 2022-08-07 22:14:51 +02:00
AlberLC
1c1709bea3 Fix asyncio.gather return_exceptions 2022-08-07 20:02:44 +02:00
AlberLC
b64e279eaf Fix tiktok.find_tiktok_ids async asynchronous call 2022-08-06 04:09:45 +02:00
AlberLC
f1c0e1179e Upgrade scrapers structures 2022-08-06 03:57:21 +02:00
AlberLC
1eb019d5ff Upgrade bot_state_message 2022-08-05 16:08:37 +02:00
AlberLC
7d5bff9b89 Update README.rst 2022-08-05 06:09:05 +02:00
AlberLC
5db2cc0ae2 Update README.rst 2022-08-05 05:53:00 +02:00
AlberLC
8a69110fee Update README.rst 2022-08-05 05:51:29 +02:00
AlberLC
3be66803f7 Update Dockerfile 2022-08-04 23:04:09 +02:00
AlberLC
b816b2f26d Add YouTube scraper 2022-08-04 11:21:07 +02:00
AlberLC
fabd8ba375 Fix _on_roles bot-mentioned and in groups 2022-07-29 20:02:33 +02:00
AlberLC
9fe9fe5757 Add changeable roles 2022-07-29 07:15:58 +02:00
AlberLC
b58d7c2e56 Fix parse_callback test 2022-07-29 07:15:16 +02:00
AlberLC
a2db01fd46 Fix polls capitalize 2022-07-19 23:44:02 +02:00
AlberLC
e005bb3670 Add delete message at config show 2022-07-18 21:55:59 +02:00
AlberLC
e13796273d Improve a minor legibility error 2022-07-17 09:55:11 +02:00
AlberLC
866599261f Update HEAT_NAMES 2022-07-17 09:29:50 +02:00
AlberLC
5892cc1579 Fix get_group_roles call 2022-07-17 09:26:56 +02:00
AlberLC
5a923b01c7 Add remembered user roles at rejoin 2022-07-16 06:29:27 +02:00
AlberLC
4ecf847686 Fix bot used to delete some messages 2022-07-14 20:08:50 +02:00
AlberLC
db9ccbf81a Use str.capitalize 2022-07-07 07:39:42 +02:00
AlberLC
b26724b0a5 Fix regex pattern warning 2022-07-07 07:16:36 +02:00
AlberLC
bb93ffbf0d Add _get_options 2022-07-07 07:13:45 +02:00
AlberLC
1cc7464059 Update polls 2022-07-07 03:59:29 +02:00
AlberLC
86fcb0e0b6 Fix typo 2022-07-06 05:48:42 +02:00
AlberLC
ebeecbff5d Fix _check_message_flood
Check messages in its chat
2022-06-29 22:17:58 +02:00
AlberLC
d137f220cc Fix message find by date 2022-06-29 06:48:06 +02:00
AlberLC
00323f99fb Update HEAT_NAMES 2022-06-27 05:08:08 +02:00
AlberLC
6237ccf8b6 Fix _on_choose 2022-06-27 04:59:14 +02:00
AlberLC
f402293c99 Upgrade _on_choose 2022-06-27 04:22:47 +02:00
AlberLC
99effad710 Fix constants accents 2022-06-23 04:51:49 +02:00
AlberLC
54cada0649 Fix _on_choose 2022-06-23 04:31:45 +02:00
AlberLC
0f5d3c333a Fix commands at private 2022-06-23 04:13:36 +02:00
AlberLC
5fb55404cf Fix commands at private 2022-06-23 03:10:49 +02:00
AlberLC
2768d8e949 Fix commands at private 2022-06-23 02:17:59 +02:00
AlberLC
b0ca5a2ded Add ports to db local access 2022-06-23 02:17:33 +02:00
AlberLC
697ba9e89e Add polls 2022-06-22 09:18:42 +02:00
AlberLC
09df75ea0b Update buttons logic (add ButtonsInfo) and add choose and dice functionalities 2022-06-19 13:34:12 +02:00
AlberLC
c2e7e68619 Fix _check_message_flood 2022-06-15 00:46:31 +02:00
AlberLC
f12d0c18c1 Fix insults 2022-06-14 04:41:26 +02:00
AlberLC
b55f933a32 Add new insults 2022-06-14 04:01:16 +02:00
AlberLC
5fa102b157 Fix _on_scraping with replied message 2022-06-12 00:13:33 +02:00
AlberLC
1e8a33c0c4 Improve button registration 2022-06-08 22:05:02 +02:00
AlberLC
daef039ce5 Change _manage_exceptions context arg 2022-06-06 04:22:26 +02:00
AlberLC
7ad4c4052e Remove twitch bot at main 2022-06-06 02:31:16 +02:00
AlberLC
641dc72738 Update requirements.txt 2022-06-04 03:33:59 +02:00
AlberLC
cafa7ce1c5 Fix recover original message 2022-06-04 02:58:32 +02:00
AlberLC
c9a46b2b07 Fix roles mentions in weather 2022-06-01 03:16:45 +02:00
AlberLC
92ee0e405d Fix env variable names 2022-05-31 23:08:36 +02:00
AlberLC
dc6bf7accb Refactor punishments and optimize chat objects 2022-05-31 22:11:16 +02:00
AlberLC
9e5f7a81ff Add user who pressed the button 2022-05-26 05:45:16 +02:00
AlberLC
28ff804d5a Improve discord user name printing 2022-05-25 05:30:09 +02:00
AlberLC
d47fdfb57e Fix bot insults ratio 2022-05-25 04:00:47 +02:00
AlberLC
20fb6e3223 Fix bot insults ratio 2022-05-25 02:55:28 +02:00
AlberLC
952672946b Add Discord bot 2022-05-25 00:46:32 +02:00
34 changed files with 3942 additions and 1156 deletions

View File

@@ -3,8 +3,8 @@ FROM flanaganvaquero/flanawright
WORKDIR /application
COPY flanabot flanabot
COPY venv/lib/python3.10/site-packages /usr/local/lib/python3.10/site-packages
COPY .venv/lib/python3.10/site-packages /usr/local/lib/python3.10/site-packages
ENV PYTHONPATH=/application
CMD python3.10 flanabot/main.py
CMD python3.10 -u flanabot/main.py

View File

@@ -30,7 +30,7 @@ Features
- Mute users temporarily or forever.
- Ban users temporarily or forever.
- Configurable default behavior for each chat, just talk to him to configure it.
- Get media from twitter, instagram and tiktok and send it to the chat. From tiktok also obtains data about the song that is playing in the video.
- Get media from Instagram, Reddit, TikTok, Twitter, YouTube and others and send it to the chat. From TikTok also obtains data about the song that is playing in the video.
.. |license| image:: https://img.shields.io/github/license/AlberLC/flanabot?style=flat
@@ -45,4 +45,4 @@ Features
:target: https://www.python.org/downloads/
:alt: PyPI - Python Version
.. _github.com/AlberLC/multibot: https://github.com/AlberLC/multibot
.. _github.com/AlberLC/multibot: https://github.com/AlberLC/multibot

View File

@@ -7,6 +7,8 @@ services:
mongodb:
image: mongo
ports:
- "27017:27017"
environment:
- MONGO_INITDB_ROOT_USERNAME=${MONGO_USER}
- MONGO_INITDB_ROOT_PASSWORD=${MONGO_PASSWORD}

10
flanabot/bots/__init__.py Normal file
View File

@@ -0,0 +1,10 @@
from flanabot.bots.connect_4_bot import *
from flanabot.bots.flana_bot import *
from flanabot.bots.flana_disc_bot import *
from flanabot.bots.flana_tele_bot import *
from flanabot.bots.penalty_bot import *
from flanabot.bots.poll_bot import *
from flanabot.bots.scraper_bot import *
from flanabot.bots.steam_bot import *
from flanabot.bots.ubereats_bot import *
from flanabot.bots.weather_bot import *

View File

@@ -0,0 +1,309 @@
from __future__ import annotations # todo0 remove when it's by default
__all__ = ['BtcOffersBot']
import asyncio
import functools
import json
import os
from abc import ABC
from collections.abc import Callable
from typing import Any
import aiohttp
import flanautils
import websockets
from multibot import MultiBot, constants as multibot_constants
from flanabot import constants
from flanabot.models import Chat, Message
# ---------------------------------------------------- #
# -------------------- DECORATORS -------------------- #
# ---------------------------------------------------- #
def preprocess_btc_offers(func: Callable) -> Callable:
@functools.wraps(func)
async def wrapper(self: BtcOffersBot, message: Message) -> Any:
if message.chat.is_group and not self.is_bot_mentioned(message):
return
eur_mode = (
'' in message.text
or
bool(
flanautils.cartesian_product_string_matching(
message.text,
constants.KEYWORDS['eur'],
multibot_constants.PARSER_MIN_SCORE_DEFAULT
)
)
)
usd_mode = (
'$' in message.text
or
bool(
flanautils.cartesian_product_string_matching(
message.text,
constants.KEYWORDS['usd'],
multibot_constants.PARSER_MIN_SCORE_DEFAULT
)
)
)
premium_mode = (
'%' in message.text
or
bool(
flanautils.cartesian_product_string_matching(
message.text,
constants.KEYWORDS['premium'],
multibot_constants.PARSER_MIN_SCORE_DEFAULT
)
)
)
if len([arg for arg in (eur_mode, usd_mode, premium_mode) if arg]) > 1:
await self.send_error(
'Indica únicamente uno de los siguientes: precio en euros, precio en dólares o prima.',
message
)
return
parsed_number = flanautils.text_to_number(message.text)
if parsed_number and not any((eur_mode, usd_mode, premium_mode)):
eur_mode = True
if (eur_mode or usd_mode) and parsed_number < 0 or not flanautils.validate_mongodb_number(parsed_number):
await self.send_error('❌ Por favor, introduce un número válido.', message)
return
if eur_mode:
query = {'max_price_eur': parsed_number}
elif usd_mode:
query = {'max_price_usd': parsed_number}
elif premium_mode:
query = {'max_premium': parsed_number}
else:
query = {}
return await func(self, message, query)
return wrapper
# ---------------------------------------------------------------------------------------------------- #
# ------------------------------------------ BTC_OFFERS_BOT ------------------------------------------ #
# ---------------------------------------------------------------------------------------------------- #
class BtcOffersBot(MultiBot, ABC):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._websocket: websockets.ClientConnection | None = None
self._notification_task: asyncio.Task[None] | None = None
self._api_endpoint = f"{os.environ['BTC_OFFERS_API_HOST']}:{os.environ['BTC_OFFERS_API_PORT']}/offers"
self._websocket_lock = asyncio.Lock()
# -------------------------------------------------------- #
# ------------------- PROTECTED METHODS ------------------ #
# -------------------------------------------------------- #
def _add_handlers(self):
super()._add_handlers()
self.register(self._on_btc_offers, keywords=constants.KEYWORDS['offer'])
self.register(self._on_btc_offers, keywords=constants.KEYWORDS['money'])
self.register(self._on_btc_offers, keywords=(constants.KEYWORDS['offer'], constants.KEYWORDS['money']))
self.register(self._on_notify_btc_offers, keywords=constants.KEYWORDS['notify'])
self.register(self._on_notify_btc_offers, keywords=(constants.KEYWORDS['notify'], constants.KEYWORDS['offer']))
self.register(self._on_notify_btc_offers, keywords=(constants.KEYWORDS['notify'], constants.KEYWORDS['money']))
self.register(self._on_notify_btc_offers, keywords=(constants.KEYWORDS['notify'], constants.KEYWORDS['offer'], constants.KEYWORDS['money']))
self.register(self._on_stop_btc_offers_notification, keywords=(multibot_constants.KEYWORDS['deactivate'], constants.KEYWORDS['offer']))
self.register(self._on_stop_btc_offers_notification, keywords=(multibot_constants.KEYWORDS['deactivate'], constants.KEYWORDS['money']))
self.register(self._on_stop_btc_offers_notification, keywords=(multibot_constants.KEYWORDS['deactivate'], constants.KEYWORDS['notify']))
self.register(self._on_stop_btc_offers_notification, keywords=(multibot_constants.KEYWORDS['stop'], constants.KEYWORDS['offer']))
self.register(self._on_stop_btc_offers_notification, keywords=(multibot_constants.KEYWORDS['stop'], constants.KEYWORDS['money']))
self.register(self._on_stop_btc_offers_notification, keywords=(multibot_constants.KEYWORDS['stop'], constants.KEYWORDS['notify']))
def _find_chats_to_notify(self) -> list[Chat]:
return self.Chat.find({'platform': self.platform.value, 'btc_offers_query': {'$exists': True, '$ne': {}}})
def _is_websocket_connected(self) -> bool:
return self._websocket and self._websocket.state in {websockets.State.CONNECTING, websockets.State.OPEN}
async def _send_offers(self, offers: list[dict], chat: Chat, notifications_disabled=False):
offers_parts = []
for i, offer in enumerate(offers, start=1):
offer_parts = [
f'<b>{i}.</b>',
f"<b>Plataforma:</b> <code>{offer['exchange']}</code>",
f"<b>Id:</b> <code>{offer['id']}</code>"
]
if offer['author']:
offer_parts.append(f"<b>Autor:</b> <code>{offer['author']}</code>")
payment_methods_text = ''.join(
f'\n <code>{payment_method}</code>' for payment_method in offer['payment_methods']
)
rounded_premium = round(offer['premium'], 2)
offer_parts.extend(
(
f"<b>Cantidad:</b> <code>{offer['amount']}</code>",
f"<b>Precio (EUR):</b> <code>{offer['price_eur']:.2f} €</code>",
f"<b>Precio (USD):</b> <code>{offer['price_usd']:.2f} $</code>",
f"<b>Prima:</b> <code>{rounded_premium if rounded_premium else '0.00'} %</code>",
f'<b>Métodos de pago:</b>{payment_methods_text}'
)
)
if offer['description']:
offer_parts.append(
f"<b>Descripción:</b>\n<code><code><code>{offer['description']}</code></code></code>"
)
offers_parts.append('\n'.join(offer_parts))
offers_parts_chunks = flanautils.chunks(offers_parts, 5)
messages_parts = [
[
'<b>💰💰💰 OFERTAS BTC 💰💰💰</b>',
'',
'\n\n'.join(offers_parts_chunks[0])
]
]
for offers_parts_chunk in offers_parts_chunks[1:]:
messages_parts.append(
[
'­',
'\n\n'.join(offers_parts_chunk)
]
)
if notifications_disabled:
messages_parts[-1].extend(
(
'',
'-' * 70,
'<b> Los avisos de ofertas BTC se han eliminado. Si quieres volver a recibirlos, no dudes en pedírmelo.</b>'
)
)
for message_parts in messages_parts:
await self.send('\n'.join(message_parts), chat)
async def _wait_btc_offers_notification(self):
while True:
while True:
try:
data = json.loads(await self._websocket.recv())
except websockets.ConnectionClosed:
await self.start_all_btc_offers_notifications()
else:
break
chat = await self.get_chat(data['chat_id'])
chat.btc_offers_query = {}
chat.save(pull_exclude_fields=('btc_offers_query',))
await self._send_offers(data['offers'], chat, notifications_disabled=True)
# ---------------------------------------------- #
# HANDLERS #
# ---------------------------------------------- #
@preprocess_btc_offers
async def _on_btc_offers(self, message: Message, query: dict[str, float]):
bot_state_message = await self.send('Obteniendo ofertas BTC...', message)
try:
async with aiohttp.ClientSession() as session:
async with session.get(f'http://{self._api_endpoint}', params=query) as response:
offers = await response.json()
except aiohttp.ClientConnectorError:
await self.send_error('❌🌐 El servidor de ofertas BTC está desconectado.', bot_state_message, edit=True)
return
if offers:
await self._send_offers(offers, message.chat)
await self.delete_message(bot_state_message)
else:
await self.edit('No hay ofertas BTC actualmente que cumplan esa condición.', bot_state_message)
@preprocess_btc_offers
async def _on_notify_btc_offers(self, message: Message, query: dict[str, float]):
if not query:
await self.send_error('❌ Especifica una cantidad para poder avisarte.', message)
return
match query:
case {'max_price_eur': max_price_eur}:
response_text = f'✅ ¡Perfecto! Te avisaré cuando existan ofertas por {max_price_eur:.2f} € o menos.'
case {'max_price_usd': max_price_usd}:
response_text = f'✅ ¡Perfecto! Te avisaré cuando existan ofertas por {max_price_usd:.2f} $ o menos.'
case _:
rounded_max_premium = round(query['max_premium'], 2)
response_text = f"✅ ¡Perfecto! Te avisaré cuando existan ofertas con una prima del {rounded_max_premium if rounded_max_premium else '0.00'} % o menor."
await self.send(response_text, message)
await self.start_btc_offers_notification(message.chat, query)
async def _on_ready(self):
await super()._on_ready()
asyncio.create_task(self.start_all_btc_offers_notifications())
async def _on_stop_btc_offers_notification(self, message: Message):
if message.chat.is_group and not self.is_bot_mentioned(message):
return
previous_btc_offers_query = message.chat.btc_offers_query
await self.stop_btc_offers_notification(message.chat)
if previous_btc_offers_query:
await self.send('🛑 Los avisos de ofertas BTC se han eliminado.', message)
else:
await self.send('🤔 No existía ningún aviso de ofertas BTC configurado.', message)
# -------------------------------------------------------- #
# -------------------- PUBLIC METHODS -------------------- #
# -------------------------------------------------------- #
async def start_all_btc_offers_notifications(self):
if chats := self._find_chats_to_notify():
for chat in chats:
chat = await self.get_chat(chat.id)
chat.pull_from_database(overwrite_fields=('_id', 'btc_offers_query'))
await self.start_btc_offers_notification(chat, chat.btc_offers_query)
elif self._notification_task and not self._notification_task.done():
self._notification_task.cancel()
await asyncio.sleep(0)
async def start_btc_offers_notification(self, chat: Chat, query: dict[str, float]):
async with self._websocket_lock:
if not self._is_websocket_connected():
while True:
try:
self._websocket = await websockets.connect(f'ws://{self._api_endpoint}')
except ConnectionRefusedError:
await asyncio.sleep(constants.BTC_OFFERS_WEBSOCKET_RETRY_DELAY_SECONDS)
else:
break
if not self._notification_task or self._notification_task.done():
self._notification_task = asyncio.create_task(self._wait_btc_offers_notification())
chat.btc_offers_query = query
chat.save()
await self._websocket.send(json.dumps({'action': 'start', 'chat_id': chat.id, 'query': query}))
async def stop_all_btc_offers_notification(self):
for chat in self._find_chats_to_notify():
await self.stop_btc_offers_notification(chat)
async def stop_btc_offers_notification(self, chat: Chat):
if self._is_websocket_connected():
await self._websocket.send(json.dumps({'action': 'stop', 'chat_id': chat.id}))
chat.btc_offers_query = {}
chat.save(pull_exclude_fields=('btc_offers_query',))

View File

@@ -0,0 +1,586 @@
__all__ = ['Connect4Bot']
import asyncio
import copy
import random
from abc import ABC
from collections import defaultdict
from typing import Iterable
from flanautils import Media, MediaType, Source
from multibot import MultiBot
from flanabot import connect_4_frontend, constants
from flanabot.models import ButtonsGroup, Message, Player
# ----------------------------------------------------------------------------------------------------- #
# ------------------------------------------- CONNECT_4_BOT ------------------------------------------- #
# ----------------------------------------------------------------------------------------------------- #
class Connect4Bot(MultiBot, ABC):
# -------------------------------------------------------- #
# ------------------- PROTECTED METHODS ------------------ #
# -------------------------------------------------------- #
def _add_handlers(self):
super()._add_handlers()
self.register(self._on_connect_4, keywords=constants.KEYWORDS['connect_4'])
self.register(self._on_connect_4_vs_itself, keywords=(*constants.KEYWORDS['connect_4'], *constants.KEYWORDS['self']))
self.register_button(self._on_connect_4_button_press, key=ButtonsGroup.CONNECT_4)
def _ai_insert(
self,
current_player_num: int,
next_player_num: int,
board: list[list[int | None]]
) -> tuple[int, int]:
available_positions_ = self._available_positions(board)
# check if current player can win
for i, j in available_positions_:
if current_player_num in self._check_winners(i, j, board):
return self.insert_piece(j, current_player_num, board)
# check if next player can win
for i, j in available_positions_:
if next_player_num in self._check_winners(i, j, board):
return self.insert_piece(j, current_player_num, board)
# future possibility (above the move)
next_player_winning_positions_above = []
current_player_winning_positions_above = []
for i, j in available_positions_:
if i < 1:
continue
board_copy = copy.deepcopy(board)
board_copy[i][j] = current_player_num
winners = self._check_winners(i - 1, j, board_copy)
if next_player_num in winners:
next_player_winning_positions_above.append((i, j))
elif current_player_num in winners:
current_player_winning_positions_above.append((i, j))
# check if after the current player moves, it will have 2 positions to win
for i, j in available_positions_:
if (i, j) in next_player_winning_positions_above:
continue
board_copy = copy.deepcopy(board)
board_copy[i][j] = current_player_num
if len(self._winning_positions(board_copy)[current_player_num]) >= 2:
return self.insert_piece(j, current_player_num, board)
# check if after the next player moves, he will have 2 positions to win
for i, j in available_positions_:
if (i, j) in current_player_winning_positions_above:
continue
board_copy = copy.deepcopy(board)
board_copy[i][j] = next_player_num
future_winning_positions = self._winning_positions(board_copy)[next_player_num]
if len(future_winning_positions) < 2:
continue
if (i, j) not in next_player_winning_positions_above:
return self.insert_piece(j, current_player_num, board)
for i_2, j_2 in future_winning_positions:
if (i_2, j_2) in available_positions_ and (i_2, j_2) not in next_player_winning_positions_above:
return self.insert_piece(j_2, current_player_num, board)
good_positions = [pos for pos in available_positions_ if pos not in next_player_winning_positions_above and pos not in current_player_winning_positions_above]
if good_positions:
j = random.choice(self._best_moves(good_positions, current_player_num, board))[1]
elif current_player_winning_positions_above:
j = random.choice(self._best_moves(current_player_winning_positions_above, current_player_num, board))[1]
else:
j = random.choice(self._best_moves(next_player_winning_positions_above, current_player_num, board))[1]
return self.insert_piece(j, current_player_num, board)
async def _ai_turn(
self,
player_1: Player,
player_2: Player,
current_player: Player,
next_player: Player,
next_turn: int,
delay: float,
board: list[list[int | None]],
message: Message
) -> bool:
await asyncio.sleep(delay)
i, j = self._ai_insert(current_player.number, next_player.number, board)
if await self._check_game_finished(i, j, player_1, player_2, next_turn, board, message):
return True
return not await self.edit(
Media(
connect_4_frontend.make_image(board, next_player, highlight=(i, j)),
MediaType.IMAGE,
'png',
Source.LOCAL
),
message
)
@staticmethod
def _available_positions(board: list[list[int | None]]) -> list[tuple[int, int]]:
available_positions = []
for j in range(constants.CONNECT_4_N_COLUMNS):
for i in range(constants.CONNECT_4_N_ROWS - 1, -1, -1):
if board[i][j] is None:
available_positions.append((i, j))
break
return available_positions
# noinspection DuplicatedCode
@staticmethod
def _best_moves(
possible_positions: Iterable[tuple[int, int]],
player_num: int,
board: list[list[int | None]]
) -> list[tuple[int, int]]:
best_moves = []
max_points = float('-inf')
for i, j in possible_positions:
if 3 <= j <= constants.CONNECT_4_N_COLUMNS - 4:
points = constants.CONNECT_4_CENTER_COLUMN_POINTS
else:
points = 0
# left
for j_left in range(j - 1, j - 4, -1):
if j_left < 0:
points -= 1
break
if board[i][j_left] is not None:
if board[i][j_left] == player_num:
points += 1
else:
points -= 1
break
# right
for j_right in range(j + 1, j + 4):
if j_right >= constants.CONNECT_4_N_COLUMNS:
points -= 1
break
if board[i][j_right] is not None:
if board[i][j_right] == player_num:
points += 1
else:
points -= 1
break
# up
for i_up in range(i - 1, i - 4, -1):
if i_up < 0:
points -= 1
break
if board[i_up][j] is not None:
if board[i_up][j] == player_num:
points += 1
else:
points -= 1
break
# down
for i_down in range(i + 1, i + 4):
if i_down >= constants.CONNECT_4_N_ROWS:
points -= 1
break
if board[i_down][j] is not None:
if board[i_down][j] == player_num:
points += 1
else:
points -= 1
break
# up left
for n in range(1, 4):
i_up = i - n
j_left = j - n
if i_up < 0 or j_left < 0:
points -= 1
break
if board[i_up][j_left] is not None:
if board[i_up][j_left] == player_num:
points += 1
else:
points -= 1
break
# up right
for n in range(1, 4):
i_up = i - n
j_right = j + n
if i_up < 0 or j_right >= constants.CONNECT_4_N_COLUMNS:
points -= 1
break
if board[i_up][j_right] is not None:
if board[i_up][j_right] == player_num:
points += 1
else:
points -= 1
break
# down left
for n in range(1, 4):
i_down = i + n
j_left = j - n
if i_down >= constants.CONNECT_4_N_ROWS or j_left < 0:
points -= 1
break
if board[i_down][j_left] is not None:
if board[i_down][j_left] == player_num:
points += 1
else:
points -= 1
break
# down right
for n in range(1, 4):
i_down = i + n
j_right = j + n
if i_down >= constants.CONNECT_4_N_ROWS or j_right >= constants.CONNECT_4_N_COLUMNS:
points -= 1
break
if board[i_down][j_right] is not None:
if board[i_down][j_right] == player_num:
points += 1
else:
points -= 1
break
if points > max_points:
best_moves = [(i, j)]
max_points = points
elif points == max_points:
best_moves.append((i, j))
return best_moves
async def _check_game_finished(
self,
i: int,
j: int,
player_1: Player,
player_2: Player,
turn: int,
board: list[list[int | None]],
message: Message
) -> bool:
if board[i][j] in self._check_winners(i, j, board):
winner, loser = (player_1, player_2) if board[i][j] == player_1.number else (player_2, player_1)
edit_kwargs = {'winner': winner, 'loser': loser, 'win_position': (i, j)}
elif turn >= constants.CONNECT_4_N_ROWS * constants.CONNECT_4_N_COLUMNS:
edit_kwargs = {'tie': True}
else:
return False
try:
message.data['connect_4']['is_active'] = False
except KeyError:
pass
await self.edit(
Media(
connect_4_frontend.make_image(board, highlight=(i, j), **edit_kwargs),
MediaType.IMAGE,
'png',
Source.LOCAL
),
message,
buttons=[]
)
return True
@staticmethod
def _check_winner_left(i: int, j: int, board: list[list[int | None]]) -> int | None:
if (
(
2 < j and board[i][j - 3] == board[i][j - 2] == board[i][j - 1]
or
1 < j < constants.CONNECT_4_N_COLUMNS - 1 and board[i][j - 2] == board[i][j - 1] == board[i][j + 1]
)
and
board[i][j - 1] is not None
):
return board[i][j - 1]
@staticmethod
def _check_winner_right(i: int, j: int, board: list[list[int | None]]) -> int | None:
if (
(
j < constants.CONNECT_4_N_COLUMNS - 3 and board[i][j + 1] == board[i][j + 2] == board[i][j + 3]
or
0 < j < constants.CONNECT_4_N_COLUMNS - 2 and board[i][j - 1] == board[i][j + 1] == board[i][j + 2]
)
and
board[i][j + 1] is not None
):
return board[i][j + 1]
@staticmethod
def _check_winner_up(i: int, j: int, board: list[list[int | None]]) -> int | None:
if (
(
2 < i and board[i - 3][j] == board[i - 2][j] == board[i - 1][j]
or
1 < i < constants.CONNECT_4_N_ROWS - 1 and board[i - 2][j] == board[i - 1][j] == board[i + 1][j]
)
and
board[i - 1][j] is not None
):
return board[i - 1][j]
@staticmethod
def _check_winner_down(i: int, j: int, board: list[list[int | None]]) -> int | None:
if (
(
i < constants.CONNECT_4_N_ROWS - 3 and board[i + 1][j] == board[i + 2][j] == board[i + 3][j]
or
0 < i < constants.CONNECT_4_N_ROWS - 2 and board[i - 1][j] == board[i + 1][j] == board[i + 2][j]
)
and
board[i + 1][j] is not None
):
return board[i + 1][j]
@staticmethod
def _check_winner_up_left(i: int, j: int, board: list[list[int | None]]) -> int | None:
if (
(
2 < i and 2 < j and board[i - 3][j - 3] == board[i - 2][j - 2] == board[i - 1][j - 1]
or
1 < i < constants.CONNECT_4_N_ROWS - 1 and 1 < j < constants.CONNECT_4_N_COLUMNS - 1 and board[i - 2][j - 2] == board[i - 1][j - 1] == board[i + 1][j + 1]
)
and
board[i - 1][j - 1] is not None
):
return board[i - 1][j - 1]
@staticmethod
def _check_winner_up_right(i: int, j: int, board: list[list[int | None]]) -> int | None:
if (
(
2 < i and j < constants.CONNECT_4_N_COLUMNS - 3 and board[i - 1][j + 1] == board[i - 2][j + 2] == board[i - 3][j + 3]
or
1 < i < constants.CONNECT_4_N_ROWS - 1 and 0 < j < constants.CONNECT_4_N_COLUMNS - 2 and board[i + 1][j - 1] == board[i - 1][j + 1] == board[i - 2][j + 2]
)
and
board[i - 1][j + 1] is not None
):
return board[i - 1][j + 1]
@staticmethod
def _check_winner_down_left(i: int, j: int, board: list[list[int | None]]) -> int | None:
if (
(
i < constants.CONNECT_4_N_ROWS - 3 and 2 < j and board[i + 3][j - 3] == board[i + 2][j - 2] == board[i + 1][j - 1]
or
0 < i < constants.CONNECT_4_N_ROWS - 2 and 1 < j < constants.CONNECT_4_N_COLUMNS - 1 and board[i + 2][j - 2] == board[i + 1][j - 1] == board[i - 1][j + 1]
)
and
board[i + 1][j - 1] is not None
):
return board[i + 1][j - 1]
@staticmethod
def _check_winner_down_right(i: int, j: int, board: list[list[int | None]]) -> int | None:
if (
(
i < constants.CONNECT_4_N_ROWS - 3 and j < constants.CONNECT_4_N_COLUMNS - 3 and board[i + 1][j + 1] == board[i + 2][j + 2] == board[i + 3][j + 3]
or
0 < i < constants.CONNECT_4_N_ROWS - 2 and 0 < j < constants.CONNECT_4_N_COLUMNS - 2 and board[i - 1][j - 1] == board[i + 1][j + 1] == board[i + 2][j + 2]
)
and
board[i + 1][j + 1] is not None
):
return board[i + 1][j + 1]
def _check_winners(self, i: int, j: int, board: list[list[int | None]]) -> set[int]:
winners = set()
if winner := self._check_winner_left(i, j, board):
winners.add(winner)
if winner := self._check_winner_up(i, j, board):
winners.add(winner)
if len(winners) == 2:
return winners
if winner := self._check_winner_right(i, j, board):
winners.add(winner)
if len(winners) == 2:
return winners
if winner := self._check_winner_down(i, j, board):
winners.add(winner)
if len(winners) == 2:
return winners
if winner := self._check_winner_up_left(i, j, board):
winners.add(winner)
if len(winners) == 2:
return winners
if winner := self._check_winner_up_right(i, j, board):
winners.add(winner)
if len(winners) == 2:
return winners
if winner := self._check_winner_down_right(i, j, board):
winners.add(winner)
if len(winners) == 2:
return winners
if winner := self._check_winner_down_left(i, j, board):
winners.add(winner)
return winners
def _winning_positions(self, board: list[list[int | None]]) -> defaultdict[int, list[tuple[int, int]]]:
winning_positions: defaultdict[int, list[tuple[int, int]]] = defaultdict(list)
for next_i, next_j in self._available_positions(board):
for player_number in self._check_winners(next_i, next_j, board):
winning_positions[player_number].append((next_i, next_j))
return winning_positions
# ---------------------------------------------- #
# HANDLERS #
# ---------------------------------------------- #
async def _on_connect_4(self, message: Message):
if message.chat.is_group and not self.is_bot_mentioned(message):
return
board = [[None for _ in range(constants.CONNECT_4_N_COLUMNS)] for _ in range(constants.CONNECT_4_N_ROWS)]
player_1 = Player(message.author.id, message.author.name.split('#')[0], 1)
try:
user_2 = next(user for user in message.mentions if user.id != self.id)
except StopIteration:
player_2 = Player(self.id, self.name.split('#')[0], 2)
else:
player_2 = Player(user_2.id, user_2.name.split('#')[0], 2)
await self.send(
media=Media(connect_4_frontend.make_image(board, player_1), MediaType.IMAGE, 'png', Source.LOCAL),
message=message,
buttons=self.distribute_buttons([str(n) for n in range(1, constants.CONNECT_4_N_COLUMNS + 1)]),
buttons_key=ButtonsGroup.CONNECT_4,
data={
'connect_4': {
'is_active': True,
'board': board,
'player_1': player_1.to_dict(),
'player_2': player_2.to_dict(),
'turn': 0
}
}
)
await self.delete_message(message)
async def _on_connect_4_button_press(self, message: Message):
await self.accept_button_event(message)
connect_4_data = message.data['connect_4']
is_active = connect_4_data['is_active']
board = connect_4_data['board']
player_1 = Player.from_dict(connect_4_data['player_1'])
player_2 = Player.from_dict(connect_4_data['player_2'])
if connect_4_data['turn'] % 2 == 0:
current_player = player_1
next_player = player_2
else:
current_player = player_2
next_player = player_1
presser_id = message.buttons_info.presser_user.id
move_column = int(message.buttons_info.pressed_text) - 1
if not is_active or current_player.id != presser_id or board[0][move_column] is not None:
return
connect_4_data['is_active'] = False
i, j = self.insert_piece(move_column, current_player.number, board)
connect_4_data['turn'] += 1
if await self._check_game_finished(i, j, player_1, player_2, connect_4_data['turn'], board, message):
return
await self.edit(
Media(
connect_4_frontend.make_image(board, next_player, highlight=(i, j)),
MediaType.IMAGE,
'png',
Source.LOCAL
),
message
)
if player_2.id == self.id:
connect_4_data['turn'] += 1
if await self._ai_turn(
player_1,
player_2,
next_player,
current_player,
connect_4_data['turn'],
constants.CONNECT_4_AI_DELAY_SECONDS,
board,
message
):
return
connect_4_data['is_active'] = True
async def _on_connect_4_vs_itself(self, message: Message):
if message.chat.is_group and not self.is_bot_mentioned(message):
return
board = [[None for _ in range(constants.CONNECT_4_N_COLUMNS)] for _ in range(constants.CONNECT_4_N_ROWS)]
player_1 = Player(self.id, self.name.split('#')[0], 1)
player_2 = Player(self.id, self.name.split('#')[0], 2)
current_player = player_1
next_player = player_2
turn = 0
bot_message = await self.send(
media=Media(connect_4_frontend.make_image(board, current_player), MediaType.IMAGE, 'png', Source.LOCAL),
message=message
)
await self.delete_message(message)
while True:
turn += 1
if await self._ai_turn(
player_1,
player_2,
current_player,
next_player,
turn,
constants.CONNECT_4_AI_DELAY_SECONDS / 2,
board,
bot_message
):
break
current_player, next_player = next_player, current_player
# -------------------------------------------------------- #
# -------------------- PUBLIC METHODS -------------------- #
# -------------------------------------------------------- #
@staticmethod
def insert_piece(j: int, player_number: int, board: list[list[int | None]]) -> tuple[int, int] | None:
for i in range(constants.CONNECT_4_N_ROWS - 1, -1, -1):
if board[i][j] is None:
board[i][j] = player_number
return i, j

File diff suppressed because it is too large Load Diff

View File

@@ -1,37 +1,25 @@
import asyncio
import os
__all__ = ['FlanaDiscBot']
import asyncio
import datetime
import math
import os
import random
import urllib.parse
import uuid
from collections import defaultdict
from pathlib import Path
import aiohttp
import discord
from multibot import DiscordBot
import pytz
from flanautils import Media, MediaType, NotFoundError, OrderedSet
from multibot import BadRoleError, DiscordBot, LimitError, Platform, Role, User, admin, bot_mentioned, constants as multibot_constants, group
from flanabot import constants
from flanabot.bots.flana_bot import FlanaBot
from flanabot.exceptions import BadRoleError, UserDisconnectedError
from flanabot.models import User
HEAT_NAMES = [
'Canal Congelado',
'Canal Fresquito',
'Canal Templaillo',
'Canal Calentito',
'Canal Caloret',
'Canal Caliente',
'Canal Olor a Vasco',
'Verano Cordobés al Sol',
'Canal Ardiendo',
'abrid las putas ventanas y traed el extintor',
'Canal INFIERNO',
'La Palma 🌋'
]
HOT_CHANNEL_ID = 493530483045564417
ROLES = {
'Administrador': 387344390030360587,
'Carroñero': 493523298429435905,
'al lol': 881238165476741161,
'Persona': 866046517998387220,
'Castigado': 877662459568209921,
'Bot': 493784221085597706
}
from flanabot.models import Chat, Message, Punishment
from flanabot.models.heating_context import ChannelData, HeatingContext
# ---------------------------------------------------------------------------------------------------- #
@@ -40,94 +28,238 @@ ROLES = {
class FlanaDiscBot(DiscordBot, FlanaBot):
def __init__(self):
super().__init__(os.environ['DISCORD_BOT_TOKEN'])
self.heating = False
self.heat_level = 0
self.heating_contexts: dict[int, HeatingContext] = defaultdict(HeatingContext)
self._flanaserver_api_base_url = f"http://{os.environ['FLANASERVER_API_HOST']}:{os.environ['FLANASERVER_API_PORT']}"
# ----------------------------------------------------------- #
# -------------------- PROTECTED METHODS -------------------- #
# ----------------------------------------------------------- #
# -------------------------------------------------------- #
# ------------------- PROTECTED METHODS ------------------ #
# -------------------------------------------------------- #
def _add_handlers(self):
super()._add_handlers()
self.bot_client.add_listener(self._on_voice_state_update, 'on_voice_state_update')
self.client.add_listener(self._on_member_join, 'on_member_join')
self.client.add_listener(self._on_member_remove, 'on_member_remove')
self.client.add_listener(self._on_voice_state_update, 'on_voice_state_update')
self.register(self._on_audit_log, keywords=multibot_constants.KEYWORDS['audit'])
self.register(self._on_restore_channel_names, keywords=(multibot_constants.KEYWORDS['reset'], multibot_constants.KEYWORDS['chat']))
async def _changeable_roles(self, group_: int | str | Chat | Message) -> list[Role]:
group_roles = await self.get_group_roles(group_)
group_id = self.get_group_id(group_)
return [role for role in group_roles if role.id in constants.CHANGEABLE_ROLES[Platform.DISCORD][group_id]]
async def _heat_channel(self, channel: discord.VoiceChannel):
async def set_fire_to(channel_key: str, depends_on: str, firewall=0):
fire_score = random.randint(0, channels_data[depends_on].n_fires - channels_data[channel_key].n_fires) - firewall // 2
if fire_score < 1:
if not channels_data[channel_key].n_fires:
return
channels_data[channel_key].n_fires -= 1
elif fire_score == 1:
return
else:
channels_data[channel_key].n_fires += 1
if channels_data[channel_key].n_fires:
new_name_ = '🔥' * channels_data[channel_key].n_fires
else:
new_name_ = channels_data[channel_key].original_name
await channels_data[channel_key].channel.edit(name=new_name_)
voice_channels = {}
for voice_channel in channel.guild.voice_channels:
voice_channels[voice_channel.id] = voice_channel
channels_data = {}
for letter, channel_id in constants.DISCORD_HOT_CHANNEL_IDS.items():
channels_data[letter] = ChannelData(
channel=voice_channels[channel_id],
original_name=voice_channels[channel_id].name
)
heating_context = self.heating_contexts[channel.guild.id]
heating_context.channels_data = channels_data
while True:
await asyncio.sleep(constants.HEAT_PERIOD_SECONDS)
if channel.members:
if self.heat_level == len(HEAT_NAMES) - 1:
return
self.heat_level += 0.5
elif not channel.members:
if not self.heat_level:
return
self.heat_level -= 0.5
heating_context.heat_level += 0.5
else:
if heating_context.heat_level == constants.HEAT_FIRST_LEVEL:
return
heating_context.heat_level -= 0.5
if heating_context.heat_level > len(constants.DISCORD_HEAT_NAMES) - 1:
heating_context.heat_level = float(int(heating_context.heat_level))
if not heating_context.heat_level.is_integer():
continue
await channel.edit(name=HEAT_NAMES[int(self.heat_level)])
i = int(heating_context.heat_level)
if i == constants.HEAT_FIRST_LEVEL:
n_fires = 0
new_name = channels_data['C'].original_name
elif i < len(constants.DISCORD_HEAT_NAMES):
n_fires = 0
new_name = constants.DISCORD_HEAT_NAMES[i]
else:
n_fires = i - len(constants.DISCORD_HEAT_NAMES) + 1
n_fires = round(math.log(n_fires + 4, 1.2) - 8)
new_name = '🔥' * n_fires
channels_data['C'].n_fires = n_fires
if channel.name != new_name:
await channel.edit(name=new_name)
async def _mute(self, user_id: int, group_id: int):
user = await self.get_user(user_id, group_id)
try:
await user.original_object.edit(mute=True)
except discord.errors.HTTPException:
raise UserDisconnectedError
await set_fire_to('B', depends_on='C', firewall=len(channels_data['B'].channel.members))
await set_fire_to('A', depends_on='B', firewall=len(channels_data['A'].channel.members))
await set_fire_to('D', depends_on='C', firewall=len(channels_data['C'].channel.members))
await set_fire_to('E', depends_on='D', firewall=len(channels_data['D'].channel.members))
async def _punish(self, user_id: int, group_id: int):
user = await self.get_user(user_id, group_id)
async def _punish(self, user: int | str | User, group_: int | str | Chat | Message, message: Message = None):
user_id = self.get_user_id(user)
try:
await user.original_object.remove_roles(self._find_role_by_id(ROLES['Persona'], user.original_object.guild.roles))
await user.original_object.add_roles(self._find_role_by_id(ROLES['Castigado'], user.original_object.guild.roles))
await self.add_role(user_id, group_, 'Castigado')
await self.remove_role(user_id, group_, 'Persona')
except AttributeError:
raise BadRoleError(str(self._punish))
async def _unmute(self, user_id: int, group_id: int):
user = await self.get_user(user_id, group_id)
try:
await user.original_object.edit(mute=False)
except discord.errors.HTTPException:
raise UserDisconnectedError
async def _search_medias(
self,
message: Message,
force=False,
audio_only=False,
timeout_for_media: int | float = constants.SCRAPING_TIMEOUT_SECONDS
) -> OrderedSet[Media]:
return await super()._search_medias(message, force, audio_only, timeout_for_media)
async def _unpunish(self, user_id: int, group_id: int):
user = await self.get_user(user_id, group_id)
async def _send_media(self, media: Media, bot_state_message: Message, message: Message) -> Message | None:
# noinspection PyBroadException
try:
await user.original_object.remove_roles(self._find_role_by_id(ROLES['Castigado'], user.original_object.guild.roles))
await user.original_object.add_roles(self._find_role_by_id(ROLES['Persona'], user.original_object.guild.roles))
return await self.send(media, message, reply_to=message.replied_message, raise_exceptions=True)
except LimitError:
if bot_state_message:
await self.edit('No cabe porque Discord es una mierda. Subiendo a FlanaServer...', bot_state_message)
async with aiohttp.ClientSession() as session:
form = aiohttp.FormData()
file_name = urllib.parse.unquote(media.title or Path(media.url).name or uuid.uuid4().hex)
if media.extension and not file_name.endswith(media.extension):
file_name = f'{file_name}.{media.extension}'
match media.type_:
case MediaType.AUDIO:
content_type = f"audio/{'mpeg' if media.extension == 'mp3' else media.extension}"
case MediaType.GIF:
content_type = 'image/gif'
case MediaType.IMAGE:
content_type = f'image/{media.extension}'
case MediaType.VIDEO:
content_type = f'video/{media.extension}'
form.add_field('file', media.bytes_, content_type=content_type, filename=file_name)
form.add_field('expires_in', str(constants.FLANASERVER_FILE_EXPIRATION_SECONDS))
async with session.post(f'{self._flanaserver_api_base_url}/files', data=form) as response:
if response.status != 201:
return
file_info = await response.json()
return await self.send(f"{constants.FLANASERVER_BASE_URL}{file_info['embed_url']}", message)
except Exception:
pass
async def _unpunish(self, user: int | str | User, group_: int | str | Chat | Message, message: Message = None):
user_id = self.get_user_id(user)
try:
await self.add_role(user_id, group_, 'Persona')
await self.remove_role(user_id, group_, 'Castigado')
except AttributeError:
raise BadRoleError(str(self._unpunish))
# ---------------------------------------------- #
# HANDLERS #
# ---------------------------------------------- #
@group
@bot_mentioned
async def _on_audit_log(self, message: Message):
audit_entries = await self.find_audit_entries(
message,
limit=constants.AUDIT_LOG_LIMIT,
actions=(discord.AuditLogAction.member_disconnect, discord.AuditLogAction.member_move),
after=datetime.datetime.now(datetime.timezone.utc) - constants.AUDIT_LOG_AGE
)
await self.delete_message(message)
if not audit_entries:
await self.send_error(f'No hay entradas en el registro de auditoría <i>(desconectar y mover)</i> en la última hora.', message)
return
message_parts = ['<b>Registro de auditoría (solo desconectar y mover):</b>', '']
for entry in audit_entries:
author = await self._create_user_from_discord_user(entry.user)
date_string = entry.created_at.astimezone(pytz.timezone('Europe/Madrid')).strftime('%d/%m/%Y %H:%M:%S')
if entry.action is discord.AuditLogAction.member_disconnect:
message_parts.append(f"<b>{author.name}</b> ha <b>desconectado</b> {entry.extra.count} {'usuario' if entry.extra.count == 1 else 'usuarios'} <i>({date_string})</i>")
elif entry.action is discord.AuditLogAction.member_move:
message_parts.append(f"<b>{author.name}</b> ha <b>movido</b> {entry.extra.count} {'usuario' if entry.extra.count == 1 else 'usuarios'} a {entry.extra.channel.name} <i>({date_string})</i>")
await self.send('\n'.join(message_parts), message)
async def _on_member_join(self, member: discord.Member):
user = await self._create_user_from_discord_user(member)
user.pull_from_database(overwrite_fields=('roles',))
for role in user.roles:
try:
await self.add_role(user, member.guild.id, role.id)
except NotFoundError:
pass
async def _on_member_remove(self, member: discord.Member):
(await self._create_user_from_discord_user(member)).save()
@group
@bot_mentioned
@admin(send_negative=True)
async def _on_restore_channel_names(self, message: Message):
await self.delete_message(message)
await self.restore_channel_names(self.get_group_id(message))
async def _on_voice_state_update(self, _: discord.Member, before: discord.VoiceState, after: discord.VoiceState):
if getattr(before.channel, 'id', None) == HOT_CHANNEL_ID:
if getattr(before.channel, 'id', None) == constants.DISCORD_HOT_CHANNEL_IDS['C']:
channel = before.channel
elif getattr(after.channel, 'id', None) == HOT_CHANNEL_ID:
elif getattr(after.channel, 'id', None) == constants.DISCORD_HOT_CHANNEL_IDS['C']:
channel = after.channel
else:
return
if not self.heating:
self.heating = True
heating_context = self.heating_contexts[channel.guild.id]
if not heating_context.is_active:
heating_context.is_active = True
await self._heat_channel(channel)
self.heating = False
heating_context.is_active = False
# -------------------------------------------------------- #
# -------------------- PUBLIC METHODS -------------------- #
# -------------------------------------------------------- #
async def is_deaf(self, user_id: int, group_id: int) -> bool:
user = await self.get_user(user_id, group_id)
return user.original_object.voice.deaf
async def is_punished(self, user: int | str | User, group_: int | str | Chat | Message) -> bool:
user = await self.get_user(user, group_)
group_id = self.get_group_id(group_)
async def is_muted(self, user_id: int, group_id: int) -> bool:
user = await self.get_user(user_id, group_id)
return user.original_object.voice.mute
return bool(Punishment.find({
'platform': self.platform.value,
'user_id': user.id,
'group_id': group_id,
'is_active': True
}))
@staticmethod
def is_self_deaf(user: User) -> bool:
return user.original_object.voice.self_deaf
async def restore_channel_names(self, group_id: int):
heating_context = self.heating_contexts[group_id]
@staticmethod
def is_self_muted(user: User) -> bool:
return user.original_object.voice.self_mute
for channel_data in heating_context.channels_data.values():
if channel_data.channel.name != channel_data.original_name:
await channel_data.channel.edit(name=channel_data.original_name)
channel_data.n_fires = 0
heating_context.heat_level = constants.HEAT_FIRST_LEVEL

View File

@@ -1,33 +1,31 @@
from __future__ import annotations # todo0 remove in 3.11
from __future__ import annotations # todo0 remove when it's by default
__all__ = ['whitelisted', 'FlanaTeleBot']
import functools
import os
from typing import Callable
from typing import Any, Callable
import telethon.tl.functions
from flanaapis.weather.constants import WeatherEmoji
from flanautils import Media, MediaType, return_if_first_empty
from multibot import TelegramBot, constants as multibot_constants, find_message, user_client
from flanautils import Media, OrderedSet
from multibot import RegisteredCallback, TelegramBot, find_message, use_user_client, user_client
from flanabot import constants
from flanabot.bots.flana_bot import FlanaBot
from flanabot.models.chat import Chat
from flanabot.models.message import Message
from flanabot.models.user import User
from flanabot.models import Message
# ---------------------------------------------------------- #
# ----------------------- DECORATORS ----------------------- #
# ---------------------------------------------------------- #
def whitelisted_event(func: Callable) -> Callable:
# ---------------------------------------------------- #
# -------------------- DECORATORS -------------------- #
# ---------------------------------------------------- #
def whitelisted(func: Callable) -> Callable:
@functools.wraps(func)
@find_message
async def wrapper(self: FlanaTeleBot, message: Message):
async def wrapper(self: FlanaTeleBot, message: Message, *args, **kwargs) -> Any:
if message.author.id not in self.whitelist_ids:
return
return await func(self, message)
return await func(self, message, *args, **kwargs)
return wrapper
@@ -45,76 +43,44 @@ class FlanaTeleBot(TelegramBot, FlanaBot):
)
self.whitelist_ids = []
# ----------------------------------------------------------- #
# -------------------- PROTECTED METHODS -------------------- #
# ----------------------------------------------------------- #
def _add_handlers(self):
super()._add_handlers()
self.register_button(self._on_button_press)
@return_if_first_empty(exclude_self_types='FlanaTeleBot', globals_=globals())
async def _create_chat_from_telegram_chat(self, telegram_chat: multibot_constants.TELEGRAM_CHAT) -> Chat | None:
chat = await super()._create_chat_from_telegram_chat(telegram_chat)
chat.config = Chat.DEFAULT_CONFIG
return Chat.from_dict(chat.to_dict())
@return_if_first_empty(exclude_self_types='FlanaTeleBot', globals_=globals())
async def _create_user_from_telegram_user(self, original_user: multibot_constants.TELEGRAM_USER, group_id: int = None) -> User | None:
return User.from_dict((await super()._create_user_from_telegram_user(original_user, group_id)).to_dict())
# -------------------------------------------------------- #
# ------------------- PROTECTED METHODS ------------------ #
# -------------------------------------------------------- #
@user_client
async def _get_contacts_ids(self) -> list[int]:
async with self.user_client:
async with use_user_client(self):
contacts_data = await self.user_client(telethon.tl.functions.contacts.GetContactsRequest(hash=0))
return [contact.user_id for contact in contacts_data.contacts]
async def _search_medias(
self,
message: Message,
force=False,
audio_only=False,
timeout_for_media: int | float = constants.SCRAPING_TIMEOUT_SECONDS
) -> OrderedSet[Media]:
return await super()._search_medias(message, force, audio_only, timeout_for_media)
@user_client
async def _update_whitelist(self):
self.whitelist_ids = [self.owner_id, self.bot_id] + await self._get_contacts_ids()
self.whitelist_ids = [self.owner_id, self.id] + await self._get_contacts_ids()
# ---------------------------------------------- #
# HANDLERS #
# ---------------------------------------------- #
@whitelisted_event
async def _on_button_press(self, message: Message):
await message.original_event.answer()
match message.button_text:
case WeatherEmoji.ZOOM_IN.value:
message.weather_chart.zoom_in()
case WeatherEmoji.ZOOM_OUT.value:
message.weather_chart.zoom_out()
case WeatherEmoji.LEFT.value:
message.weather_chart.move_left()
case WeatherEmoji.RIGHT.value:
message.weather_chart.move_right()
case WeatherEmoji.PRECIPITATION_VOLUME.value:
message.weather_chart.trace_metadatas['rain_volume'].show = not message.weather_chart.trace_metadatas['rain_volume'].show
message.weather_chart.trace_metadatas['snow_volume'].show = not message.weather_chart.trace_metadatas['snow_volume'].show
case emoji if emoji in WeatherEmoji.values:
trace_metadata_name = WeatherEmoji(emoji).name.lower()
message.weather_chart.trace_metadatas[trace_metadata_name].show = not message.weather_chart.trace_metadatas[trace_metadata_name].show
message.weather_chart.apply_zoom()
message.weather_chart.draw()
message.save()
image_bytes = message.weather_chart.to_image()
file = await self._prepare_media_to_send(Media(image_bytes, MediaType.IMAGE))
try:
await message.original_object.edit(file=file)
except telethon.errors.rpcerrorlist.MessageNotModifiedError:
pass
@whitelisted_event
@whitelisted
async def _on_inline_query_raw(self, message: Message):
await super()._on_new_message_raw(message)
await super()._on_inline_query_raw(message)
@whitelisted_event
async def _on_new_message_raw(self, message: Message):
await super()._on_new_message_raw(message)
@whitelisted
async def _on_new_message_raw(
self,
message: Message,
whitelist_callbacks: set[RegisteredCallback] | None = None,
blacklist_callbacks: set[RegisteredCallback] | None = None
):
await super()._on_new_message_raw(message, whitelist_callbacks, blacklist_callbacks)
async def _on_ready(self):
await super()._on_ready()

View File

@@ -0,0 +1,255 @@
__all__ = ['PenaltyBot']
import asyncio
import datetime
from abc import ABC
import flanautils
from flanautils import TimeUnits
from multibot import MultiBot, RegisteredCallback, User, admin, bot_mentioned, constants as multibot_constants, group, ignore_self_message
from flanabot import constants
from flanabot.models import Chat, Message, Punishment
# ----------------------------------------------------------------------------------------------------- #
# -------------------------------------------- PENALTY_BOT -------------------------------------------- #
# ----------------------------------------------------------------------------------------------------- #
class PenaltyBot(MultiBot, ABC):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.lock = asyncio.Lock()
# -------------------------------------------------------- #
# ------------------- PROTECTED METHODS ------------------ #
# -------------------------------------------------------- #
def _add_handlers(self):
super()._add_handlers()
self.register(self._on_ban, keywords=multibot_constants.KEYWORDS['ban'])
self.register(self._on_mute, keywords=multibot_constants.KEYWORDS['mute'])
self.register(self._on_mute, keywords=(('haz', 'se'), multibot_constants.KEYWORDS['mute']))
self.register(self._on_mute, keywords=(multibot_constants.KEYWORDS['deactivate'], multibot_constants.KEYWORDS['unmute']))
self.register(self._on_mute, keywords=(multibot_constants.KEYWORDS['deactivate'], multibot_constants.KEYWORDS['sound']))
self.register(self._on_punish, keywords=constants.KEYWORDS['punish'])
self.register(self._on_punish, keywords=(multibot_constants.KEYWORDS['deactivate'], constants.KEYWORDS['unpunish']))
self.register(self._on_punish, keywords=(multibot_constants.KEYWORDS['deactivate'], multibot_constants.KEYWORDS['permission']))
self.register(self._on_unban, keywords=multibot_constants.KEYWORDS['unban'])
self.register(self._on_unmute, keywords=multibot_constants.KEYWORDS['unmute'])
self.register(self._on_unmute, keywords=(multibot_constants.KEYWORDS['deactivate'], multibot_constants.KEYWORDS['mute']))
self.register(self._on_unmute, keywords=(multibot_constants.KEYWORDS['activate'], multibot_constants.KEYWORDS['sound']))
self.register(self._on_unpunish, keywords=constants.KEYWORDS['unpunish'])
self.register(self._on_unpunish, keywords=(multibot_constants.KEYWORDS['deactivate'], constants.KEYWORDS['punish']))
self.register(self._on_unpunish, keywords=(multibot_constants.KEYWORDS['activate'], multibot_constants.KEYWORDS['permission']))
@admin(False)
@group
async def _check_message_flood(self, message: Message):
if await self.is_punished(message.author, message.chat):
return
last_2s_messages = self.Message.find({
'platform': self.platform.value,
'author': message.author.object_id,
'chat': message.chat.object_id,
'date': {'$gte': datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(seconds=2)}
})
last_7s_messages = self.Message.find({
'platform': self.platform.value,
'author': message.author.object_id,
'chat': message.chat.object_id,
'date': {'$gte': datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(seconds=7)}
})
if len(last_2s_messages) > constants.FLOOD_2s_LIMIT or len(last_7s_messages) > constants.FLOOD_7s_LIMIT:
punishment = Punishment.find_one({
'platform': self.platform.value,
'user_id': message.author.id,
'group_id': message.chat.group_id
})
punishment_seconds = (getattr(punishment, 'level', 0) + 2) ** constants.PUNISHMENT_INCREMENT_EXPONENT
await self.punish(message.author.id, message.chat.group_id, punishment_seconds, message, flood=True)
await self.send(f'Castigado durante {TimeUnits(seconds=punishment_seconds).to_words()}.', message)
@admin(False)
@group
async def _check_message_spam(self, message: Message) -> bool:
if await self.is_punished(message.author, message.chat):
return True
spam_messages = self.Message.find({
'text': message.text,
'platform': self.platform.value,
'author': message.author.object_id,
'date': {'$gte': datetime.datetime.now(datetime.timezone.utc) - constants.SPAM_TIME_RANGE},
})
chats = {message.chat for message in spam_messages}
if len(chats) <= constants.SPAM_CHANNELS_LIMIT:
return False
await self.punish(message.author.id, message.chat.group_id)
await asyncio.sleep(constants.SPAM_DELETION_DELAY.total_seconds()) # We make sure to also delete any messages they may have sent before the punishment
spam_messages = self.Message.find({
'text': message.text,
'platform': self.platform.value,
'author': message.author.object_id,
'date': {
'$gte': datetime.datetime.now(datetime.timezone.utc)
-
constants.SPAM_TIME_RANGE
-
constants.SPAM_DELETION_DELAY
},
'is_deleted': False
})
chats = {message.chat for message in spam_messages}
for message in spam_messages:
await self.delete_message(await self.get_message(message.id, message.chat.id))
groups_data = {chat.group_id: chat.group_name for chat in chats}
owner_message_parts = (
'<b>Spammer castigado:</b>',
'<b>User:</b>',
f' <b>id:</b> <code>{message.author.id}</code>',
f' <b>name:</b> <code>{message.author.name}</code>',
f' <b>is_admin:<b> <code>{message.author.is_admin}</code>',
f' <b>is_bot:</b> <code>{message.author.is_bot}</code>',
'',
f'<b>Chats: {len(chats)}</b>',
'',
'<b>Groups:</b>',
'\n\n'.join(
f' <b>group_id:</b> <code>{group_id}</code>\n'
f' <b>group_name:</b> <code>{group_name}</code>'
for group_id, group_name in groups_data.items()
)
)
await self.send('\n'.join(owner_message_parts), await self.owner_chat)
return True
async def _punish(self, user: int | str | User, group_: int | str | Chat | Message, message: Message = None):
pass
async def _unpunish(self, user: int | str | User, group_: int | str | Chat | Message, message: Message = None):
pass
# ---------------------------------------------- #
# HANDLERS #
# ---------------------------------------------- #
@bot_mentioned
@group
@admin(send_negative=True)
async def _on_ban(self, message: Message):
for user in await self._find_users_to_punish(message):
await self.ban(user, message, flanautils.text_to_time(await self.filter_mention_ids(message.text, message)), message)
@group
@bot_mentioned
@admin(send_negative=True)
async def _on_mute(self, message: Message):
for user in await self._find_users_to_punish(message):
await self.mute(user, message, flanautils.text_to_time(await self.filter_mention_ids(message.text, message)), message)
@ignore_self_message
async def _on_new_message_raw(
self,
message: Message,
whitelist_callbacks: set[RegisteredCallback] | None = None,
blacklist_callbacks: set[RegisteredCallback] | None = None
):
await super()._on_new_message_raw(message, whitelist_callbacks, blacklist_callbacks)
if message.chat.config['check_flood'] and message.chat.config['punish'] and not message.is_inline:
async with self.lock:
if not await self._check_message_spam(message):
await self._check_message_flood(message)
@bot_mentioned
@group
@admin(send_negative=True)
async def _on_punish(self, message: Message):
if not message.chat.config['punish']:
return
for user in await self._find_users_to_punish(message):
await self.punish(user, message, flanautils.text_to_time(await self.filter_mention_ids(message.text, message)), message)
async def _on_ready(self):
if not self._is_initialized:
flanautils.do_every(constants.CHECK_PUNISHMENTS_EVERY_SECONDS, self.check_old_punishments)
await super()._on_ready()
@bot_mentioned
@group
@admin(send_negative=True)
async def _on_unban(self, message: Message):
for user in await self._find_users_to_punish(message):
await self.unban(user, message, message)
@group
@bot_mentioned
@admin(send_negative=True)
async def _on_unmute(self, message: Message):
for user in await self._find_users_to_punish(message):
await self.unmute(user, message, message)
@group
@bot_mentioned
@admin(send_negative=True)
async def _on_unpunish(self, message: Message):
if not message.chat.config['punish']:
return
for user in await self._find_users_to_punish(message):
await self.unpunish(user, message, message)
# -------------------------------------------------------- #
# -------------------- PUBLIC METHODS -------------------- #
# -------------------------------------------------------- #
async def check_old_punishments(self):
punishments = Punishment.find({'platform': self.platform.value}, lazy=True)
for punishment in punishments:
now = datetime.datetime.now(datetime.timezone.utc)
if not punishment.until or now < punishment.until:
continue
await self._remove_penalty(punishment, self._unpunish)
if punishment.last_update + constants.PUNISHMENTS_RESET_TIME <= now:
punishment.level -= 1
punishment.delete()
async def is_punished(self, user: int | str | User, group_: int | str | Chat | Message) -> bool:
pass
async def punish(
self,
user: int | str | User,
group_: int | str | Chat | Message,
time: int | datetime.timedelta = None,
message: Message = None,
flood=False
):
# noinspection PyTypeChecker
punishment = Punishment(self.platform, self.get_user_id(user), self.get_group_id(group_), time)
punishment.pull_from_database(overwrite_fields=('level',), exclude_fields=('until',))
if flood:
punishment.level += 1
await self._punish(punishment.user_id, punishment.group_id)
punishment.save(pull_exclude_fields=('until',))
await self._unpenalize_later(punishment, self._unpunish, message)
async def unpunish(self, user: int | str | User, group_: int | str | Chat | Message, message: Message = None):
# noinspection PyTypeChecker
punishment = Punishment(self.platform, self.get_user_id(user), self.get_group_id(group_))
await self._remove_penalty(punishment, self._unpunish, message)

287
flanabot/bots/poll_bot.py Normal file
View File

@@ -0,0 +1,287 @@
__all__ = ['PollBot']
import math
import random
import re
from abc import ABC
from typing import Iterable
import flanautils
from flanautils import OrderedSet
from multibot import MultiBot, RegisteredCallback, constants as multibot_constants
from flanabot import constants
from flanabot.models import ButtonsGroup, Message
# ---------------------------------------------------------------------------------------------------- #
# --------------------------------------------- POLL_BOT --------------------------------------------- #
# ---------------------------------------------------------------------------------------------------- #
class PollBot(MultiBot, ABC):
# -------------------------------------------------------- #
# ------------------- PROTECTED METHODS ------------------ #
# -------------------------------------------------------- #
def _add_handlers(self):
super()._add_handlers()
self.register(self._on_choose, keywords=constants.KEYWORDS['choose'], priority=2)
self.register(self._on_choose, keywords=multibot_constants.KEYWORDS['random'], priority=2)
self.register(self._on_choose, keywords=(constants.KEYWORDS['choose'], multibot_constants.KEYWORDS['random']), priority=2)
self.register(self._on_delete_votes, extra_kwargs={'all_': True}, keywords=(multibot_constants.KEYWORDS['deactivate'], multibot_constants.KEYWORDS['all'], constants.KEYWORDS['vote']))
self.register(self._on_delete_votes, extra_kwargs={'all_': True}, keywords=(multibot_constants.KEYWORDS['delete'], multibot_constants.KEYWORDS['all'], constants.KEYWORDS['vote']))
self.register(self._on_delete_votes, keywords=(multibot_constants.KEYWORDS['deactivate'], constants.KEYWORDS['vote']))
self.register(self._on_delete_votes, keywords=(multibot_constants.KEYWORDS['delete'], constants.KEYWORDS['vote']))
self.register(self._on_dice, keywords=constants.KEYWORDS['dice'])
self.register(self._on_poll, keywords=constants.KEYWORDS['poll'], priority=2)
self.register(self._on_poll_multi, keywords=(constants.KEYWORDS['poll'], constants.KEYWORDS['multiple_answer']), priority=2)
self.register(self._on_stop_poll, keywords=multibot_constants.KEYWORDS['deactivate'])
self.register(self._on_stop_poll, keywords=(multibot_constants.KEYWORDS['deactivate'], constants.KEYWORDS['poll']))
self.register(self._on_stop_poll, keywords=multibot_constants.KEYWORDS['stop'])
self.register(self._on_stop_poll, keywords=(multibot_constants.KEYWORDS['stop'], constants.KEYWORDS['poll']))
self.register(self._on_voting_ban, keywords=(multibot_constants.KEYWORDS['deactivate'], multibot_constants.KEYWORDS['permission'], constants.KEYWORDS['vote']))
self.register(self._on_voting_unban, keywords=(multibot_constants.KEYWORDS['activate'], multibot_constants.KEYWORDS['permission'], constants.KEYWORDS['vote']))
self.register_button(self._on_poll_button_press, key=ButtonsGroup.POLL)
@staticmethod
def _get_options(text: str, discarded_words: Iterable = ()) -> list[str]:
options = (option for option in text.split() if not flanautils.cartesian_product_string_matching(option.lower(), discarded_words, multibot_constants.PARSER_MIN_SCORE_DEFAULT))
text = ' '.join(options)
conjunctions = [f' {conjunction} ' for conjunction in flanautils.CommonWords.get('conjunctions')]
if any(char in text for char in (',', ';', *conjunctions)):
conjunction_parts = [f'(?:[,;]*{conjunction}[,;]*)+' for conjunction in conjunctions]
options = re.split(f"{'|'.join(conjunction_parts)}|[,;]+", text)
return list(OrderedSet(stripped_option for option in options if (stripped_option := option.strip())))
else:
return list(OrderedSet(text.split()))
@staticmethod
def _get_poll_message(message: Message) -> Message | None:
if (poll_message := message.replied_message) and poll_message.buttons_info and poll_message.buttons_info.key == ButtonsGroup.POLL:
return poll_message
async def _update_poll_buttons(self, message: Message):
poll_data = message.data['poll']
if poll_data['is_multiple_answer']:
total_votes = len({option_vote[0] for option_votes in poll_data['votes'].values() if option_votes for option_vote in option_votes})
else:
total_votes = sum(len(option_votes) for option_votes in poll_data['votes'].values())
if total_votes:
buttons = []
for option, option_votes in poll_data['votes'].items():
ratio = f'{len(option_votes)}/{total_votes}'
names = f"({', '.join(option_vote[1] for option_vote in option_votes)})" if option_votes else ''
buttons.append(f"{option}{ratio}{f' {names}' if names else ''}")
else:
buttons = list(poll_data['votes'].keys())
await self.edit(self.distribute_buttons(buttons, vertically=True), message)
# ---------------------------------------------- #
# HANDLERS #
# ---------------------------------------------- #
async def _on_choose(self, message: Message):
if message.chat.is_group and not self.is_bot_mentioned(message):
return
discarded_words = {
*constants.KEYWORDS['choose'],
*multibot_constants.KEYWORDS['random'],
self.name.lower(), f'<@{self.id}>',
'entre', 'between'
}
if options := self._get_options(message.text, discarded_words):
for i in range(1, len(options) - 1):
try:
n1 = flanautils.cast_number(options[i - 1])
except ValueError:
try:
n1 = flanautils.text_to_number(options[i - 1], ignore_no_numbers=False)
except KeyError:
continue
try:
n2 = flanautils.cast_number(options[i + 1])
except ValueError:
try:
n2 = flanautils.text_to_number(options[i + 1], ignore_no_numbers=False)
except KeyError:
continue
if options[i] in ('al', 'to'):
await self.send(random.randint(math.ceil(n1), math.floor(n2)), message)
return
await self.send(random.choice(options), message)
else:
await self.send(random.choice(('¿Que elija el qué?', '¿Y las opciones?', '?', '🤔')), message)
async def _on_delete_votes(self, message: Message, all_=False):
if not (poll_message := self._get_poll_message(message)):
return
if message.chat.is_group and not message.author.is_admin:
await self.send_negative(message)
return
poll_data = poll_message.data['poll']
if all_:
for option_votes in poll_data['votes'].values():
option_votes.clear()
else:
user_ids = [user.id for user in await self._find_users_to_punish(message)]
for option_votes in poll_data['votes'].values():
option_votes[:] = [option_vote for option_vote in option_votes if option_vote[0] not in user_ids]
await self.delete_message(message)
await self._update_poll_buttons(poll_message)
async def _on_dice(self, message: Message):
if message.chat.is_group and not self.is_bot_mentioned(message):
return
if top_number := flanautils.text_to_number(message.text):
await self.send(random.randint(1, math.floor(top_number)), message)
else:
await self.send(random.choice(('¿De cuántas caras?', '¿Y el número?', '?', '🤔')), message)
async def _on_poll(self, message: Message, is_multiple_answer=False):
if (
self._get_poll_message(message)
and
self._parse_callbacks(message.text, [RegisteredCallback(..., keywords=multibot_constants.KEYWORDS['reset'])])
):
await self._on_delete_votes(message, all_=True)
return
if message.chat.is_group and not self.is_bot_mentioned(message):
return
discarded_words = {*constants.KEYWORDS['poll'], *constants.KEYWORDS['vote'], *constants.KEYWORDS['multiple_answer'], self.name.lower(), f'<@{self.id}>'}
if final_options := [f'{option[0].upper()}{option[1:]}' for option in self._get_options(message.text, discarded_words)]:
buttons = self.distribute_buttons(final_options, vertically=True)
await self.send(
f"Encuesta {'multirespuesta ' if is_multiple_answer else ''}en curso...",
buttons,
message,
buttons_key=ButtonsGroup.POLL,
data={
'poll': {
'is_active': True,
'is_multiple_answer': is_multiple_answer,
'votes': {option: [] for option in (flanautils.flatten(buttons, lazy=True))},
'banned_users_tries': {}
}
}
)
else:
await self.send(random.choice(('¿Y las opciones?', '?', '🤔')), message)
await self.delete_message(message)
async def _on_poll_button_press(self, message: Message):
await self.accept_button_event(message)
poll_data = message.data['poll']
if not poll_data['is_active'] or not message.buttons_info.pressed_button or not message.buttons_info.presser_user:
return
presser_id = message.buttons_info.presser_user.id
presser_name = message.buttons_info.presser_user.name.split('#')[0]
if (presser_id_str := str(presser_id)) in poll_data['banned_users_tries']:
poll_data['banned_users_tries'][presser_id_str] += 1
if poll_data['banned_users_tries'][presser_id_str] == 3:
await self.send(
random.choice(constants.BANNED_POLL_PHRASES.format(presser_name=presser_name)),
reply_to=message
)
return
option_name = results[0] if (results := re.findall('(.*?) ➜.+', message.buttons_info.pressed_text)) else message.buttons_info.pressed_text
selected_option_votes = poll_data['votes'][option_name]
if [presser_id, presser_name] in selected_option_votes:
selected_option_votes.remove([presser_id, presser_name])
else:
if not poll_data['is_multiple_answer']:
for option_votes in poll_data['votes'].values():
try:
option_votes.remove([presser_id, presser_name])
except ValueError:
pass
else:
break
selected_option_votes.append([presser_id, presser_name])
await self._update_poll_buttons(message)
async def _on_poll_multi(self, message: Message):
await self._on_poll(message, is_multiple_answer=True)
async def _on_stop_poll(self, message: Message):
if not (poll_message := self._get_poll_message(message)):
return
winners = []
max_votes = 1
for option, votes in poll_message.data['poll']['votes'].items():
if len(votes) > max_votes:
winners = [option]
max_votes = len(votes)
elif len(votes) == max_votes:
winners.append(option)
match winners:
case [_, _, *_]:
winners = [f'<b>{winner}</b>' for winner in winners]
text = f"Encuesta finalizada. Los ganadores son: {flanautils.join_last_separator(winners, ', ', ' y ')}."
case [winner]:
text = f'Encuesta finalizada. Ganador: <b>{winner}</b>.'
case _:
text = 'Encuesta finalizada.'
poll_message.data['poll']['is_active'] = False
await self.edit(text, poll_message)
async def _on_voting_ban(self, message: Message):
if not (poll_message := self._get_poll_message(message)):
return
if message.chat.is_group and not message.author.is_admin:
await self.send_negative(message)
return
await self.delete_message(message)
for user in await self._find_users_to_punish(message):
if str(user.id) not in poll_message.data['poll']['banned_users_tries']:
poll_message.data['poll']['banned_users_tries'][str(user.id)] = 0
async def _on_voting_unban(self, message: Message):
if not (poll_message := self._get_poll_message(message)):
return
if message.chat.is_group and not message.author.is_admin:
await self.send_negative(message)
return
await self.delete_message(message)
for user in await self._find_users_to_punish(message):
try:
del poll_message.data['poll']['banned_users_tries'][str(user.id)]
except KeyError:
pass
# -------------------------------------------------------- #
# -------------------- PUBLIC METHODS -------------------- #
# -------------------------------------------------------- #

View File

@@ -0,0 +1,417 @@
__all__ = ['ScraperBot']
import asyncio
import random
from abc import ABC
from collections import defaultdict
from typing import Iterable
import flanautils
from flanaapis import RedditMediaNotFoundError, reddit, tiktok, yt_dlp_wrapper
from flanautils import Media, MediaType, OrderedSet, return_if_first_empty
from multibot import MultiBot, RegisteredCallback, SendError, constants as multibot_constants, reply
from flanabot import constants
from flanabot.models import Action, BotAction, Message
# ----------------------------------------------------------------------------------------------------- #
# -------------------------------------------- SCRAPER_BOT -------------------------------------------- #
# ----------------------------------------------------------------------------------------------------- #
class ScraperBot(MultiBot, ABC):
# -------------------------------------------------------- #
# ------------------- PROTECTED METHODS ------------------ #
# -------------------------------------------------------- #
def _add_handlers(self):
super()._add_handlers()
self.register(self._on_no_scraping, keywords=(multibot_constants.KEYWORDS['negate'], constants.KEYWORDS['scraping']))
self.register(self._on_scraping, keywords=constants.KEYWORDS['scraping'])
self.register(self._on_scraping, keywords=constants.KEYWORDS['force'])
self.register(self._on_scraping, keywords=multibot_constants.KEYWORDS['audio'])
self.register(self._on_scraping, extra_kwargs={'delete': False}, keywords=(multibot_constants.KEYWORDS['negate'], multibot_constants.KEYWORDS['delete']))
self.register(self._on_scraping, extra_kwargs={'delete': False}, keywords=(multibot_constants.KEYWORDS['negate'], multibot_constants.KEYWORDS['message']))
self.register(self._on_scraping, extra_kwargs={'delete': False}, keywords=(multibot_constants.KEYWORDS['negate'], multibot_constants.KEYWORDS['delete'], multibot_constants.KEYWORDS['message']))
self.register(self._on_song_info, keywords=constants.KEYWORDS['song_info'])
@staticmethod
async def _find_ids(text: str) -> tuple[OrderedSet[str], ...]:
return (
reddit.find_ids(text),
await tiktok.find_users_and_ids(text),
tiktok.find_download_urls(text)
)
@staticmethod
def _get_keywords(delete=True, force=False, full=False, audio_only=False) -> list[str]:
keywords = list(constants.KEYWORDS['scraping'])
if not delete:
keywords += [
*multibot_constants.KEYWORDS['negate'],
*multibot_constants.KEYWORDS['deactivate'],
*multibot_constants.KEYWORDS['delete'],
*multibot_constants.KEYWORDS['message']
]
if force:
keywords += constants.KEYWORDS['force']
if full:
keywords += ['sin', 'timeout', 'limite', *multibot_constants.KEYWORDS['all']]
if audio_only:
keywords += multibot_constants.KEYWORDS['audio']
return keywords
@staticmethod
def _medias_sended_info(medias: Iterable[Media]) -> str:
medias_count: dict = defaultdict(lambda: defaultdict(int))
for media in medias:
if not media.source or isinstance(media.source, str):
medias_count[media.source][media.type_] += 1
else:
medias_count[media.source.name][media.type_] += 1
medias_sended_info = []
for source, media_type_count in medias_count.items():
source_medias_sended_info = []
for media_type, count in media_type_count.items():
if count:
if count == 1:
type_text = {MediaType.IMAGE: 'imagen',
MediaType.AUDIO: 'audio',
MediaType.GIF: 'gif',
MediaType.VIDEO: 'vídeo',
None: 'cosa que no sé que tipo de archivo es',
MediaType.ERROR: 'error'}[media_type]
else:
type_text = {MediaType.IMAGE: 'imágenes',
MediaType.AUDIO: 'audios',
MediaType.GIF: 'gifs',
MediaType.VIDEO: 'vídeos',
None: 'cosas que no sé que tipos de archivos son',
MediaType.ERROR: 'errores'}[media_type]
source_medias_sended_info.append(f'{count} {type_text}')
if source_medias_sended_info:
medias_sended_info.append(f"{flanautils.join_last_separator(source_medias_sended_info, ', ', ' y ')} de {source if source else 'algún sitio'}")
medias_sended_info_joined = flanautils.join_last_separator(medias_sended_info, ',\n', ' y\n')
new_line = ' ' if len(medias_sended_info) == 1 else '\n'
return f'{new_line}{medias_sended_info_joined}:'
async def _scrape_and_send(
self,
message: Message,
force=False,
full=False,
audio_only=False,
send_user_context=True,
keywords: list[str] = None,
sended_media_messages: OrderedSet[Message] = None
) -> OrderedSet[Message]:
if not keywords:
keywords = []
if sended_media_messages is None:
sended_media_messages = OrderedSet()
kwargs = {'timeout_for_media': None} if full else {}
if not (medias := await self._search_medias(message, force, audio_only, **kwargs)):
return OrderedSet()
new_sended_media_messages, _ = await self.send_medias(medias, message, send_user_context=send_user_context, keywords=keywords)
sended_media_messages |= new_sended_media_messages
await self.send_inline_results(message)
return sended_media_messages
async def _scrape_send_and_delete(
self,
message: Message,
force=False,
full=False,
audio_only=False,
send_user_context=True,
keywords: list[str] = None,
sended_media_messages: OrderedSet[Message] = None
) -> OrderedSet[Message]:
if not keywords:
keywords = []
if sended_media_messages is None:
sended_media_messages = OrderedSet()
sended_media_messages += await self._scrape_and_send(message, force, full, audio_only, send_user_context, keywords)
if sended_media_messages and message.chat.config['scraping_delete_original']:
# noinspection PyTypeChecker
BotAction(self.platform.value, Action.MESSAGE_DELETED, message, affected_objects=[message, *sended_media_messages]).save()
await self.delete_message(message)
return sended_media_messages
async def _search_medias(
self,
message: Message,
force=False,
audio_only=False,
timeout_for_media: int | float = None
) -> OrderedSet[Media]:
medias = OrderedSet()
exceptions: list[Exception] = []
if audio_only:
preferred_video_codec = None
preferred_extension = None
else:
preferred_video_codec = 'h264'
preferred_extension = 'mp4'
ids = []
media_urls = []
for text_part in message.text.split():
for i, platform_ids in enumerate(await self._find_ids(text_part)):
try:
ids[i] |= platform_ids
except IndexError:
ids.append(platform_ids)
if (
not any(ids)
and
flanautils.find_urls(text_part)
and
(
force
or
not any(domain.lower() in text_part for domain in multibot_constants.GIF_DOMAINS)
)
):
media_urls.append(text_part)
if not any(ids) and not media_urls:
return medias
bot_state_message = await self.send(random.choice(constants.SCRAPING_PHRASES), message)
reddit_ids, tiktok_users_and_ids, tiktok_download_urls = ids
try:
reddit_medias = await reddit.get_medias(reddit_ids, preferred_video_codec, preferred_extension, force, audio_only, timeout_for_media)
except RedditMediaNotFoundError as e:
exceptions.append(e)
reddit_medias = ()
reddit_urls = []
for reddit_media in reddit_medias:
if reddit_media.source:
medias.add(reddit_media)
else:
reddit_urls.append(reddit_media.url)
if force:
media_urls.extend(reddit_urls)
else:
for reddit_url in reddit_urls:
for domain in multibot_constants.GIF_DOMAINS:
if domain.lower() in reddit_url:
medias.add(Media(reddit_url, MediaType.GIF, source=domain))
break
else:
media_urls.append(reddit_url)
gather_results = await asyncio.gather(
tiktok.get_medias(tiktok_users_and_ids, tiktok_download_urls, preferred_video_codec, preferred_extension, force, audio_only, timeout_for_media),
yt_dlp_wrapper.get_medias(media_urls, preferred_video_codec, preferred_extension, force, audio_only, timeout_for_media),
return_exceptions=True
)
await self.delete_message(bot_state_message)
gather_medias, gather_exceptions = flanautils.filter_exceptions(gather_results)
await self._manage_exceptions(exceptions + gather_exceptions, message, print_traceback=True)
return medias | gather_medias
async def _send_media(self, media: Media, bot_state_message: Message, message: Message) -> Message | None:
return await self.send(media, message, reply_to=message.replied_message)
# ---------------------------------------------- #
# HANDLERS #
# ---------------------------------------------- #
async def _on_no_scraping(self, message: Message):
pass
async def _on_recover_message(self, message: Message):
pass
async def _on_scraping(
self,
message: Message,
delete=True,
force: bool = None,
full: bool = None,
audio_only: bool = None,
scrape_replied=True,
) -> OrderedSet[Message]:
sended_media_messages = OrderedSet()
if not message.chat.config['auto_scraping'] and not self.is_bot_mentioned(message):
return sended_media_messages
if force is None:
force = bool(
flanautils.cartesian_product_string_matching(
message.text,
constants.KEYWORDS['force'],
multibot_constants.PARSER_MIN_SCORE_DEFAULT
)
)
if full is None:
full = bool(
self._parse_callbacks(
message.text,
[
RegisteredCallback(..., keywords=(('sin',), ('timeout', 'limite'))),
RegisteredCallback(..., keywords=multibot_constants.KEYWORDS['all'])
]
)
)
if audio_only is None:
audio_only = bool(
flanautils.cartesian_product_string_matching(
message.text,
multibot_constants.KEYWORDS['audio'],
multibot_constants.PARSER_MIN_SCORE_DEFAULT
)
)
keywords = self._get_keywords(delete, force, full, audio_only)
if scrape_replied and message.replied_message:
sended_media_messages += await self._scrape_and_send(
message.replied_message,
force,
full,
audio_only,
send_user_context=False
)
kwargs = {
'message': message,
'force': force,
'full': full,
'audio_only': audio_only,
'keywords': keywords,
'sended_media_messages': sended_media_messages
}
if delete:
sended_media_messages |= await self._scrape_send_and_delete(**kwargs)
else:
sended_media_messages |= await self._scrape_and_send(**kwargs)
if not sended_media_messages:
await self._on_recover_message(message)
return sended_media_messages
@reply
async def _on_song_info(self, message: Message):
song_infos = message.replied_message.song_infos if message.replied_message else []
if song_infos:
for song_info in song_infos:
await self.send_song_info(song_info, message)
elif message.chat.is_private or self.is_bot_mentioned(message):
raise SendError('No hay información musical en ese mensaje.')
# -------------------------------------------------------- #
# -------------------- PUBLIC METHODS -------------------- #
# -------------------------------------------------------- #
@return_if_first_empty(([], 0), exclude_self_types='ScraperBot', globals_=globals())
async def send_medias(
self,
medias: OrderedSet[Media],
message: Message,
send_song_info=False,
send_user_context=True,
keywords: list[str] = None
) -> tuple[list[Message], int]:
if not keywords:
keywords = []
sended_media_messages = []
fails = 0
bot_state_message: Message | None = None
sended_info_message: Message | None = None
user_text_bot_message: Message | None = None
if not message.is_inline:
bot_state_message: Message = await self.send('Enviando...', message)
if message.chat.is_group:
sended_info_message = await self.send(f"{message.author.name.split('#')[0]} compartió{self._medias_sended_info(medias)}", message, reply_to=message.replied_message)
if (
send_user_context
and
(user_text := ' '.join(
[word for word in message.text.split()
if (
not any(await self._find_ids(word))
and
not flanautils.find_urls(word)
and
not flanautils.cartesian_product_string_matching(word, keywords, multibot_constants.PARSER_MIN_SCORE_DEFAULT)
and
flanautils.remove_symbols(word).lower() not in (str(self.id), self.name.lower())
)]
))
):
user_text_bot_message = await self.send(user_text, message, reply_to=message.replied_message)
for media in medias:
if not media.content:
fails += 1
continue
if media.song_info:
message.song_infos.add(media.song_info)
message.save()
if bot_message := await self._send_media(media, bot_state_message, message):
sended_media_messages.append(bot_message)
if media.song_info and bot_message:
bot_message.song_infos.add(media.song_info)
bot_message.save()
else:
fails += 1
if send_song_info and media.song_info:
await self.send_song_info(media.song_info, message)
if fails == len(medias):
if sended_info_message:
await self.delete_message(sended_info_message)
if user_text_bot_message:
await self.delete_message(user_text_bot_message)
if bot_state_message:
await self.delete_message(bot_state_message)
return sended_media_messages, fails
@return_if_first_empty(exclude_self_types='ScraperBot', globals_=globals())
async def send_song_info(self, song_info: Media, message: Message):
attributes = (
f'<b>Título:</b> {song_info.title}\n' if song_info.title else '',
f'<b>Autor:</b> {song_info.author}\n' if song_info.author else '',
f'<b>Álbum:</b> {song_info.album}\n' if song_info.album else '',
f'<b>Previa:</b>'
)
await self.send(''.join(attributes), message)
if song_info:
await self.send(song_info, message)

368
flanabot/bots/steam_bot.py Normal file
View File

@@ -0,0 +1,368 @@
__all__ = ['SteamBot']
import asyncio
import base64
import os
import random
import re
import urllib.parse
from abc import ABC
from collections import defaultdict
from collections.abc import Awaitable, Callable, Iterable
from contextlib import asynccontextmanager
from typing import Any, AsyncIterator
import aiohttp
import flanautils
import playwright.async_api
import plotly
from flanautils import Media, MediaType, Source
from multibot import LimitError, MultiBot, RegisteredCallback, constants as multibot_constants
from flanabot import constants
from flanabot.models import Message, SteamRegion
# ----------------------------------------------------------------------------------------------------- #
# --------------------------------------------- STEAM_BOT --------------------------------------------- #
# ----------------------------------------------------------------------------------------------------- #
class SteamBot(MultiBot, ABC):
# -------------------------------------------------------- #
# ------------------- PROTECTED METHODS ------------------ #
# -------------------------------------------------------- #
def _add_handlers(self):
super()._add_handlers()
self.register(self._on_steam, keywords='steam', priority=3)
async def _add_app_price(
self,
app_prices: dict[str, dict[str, float]],
steam_region: SteamRegion,
app_ids: Iterable[str],
session: aiohttp.ClientSession
) -> None:
for ids_batch_batch in flanautils.chunks(
flanautils.chunks(list(app_ids), constants.STEAM_IDS_BATCH),
constants.STEAM_MAX_CONCURRENT_REQUESTS
):
gather_results = await asyncio.gather(
*(self._get_app_data(session, ids_batch, steam_region.code) for ids_batch in ids_batch_batch)
)
for gather_result in gather_results:
for app_id, app_data in gather_result.items():
if (
(data := app_data.get('data'))
and
(price := data.get('price_overview', {}).get('final')) is not None
):
app_prices[app_id][steam_region.code] = price / 100 / steam_region.eur_conversion_rate
@staticmethod
@asynccontextmanager
async def _create_browser_context(
browser: playwright.async_api.Browser
) -> AsyncIterator[playwright.async_api.BrowserContext]:
async with await browser.new_context(
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.5615.49 Safari/537.36',
screen={
'width': 1920,
'height': 1080
},
viewport={
'width': 1920,
'height': 945
},
device_scale_factor=1,
is_mobile=False,
has_touch=False,
default_browser_type='chromium',
locale='es-ES'
) as context:
yield context
@staticmethod
async def _get_app_data(
session: aiohttp.ClientSession,
ids_batch: Iterable[str],
country_code: str
) -> dict[str, Any]:
async with session.get(
constants.STEAM_APP_ENDPOINT_TEMPLATE.format(ids=','.join(ids_batch), country_code=country_code)
) as response:
if response.status != 200:
raise LimitError('🚫 Steam ban: espera 5 minutos antes de consultar de nuevo.')
return await response.json()
async def _get_most_apps_ids(self, browser: playwright.async_api.Browser) -> set[str]:
app_ids = set()
re_pattern = fr'{urllib.parse.urlparse(constants.STEAM_MOST_URLS[0]).netloc}/\w+/(\d+)'
async with self._create_browser_context(browser) as context:
context.set_default_timeout(10000)
page = await context.new_page()
for url in constants.STEAM_MOST_URLS:
await page.goto(url)
try:
await page.wait_for_selector('tr td a[href]')
except playwright.async_api.TimeoutError:
raise LimitError('🚫 Steam ban: espera 5 minutos antes de consultar de nuevo.')
locator = page.locator('tr td a[href]')
for i in range(await locator.count()):
href = await locator.nth(i).get_attribute('href')
app_ids.add(re.search(re_pattern, href).group(1))
return app_ids
@staticmethod
async def _insert_conversion_rates(
session: aiohttp.ClientSession,
steam_regions: list[SteamRegion]
) -> list[SteamRegion]:
async with session.get(
constants.STEAM_EXCHANGERATE_API_ENDPOINT.format(api_key=os.environ['EXCHANGERATE_API_KEY'])
) as response:
exchange_data = await response.json()
for steam_region in steam_regions:
alpha_3_code = constants.STEAM_REGION_CODE_MAPPING[steam_region.code]
steam_region.eur_conversion_rate = exchange_data['conversion_rates'][alpha_3_code]
return steam_regions
async def _scrape_steam_data(
self,
update_state: Callable[[str], Awaitable[Message]],
update_steam_regions=False,
most_apps=True
) -> tuple[list[SteamRegion], set[str]]:
steam_regions = SteamRegion.find()
most_apps_ids = set()
if update_steam_regions or most_apps:
async with playwright.async_api.async_playwright() as playwright_:
async with await playwright_.chromium.launch() as browser:
if update_steam_regions:
await update_state('Actualizando las regiones de Steam...')
SteamRegion.delete_many_raw({})
steam_regions = await self._update_steam_regions(browser)
if most_apps:
bot_state_message = await update_state(
'Obteniendo los productos más vendidos y jugados de Steam...'
)
try:
most_apps_ids = await self._get_most_apps_ids(browser)
except LimitError:
await self.delete_message(bot_state_message)
raise
return steam_regions, most_apps_ids
else:
return steam_regions, most_apps_ids
async def _update_steam_regions(self, browser: playwright.async_api.Browser) -> list[SteamRegion]:
steam_regions = []
for app_id in constants.STEAM_APP_IDS_FOR_SCRAPE_COUNTRIES:
async with self._create_browser_context(browser) as context:
page = await context.new_page()
await page.goto(f'{constants.STEAM_DB_URL}/app/{app_id}/')
locator = page.locator("#prices td[class='price-line']")
for i in range(await locator.count()):
td = locator.nth(i)
src = (await td.locator('img').get_attribute('src'))
name = (await td.text_content()).strip()
flag_url = f'{constants.STEAM_DB_URL}{src}'
region_code = await td.get_attribute('data-cc')
steam_region = SteamRegion(region_code, name, flag_url)
steam_region.save()
steam_regions.append(steam_region)
await asyncio.sleep(2)
return steam_regions
# ---------------------------------------------- #
# HANDLERS #
# ---------------------------------------------- #
async def _on_steam(
self,
message: Message,
update_steam_regions: bool | None = None,
most_apps: bool | None = None,
last_apps: bool | None = None,
random_apps: bool | None = None
):
if message.chat.is_group and not self.is_bot_mentioned(message):
return
if update_steam_regions is None:
update_steam_regions = bool(
self._parse_callbacks(
message.text,
[
RegisteredCallback(
...,
keywords=(multibot_constants.KEYWORDS['update'], constants.KEYWORDS['region'])
)
]
)
)
if most_apps is None:
most_apps = bool(
flanautils.cartesian_product_string_matching(
message.text,
('jugados', 'played', 'sellers', 'selling', 'vendidos'),
multibot_constants.PARSER_MIN_SCORE_DEFAULT
)
)
if last_apps is None:
last_apps = bool(
flanautils.cartesian_product_string_matching(
message.text,
multibot_constants.KEYWORDS['last'] + ('new', 'novedades', 'nuevos'),
multibot_constants.PARSER_MIN_SCORE_DEFAULT
)
)
if random_apps is None:
random_apps = bool(
flanautils.cartesian_product_string_matching(
message.text,
multibot_constants.KEYWORDS['random'],
multibot_constants.PARSER_MIN_SCORE_DEFAULT
)
)
if not any((most_apps, last_apps, random_apps)):
most_apps = True
last_apps = True
random_apps = True
update_state = self.create_message_updater(message, delete_user_message=True)
chart_title_parts = []
steam_regions, selected_app_ids = await self._scrape_steam_data(update_state, update_steam_regions, most_apps)
if most_apps:
chart_title_parts.append(f'los {len(selected_app_ids)} más vendidos/jugados')
async with aiohttp.ClientSession() as session:
if last_apps or random_apps:
await update_state('Obteniendo todos los productos de Steam...')
async with session.get(constants.STEAM_ALL_APPS_ENDPOINT) as response:
all_apps_data = await response.json()
app_ids = [
str(app_id) for app_data in all_apps_data['applist']['apps'] if (app_id := app_data['appid'])
]
if last_apps:
selected_app_ids.update(app_ids[-constants.STEAM_LAST_APPS:])
chart_title_parts.append(f'los {constants.STEAM_LAST_APPS} más nuevos')
if random_apps:
selected_app_ids.update(random.sample(app_ids, constants.STEAM_RANDOM_APPS))
chart_title_parts.append(f'{constants.STEAM_RANDOM_APPS} aleatorios')
steam_regions = await self._insert_conversion_rates(session, steam_regions)
bot_state_message = await update_state('Obteniendo los precios para todas las regiones...')
apps_prices = defaultdict(dict)
try:
await asyncio.gather(
*(
self._add_app_price(apps_prices, steam_region, selected_app_ids, session)
for steam_region in steam_regions
)
)
except LimitError:
await self.delete_message(bot_state_message)
raise
apps_prices = {k: v for k, v in apps_prices.items() if len(v) == len(steam_regions)}
total_prices = defaultdict(float)
for app_prices in apps_prices.values():
for region_code, price in app_prices.items():
total_prices[region_code] += price
for steam_region in steam_regions:
steam_region.mean_price = total_prices[steam_region.code] / len(apps_prices)
await update_state('Creando gráfico...')
steam_regions = sorted(steam_regions, key=lambda steam_region: steam_region.mean_price)
region_names = []
region_total_prices = []
bar_colors = []
bar_line_colors = []
images = []
for i, steam_region in enumerate(steam_regions):
region_names.append(steam_region.name)
region_total_prices.append(steam_region.mean_price)
bar_colors.append(steam_region.bar_color)
bar_line_colors.append(steam_region.bar_line_color)
async with session.get(steam_region.flag_url) as response:
images.append({
'source': f'data:image/svg+xml;base64,{base64.b64encode(await response.read()).decode()}',
'xref': 'x',
'yref': 'paper',
'x': i,
'y': 0.04,
'sizex': 0.425,
'sizey': 0.0225,
'xanchor': 'center',
'opacity': 1,
'layer': 'above'
})
figure = plotly.graph_objs.Figure(
[
plotly.graph_objs.Bar(
x=region_names,
y=region_total_prices,
marker={'color': bar_colors},
)
]
)
figure.update_layout(
width=1920,
height=945,
margin={'t': 20, 'r': 20, 'b': 20, 'l': 20},
title={
'text': f"Media de {len(apps_prices)} productos de Steam<br><sup>(solo los comparables entre {flanautils.join_last_separator(chart_title_parts, ', ', ' y ')})</sup>",
'xref': 'paper',
'yref': 'paper',
'x': 0.5,
'y': 0.93,
'font': {
'size': 35
}
},
images=images,
xaxis={
'tickfont': {
'size': 14
}
},
yaxis={
'ticksuffix': '',
'tickfont': {
'size': 18
}
}
)
bot_state_message = await update_state('Enviando...')
await self.send(Media(figure.to_image(), MediaType.IMAGE, 'png', Source.LOCAL), message)
await self.delete_message(bot_state_message)
# -------------------------------------------------------- #
# -------------------- PUBLIC METHODS -------------------- #
# -------------------------------------------------------- #

View File

@@ -0,0 +1,195 @@
__all__ = ['UberEatsBot']
import asyncio
import datetime
import random
from abc import ABC
from collections import defaultdict
import flanautils
import playwright.async_api
from multibot import MultiBot, group
from flanabot import constants
from flanabot.models import Chat, Message
# ---------------------------------------------------------------------------------------------------- #
# --------------------------------------------- POLL_BOT --------------------------------------------- #
# ---------------------------------------------------------------------------------------------------- #
class UberEatsBot(MultiBot, ABC):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._task_contexts: dict[int, dict] = defaultdict(lambda: defaultdict(lambda: None))
# -------------------------------------------------------- #
# ------------------- PROTECTED METHODS ------------------ #
# -------------------------------------------------------- #
def _add_handlers(self):
super()._add_handlers()
self.register(self._on_ubereats, keywords='ubereats', priority=2)
async def _cancel_scraping_task(self, chat: Chat):
if not (task := self._task_contexts[chat.id]['task']) or task.done():
return
await self._close_playwright(chat)
task.cancel()
del self._task_contexts[chat.id]
async def _close_playwright(self, chat: Chat):
if browser := self._task_contexts[chat.id]['browser']:
await browser.close()
if playwright_ := self._task_contexts[chat.id]['playwright']:
await playwright_.stop()
async def _scrape_codes(self, chat: Chat) -> list[str | None]:
async def get_code() -> str:
return await page.input_value("input[class='code toCopy']")
codes: list[str | None] = [None] * len(chat.ubereats['cookies'])
async with playwright.async_api.async_playwright() as playwright_:
self._task_contexts[chat.id]['playwright'] = playwright_
for i, cookies in enumerate(chat.ubereats['cookies']):
for _ in range(3):
try:
async with await playwright_.chromium.launch() as browser:
self._task_contexts[chat.id]['browser'] = browser
context: playwright.async_api.BrowserContext = await browser.new_context(
storage_state={'cookies': cookies},
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.5615.49 Safari/537.36',
screen={
'width': 1920,
'height': 1080
},
viewport={
'width': 1280,
'height': 720
},
device_scale_factor=1,
is_mobile=False,
has_touch=False,
default_browser_type='chromium',
locale='es-ES'
)
context.set_default_timeout(3000)
page = await context.new_page()
await page.goto('https://www.myunidays.com/ES/es-ES/partners/ubereats/access/online', timeout=30000)
if button := await page.query_selector("button[class='button highlight']"):
await button.click()
else:
await page.click("'Revelar código'")
for _ in range(5):
if len(context.pages) == 2:
break
await asyncio.sleep(0.5)
else:
continue
page = context.pages[1]
await page.wait_for_load_state()
code = await get_code()
if not (new_code_button := await page.query_selector("button[class='getNewCode button secondary']")):
new_code_button = page.locator("'Obtener nuevo código'")
if await new_code_button.is_enabled():
await new_code_button.click()
for _ in range(5):
if (new_code := await get_code()) != code:
code = new_code
break
await asyncio.sleep(0.5)
codes[i] = code
chat.ubereats['cookies'][i] = await context.cookies('https://www.myunidays.com')
except playwright.async_api.Error:
await asyncio.sleep(1)
else:
break
return codes
# ---------------------------------------------- #
# HANDLERS #
# ---------------------------------------------- #
async def _on_ready(self):
await super()._on_ready()
for chat in self.Chat.find({
'platform': self.platform.value,
'config.ubereats': True,
'ubereats.cookies': {"$exists": True, "$ne": []}
}):
chat = await self.get_chat(chat.id)
chat.pull_from_database(overwrite_fields=('_id', 'config', 'ubereats'))
if (
chat.ubereats['next_execution']
and
(delta_time := chat.ubereats['next_execution'] - datetime.datetime.now(datetime.timezone.utc)) > datetime.timedelta()
):
flanautils.do_later(delta_time, self.start_ubereats, chat)
else:
await self.start_ubereats(chat)
@group(False)
async def _on_ubereats(self, message: Message):
if not message.chat.ubereats['cookies']:
return
time = flanautils.text_to_time(message.text)
if not time:
bot_state_message = await self.send(random.choice(constants.SCRAPING_PHRASES), message)
await self.send_ubereats_code(message.chat, update_next_execution=False)
await self.delete_message(bot_state_message)
return
if time < datetime.timedelta(days=1, minutes=5):
await self.send('El mínimo es 1 día y 5 minutos.', message)
return
seconds = int(time.total_seconds())
message.chat.ubereats['seconds'] = seconds
message.save()
period = flanautils.TimeUnits(seconds=seconds)
await self.send(f'A partir de ahora te enviaré un código de UberEats cada <b>{period.to_words()}</b>.', message)
await self.start_ubereats(message.chat)
# -------------------------------------------------------- #
# -------------------- PUBLIC METHODS -------------------- #
# -------------------------------------------------------- #
async def send_ubereats_code(self, chat: Chat, update_next_execution=True):
chat.pull_from_database(overwrite_fields=('ubereats',))
codes = await self._scrape_codes(chat)
for i, code in enumerate(codes):
if code:
if code in chat.ubereats['last_codes']:
warning_text = '<i>Código ya enviado anteriormente:</i> '
else:
warning_text = ''
await self.send(f'{warning_text}<code>{code}</code>', chat, silent=True)
else:
try:
codes[i] = chat.ubereats['last_codes'][i]
except IndexError:
codes[i] = None
chat.ubereats['last_codes'] = codes
if update_next_execution:
chat.ubereats['next_execution'] = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(seconds=chat.ubereats['seconds'])
chat.save()
async def start_ubereats(self, chat: Chat, send_code_now=True):
await self._cancel_scraping_task(chat)
chat.config['ubereats'] = True
chat.save(pull_overwrite_fields=('ubereats',))
self._task_contexts[chat.id]['task'] = flanautils.do_every(chat.ubereats['seconds'], self.send_ubereats_code, chat, do_first_now=send_code_now)
async def stop_ubereats(self, chat: Chat):
await self._cancel_scraping_task(chat)
chat.config['ubereats'] = False
chat.save(pull_overwrite_fields=('ubereats',))

View File

@@ -0,0 +1,193 @@
__all__ = ['WeatherBot']
import datetime
import random
from abc import ABC
import flanaapis.geolocation.functions
import flanaapis.weather.functions
import flanautils
import plotly.graph_objects
from flanaapis import Place, PlaceNotFoundError, WeatherEmoji
from flanautils import Media, MediaType, NotFoundError, OrderedSet, Source, TraceMetadata
from multibot import MultiBot, constants as multibot_constants
from flanabot import constants
from flanabot.models import Action, BotAction, ButtonsGroup, Message, WeatherChart
# ----------------------------------------------------------------------------------------------------- #
# -------------------------------------------- WEATHER_BOT -------------------------------------------- #
# ----------------------------------------------------------------------------------------------------- #
class WeatherBot(MultiBot, ABC):
# -------------------------------------------------------- #
# ------------------- PROTECTED METHODS ------------------ #
# -------------------------------------------------------- #
def _add_handlers(self):
super()._add_handlers()
self.register(self._on_weather, keywords=constants.KEYWORDS['weather'])
self.register(self._on_weather, keywords=(multibot_constants.KEYWORDS['show'], constants.KEYWORDS['weather']))
self.register_button(self._on_weather_button_press, key=ButtonsGroup.WEATHER)
# ---------------------------------------------- #
# HANDLERS #
# ---------------------------------------------- #
async def _on_weather(self, message: Message):
bot_state_message: Message | None = None
if message.is_inline:
show_progress_state = False
elif message.chat.is_group and not self.is_bot_mentioned(message):
if message.chat.config['auto_weather_chart']:
if BotAction.find_one({'platform': self.platform.value, 'action': Action.AUTO_WEATHER_CHART.value, 'chat': message.chat.object_id, 'date': {'$gt': datetime.datetime.now(datetime.timezone.utc) - constants.AUTO_WEATHER_EVERY}}):
return
show_progress_state = False
else:
return
else:
show_progress_state = True
original_text_words = flanautils.remove_accents(message.text.lower())
original_text_words = flanautils.remove_symbols(original_text_words, ignore=('-', '.'), replace_with=' ').split()
original_text_words = await self.filter_mention_ids(original_text_words, message, delete_names=True)
# noinspection PyTypeChecker
place_words = (
OrderedSet(original_text_words)
- flanautils.cartesian_product_string_matching(original_text_words, multibot_constants.KEYWORDS['show'], min_score=0.85).keys()
- flanautils.cartesian_product_string_matching(original_text_words, constants.KEYWORDS['weather'], min_score=0.85).keys()
- flanautils.cartesian_product_string_matching(original_text_words, multibot_constants.KEYWORDS['date'], min_score=0.85).keys()
- flanautils.cartesian_product_string_matching(original_text_words, multibot_constants.KEYWORDS['thanks'], min_score=0.85).keys()
- flanautils.CommonWords.get()
)
if not place_words:
if not message.is_inline:
await self.send_error(random.choice(('¿Tiempo dónde?', 'Indica el sitio.', 'Y el sitio?', 'y el sitio? me lo invento?')), message)
return
if 'calle' in original_text_words:
place_words.insert(0, 'calle')
place_query = ' '.join(place_words)
if len(place_query) >= constants.MAX_PLACE_QUERY_LENGTH:
if show_progress_state:
await self.send_error(Media(str(flanautils.resolve_path('resources/mucho_texto.png')), MediaType.IMAGE, 'jpg', Source.LOCAL), message, send_as_file=False)
return
if show_progress_state:
bot_state_message = await self.send(f'Buscando "{place_query}" en el mapa 🧐...', message)
result: str | Place | None = None
async for result in flanaapis.geolocation.functions.find_place_showing_progress(place_query):
if isinstance(result, str) and bot_state_message:
await self.edit(result, bot_state_message)
place: Place = result
if not place:
if bot_state_message:
await self.delete_message(bot_state_message)
raise PlaceNotFoundError(place_query)
if bot_state_message:
bot_state_message = await self.edit(f'Obteniendo datos del tiempo para "{place_query}"...', bot_state_message)
current_weather, day_weathers = await flanaapis.weather.functions.get_day_weathers_by_place(place)
if bot_state_message:
bot_state_message = await self.edit('Creando gráficas del tiempo...', bot_state_message)
weather_chart = WeatherChart(
_font={'size': 30},
_title={
'text': place.name[:40].strip(' ,-'),
'xref': 'paper',
'yref': 'paper',
'xanchor': 'left',
'yanchor': 'top',
'x': 0.025,
'y': 0.975,
'font': {
'size': 50,
'family': 'open sans'
}
},
_legend={'x': 0.99, 'y': 0.99, 'xanchor': 'right', 'yanchor': 'top', 'bgcolor': 'rgba(0,0,0,0)'},
_margin={'l': 20, 'r': 20, 't': 20, 'b': 20},
trace_metadatas={
'temperature': TraceMetadata(name='temperature', group='temperature', legend='Temperatura', show=False, color='#ff8400', default_min=0, default_max=40, y_tick_suffix=' ºC', y_axis_width=130),
'temperature_feel': TraceMetadata(name='temperature_feel', group='temperature', legend='Sensación de temperatura', show=True, color='red', default_min=0, default_max=40, y_tick_suffix=' ºC', y_axis_width=130),
'clouds': TraceMetadata(name='clouds', legend='Nubes', show=False, color='#86abe3', default_min=-100, default_max=100, y_tick_suffix=' %', hide_y_ticks_if='{tick} < 0'),
'visibility': TraceMetadata(name='visibility', legend='Visibilidad', show=False, color='#c99a34', default_min=0, default_max='{max_y_data} * 2', y_tick_suffix=' km', y_delta_tick=2, hide_y_ticks_if='{tick} > {max_y_data}'),
'uvi': TraceMetadata(name='uvi', legend='UVI', show=False, color='#ffd000', default_min=-12, default_max=12, hide_y_ticks_if='{tick} < 0', y_delta_tick=1, y_axis_width=75),
'humidity': TraceMetadata(name='humidity', legend='Humedad', show=False, color='#2baab5', default_min=0, default_max=100, y_tick_suffix=' %'),
'precipitation_probability': TraceMetadata(name='precipitation_probability', legend='Probabilidad de precipitaciones', show=True, color='#0033ff', default_min=-100, default_max=100, y_tick_suffix=' %', hide_y_ticks_if='{tick} < 0'),
'rain_volume': TraceMetadata(plotly.graph_objects.Histogram, name='rain_volume', group='precipitation', legend='Volumen de lluvia', show=True, color='#34a4eb', opacity=0.3, default_min=-10, default_max=10, y_tick_suffix=' mm', y_delta_tick=1, hide_y_ticks_if='{tick} < 0', y_axis_width=130),
'snow_volume': TraceMetadata(plotly.graph_objects.Histogram, name='snow_volume', group='precipitation', legend='Volumen de nieve', show=True, color='#34a4eb', opacity=0.8, pattern={'shape': '.', 'fgcolor': '#ffffff', 'bgcolor': '#b0d6f3', 'solidity': 0.5, 'size': 14}, default_min=-10, default_max=10, y_tick_suffix=' mm', y_delta_tick=1, hide_y_ticks_if='{tick} < 0', y_axis_width=130),
'pressure': TraceMetadata(name='pressure', legend='Presión', show=False, color='#31a339', default_min=1013.25 - 90, default_max=1013.25 + 90, y_tick_suffix=' hPa', y_axis_width=225),
'wind_speed': TraceMetadata(name='wind_speed', legend='Velocidad del viento', show=False, color='#d8abff', default_min=-120, default_max=120, y_tick_suffix=' km/h', hide_y_ticks_if='{tick} < 0', y_axis_width=165)
},
x_data=[instant_weather.date_time for day_weather in day_weathers for instant_weather in day_weather.instant_weathers],
all_y_data=[],
current_weather=current_weather,
day_weathers=day_weathers,
timezone=(timezone := day_weathers[0].timezone),
place=place,
view_position=datetime.datetime.now(timezone)
)
weather_chart.apply_zoom()
weather_chart.draw()
if not (image_bytes := weather_chart.to_image()):
if bot_state_message:
await self.delete_message(bot_state_message)
raise NotFoundError('No hay suficientes datos del tiempo.')
if bot_state_message:
bot_state_message = await self.edit('Enviando...', bot_state_message)
bot_message: Message = await self.send(
Media(image_bytes, MediaType.IMAGE, 'jpg'),
[
[WeatherEmoji.ZOOM_IN.value, WeatherEmoji.ZOOM_OUT.value, WeatherEmoji.LEFT.value, WeatherEmoji.RIGHT.value],
[WeatherEmoji.TEMPERATURE.value, WeatherEmoji.TEMPERATURE_FEEL.value, WeatherEmoji.CLOUDS.value, WeatherEmoji.VISIBILITY.value, WeatherEmoji.UVI.value],
[WeatherEmoji.HUMIDITY.value, WeatherEmoji.PRECIPITATION_PROBABILITY.value, WeatherEmoji.PRECIPITATION_VOLUME.value, WeatherEmoji.PRESSURE.value, WeatherEmoji.WIND_SPEED.value]
],
message,
buttons_key=ButtonsGroup.WEATHER,
data={'weather_chart': weather_chart},
send_as_file=False
)
await self.send_inline_results(message)
if bot_state_message:
await self.delete_message(bot_state_message)
if bot_message and not self.is_bot_mentioned(message):
# noinspection PyTypeChecker
BotAction(self.platform.value, Action.AUTO_WEATHER_CHART, message, affected_objects=[bot_message]).save()
async def _on_weather_button_press(self, message: Message):
await self.accept_button_event(message)
weather_chart = message.data['weather_chart']
match message.buttons_info.pressed_text:
case WeatherEmoji.ZOOM_IN.value:
weather_chart.zoom_in()
case WeatherEmoji.ZOOM_OUT.value:
weather_chart.zoom_out()
case WeatherEmoji.LEFT.value:
weather_chart.move_left()
case WeatherEmoji.RIGHT.value:
weather_chart.move_right()
case WeatherEmoji.PRECIPITATION_VOLUME.value:
weather_chart.trace_metadatas['rain_volume'].show = not weather_chart.trace_metadatas['rain_volume'].show
weather_chart.trace_metadatas['snow_volume'].show = not weather_chart.trace_metadatas['snow_volume'].show
case emoji if emoji in WeatherEmoji.values:
trace_metadata_name = WeatherEmoji(emoji).name.lower()
weather_chart.trace_metadatas[trace_metadata_name].show = not weather_chart.trace_metadatas[trace_metadata_name].show
case _:
return
weather_chart.apply_zoom()
weather_chart.draw()
image_bytes = weather_chart.to_image()
await self.edit(Media(image_bytes, MediaType.IMAGE, 'jpg'), message)

View File

@@ -0,0 +1,261 @@
import io
import math
import random
from typing import Sequence
import cairo
from flanabot import constants
from flanabot.models.player import Player
SIZE_MULTIPLIER = 1
LEFT_MARGIN = 20
TOP_MARGIN = 20
CELL_LENGTH = 100 * SIZE_MULTIPLIER
NUMBERS_HEIGHT = 30 * SIZE_MULTIPLIER
TEXT_HEIGHT = 70 * SIZE_MULTIPLIER
NUMBERS_X_INITIAL_POSITION = LEFT_MARGIN + CELL_LENGTH / 2 - 8 * SIZE_MULTIPLIER
NUMBERS_Y_POSITION = TOP_MARGIN + CELL_LENGTH * constants.CONNECT_4_N_ROWS + 40 * SIZE_MULTIPLIER
TEXT_POSITION = (LEFT_MARGIN, TOP_MARGIN + CELL_LENGTH * constants.CONNECT_4_N_ROWS + NUMBERS_HEIGHT + 70 * SIZE_MULTIPLIER)
SURFACE_WIDTH = LEFT_MARGIN * 2 + CELL_LENGTH * constants.CONNECT_4_N_COLUMNS
SURFACE_HEIGHT = TOP_MARGIN * 2 + CELL_LENGTH * constants.CONNECT_4_N_ROWS + NUMBERS_HEIGHT + TEXT_HEIGHT
CIRCLE_RADIUS = 36 * SIZE_MULTIPLIER
CROSS_LINE_WIDTH = 24 * SIZE_MULTIPLIER
FONT_SIZE = 32 * SIZE_MULTIPLIER
TABLE_LINE_WIDTH = 4 * SIZE_MULTIPLIER
BLUE = (66 / 255, 135 / 255, 245 / 255)
BACKGROUND_COLOR = (49 / 255, 51 / 255, 56 / 255)
GRAY = (200 / 255, 200 / 255, 200 / 255)
HIGHLIGHT_COLOR = (104 / 255, 107 / 255, 113 / 255)
RED = (255 / 255, 70 / 255, 70 / 255)
PLAYER_1_COLOR = BLUE
PLAYER_2_COLOR = RED
def center_point(board_position: Sequence[int]) -> tuple[float, float]:
return LEFT_MARGIN + (board_position[1] + 0.5) * CELL_LENGTH, TOP_MARGIN + (board_position[0] + 0.5) * CELL_LENGTH
def draw_circle(board_position: Sequence[int], radius: float, color: tuple[float, float, float], context: cairo.Context):
context.set_source_rgba(*color)
context.arc(*center_point(board_position), radius, 0, 2 * math.pi)
context.fill()
def draw_line(
board_position_start: Sequence[int],
board_position_end: Sequence[int],
line_width: float,
color: tuple[float, float, float],
context: cairo.Context
):
context.set_line_width(line_width)
context.set_line_cap(cairo.LINE_CAP_ROUND)
context.set_source_rgba(*color)
context.move_to(*center_point(board_position_start))
context.line_to(*center_point(board_position_end))
context.stroke()
def draw_table(line_width: float, color: tuple[float, float, float], context: cairo.Context):
context.set_line_width(line_width)
context.set_line_cap(cairo.LINE_CAP_ROUND)
context.set_source_rgba(*color)
x = LEFT_MARGIN
y = TOP_MARGIN
for _ in range(constants.CONNECT_4_N_ROWS + 1):
context.move_to(x, y)
context.line_to(x + CELL_LENGTH * constants.CONNECT_4_N_COLUMNS, y)
context.stroke()
y += CELL_LENGTH
x = LEFT_MARGIN
y = TOP_MARGIN
for _ in range(constants.CONNECT_4_N_COLUMNS + 1):
context.move_to(x, y)
context.line_to(x, y + CELL_LENGTH * constants.CONNECT_4_N_ROWS)
context.stroke()
x += CELL_LENGTH
def draw_text(
text: str,
point: Sequence[float],
color: tuple[float, float, float],
font_size: float,
italic: bool,
context: cairo.Context
):
context.move_to(*point)
context.set_source_rgba(*color)
context.select_font_face("Sans", cairo.FONT_SLANT_ITALIC if italic else cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD)
context.set_font_size(font_size)
context.show_text(text)
def draw_winner_lines(
win_position: Sequence[int],
board: list[list[int | None]],
color: tuple[float, float, float],
context: cairo.Context
):
i, j = win_position
player_number = board[i][j]
# horizontal
j_a = j - 1
while j_a >= 0 and board[i][j_a] == player_number:
j_a -= 1
j_b = j + 1
while j_b < constants.CONNECT_4_N_COLUMNS and board[i][j_b] == player_number:
j_b += 1
if abs(j_a - j) + abs(j_b - j) - 1 >= 4:
draw_line((i, j_a + 1), (i, j_b - 1), CROSS_LINE_WIDTH, color, context)
# vertical
i_a = i - 1
while i_a >= 0 and board[i_a][j] == player_number:
i_a -= 1
i_b = i + 1
while i_b < constants.CONNECT_4_N_ROWS and board[i_b][j] == player_number:
i_b += 1
if abs(i_a - i) + abs(i_b - i) - 1 >= 4:
draw_line((i_a + 1, j), (i_b - 1, j), CROSS_LINE_WIDTH, color, context)
# diagonal 1
i_a = i - 1
j_a = j - 1
while i_a >= 0 and j_a >= 0 and board[i_a][j_a] == player_number:
i_a -= 1
j_a -= 1
i_b = i + 1
j_b = j + 1
while i_b < constants.CONNECT_4_N_ROWS and j_b < constants.CONNECT_4_N_COLUMNS and board[i_b][j_b] == player_number:
i_b += 1
j_b += 1
if abs(i_a - i) + abs(i_b - i) - 1 >= 4:
draw_line((i_a + 1, j_a + 1), (i_b - 1, j_b - 1), CROSS_LINE_WIDTH, color, context)
# diagonal 2
i_a = i - 1
j_a = j + 1
while i_a >= 0 and j_a < constants.CONNECT_4_N_COLUMNS and board[i_a][j_a] == player_number:
i_a -= 1
j_a += 1
i_b = i + 1
j_b = j - 1
while i_b < constants.CONNECT_4_N_ROWS and j_b >= 0 and board[i_b][j_b] == player_number:
i_b += 1
j_b -= 1
if abs(i_a - i) + abs(i_b - i) - 1 >= 4:
draw_line((i_a + 1, j_a - 1), (i_b - 1, j_b + 1), CROSS_LINE_WIDTH, color, context)
def highlight_cell(
board_position: Sequence[int],
color: tuple[float, float, float],
context: cairo.Context
):
x, y = top_left_point(board_position)
context.move_to(x, y)
x += CELL_LENGTH
context.line_to(x, y)
y += CELL_LENGTH
context.line_to(x, y)
x -= CELL_LENGTH
context.line_to(x, y)
context.close_path()
context.set_source_rgba(*color)
context.fill()
def make_image(
board: list[list[int | None]],
next_turn_player: Player = None,
winner: Player = None,
loser: Player = None,
highlight=None,
win_position: Sequence[int] = None,
tie=False
) -> bytes:
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, SURFACE_WIDTH, SURFACE_HEIGHT)
context = cairo.Context(surface)
paint_background(BACKGROUND_COLOR, context)
if highlight:
highlight_cell(highlight, HIGHLIGHT_COLOR, context)
draw_table(TABLE_LINE_WIDTH, GRAY, context)
write_numbers(GRAY, context)
for i in range(constants.CONNECT_4_N_ROWS):
for j in range(constants.CONNECT_4_N_COLUMNS):
match board[i][j]:
case 1:
draw_circle((i, j), CIRCLE_RADIUS, PLAYER_1_COLOR, context)
case 2:
draw_circle((i, j), CIRCLE_RADIUS, PLAYER_2_COLOR, context)
if tie:
write_tie(context)
elif winner:
player_color = PLAYER_1_COLOR if winner.number == 1 else PLAYER_2_COLOR
draw_winner_lines(win_position, board, player_color, context)
write_winner(winner, loser, context)
else:
write_player_turn(next_turn_player.name, PLAYER_1_COLOR if next_turn_player.number == 1 else PLAYER_2_COLOR, context)
buffer = io.BytesIO()
surface.write_to_png(buffer)
return buffer.getvalue()
def paint_background(color: tuple[float, float, float], context: cairo.Context):
context.set_source_rgba(*color)
context.paint()
def top_left_point(board_position: Sequence[int]) -> tuple[float, float]:
return LEFT_MARGIN + board_position[1] * CELL_LENGTH, TOP_MARGIN + board_position[0] * CELL_LENGTH
def write_numbers(color: tuple[float, float, float], context: cairo.Context):
x = NUMBERS_X_INITIAL_POSITION
for j in range(constants.CONNECT_4_N_COLUMNS):
draw_text(str(j + 1), (x, NUMBERS_Y_POSITION), color, FONT_SIZE, italic=False, context=context)
x += CELL_LENGTH
def write_player_turn(name: str, color: tuple[float, float, float], context: cairo.Context):
text = 'Turno de '
point = TEXT_POSITION
draw_text(text, point, GRAY, FONT_SIZE, True, context)
point = (point[0] + context.text_extents(text).width + 9 * SIZE_MULTIPLIER, point[1])
draw_text(name, point, color, FONT_SIZE, True, context)
point = (point[0] + context.text_extents(name).width, point[1])
draw_text('.', point, GRAY, FONT_SIZE, True, context)
def write_tie(context: cairo.Context):
draw_text(f"Empate{random.choice(('.', ' :c', ' :/', ' :s'))}", TEXT_POSITION, GRAY, FONT_SIZE, True, context)
def write_winner(winner: Player, loser: Player, context: cairo.Context):
winner_color, loser_color = (PLAYER_1_COLOR, PLAYER_2_COLOR) if winner.number == 1 else (PLAYER_2_COLOR, PLAYER_1_COLOR)
point = TEXT_POSITION
draw_text(winner.name, point, winner_color, FONT_SIZE, True, context)
text = ' le ha ganado a '
point = (point[0] + context.text_extents(winner.name).width + 3 * SIZE_MULTIPLIER, point[1])
draw_text(text, point, GRAY, FONT_SIZE, True, context)
point = (point[0] + context.text_extents(text).width + 10 * SIZE_MULTIPLIER, point[1])
draw_text(loser.name, point, loser_color, FONT_SIZE, True, context)
point = (point[0] + context.text_extents(loser.name).width + 3 * SIZE_MULTIPLIER, point[1])
draw_text('!!!', point, GRAY, FONT_SIZE, True, context)

View File

@@ -1,113 +1,156 @@
import datetime
from collections import defaultdict
import flanautils
from multibot import Platform
AUDIT_LOG_AGE = datetime.timedelta(hours=1)
AUDIT_LOG_LIMIT = 5
AUTO_WEATHER_EVERY = datetime.timedelta(hours=6)
CHECK_MESSAGE_EVERY_SECONDS = datetime.timedelta(days=1).total_seconds()
CHECK_MUTES_EVERY_SECONDS = datetime.timedelta(hours=1).total_seconds()
BTC_OFFERS_WEBSOCKET_RETRY_DELAY_SECONDS = datetime.timedelta(hours=1).total_seconds()
CHECK_PUNISHMENTS_EVERY_SECONDS = datetime.timedelta(hours=1).total_seconds()
CONNECT_4_AI_DELAY_SECONDS = 1
CONNECT_4_CENTER_COLUMN_POINTS = 2
CONNECT_4_N_COLUMNS = 7
CONNECT_4_N_ROWS = 6
FLANASERVER_BASE_URL = 'https://flanaserver.duckdns.org'
FLANASERVER_FILE_EXPIRATION_SECONDS = 3 * 24 * 60 * 60
FLOOD_2s_LIMIT = 2
FLOOD_7s_LIMIT = 4
HEAT_FIRST_LEVEL = -1
HEAT_PERIOD_SECONDS = datetime.timedelta(minutes=15).total_seconds()
HELP_MINUTES_LIMIT = 1
INSTAGRAM_BAN_SLEEP = datetime.timedelta(days=1)
INSULT_PROBABILITY = 0.00166666667
MAX_PLACE_QUERY_LENGTH = 50
PUNISHMENT_INCREMENT_EXPONENT = 6
PUNISHMENTS_RESET = datetime.timedelta(weeks=6 * flanautils.WEEKS_IN_A_MONTH)
PUNISHMENTS_RESET_TIME = datetime.timedelta(weeks=2)
RECOVERY_DELETED_MESSAGE_BEFORE = datetime.timedelta(hours=1)
TIME_THRESHOLD_TO_MANUAL_UNMUTE = datetime.timedelta(days=3)
TIME_THRESHOLD_TO_MANUAL_UNPUNISH = datetime.timedelta(days=3)
SCRAPING_MESSAGE_WAITING_TIME = 0.1
SCRAPING_TIMEOUT_SECONDS = 20
SPAM_CHANNELS_LIMIT = 2
SPAM_DELETION_DELAY = datetime.timedelta(seconds=5)
SPAM_TIME_RANGE = datetime.timedelta(hours=1)
STEAM_ALL_APPS_ENDPOINT = 'https://api.steampowered.com/ISteamApps/GetAppList/v2'
STEAM_APP_ENDPOINT_TEMPLATE = 'https://store.steampowered.com/api/appdetails?appids={ids}&cc={country_code}&filters=price_overview'
STEAM_APP_IDS_FOR_SCRAPE_COUNTRIES = (400, 620, 730, 210970, 252490, 292030, 427520, 1712350)
STEAM_DB_URL = 'https://steamdb.info'
STEAM_EXCHANGERATE_API_ENDPOINT_TEMPLATE = 'https://v6.exchangerate-api.com/v6/{api_key}/latest/EUR'
STEAM_IDS_BATCH = 750
STEAM_LAST_APPS = 1500
STEAM_MAX_CONCURRENT_REQUESTS = 10
STEAM_MOST_URLS = (
'https://store.steampowered.com/charts/topselling/global',
'https://store.steampowered.com/charts/mostplayed'
)
STEAM_RANDOM_APPS = 1000
STEAM_REGION_CODE_MAPPING = {'eu': 'EUR', 'ru': 'RUB', 'pk': 'USD', 'ua': 'UAH', 'za': 'ZAR', 'vn': 'VND', 'tw': 'TWD',
'id': 'IDR', 'my': 'MYR', 'ar': 'USD', 'tr': 'USD', 'ph': 'PHP', 'in': 'INR', 'cn': 'CNY',
'br': 'BRL', 'sa': 'SAR', 'th': 'THB', 'pe': 'PEN', 'cl': 'CLP', 'kw': 'KWD', 'az': 'USD',
'kz': 'KZT', 'co': 'COP', 'mx': 'MXN', 'qa': 'QAR', 'sg': 'SGD', 'jp': 'JPY', 'uy': 'UYU',
'ae': 'AED', 'kr': 'KRW', 'hk': 'HKD', 'cr': 'CRC', 'nz': 'NZD', 'ca': 'CAD', 'au': 'AUD',
'il': 'ILS', 'us': 'USD', 'no': 'NOK', 'uk': 'GBP', 'pl': 'PLN', 'ch': 'CHF'}
YADIO_API_ENDPOINT = 'https://api.yadio.io/exrates/EUR'
BAD_PHRASES = (
'Cállate ya anda.',
'¿Quién te ha preguntado?',
'¿Tú eres así o te dan apagones cerebrales?',
'Ante la duda mi dedo corazón te saluda.',
'Enjoy cancer brain.',
'Calla noob.',
'Hablas tanta mierda que tu culo tiene envidia de tu boca.',
'jAJjajAJjajAJjajAJajJAJajJA',
'enjoy xd',
'Reported.',
'Baneito pa ti en breve.',
'Despídete de tu cuenta.',
'Flanagan es más guapo que tú.',
'jajaj',
'xd',
'Hay un concurso de hostias y tienes todas las papeletas.',
'¿Por qué no te callas?',
'Das penilla.',
'Deberían hacerte la táctica del C4.',
'Te voy romper las pelotas.',
'Más tonto y no naces.',
'Eres más tonto que peinar bombillas.',
'Eres más tonto que pellizcar cristales.',
'Eres más malo que pegarle a un padre.'
BANNED_POLL_PHRASES = (
'Deja de dar por culo {presser_name} que no puedes votar aqui',
'No es pesao {presser_name}, que no tienes permitido votar aqui',
'Deja de pulsar botones que no puedes votar aqui {presser_name}',
'{presser_name} deja de intentar votar aqui que no puedes',
'Te han prohibido votar aquí {presser_name}.',
'No puedes votar aquí, {presser_name}.'
)
BYE_PHRASES = ('Adiós.', 'adieu', 'adio', 'adioh', 'adios', 'adió', 'adiós', 'agur', 'bye', 'byyeeee', 'chao',
'hasta la vista', 'hasta luego', 'hasta nunca', ' hasta pronto', 'hasta la próxima',
'nos vemos', 'taluego')
BYE_PHRASES = ('Adiós.', 'adio', 'adioh', 'adios', 'adió', 'adiós', 'agur', 'bye', 'byyeeee', 'chao', 'hasta la vista',
'hasta luego', 'hasta nunca', ' hasta pronto', 'hasta la próxima', 'nos vemos', 'taluego')
CHANGEABLE_ROLES = defaultdict(
lambda: defaultdict(list),
{
Platform.DISCORD: defaultdict(
list,
{
360868977754505217: [881238165476741161, 991454395663401072, 1033098591725699222, 1176639571677696173],
862823584670285835: [976660580939202610, 984269640752590868]
}
)
}
)
DISCORD_HEAT_NAMES = [
'Canal Fresquito',
'Canal Templaillo',
'Canal Calentito',
'Canal Caloret',
'Canal Caliente',
'Canal Olor a Vasco',
'Canal Verano Cordobés al Sol',
'Canal Al rojo vivo',
'Canal Ardiendo',
'Canal INFIERNO'
]
DISCORD_HOT_CHANNEL_IDS = {
'A': 493529846027386900,
'B': 493529881125060618,
'C': 493530483045564417,
'D': 829032476949217302,
'E': 829032505645596742
}
HELLO_PHRASES = ('alo', 'aloh', 'buenas', 'Hola.', 'hello', 'hey', 'hi', 'hola', 'holaaaa', 'holaaaaaaa', 'ola',
'ola k ase', 'pa ti mi cola', 'saludos')
KEYWORDS = {
'activate': ('activa', 'activar', 'activate', 'deja', 'dejale', 'devuelve', 'devuelvele', 'enable', 'encender',
'enciende', 'habilita', 'habilitar'),
'bye': ('adieu', 'adio', 'adiooooo', 'adios', 'agur', 'buenas', 'bye', 'cama', 'chao', 'farewell', 'goodbye',
'hasta', 'luego', 'noches', 'pronto', 'taluego', 'taluegorl', 'tenga', 'vemos', 'vista', 'voy'),
'change': ('alter', 'alternar', 'alternate', 'cambiar', 'change', 'default', 'defecto', 'edit', 'editar',
'exchange', 'modificar', 'modify', 'permutar', 'predeterminado', 'shift', 'swap', 'switch', 'turn',
'vary'),
'config': ('ajustar', 'ajuste', 'ajustes', 'automatico', 'automatic', 'config', 'configs', 'configuracion',
'configuration', 'default', 'defecto', 'setting', 'settings'),
'covid_chart': ('case', 'caso', 'contagiado', 'contagio', 'corona', 'coronavirus', 'covid', 'covid19', 'death',
'disease', 'enfermedad', 'enfermos', 'fallecido', 'incidencia', 'jacovid', 'mascarilla', 'muerte',
'muerto', 'pandemia', 'sick', 'virus'),
'currency_chart': ('argentina', 'bitcoin', 'cardano', 'cripto', 'crypto', 'criptodivisa', 'cryptodivisa',
'cryptomoneda', 'cryptocurrency', 'currency', 'dinero', 'divisa', 'ethereum', 'inversion',
'moneda', 'pasta'),
'date': ('ayer', 'de', 'domingo', 'fin', 'finde', 'friday', 'hoy', 'jueves', 'lunes', 'martes', 'mañana',
'miercoles', 'monday', 'pasado', 'sabado', 'saturday', 'semana', 'sunday', 'thursday', 'today', 'tomorrow',
'tuesday', 'viernes', 'wednesday', 'week', 'weekend', 'yesterday'),
'deactivate': ('apaga', 'apagar', 'deactivate', 'deactivate', 'desactivar', 'deshabilita', 'deshabilitar',
'disable', 'forbids', 'prohibe', 'quita', 'remove', 'return'),
'hello': ('alo', 'aloh', 'buenas', 'dias', 'hello', 'hey', 'hi', 'hola', 'holaaaaaa', 'ola', 'saludos', 'tardes'),
'help': ('ayuda', 'help'),
'mute': ('calla', 'calle', 'cierra', 'close', 'mute', 'mutea', 'mutealo', 'noise', 'ruido', 'shut', 'silence',
'silencia'),
'negate': ('no', 'ocurra', 'ocurre'),
'permission': ('permiso', 'permission'),
'punish': ('acaba', 'aprende', 'ataca', 'atalo', 'azota', 'boss', 'castiga', 'castigo', 'condena', 'controla',
'destroy', 'destroza', 'duro', 'ejecuta', 'enseña', 'escarmiento', 'execute', 'finish', 'fuck', 'fusila',
'hell', 'humos', 'infierno', 'jefe', 'jode', 'learn', 'leccion', 'lesson', 'manda', 'purgatorio',
'sancion', 'shoot', 'teach', 'termina', 'whip'),
'reset': ('recover', 'recovery', 'recupera', 'reinicia', 'reset', 'resetea', 'restart'),
'scraping': ('api', 'aqui', 'busca', 'contenido', 'content', 'descarga', 'descargar', 'download', 'envia', 'habia',
'media', 'redes', 'scrap', 'scraping', 'search', 'send', 'social', 'sociales', 'tenia', 'video',
'videos'),
'show': ('actual', 'enseña', 'estado', 'how', 'muestra', 'show', 'como'),
'song_info': ('aqui', 'cancion', 'data', 'datos', 'info', 'informacion', 'information', 'llama', 'media', 'name',
'nombre', 'sonaba', 'sonando', 'song', 'sono', 'sound', 'suena', 'title', 'titulo',
'video'),
'sound': ('hablar', 'hable', 'micro', 'microfono', 'microphone', 'sonido', 'sound', 'talk', 'volumen'),
'thanks': ('gracia', 'gracias', 'grasia', 'grasias', 'grax', 'thank', 'thanks', 'ty'),
'unmute': ('desilencia', 'desmutea', 'desmutealo', 'unmute'),
'unpunish': ('absolve', 'forgive', 'innocent', 'inocente', 'perdona', 'spare'),
'weather_chart': ('atmosfera', 'atmosferico', 'calle', 'calor', 'caloret', 'clima', 'climatologia', 'cloud',
'cloudless', 'cloudy', 'cold', 'congelar', 'congelado', 'denbora', 'despejado', 'diluvio', 'frio',
'frost', 'hielo', 'humedad', 'llover', 'llueva', 'llueve', 'lluvia', 'nevada', 'nieva', 'nieve',
'nube', 'nubes', 'nublado', 'meteorologia', 'rain', 'snow', 'snowfall', 'snowstorm', 'sol',
'solano', 'storm', 'sun', 'temperatura', 'tempo', 'tiempo', 'tormenta', 'ventisca', 'weather',
'wetter')
}
INSULTS = ('._.', 'aha', 'Aléjate de mi.', 'Ante la duda mi dedo corazón te saluda.', 'Baneito pa ti en breve.',
'Calla noob.', 'Cansino.', 'Cuéntame menos.', 'Cuéntame más.', 'Cállate ya anda.', 'Cállate.',
'Das penilla.', 'De verdad. Estás para encerrarte.', 'Deberían hacerte la táctica del C4.',
'Despídete de tu cuenta.', 'Déjame tranquilo.', 'Enjoy cancer brain.', 'Eres cortito, ¿eh?',
'Eres más malo que pegarle a un padre.', 'Eres más tonto que peinar bombillas.',
'Eres más tonto que pellizcar cristales.', 'Estás mal de la azotea.', 'Estás mal de la cabeza.',
'Flanagan es más guapo que tú.', 'Hablas tanta mierda que tu culo tiene envidia de tu boca.',
'Hay un concurso de hostias y tienes todas las papeletas.', 'Loco.', 'Más tonto y no naces.',
'No eres muy avispado tú...', 'Pesado.', 'Qué bien, ¿eh?', 'Que me dejes en paz.', 'Qué pesado.',
'Quita bicho.', 'Reportaito mi arma.', 'Reported.', 'Retard.', 'Te voy romper las pelotas.',
'Tú... no estás muy bien, ¿no?', 'Ya estamos otra vez...', 'Ya estamos...', 'enjoy xd',
'jAJjajAJjajAJjajAJajJAJajJA', 'jajaj', 'o_O', 'xd', '¿Otra vez tú?', '¿Pero cuándo te vas a callar?',
'¿Por qué no te callas?', '¿Quién te ha preguntado?', '¿Qué quieres?', '¿Te callas o te callo?',
'¿Te imaginas que me interesa?', '¿Te quieres callar?', '¿Todo bien?',
'¿Tú eres así o te dan apagones cerebrales?', '🖕', '😑', '🙄', '🤔', '🤨')
RECOVER_PHRASES = (
'No hay nada que recuperar.',
'Ya lo he recuperado y enviado, así que callate ya.',
'Ya lo he recuperado y enviado, así que mejor estás antento antes de dar por culo.',
'Ya lo he recuperado y enviado, no lo voy a hacer dos veces.',
'Ya lo he recuperado y enviado. A ver si leemos más y jodemos menos.',
'Ya lo he reenviado.'
)
KEYWORDS = {
'choose': ('choose', 'elige', 'escoge'),
'connect_4': (('conecta', 'connect', 'ralla', 'raya'), ('4', 'cuatro', 'four')),
'dice': ('dado', 'dice'),
'eur': ('eur', 'euro', 'euros', ''),
'force': ('force', 'forzar', 'fuerza'),
'money': ('bitcoin', 'btc', 'cripto', 'criptomoneda', 'crypto', 'cryptocurrency', 'currency', 'currency', 'dinero',
'divisa', 'moneda', 'money', 'precio', 'price', 'satoshi'),
'multiple_answer': ('multi', 'multi-answer', 'multiple', 'multirespuesta'),
'notify': ('alert', 'alertame', 'alertar', 'avisame', 'avisar', 'aviso', 'inform', 'informame', 'informar',
'notificacion', 'notificame', 'notificar', 'notification'),
'offer': ('oferta', 'offer', 'orden', 'order', 'post', 'publicacion'),
'poll': ('encuesta', 'quiz', 'votacion', 'votar', 'voting'),
'premium': ('%', 'premium', 'prima'),
'punish': ('acaba', 'aprende', 'ataca', 'atalo', 'azota', 'beating', 'boss', 'castiga', 'castigo', 'condena',
'controla', 'destroy', 'destroza', 'duro', 'ejecuta', 'enseña', 'escarmiento', 'execute', 'fuck',
'fusila', 'hell', 'humos', 'infierno', 'jefe', 'jode', 'learn', 'leccion', 'lesson', 'manda', 'paliza',
'purgatorio', 'purgatory', 'sancion', 'shoot', 'teach', 'whip'),
'region': ('countries', 'country', 'pais', 'paises', 'region', 'regiones', 'regions', 'zona', 'zonas', 'zone',
'zones'),
'scraping': ('busca', 'contenido', 'content', 'descarga', 'descargar', 'descargues', 'download', 'envia', 'scrap',
'scrapea', 'scrapees', 'scraping', 'search', 'send'),
'self': (('contigo', 'contra', 'ti'), ('mismo', 'ti')),
'song_info': ('cancion', 'data', 'datos', 'info', 'informacion', 'information', 'sonaba', 'sonando', 'song', 'sono',
'sound', 'suena'),
'tunnel': ('canal', 'channel', 'tunel', 'tunnel'),
'unpunish': ('absolve', 'forgive', 'innocent', 'inocente', 'perdona', 'spare'),
'until': ('hasta', 'until'),
'usd': ('$', 'dolar', 'dolares', 'dollar', 'dollars', 'usd'),
'vote': ('vote', 'voto'),
'weather': ('atmosfera', 'atmosferico', 'calle', 'calor', 'caloret', 'clima', 'climatologia', 'cloud', 'cloudless',
'cloudy', 'cold', 'congelar', 'congelado', 'denbora', 'despejado', 'diluvio', 'frio', 'frost', 'hielo',
'humedad', 'llover', 'llueva', 'llueve', 'lluvia', 'nevada', 'nieva', 'nieve', 'nube', 'nubes',
'nublado', 'meteorologia', 'rain', 'snow', 'snowfall', 'snowstorm', 'sol', 'solano', 'storm', 'sun',
'temperatura', 'tempo', 'tiempo', 'tormenta', 'ventisca', 'weather', 'wetter')
}
SCRAPING_PHRASES = ('Analizando...', 'Buscando...', 'Hackeando internet... 👀', 'Rebuscando en la web...',
'Robando cosas...', 'Scrapeando...', 'Scraping...')

View File

@@ -1,6 +0,0 @@
class BadRoleError(Exception):
pass
class UserDisconnectedError(Exception):
pass

View File

@@ -1,18 +1,19 @@
import asyncio
import os
import flanautils
os.environ |= flanautils.find_environment_variables('../.env')
import asyncio
from flanabot.bots.flana_disc_bot import FlanaDiscBot
from flanabot.bots.flana_tele_bot import FlanaTeleBot
async def main():
os.environ |= flanautils.find_environment_variables('../.env')
flana_disc_bot = FlanaDiscBot()
flana_tele_bot = FlanaTeleBot()
await asyncio.gather(
flana_disc_bot.start(),
flana_tele_bot.start()
)

View File

@@ -1,5 +1,9 @@
from flanabot.models.bot_action import *
from flanabot.models.chat import *
from flanabot.models.enums import *
from flanabot.models.heating_context import *
from flanabot.models.message import *
from flanabot.models.punishments import *
from flanabot.models.user import *
from flanabot.models.player import *
from flanabot.models.punishment import *
from flanabot.models.steam_region import *
from flanabot.models.weather_chart import *

View File

@@ -0,0 +1,31 @@
__all__ = ['BotAction']
import datetime
from dataclasses import dataclass, field
from flanautils import DCMongoBase, FlanaBase
from multibot import Platform, User
from flanabot.models.chat import Chat
from flanabot.models.enums import Action
from flanabot.models.message import Message
@dataclass(eq=False)
class BotAction(DCMongoBase, FlanaBase):
collection_name = 'bot_action'
unique_keys = 'message'
nullable_unique_keys = 'message'
platform: Platform = None
action: Action = None
message: Message = None
author: User = None
chat: Chat = None
affected_objects: list = field(default_factory=list)
date: datetime.datetime = field(default_factory=datetime.datetime.now)
def __post_init__(self):
super().__post_init__()
self.author = self.author or getattr(self.message, 'author', None)
self.chat = self.chat or getattr(self.message, 'chat', None)

View File

@@ -1,21 +1,25 @@
__all__ = ['Chat']
from dataclasses import dataclass, field
from multibot import Chat as MultiBotChat
from flanabot.models.user import User
@dataclass(eq=False)
class Chat(MultiBotChat):
DEFAULT_CONFIG = {'auto_clear': False,
'auto_covid_chart': True,
'auto_currency_chart': True,
'auto_delete_original': True,
'auto_scraping': True,
'auto_weather_chart': True}
users: list[User] = field(default_factory=list)
def __post_init__(self):
super().__post_init__()
if not self.config:
self.config = self.DEFAULT_CONFIG
config: dict = field(default_factory=lambda: {
'auto_insult': True,
'auto_scraping': True,
'auto_weather_chart': False,
'check_flood': False,
'punish': False,
'scraping_delete_original': True,
'ubereats': False
})
btc_offers_query: dict[str, float] = field(default_factory=lambda: {})
ubereats: dict = field(default_factory=lambda: {
'cookies': [],
'last_codes': [],
'seconds': 86700,
'next_execution': None
})

19
flanabot/models/enums.py Normal file
View File

@@ -0,0 +1,19 @@
__all__ = ['Action', 'ButtonsGroup']
from enum import auto
from flanautils import FlanaEnum
class Action(FlanaEnum):
AUTO_WEATHER_CHART = auto()
MESSAGE_DELETED = auto()
class ButtonsGroup(FlanaEnum):
CONFIG = auto()
CONNECT_4 = auto()
POLL = auto()
ROLES = auto()
USERS = auto()
WEATHER = auto()

View File

@@ -0,0 +1,19 @@
__all__ = ['ChannelData', 'HeatingContext']
from dataclasses import dataclass, field
import discord
@dataclass
class ChannelData:
channel: discord.VoiceChannel
original_name: str
n_fires: int = 0
@dataclass
class HeatingContext:
channels_data: dict[str, ChannelData] = field(default_factory=dict)
is_active: bool = False
heat_level: float = -1

View File

@@ -1,21 +1,19 @@
from __future__ import annotations # todo0 remove in 3.11
from __future__ import annotations # todo0 remove when it's by default
__all__ = ['Message']
from dataclasses import dataclass, field
from typing import Iterable
from flanautils import Media, OrderedSet
from multibot import Message as MultiBotMessage
from multibot import Message as MultiBotMessage, User
from flanabot.models.chat import Chat
from flanabot.models.user import User
from flanabot.models.weather_chart import WeatherChart
@dataclass(eq=False)
class Message(MultiBotMessage):
author: User = None
mentions: Iterable[User] = field(default_factory=list)
mentions: list[User] = field(default_factory=list)
chat: Chat = None
replied_message: Message = None
weather_chart: WeatherChart = None
song_infos: OrderedSet[Media] = field(default_factory=OrderedSet)

12
flanabot/models/player.py Normal file
View File

@@ -0,0 +1,12 @@
__all__ = ['Player']
from dataclasses import dataclass
from flanautils import FlanaBase
@dataclass
class Player(FlanaBase):
id: int
name: str
number: int

View File

@@ -0,0 +1,27 @@
__all__ = ['Punishment']
import datetime
from dataclasses import dataclass
from typing import Any
from multibot import Penalty
@dataclass(eq=False)
class Punishment(Penalty):
collection_name = 'punishment'
level: int = 0
def _mongo_repr(self) -> Any:
self_vars = super()._mongo_repr()
self_vars['level'] = self.level
return self_vars
def delete(self, cascade=False):
if self.level == 0:
super().delete(cascade)
elif self.is_active:
self.is_active = False
self.last_update = datetime.datetime.now(datetime.timezone.utc)
self.save()

View File

@@ -1,23 +0,0 @@
import datetime
from dataclasses import dataclass
from flanautils import DCMongoBase, FlanaBase
from multibot.models.database import db
@dataclass(eq=False)
class PunishmentBase(DCMongoBase, FlanaBase):
user_id: int = None
group_id: int = None
until: datetime.datetime = None
is_active: bool = True
@dataclass(eq=False)
class Punishment(PunishmentBase):
collection = db.punishment
@dataclass(eq=False)
class Mute(PunishmentBase):
collection = db.mute

View File

@@ -0,0 +1,40 @@
__all__ = ['SteamRegion']
from dataclasses import dataclass
from typing import Any
from flanautils import DCMongoBase, FlanaBase
@dataclass
class SteamRegion(DCMongoBase, FlanaBase):
collection_name = 'steam_region'
unique_keys = ('code',)
code: str
name: str
flag_url: str
eur_conversion_rate: float | None = None
mean_price: float = 0.0
def __post_init__(self):
super().__post_init__()
match self.code:
case 'eu':
self.bar_color = '#2b6ced'
self.bar_line_color = '#0055ff'
case 'in':
self.bar_color = '#ffc091'
self.bar_line_color = '#ff6600'
case 'ar':
self.bar_color = '#8adcff'
self.bar_line_color = '#00b3ff'
case 'tr':
self.bar_color = '#ff7878'
self.bar_line_color = '#ff0000'
case _:
self.bar_color = '#68a9f2'
self.bar_line_color = '#68a9f2'
def _mongo_repr(self) -> Any:
return {k: v for k, v in super()._mongo_repr().items() if k in ('code', 'name', 'flag_url')}

View File

@@ -1,22 +0,0 @@
from dataclasses import dataclass
from multibot import User as MultiBotUser
from flanabot.models.punishments import Mute, Punishment
@dataclass(eq=False)
class User(MultiBotUser):
def is_muted_on(self, group_id: int):
return group_id in self.muted_on
def is_punished_on(self, group_id: int):
return group_id in self.punished_on
@property
def muted_on(self):
return {mute.group_id for mute in Mute.find({'user_id': self.id, 'is_active': True})}
@property
def punished_on(self):
return {punishment for punishment in Punishment.find({'user_id': self.id, 'is_active': True})}

View File

@@ -1,3 +1,5 @@
__all__ = ['Direction', 'WeatherChart']
import datetime
from dataclasses import dataclass, field
@@ -44,7 +46,7 @@ class WeatherChart(DateChart):
if self.show_now_vertical_line and first_dt <= now <= last_dt:
self.figure.add_vline(x=now, line_width=1, line_dash='dot')
self.figure.add_annotation(text="Ahora", yref="paper", x=now, y=0.01, showarrow=False)
self.figure.add_annotation(text='Ahora', yref='paper', x=now, y=0.01, showarrow=False)
for day_weather in self.day_weathers:
date_time = datetime.datetime(year=day_weather.date.year, month=day_weather.date.month, day=day_weather.date.day, tzinfo=self.timezone)

Binary file not shown.

View File

@@ -2,7 +2,6 @@
name = {project_name}
version = {project_version}
author = {author}
author_email = alberlc@outlook.com
description = {description}
long_description = file: README.rst
url = https://github.com/{author}/{project_name}
@@ -22,6 +21,8 @@ install_requires =
flanautils
multibot
plotly
pycairo
pytz
[options.packages.find]
include = {project_name}*

View File

@@ -7,7 +7,6 @@ os.environ |= flanautils.find_environment_variables('../.env')
import unittest
from typing import Iterable
from multibot import constants as multibot_constants
from flanabot.bots.flana_tele_bot import FlanaTeleBot
@@ -15,7 +14,7 @@ class TestParseCallbacks(unittest.TestCase):
def _test_no_always_callbacks(self, phrases: Iterable[str], callback: callable):
for i, phrase in enumerate(phrases):
with self.subTest(phrase):
callbacks = [registered_callback.callback for registered_callback in self.flana_tele_bot._parse_callbacks(phrase, multibot_constants.RATIO_REWARD_EXPONENT, multibot_constants.KEYWORDS_LENGHT_PENALTY, multibot_constants.MINIMUM_RATIO_TO_MATCH)
callbacks = [registered_callback.callback for registered_callback in self.flana_tele_bot._parse_callbacks(phrase, self.flana_tele_bot._registered_callbacks)
if not registered_callback.always]
self.assertEqual(1, len(callbacks))
self.assertEqual(callback, callbacks[0], f'\n\nExpected: {callback.__name__}\nActual: {callbacks[0].__name__}')
@@ -27,98 +26,31 @@ class TestParseCallbacks(unittest.TestCase):
phrases = ['adios', 'taluego', 'adiooo', 'hasta la proxima', 'nos vemos', 'hasta la vista', 'hasta pronto']
self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_bye)
def test_on_config_list_show(self):
def test_on_config(self):
phrases = [
'flanabot ajustes',
'Flanabot ajustes',
'Flanabot qué puedo ajustar?',
'flanabot ayuda'
'config',
'configuracion',
'configuración',
'configuration'
]
self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_config_list_show)
def test_on_covid_chart(self):
phrases = [
'cuantos contagios',
'casos',
'enfermos',
'muerte',
'pandemia',
'enfermedad',
'fallecidos',
'mascarillas',
'virus',
'covid-19',
'como va el covid',
'lo peta el corona'
]
self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_covid_chart)
def test_on_currency_chart(self):
phrases = [
'como van esos dineros',
'critodivisa',
'esas cryptos',
'inversion',
'moneda',
'mas caro en argentina?',
'el puto bitcoin',
'divisa'
]
self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_currency_chart)
def test_on_currency_chart_config_activate(self):
phrases = ['activa el bitcoin automatico']
self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_currency_chart_config_activate)
def test_on_currency_chart_config_change(self):
phrases = ['cambia la config del bitcoin automatico']
self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_currency_chart_config_change)
def test_on_currency_chart_config_deactivate(self):
phrases = ['desactiva el bitcoin automatico']
self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_currency_chart_config_deactivate)
def test_on_currency_chart_config_show(self):
phrases = ['enseña el bitcoin automatico', 'como esta el bitcoin automatico', 'flanabot ajustes bitcoin']
self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_currency_chart_config_show)
self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_config)
def test_on_delete(self):
phrases = ['borra ese mensaje', 'borra ese mensaje puto', 'borra', 'borra el mensaje', 'borra eso', 'borres']
self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_delete)
def test_on_delete_original_config_activate(self):
phrases = [
'activa el borrado automatico',
'flanabot pon el auto delete activado',
'flanabot activa el autodelete'
]
self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_delete_original_config_activate)
def test_on_delete_original_config_change(self):
phrases = ['cambia la config del borrado automatico']
self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_delete_original_config_change)
def test_on_delete_original_config_deactivate(self):
phrases = [
'desactiva el borrado automatico',
'flanabot pon el auto delete desactivado',
'flanabot desactiva el autodelete'
]
self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_delete_original_config_deactivate)
def test_on_delete_original_config_show(self):
phrases = ['enseña el borrado automatico', 'como esta el borrado automatico', 'flanabot ajustes delete']
self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_delete_original_config_show)
def test_on_hello(self):
phrases = ['hola', 'hello', 'buenos dias', 'holaaaaaa', 'hi', 'holaaaaa', 'saludos', 'ola k ase']
self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_hello)
def test_on_mute(self):
phrases = [
# 'silencia',
# 'silencia al pavo ese',
# 'calla a ese pesao',
'silencia',
'silencia al pavo ese',
'calla a ese pesao',
'haz que se calle',
'quitale el microfono a ese',
'quitale el micro',
@@ -160,7 +92,6 @@ class TestParseCallbacks(unittest.TestCase):
'ataca',
'acaba',
'acaba con',
'termina con el',
'acabaq con su sufri,iento',
'acaba con ese apvo',
'castigalo',
@@ -187,22 +118,6 @@ class TestParseCallbacks(unittest.TestCase):
]
self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_scraping)
def test_on_scraping_config_activate(self):
phrases = ['activa el scraping automatico', 'activa el scraping']
self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_scraping_config_activate)
def test_on_scraping_config_change(self):
phrases = ['cambia la config del scraping']
self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_scraping_config_change)
def test_on_scraping_config_deactivate(self):
phrases = ['desactiva el scraping automatico']
self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_scraping_config_deactivate)
def test_on_scraping_config_show(self):
phrases = ['enseña el scraping automatico', 'como esta el scraping automatico', 'flanabot ajustes scraping']
self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_scraping_config_show)
def test_on_song_info(self):
phrases = [
'que sonaba ahi',
@@ -267,20 +182,4 @@ class TestParseCallbacks(unittest.TestCase):
'hara mucho calor en egipto este fin de semana?',
'pfff no ve que frio ahi en oviedo este finde'
]
self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_weather_chart)
def test_on_weather_chart_config_activate(self):
phrases = ['activa el tiempo automatico', 'activa el tiempo']
self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_weather_chart_config_activate)
def test_on_weather_chart_config_change(self):
phrases = ['cambia la config del tiempo']
self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_weather_chart_config_change)
def test_on_weather_chart_config_deactivate(self):
phrases = ['desactiva el tiempo automatico']
self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_weather_chart_config_deactivate)
def test_on_weather_chart_config_show(self):
phrases = ['enseña el tiempo automatico', 'como esta el tiempo automatico', 'flanabot ajustes tiempo']
self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_weather_chart_config_show)
self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_weather)