Compare commits
413 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c2f54592d | ||
|
|
eecb488ea9 | ||
|
|
a44a71f3be | ||
|
|
aee14449ad | ||
|
|
e20a5135f2 | ||
|
|
c392e7bdde | ||
|
|
c95621ca7e | ||
|
|
8efa12ad22 | ||
|
|
b672cbf0a1 | ||
|
|
d377aae6ff | ||
|
|
789034aa9f | ||
|
|
647b9288aa | ||
|
|
ad2434ab67 | ||
|
|
fca648f8b5 | ||
|
|
ada357fcb8 | ||
|
|
b1c07ea251 | ||
|
|
5ec3c1e81b | ||
|
|
39aea44803 | ||
|
|
4dbe9e0504 | ||
|
|
d977d5d882 | ||
|
|
aac5fe951f | ||
|
|
abc4462723 | ||
|
|
bdf658ee9a | ||
|
|
6aa735ddd5 | ||
|
|
9f640014a3 | ||
|
|
edebc48b1f | ||
|
|
2babe0fee7 | ||
|
|
123498b1f1 | ||
|
|
7f8cbd6e54 | ||
|
|
37db1b3590 | ||
|
|
aa6642ea26 | ||
|
|
7d538316ad | ||
|
|
20ca2b3ab9 | ||
|
|
6f0cd1fcf6 | ||
|
|
39bf560407 | ||
|
|
76f9920de4 | ||
|
|
2465755bc0 | ||
|
|
46a50767af | ||
|
|
05e53bdc87 | ||
|
|
4e44cdd112 | ||
|
|
848d354da2 | ||
|
|
083356ac0b | ||
|
|
c33c04cba4 | ||
|
|
5f027e665a | ||
|
|
71a5cb74d9 | ||
|
|
3ea232b183 | ||
|
|
2419d36bbb | ||
|
|
778500caac | ||
|
|
4f4a05f0ff | ||
|
|
d6986d8b63 | ||
|
|
25c5391f5a | ||
|
|
df72d752c1 | ||
|
|
07ab104203 | ||
|
|
564a65c5a1 | ||
|
|
ebe87decf5 | ||
|
|
86a1beed72 | ||
|
|
e6721dab8e | ||
|
|
f1f29a089f | ||
|
|
2f0ca1e012 | ||
|
|
c57b76bf6d | ||
|
|
4c06b573ea | ||
|
|
00dbe52d84 | ||
|
|
473279d74a | ||
|
|
d0bc075e59 | ||
|
|
7a0e6ba4d8 | ||
|
|
528324253c | ||
|
|
a45f0e76b9 | ||
|
|
fd12cf198a | ||
|
|
b9beaf1a36 | ||
|
|
d57eae19b5 | ||
|
|
505ec0cf51 | ||
|
|
fb7b155368 | ||
|
|
0769b16dd6 | ||
|
|
024c01b46d | ||
|
|
94883dac1e | ||
|
|
401769f5cd | ||
|
|
55eb293e11 | ||
|
|
fed7212a3f | ||
|
|
21367e0fc9 | ||
|
|
52086be24d | ||
|
|
f0551265c4 | ||
|
|
889a6c19dd | ||
|
|
688f5fee4e | ||
|
|
4f5427093c | ||
|
|
e415516b00 | ||
|
|
a154653e9a | ||
|
|
61e60fc312 | ||
|
|
e4f5a6bab4 | ||
|
|
bf03b1c8e7 | ||
|
|
c8cea16e7d | ||
|
|
5961e1a679 | ||
|
|
b4e7287de9 | ||
|
|
818b385f2d | ||
|
|
9813064674 | ||
|
|
20592332a2 | ||
|
|
ebf58e039b | ||
|
|
dd12533a48 | ||
|
|
dad3482096 | ||
|
|
69e352d5ec | ||
|
|
7adfa819df | ||
|
|
7b03f6db63 | ||
|
|
72696397ec | ||
|
|
c3a2d2e9f0 | ||
|
|
bb9b5caa55 | ||
|
|
ce71095060 | ||
|
|
b54dd8d944 | ||
|
|
376da6f072 | ||
|
|
f396d2b232 | ||
|
|
6c3a8e5e4a | ||
|
|
d9eb01e5c0 | ||
|
|
a9a79b2705 | ||
|
|
29ebfd5e26 | ||
|
|
cf6b39e262 | ||
|
|
225bad07cc | ||
|
|
336d8f8b8a | ||
|
|
ad98464446 | ||
|
|
94fe902d41 | ||
|
|
b86fc98377 | ||
|
|
fa67733416 | ||
|
|
63e972a774 | ||
|
|
a1434e54d3 | ||
|
|
88b61cd87f | ||
|
|
4fc52128ec | ||
|
|
8788904d44 | ||
|
|
fdb02e9d65 | ||
|
|
8594351229 | ||
|
|
ae7fb1b9b2 | ||
|
|
d21978b2da | ||
|
|
35e8ec5090 | ||
|
|
8703eca5ff | ||
|
|
6be120da17 | ||
|
|
aaf17c6877 | ||
|
|
68a3fde4ed | ||
|
|
edad87b683 | ||
|
|
3be1cf5c47 | ||
|
|
ff934325c5 | ||
|
|
11ef06b1d3 | ||
|
|
d5b51e3f44 | ||
|
|
a6b0e699ea | ||
|
|
e90e081b69 | ||
|
|
8a23baf626 | ||
|
|
7f795e0b73 | ||
|
|
9fbc16df02 | ||
|
|
9da95e5e1c | ||
|
|
a27f86d1b9 | ||
|
|
26b3d714d3 | ||
|
|
afa96325fe | ||
|
|
72a803daec | ||
|
|
7ce6985fa9 | ||
|
|
12d9dd6075 | ||
|
|
cf8ec9182e | ||
|
|
060484396e | ||
|
|
94e8f72245 | ||
|
|
d359ccb39e | ||
|
|
44dcb3d987 | ||
|
|
4ebcade424 | ||
|
|
52e981fc58 | ||
|
|
722c2ffbac | ||
|
|
55e3fe53c2 | ||
|
|
34320079eb | ||
|
|
61daaa387a | ||
|
|
7fee5f382c | ||
|
|
f8be4d3e5e | ||
|
|
89207de059 | ||
|
|
14793e5eb5 | ||
|
|
488db76710 | ||
|
|
a1cefe437d | ||
|
|
506af05ebb | ||
|
|
c17ffe1010 | ||
|
|
81854375d1 | ||
|
|
154f02a1b6 | ||
|
|
786bab4e04 | ||
|
|
d045551db9 | ||
|
|
5021bdd7ae | ||
|
|
e8742eca42 | ||
|
|
a24021c2bd | ||
|
|
d145f70a42 | ||
|
|
73ad75e8b2 | ||
|
|
1da383d5eb | ||
|
|
a3893df34b | ||
|
|
6645bef22f | ||
|
|
d02d5d4df2 | ||
|
|
85b181c598 | ||
|
|
db5a1974b2 | ||
|
|
5e65b5a298 | ||
|
|
4d60281a23 | ||
|
|
0ede68d495 | ||
|
|
cba5a5a289 | ||
|
|
db5d4459d7 | ||
|
|
697e6e7fdf | ||
|
|
fccb675c3d | ||
|
|
4795c0c286 | ||
|
|
409b3b4864 | ||
|
|
94d29dafd2 | ||
|
|
bdd5bd6059 | ||
|
|
e2298bdb41 | ||
|
|
cf6844e7a1 | ||
|
|
9344aa2d16 | ||
|
|
293a5e46f1 | ||
|
|
1bc1e1ecde | ||
|
|
a99a185830 | ||
|
|
bf68225d61 | ||
|
|
fb98afed79 | ||
|
|
ca9546c567 | ||
|
|
9bb1e480f7 | ||
|
|
770e599eca | ||
|
|
64f9ebee45 | ||
|
|
333f494a20 | ||
|
|
ef5acc3f26 | ||
|
|
93cf015260 | ||
|
|
2c3fbab7a8 | ||
|
|
070d3bf572 | ||
|
|
58cdee8ccc | ||
|
|
1cb92bf3da | ||
|
|
d42a20ef14 | ||
|
|
b1f3eb9ebe | ||
|
|
3bc40c0b64 | ||
|
|
3b6d62b9e1 | ||
|
|
77a44a0f9c | ||
|
|
572800571d | ||
|
|
16b38ae257 | ||
|
|
98f5376670 | ||
|
|
bd38c094a4 | ||
|
|
5e502a7c15 | ||
|
|
203c198a84 | ||
|
|
e2427fa3c8 | ||
|
|
73bedd9b52 | ||
|
|
597bab672e | ||
|
|
9c45647793 | ||
|
|
8c65182c39 | ||
|
|
0275cb3f84 | ||
|
|
430a141dd7 | ||
|
|
96d117a9fe | ||
|
|
9925084ef4 | ||
|
|
4ae42f386d | ||
|
|
b8483b4d54 | ||
|
|
1751bf37e1 | ||
|
|
db8e020977 | ||
|
|
90c15950fb | ||
|
|
90c2e16d5d | ||
|
|
d370909e68 | ||
|
|
113dee6bd8 | ||
|
|
83bb9b1168 | ||
|
|
5fb995201a | ||
|
|
7e127f55a7 | ||
|
|
09b2c7db7e | ||
|
|
300132fb84 | ||
|
|
032fcf7801 | ||
|
|
9916f2b2c9 | ||
|
|
7f4ca8c7a5 | ||
|
|
7f5ba7cd09 | ||
|
|
025460828b | ||
|
|
32b582c5d9 | ||
|
|
008de3b43e | ||
|
|
dc29922e7c | ||
|
|
79e93c01fd | ||
|
|
bf3bf23285 | ||
|
|
8f395d3406 | ||
|
|
fce458d5ed | ||
|
|
60351ab574 | ||
|
|
e883909769 | ||
|
|
04eb2ff491 | ||
|
|
7a04a699d7 | ||
|
|
3f8b58437a | ||
|
|
9e94d8c770 | ||
|
|
f0babe7c85 | ||
|
|
c3ab1be08e | ||
|
|
28637ad684 | ||
|
|
9e8ec86e96 | ||
|
|
9ca7ea035a | ||
|
|
3bf4f16aa4 | ||
|
|
86d15d6b9a | ||
|
|
95d9272e39 | ||
|
|
47874735ec | ||
|
|
781cb8cefa | ||
|
|
6f09869a92 | ||
|
|
9b1e9f6f2f | ||
|
|
9d21855fa1 | ||
|
|
86d84e8fe4 | ||
|
|
77e21bb068 | ||
|
|
19202391ac | ||
|
|
1358018cb0 | ||
|
|
97efbec6a4 | ||
|
|
af6cab6166 | ||
|
|
1900ba3167 | ||
|
|
1adfd8febf | ||
|
|
5507236a73 | ||
|
|
569bed91ad | ||
|
|
7f1e596243 | ||
|
|
af9d284fa1 | ||
|
|
36a3592a84 | ||
|
|
910074d3aa | ||
|
|
bb30c10867 | ||
|
|
f62d224e27 | ||
|
|
4cc610d579 | ||
|
|
a967f7d594 | ||
|
|
6e3f014e24 | ||
|
|
a88ca55b04 | ||
|
|
496b4e0898 | ||
|
|
16a009fe9a | ||
|
|
113c37ee2c | ||
|
|
bdbc4d5e0d | ||
|
|
967439846f | ||
|
|
24ec450de6 | ||
|
|
d8374b7d73 | ||
|
|
ef5b156873 | ||
|
|
7f1f275f08 | ||
|
|
da41602456 | ||
|
|
ca6373d490 | ||
|
|
082ae6733b | ||
|
|
18ef15d031 | ||
|
|
81c9868460 | ||
|
|
81c8c5b6d9 | ||
|
|
7006041076 | ||
|
|
3347b52bab | ||
|
|
e291711ed4 | ||
|
|
0819b4f183 | ||
|
|
efaf90e217 | ||
|
|
b4d7541851 | ||
|
|
c05e52eda2 | ||
|
|
b426286a23 | ||
|
|
45be09354c | ||
|
|
1623aac3c8 | ||
|
|
bb9cf53673 | ||
|
|
ff887b2cd7 | ||
|
|
68c0558f65 | ||
|
|
25a1a1b601 | ||
|
|
d9f4a39c98 | ||
|
|
30d9393de4 | ||
|
|
1a410144f4 | ||
|
|
4a70a77e94 | ||
|
|
3ab840dd2f | ||
|
|
a5996fa2ca | ||
|
|
50feac35ce | ||
|
|
40ddd71bee | ||
|
|
8c49b24024 | ||
|
|
499c3162ae | ||
|
|
a86251e902 | ||
|
|
cf0837536d | ||
|
|
6dc8da51f5 | ||
|
|
44e751870a | ||
|
|
70b5103a41 | ||
|
|
66d555e20f | ||
|
|
57f94aaa4e | ||
|
|
613a16327c | ||
|
|
65970b78e5 | ||
|
|
700123a962 | ||
|
|
f9216e1746 | ||
|
|
19d22c6d74 | ||
|
|
f0e0606479 | ||
|
|
1fbe76b733 | ||
|
|
a68b779e1a | ||
|
|
ebdfe14667 | ||
|
|
8f7aa3698c | ||
|
|
237a7e93b6 | ||
|
|
a38932fe81 | ||
|
|
eee74684dc | ||
|
|
33764e9642 | ||
|
|
9fba7fd13c | ||
|
|
1c1709bea3 | ||
|
|
b64e279eaf | ||
|
|
f1c0e1179e | ||
|
|
1eb019d5ff | ||
|
|
7d5bff9b89 | ||
|
|
5db2cc0ae2 | ||
|
|
8a69110fee | ||
|
|
3be66803f7 | ||
|
|
b816b2f26d | ||
|
|
fabd8ba375 | ||
|
|
9fe9fe5757 | ||
|
|
b58d7c2e56 | ||
|
|
a2db01fd46 | ||
|
|
e005bb3670 | ||
|
|
e13796273d | ||
|
|
866599261f | ||
|
|
5892cc1579 | ||
|
|
5a923b01c7 | ||
|
|
4ecf847686 | ||
|
|
db9ccbf81a | ||
|
|
b26724b0a5 | ||
|
|
bb93ffbf0d | ||
|
|
1cc7464059 | ||
|
|
86fcb0e0b6 | ||
|
|
ebeecbff5d | ||
|
|
d137f220cc | ||
|
|
00323f99fb | ||
|
|
6237ccf8b6 | ||
|
|
f402293c99 | ||
|
|
99effad710 | ||
|
|
54cada0649 | ||
|
|
0f5d3c333a | ||
|
|
5fb55404cf | ||
|
|
2768d8e949 | ||
|
|
b0ca5a2ded | ||
|
|
697ba9e89e | ||
|
|
09df75ea0b | ||
|
|
c2e7e68619 | ||
|
|
f12d0c18c1 | ||
|
|
b55f933a32 | ||
|
|
5fa102b157 | ||
|
|
1e8a33c0c4 | ||
|
|
daef039ce5 | ||
|
|
7ad4c4052e | ||
|
|
641dc72738 | ||
|
|
cafa7ce1c5 | ||
|
|
c9a46b2b07 | ||
|
|
92ee0e405d | ||
|
|
dc6bf7accb | ||
|
|
9e5f7a81ff | ||
|
|
28ff804d5a | ||
|
|
d47fdfb57e | ||
|
|
20fb6e3223 | ||
|
|
952672946b |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
10
flanabot/bots/__init__.py
Normal 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 *
|
||||
309
flanabot/bots/btc_offers_bot.py
Normal file
309
flanabot/bots/btc_offers_bot.py
Normal 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',))
|
||||
586
flanabot/bots/connect_4_bot.py
Normal file
586
flanabot/bots/connect_4_bot.py
Normal 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
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
255
flanabot/bots/penalty_bot.py
Normal file
255
flanabot/bots/penalty_bot.py
Normal 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
287
flanabot/bots/poll_bot.py
Normal 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 -------------------- #
|
||||
# -------------------------------------------------------- #
|
||||
417
flanabot/bots/scraper_bot.py
Normal file
417
flanabot/bots/scraper_bot.py
Normal 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
368
flanabot/bots/steam_bot.py
Normal 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 -------------------- #
|
||||
# -------------------------------------------------------- #
|
||||
195
flanabot/bots/ubereats_bot.py
Normal file
195
flanabot/bots/ubereats_bot.py
Normal 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',))
|
||||
193
flanabot/bots/weather_bot.py
Normal file
193
flanabot/bots/weather_bot.py
Normal 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)
|
||||
261
flanabot/connect_4_frontend.py
Normal file
261
flanabot/connect_4_frontend.py
Normal 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)
|
||||
@@ -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...')
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
class BadRoleError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class UserDisconnectedError(Exception):
|
||||
pass
|
||||
@@ -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()
|
||||
)
|
||||
|
||||
|
||||
@@ -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 *
|
||||
|
||||
31
flanabot/models/bot_action.py
Normal file
31
flanabot/models/bot_action.py
Normal 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)
|
||||
@@ -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
19
flanabot/models/enums.py
Normal 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()
|
||||
19
flanabot/models/heating_context.py
Normal file
19
flanabot/models/heating_context.py
Normal 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
|
||||
@@ -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
12
flanabot/models/player.py
Normal 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
|
||||
27
flanabot/models/punishment.py
Normal file
27
flanabot/models/punishment.py
Normal 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()
|
||||
@@ -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
|
||||
40
flanabot/models/steam_region.py
Normal file
40
flanabot/models/steam_region.py
Normal 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')}
|
||||
@@ -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})}
|
||||
@@ -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)
|
||||
|
||||
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
@@ -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}*
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user