Compare commits
280 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -7,4 +7,4 @@ 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
|
||||
@@ -45,4 +45,4 @@ Features
|
||||
:target: https://www.python.org/downloads/
|
||||
:alt: PyPI - Python Version
|
||||
|
||||
.. _github.com/AlberLC/multibot: https://github.com/AlberLC/multibot
|
||||
.. _github.com/AlberLC/multibot: https://github.com/AlberLC/multibot
|
||||
|
||||
8
flanabot/bots/__init__.py
Normal file
8
flanabot/bots/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
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.ubereats_bot import *
|
||||
from flanabot.bots.weather_bot import *
|
||||
587
flanabot/bots/connect_4_bot.py
Normal file
587
flanabot/bots/connect_4_bot.py
Normal file
@@ -0,0 +1,587 @@
|
||||
__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
|
||||
|
||||
import connect_4_frontend
|
||||
from flanabot import 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, constants.KEYWORDS['connect_4'])
|
||||
|
||||
self.register(self._on_connect_4_vs_itself, (*constants.KEYWORDS['connect_4'], *constants.KEYWORDS['self']))
|
||||
|
||||
self.register_button(self._on_connect_4_button_press, 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,32 +1,21 @@
|
||||
__all__ = ['FlanaDiscBot']
|
||||
|
||||
import asyncio
|
||||
import datetime
|
||||
import math
|
||||
import os
|
||||
from typing import Sequence
|
||||
import random
|
||||
|
||||
import discord
|
||||
import flanautils
|
||||
from multibot import BadRoleError, DiscordBot, User, constants as multibot_constants
|
||||
import pytz
|
||||
from flanautils import Media, NotFoundError, OrderedSet
|
||||
from multibot import BadRoleError, DiscordBot, Platform, Role, User, bot_mentioned, constants as multibot_constants, group
|
||||
|
||||
from flanabot import constants
|
||||
from flanabot.bots.flana_bot import FlanaBot
|
||||
from flanabot.models import Chat, Message, Punishment
|
||||
|
||||
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'
|
||||
]
|
||||
HOT_CHANNEL_ID = 493530483045564417
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------------------------------- #
|
||||
# ------------------------------------------ FLANA_DISC_BOT ------------------------------------------ #
|
||||
@@ -35,38 +24,85 @@ class FlanaDiscBot(DiscordBot, FlanaBot):
|
||||
def __init__(self):
|
||||
super().__init__(os.environ['DISCORD_BOT_TOKEN'])
|
||||
self.heating = False
|
||||
self.heat_level = 0
|
||||
self.heat_level = 0.0
|
||||
|
||||
# ----------------------------------------------------------- #
|
||||
# -------------------- PROTECTED METHODS -------------------- #
|
||||
# ----------------------------------------------------------- #
|
||||
# -------------------------------------------------------- #
|
||||
# ------------------- PROTECTED METHODS ------------------ #
|
||||
# -------------------------------------------------------- #
|
||||
def _add_handlers(self):
|
||||
super()._add_handlers()
|
||||
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')
|
||||
|
||||
# noinspection PyTypeChecker
|
||||
def _distribute_poll_buttons(self, texts: Sequence[str]) -> list[list[str]]:
|
||||
if len(texts) <= multibot_constants.DISCORD_BUTTONS_MAX:
|
||||
return flanautils.chunks(texts, 1)
|
||||
else:
|
||||
return flanautils.chunks(texts, multibot_constants.DISCORD_BUTTONS_MAX)
|
||||
self.register(self._on_audit_log, multibot_constants.KEYWORDS['audit'])
|
||||
|
||||
async def _changeable_roles(self, group_: int | str | Chat | Message) -> list[Role]:
|
||||
group_id = self.get_group_id(group_)
|
||||
r = await self.get_group_roles(group_)
|
||||
return [role for role in r 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[depends_on]['n_fires'] - channels[channel_key]['n_fires']) - firewall // 2
|
||||
if fire_score < 1:
|
||||
if not channels[channel_key]['n_fires']:
|
||||
return
|
||||
channels[channel_key]['n_fires'] -= 1
|
||||
elif fire_score == 1:
|
||||
return
|
||||
else:
|
||||
channels[channel_key]['n_fires'] += 1
|
||||
|
||||
if channels[channel_key]['n_fires']:
|
||||
new_name_ = '🔥' * channels[channel_key]['n_fires']
|
||||
else:
|
||||
new_name_ = channels[channel_key]['original_name']
|
||||
await channels[channel_key]['object'].edit(name=new_name_)
|
||||
|
||||
channels = {}
|
||||
for letter, channel_id in constants.DISCORD_HOT_CHANNEL_IDS.items():
|
||||
channel_ = flanautils.find(channel.guild.voice_channels, condition=lambda c: c.id == channel_id)
|
||||
channels[letter] = {
|
||||
'object': channel_,
|
||||
'original_name': channel_.name,
|
||||
'n_fires': 0
|
||||
}
|
||||
|
||||
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:
|
||||
else:
|
||||
if not self.heat_level:
|
||||
return
|
||||
self.heat_level -= 0.5
|
||||
else:
|
||||
if self.heat_level > len(constants.DISCORD_HEAT_NAMES) - 1:
|
||||
self.heat_level = float(int(self.heat_level))
|
||||
|
||||
if not self.heat_level.is_integer():
|
||||
continue
|
||||
|
||||
await channel.edit(name=HEAT_NAMES[int(self.heat_level)])
|
||||
i = int(self.heat_level)
|
||||
if not i:
|
||||
n_fires = 0
|
||||
new_name = channels['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['C']['n_fires'] = n_fires
|
||||
if channel.name != new_name:
|
||||
await channel.edit(name=new_name)
|
||||
|
||||
await set_fire_to('B', depends_on='C', firewall=len(channels['B']['object'].members))
|
||||
await set_fire_to('A', depends_on='B', firewall=len(channels['A']['object'].members))
|
||||
await set_fire_to('D', depends_on='C', firewall=len(channels['C']['object'].members))
|
||||
await set_fire_to('E', depends_on='D', firewall=len(channels['D']['object'].members))
|
||||
|
||||
async def _punish(self, user: int | str | User, group_: int | str | Chat | Message, message: Message = None):
|
||||
user_id = self.get_user_id(user)
|
||||
@@ -76,6 +112,15 @@ class FlanaDiscBot(DiscordBot, FlanaBot):
|
||||
except AttributeError:
|
||||
raise BadRoleError(str(self._punish))
|
||||
|
||||
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: int | str | User, group_: int | str | Chat | Message, message: Message = None):
|
||||
user_id = self.get_user_id(user)
|
||||
try:
|
||||
@@ -87,10 +132,47 @@ class FlanaDiscBot(DiscordBot, FlanaBot):
|
||||
# ---------------------------------------------- #
|
||||
# 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()
|
||||
|
||||
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
|
||||
@@ -103,12 +185,13 @@ class FlanaDiscBot(DiscordBot, FlanaBot):
|
||||
# -------------------------------------------------------- #
|
||||
# -------------------- PUBLIC METHODS -------------------- #
|
||||
# -------------------------------------------------------- #
|
||||
async def is_punished(self, user: int | str | User, group_: int | str | Chat | Message):
|
||||
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_)
|
||||
return group_id in {punishment.group_id for punishment in Punishment.find({
|
||||
|
||||
return bool(Punishment.find({
|
||||
'platform': self.platform.value,
|
||||
'user_id': user.id,
|
||||
'group_id': group_id,
|
||||
'is_active': True
|
||||
})}
|
||||
}))
|
||||
|
||||
@@ -1,32 +1,31 @@
|
||||
from __future__ import annotations # todo0 remove in 3.11
|
||||
from __future__ import annotations # todo0 remove when it's by default
|
||||
|
||||
__all__ = ['whitelisted_event', 'FlanaTeleBot']
|
||||
__all__ = ['whitelisted', 'FlanaTeleBot']
|
||||
|
||||
import functools
|
||||
import os
|
||||
from typing import Callable, Sequence
|
||||
from typing import Callable
|
||||
|
||||
import flanautils
|
||||
import telethon.tl.functions
|
||||
from flanautils import Media, OrderedSet
|
||||
from multibot import TelegramBot, find_message, user_client
|
||||
|
||||
from flanabot import constants
|
||||
from flanabot.bots.flana_bot import FlanaBot
|
||||
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):
|
||||
if message.author.id not in self.whitelist_ids:
|
||||
return
|
||||
|
||||
return await func(self, message)
|
||||
return await func(self, message, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
@@ -44,13 +43,9 @@ class FlanaTeleBot(TelegramBot, FlanaBot):
|
||||
)
|
||||
self.whitelist_ids = []
|
||||
|
||||
# ----------------------------------------------------------- #
|
||||
# -------------------- PROTECTED METHODS -------------------- #
|
||||
# ----------------------------------------------------------- #
|
||||
def _distribute_poll_buttons(self, texts: Sequence[str]) -> list[list[str]]:
|
||||
# noinspection PyTypeChecker
|
||||
return flanautils.chunks(texts, 1)
|
||||
|
||||
# -------------------------------------------------------- #
|
||||
# ------------------- PROTECTED METHODS ------------------ #
|
||||
# -------------------------------------------------------- #
|
||||
@user_client
|
||||
async def _get_contacts_ids(self) -> list[int]:
|
||||
async with self.user_client:
|
||||
@@ -58,6 +53,15 @@ class FlanaTeleBot(TelegramBot, FlanaBot):
|
||||
|
||||
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.id] + await self._get_contacts_ids()
|
||||
@@ -65,11 +69,11 @@ class FlanaTeleBot(TelegramBot, FlanaBot):
|
||||
# ---------------------------------------------- #
|
||||
# HANDLERS #
|
||||
# ---------------------------------------------- #
|
||||
@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
|
||||
@whitelisted
|
||||
async def _on_new_message_raw(self, message: Message):
|
||||
await super()._on_new_message_raw(message)
|
||||
|
||||
|
||||
194
flanabot/bots/penalty_bot.py
Normal file
194
flanabot/bots/penalty_bot.py
Normal file
@@ -0,0 +1,194 @@
|
||||
__all__ = ['PenaltyBot']
|
||||
|
||||
import asyncio
|
||||
import datetime
|
||||
from abc import ABC
|
||||
|
||||
import flanautils
|
||||
from flanautils import TimeUnits
|
||||
from multibot import MultiBot, 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, multibot_constants.KEYWORDS['ban'])
|
||||
|
||||
self.register(self._on_mute, multibot_constants.KEYWORDS['mute'])
|
||||
self.register(self._on_mute, (('haz', 'se'), multibot_constants.KEYWORDS['mute']))
|
||||
self.register(self._on_mute, (multibot_constants.KEYWORDS['deactivate'], multibot_constants.KEYWORDS['unmute']))
|
||||
self.register(self._on_mute, (multibot_constants.KEYWORDS['deactivate'], multibot_constants.KEYWORDS['sound']))
|
||||
|
||||
self.register(self._on_punish, constants.KEYWORDS['punish'])
|
||||
self.register(self._on_punish, (multibot_constants.KEYWORDS['deactivate'], constants.KEYWORDS['unpunish']))
|
||||
self.register(self._on_punish, (multibot_constants.KEYWORDS['deactivate'], multibot_constants.KEYWORDS['permission']))
|
||||
|
||||
self.register(self._on_unban, multibot_constants.KEYWORDS['unban'])
|
||||
|
||||
self.register(self._on_unmute, multibot_constants.KEYWORDS['unmute'])
|
||||
self.register(self._on_unmute, (multibot_constants.KEYWORDS['deactivate'], multibot_constants.KEYWORDS['mute']))
|
||||
self.register(self._on_unmute, (multibot_constants.KEYWORDS['activate'], multibot_constants.KEYWORDS['sound']))
|
||||
|
||||
self.register(self._on_unpunish, constants.KEYWORDS['unpunish'])
|
||||
self.register(self._on_unpunish, (multibot_constants.KEYWORDS['deactivate'], constants.KEYWORDS['punish']))
|
||||
self.register(self._on_unpunish, (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)
|
||||
await self.send(f'Castigado durante {TimeUnits(seconds=punishment_seconds).to_words()}.', message)
|
||||
|
||||
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):
|
||||
await super()._on_new_message_raw(message)
|
||||
if message.chat.config['check_flood'] and message.chat.config['punish'] and not message.is_inline:
|
||||
async with self.lock:
|
||||
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):
|
||||
await super()._on_ready()
|
||||
flanautils.do_every(constants.CHECK_PUNISHMENTS_EVERY_SECONDS, self.check_old_punishments)
|
||||
|
||||
@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, delete=False)
|
||||
if punishment.is_active:
|
||||
punishment.is_active = False
|
||||
punishment.last_update = now
|
||||
punishment.save()
|
||||
|
||||
if punishment.last_update + constants.PUNISHMENTS_RESET_TIME <= now:
|
||||
if punishment.level == 1:
|
||||
punishment.delete()
|
||||
else:
|
||||
punishment.level -= 1
|
||||
punishment.last_update = now
|
||||
punishment.save()
|
||||
|
||||
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
|
||||
):
|
||||
# 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',))
|
||||
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)
|
||||
279
flanabot/bots/poll_bot.py
Normal file
279
flanabot/bots/poll_bot.py
Normal file
@@ -0,0 +1,279 @@
|
||||
__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, admin, 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, constants.KEYWORDS['choose'], priority=2)
|
||||
self.register(self._on_choose, constants.KEYWORDS['random'], priority=2)
|
||||
self.register(self._on_choose, (constants.KEYWORDS['choose'], constants.KEYWORDS['random']), priority=2)
|
||||
|
||||
self.register(self._on_delete_all_votes, (multibot_constants.KEYWORDS['deactivate'], multibot_constants.KEYWORDS['all'], constants.KEYWORDS['vote']))
|
||||
self.register(self._on_delete_all_votes, (multibot_constants.KEYWORDS['delete'], multibot_constants.KEYWORDS['all'], constants.KEYWORDS['vote']))
|
||||
|
||||
self.register(self._on_delete_votes, (multibot_constants.KEYWORDS['deactivate'], constants.KEYWORDS['vote']))
|
||||
self.register(self._on_delete_votes, (multibot_constants.KEYWORDS['delete'], constants.KEYWORDS['vote']))
|
||||
|
||||
self.register(self._on_dice, constants.KEYWORDS['dice'])
|
||||
|
||||
self.register(self._on_poll, constants.KEYWORDS['poll'], priority=2)
|
||||
|
||||
self.register(self._on_poll_multi, (constants.KEYWORDS['poll'], constants.KEYWORDS['multiple_answer']), priority=2)
|
||||
|
||||
self.register(self._on_stop_poll, multibot_constants.KEYWORDS['deactivate'])
|
||||
self.register(self._on_stop_poll, (multibot_constants.KEYWORDS['deactivate'], constants.KEYWORDS['poll']))
|
||||
self.register(self._on_stop_poll, multibot_constants.KEYWORDS['stop'])
|
||||
self.register(self._on_stop_poll, (multibot_constants.KEYWORDS['stop'], constants.KEYWORDS['poll']))
|
||||
|
||||
self.register(self._on_voting_ban, (multibot_constants.KEYWORDS['deactivate'], multibot_constants.KEYWORDS['permission'], constants.KEYWORDS['vote']))
|
||||
|
||||
self.register(self._on_voting_unban, (multibot_constants.KEYWORDS['activate'], multibot_constants.KEYWORDS['permission'], constants.KEYWORDS['vote']))
|
||||
|
||||
self.register_button(self._on_poll_button_press, 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} {names}')
|
||||
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'],
|
||||
*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_all_votes(self, message: Message):
|
||||
await self._on_delete_votes(message, all_=True)
|
||||
|
||||
@admin(send_negative=True)
|
||||
async def _on_delete_votes(self, message: Message, all_=False):
|
||||
if not (poll_message := self._get_poll_message(message)):
|
||||
return
|
||||
|
||||
poll_data = poll_message.data['poll']
|
||||
|
||||
if all_:
|
||||
for option_name, option_votes in poll_data['votes'].items():
|
||||
poll_data['votes'][option_name].clear()
|
||||
else:
|
||||
for user in await self._find_users_to_punish(message):
|
||||
for option_name, option_votes in poll_data['votes'].items():
|
||||
poll_data['votes'][option_name] = [option_vote for option_vote in option_votes if option_vote[0] != user.id]
|
||||
|
||||
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 message.chat.is_group and not self.is_bot_mentioned(message):
|
||||
return
|
||||
|
||||
discarded_words = {*constants.KEYWORDS['poll'], *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)]:
|
||||
await self.send(
|
||||
f"Encuesta {'multirespuesta ' if is_multiple_answer else ''}en curso...",
|
||||
self.distribute_buttons(final_options, vertically=True),
|
||||
message,
|
||||
buttons_key=ButtonsGroup.POLL,
|
||||
data={
|
||||
'poll': {
|
||||
'is_active': True,
|
||||
'is_multiple_answer': is_multiple_answer,
|
||||
'votes': {option: [] for option in final_options},
|
||||
'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']:
|
||||
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((
|
||||
f'Deja de dar por culo {presser_name} que no puedes votar aqui',
|
||||
f'No es pesao {presser_name}, que no tienes permitido votar aqui',
|
||||
f'Deja de pulsar botones que no puedes votar aqui {presser_name}',
|
||||
f'{presser_name} deja de intentar votar aqui que no puedes',
|
||||
f'Te han prohibido votar aquí {presser_name}.',
|
||||
f'No puedes votar aquí, {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)
|
||||
|
||||
@admin(send_negative=True)
|
||||
async def _on_voting_ban(self, message: Message):
|
||||
if message.chat.is_group and not self.is_bot_mentioned(message) or not (poll_message := self._get_poll_message(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
|
||||
|
||||
@admin(send_negative=True)
|
||||
async def _on_voting_unban(self, message: Message):
|
||||
if message.chat.is_group and not self.is_bot_mentioned(message) or not (poll_message := self._get_poll_message(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 -------------------- #
|
||||
# -------------------------------------------------------- #
|
||||
438
flanabot/bots/scraper_bot.py
Normal file
438
flanabot/bots/scraper_bot.py
Normal file
@@ -0,0 +1,438 @@
|
||||
__all__ = ['ScraperBot']
|
||||
|
||||
import asyncio
|
||||
import datetime
|
||||
import random
|
||||
from abc import ABC
|
||||
from collections import defaultdict
|
||||
from typing import Iterable
|
||||
|
||||
import flanautils
|
||||
from flanaapis import InstagramMediaNotFoundError, RedditMediaNotFoundError, instagram, reddit, tiktok, twitter, yt_dlp_wrapper
|
||||
from flanautils import Media, MediaType, OrderedSet, return_if_first_empty
|
||||
from multibot import MultiBot, RegisteredCallback, SendError, constants as multibot_constants, owner, reply
|
||||
|
||||
from flanabot import constants
|
||||
from flanabot.models import Action, BotAction, Message
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------------------------------- #
|
||||
# --------------------------------------------- SCRAPER_BOT --------------------------------------------- #
|
||||
# ------------------------------------------------------------------------------------------------------- #
|
||||
class ScraperBot(MultiBot, ABC):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.instagram_ban_date = None
|
||||
|
||||
# -------------------------------------------------------- #
|
||||
# ------------------- PROTECTED METHODS ------------------ #
|
||||
# -------------------------------------------------------- #
|
||||
def _add_handlers(self):
|
||||
super()._add_handlers()
|
||||
|
||||
self.register(self._on_no_scraping, (multibot_constants.KEYWORDS['negate'], constants.KEYWORDS['scraping']))
|
||||
|
||||
self.register(self._on_reset_instagram_ban, (multibot_constants.KEYWORDS['delete'], 'instagram'))
|
||||
self.register(self._on_reset_instagram_ban, (multibot_constants.KEYWORDS['reset'], 'instagram'))
|
||||
|
||||
self.register(self._on_scraping, constants.KEYWORDS['scraping'])
|
||||
self.register(self._on_scraping, constants.KEYWORDS['force'])
|
||||
self.register(self._on_scraping, multibot_constants.KEYWORDS['audio'])
|
||||
self.register(lambda message: self._on_scraping(message, delete=False), (multibot_constants.KEYWORDS['negate'], multibot_constants.KEYWORDS['delete']))
|
||||
self.register(lambda message: self._on_scraping(message, delete=False), (multibot_constants.KEYWORDS['negate'], multibot_constants.KEYWORDS['message']))
|
||||
self.register(lambda message: self._on_scraping(message, delete=False), (multibot_constants.KEYWORDS['negate'], multibot_constants.KEYWORDS['delete'], multibot_constants.KEYWORDS['message']))
|
||||
|
||||
self.register(self._on_song_info, constants.KEYWORDS['song_info'])
|
||||
|
||||
@staticmethod
|
||||
async def _find_ids(text: str) -> tuple[OrderedSet[str], ...]:
|
||||
return (
|
||||
twitter.find_ids(text),
|
||||
instagram.find_ids(text),
|
||||
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] = []
|
||||
|
||||
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):
|
||||
if force:
|
||||
media_urls.append(text_part)
|
||||
elif 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)
|
||||
|
||||
tweet_ids, instagram_ids, reddit_ids, tiktok_users_and_ids, tiktok_download_urls = ids
|
||||
|
||||
try:
|
||||
reddit_medias = await reddit.get_medias(reddit_ids, 'h264', 'mp4', 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_future = asyncio.gather(
|
||||
twitter.get_medias(tweet_ids, audio_only),
|
||||
tiktok.get_medias(tiktok_users_and_ids, tiktok_download_urls, 'h264', 'mp4', force, audio_only, timeout_for_media),
|
||||
yt_dlp_wrapper.get_medias(media_urls, 'h264', 'mp4', force, audio_only, timeout_for_media),
|
||||
return_exceptions=True
|
||||
)
|
||||
|
||||
instagram_results = []
|
||||
if instagram_ids:
|
||||
can_instagram_v1 = not self.instagram_ban_date or datetime.datetime.now(datetime.timezone.utc) - self.instagram_ban_date >= constants.INSTAGRAM_BAN_SLEEP
|
||||
if can_instagram_v1:
|
||||
try:
|
||||
instagram_results = await instagram.get_medias(instagram_ids, audio_only)
|
||||
except InstagramMediaNotFoundError:
|
||||
pass
|
||||
if not instagram_results:
|
||||
try:
|
||||
instagram_results = await instagram.get_medias_v2(instagram_ids, audio_only)
|
||||
except InstagramMediaNotFoundError as e:
|
||||
if not (instagram_results := await yt_dlp_wrapper.get_medias(instagram.make_urls(instagram_ids), 'h264', 'mp4', force, audio_only, timeout_for_media)):
|
||||
exceptions.append(e)
|
||||
|
||||
if instagram_results and can_instagram_v1:
|
||||
self.instagram_ban_date = datetime.datetime.now(datetime.timezone.utc)
|
||||
await self.send('Límite de Instagram excedido.', await self.owner_chat)
|
||||
|
||||
gather_results = await gather_future
|
||||
await self.delete_message(bot_state_message)
|
||||
|
||||
gather_medias, gather_exceptions = flanautils.filter_exceptions(gather_results + instagram_results)
|
||||
await self._manage_exceptions(exceptions + gather_exceptions, message, print_traceback=True)
|
||||
|
||||
return medias | gather_medias
|
||||
|
||||
# ---------------------------------------------- #
|
||||
# HANDLERS #
|
||||
# ---------------------------------------------- #
|
||||
async def _on_no_scraping(self, message: Message):
|
||||
pass
|
||||
|
||||
async def _on_recover_message(self, message: Message):
|
||||
pass
|
||||
|
||||
@owner
|
||||
async def _on_reset_instagram_ban(self, message: Message):
|
||||
if message.chat.is_group and not self.is_bot_mentioned(message):
|
||||
return
|
||||
|
||||
self.instagram_ban_date = None
|
||||
bot_message = await self.send('Ban de Instagram reseteado.', message)
|
||||
await self.delete_message(message)
|
||||
flanautils.do_later(multibot_constants.COMMAND_MESSAGE_DURATION, self.delete_message, bot_message)
|
||||
|
||||
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(..., (('sin',), ('timeout', 'limite'))),
|
||||
RegisteredCallback(..., 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, message, reply_to=message.replied_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)
|
||||
178
flanabot/bots/ubereats_bot.py
Normal file
178
flanabot/bots/ubereats_bot.py
Normal file
@@ -0,0 +1,178 @@
|
||||
__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
|
||||
|
||||
import constants
|
||||
from flanabot.models import Chat, Message
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------------------------------- #
|
||||
# --------------------------------------------- POLL_BOT --------------------------------------------- #
|
||||
# ---------------------------------------------------------------------------------------------------- #
|
||||
class UberEatsBot(MultiBot, ABC):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.playwright: playwright.async_api.Playwright | None = None
|
||||
self.browser: playwright.async_api.Browser | None = None
|
||||
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, '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 #
|
||||
# ---------------------------------------------- #
|
||||
@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, constants.KEYWORDS['weather'])
|
||||
self.register(self._on_weather, (multibot_constants.KEYWORDS['show'], constants.KEYWORDS['weather']))
|
||||
|
||||
self.register_button(self._on_weather_button_press, 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,91 +1,87 @@
|
||||
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_PUNISHMENTS_EVERY_SECONDS = datetime.timedelta(hours=1).total_seconds()
|
||||
FLOOD_2s_LIMIT = 4
|
||||
FLOOD_7s_LIMIT = 7
|
||||
CONNECT_4_AI_DELAY_SECONDS = 1
|
||||
CONNECT_4_CENTER_COLUMN_POINTS = 2
|
||||
CONNECT_4_N_COLUMNS = 7
|
||||
CONNECT_4_N_ROWS = 6
|
||||
FLOOD_2s_LIMIT = 2
|
||||
FLOOD_7s_LIMIT = 4
|
||||
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)
|
||||
SCRAPING_MESSAGE_WAITING_TIME = 0.1
|
||||
SCRAPING_TIMEOUT_SECONDS = 10
|
||||
|
||||
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],
|
||||
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')
|
||||
INSULTS = (
|
||||
'._.',
|
||||
'aha',
|
||||
'Aléjate de mi.',
|
||||
'Ante la duda mi dedo corazón te saluda.',
|
||||
'Baneito pa ti en breve.',
|
||||
'Calla noob.',
|
||||
'Cansino.',
|
||||
'Cuentame menos.',
|
||||
'Cuentame 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.',
|
||||
'Que 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?',
|
||||
'¿Qúe quieres?',
|
||||
'¿Te callas o te callo?',
|
||||
'¿Te imaginas que me interesa?',
|
||||
'¿Te quieres callar?',
|
||||
'¿Todo bien?',
|
||||
'¿Tú eres así o te dan apagones cerebrales?',
|
||||
'🖕',
|
||||
'😑',
|
||||
'🙄',
|
||||
'🤔',
|
||||
'🤨'
|
||||
)
|
||||
|
||||
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?', '🖕', '😑', '🙄', '🤔', '🤨')
|
||||
|
||||
KEYWORDS = {
|
||||
'choose': ('choose', 'elige'),
|
||||
'choose': ('choose', 'elige', 'escoge'),
|
||||
'connect_4': (('conecta', 'connect', 'ralla', 'raya'), ('4', 'cuatro', 'four')),
|
||||
'covid_chart': ('case', 'caso', 'contagiado', 'contagio', 'corona', 'coronavirus', 'covid', 'covid19', 'death',
|
||||
'disease', 'enfermedad', 'enfermos', 'fallecido', 'incidencia', 'jacovid', 'mascarilla', 'muerte',
|
||||
'muerto', 'pandemia', 'sick', 'virus'),
|
||||
@@ -93,25 +89,27 @@ KEYWORDS = {
|
||||
'cryptomoneda', 'cryptocurrency', 'currency', 'dinero', 'divisa', 'ethereum', 'inversion',
|
||||
'moneda', 'pasta'),
|
||||
'dice': ('dado', 'dice'),
|
||||
'poll': ('encuesta', 'poll', 'quiz'),
|
||||
'punish': ('acaba', 'aprende', 'ataca', 'atalo', 'azota', 'boss', 'castiga', 'castigo', 'condena', 'controla',
|
||||
'destroy', 'destroza', 'duro', 'ejecuta', 'enseña', 'escarmiento', 'execute', 'fuck', 'fusila', 'hell',
|
||||
'humos', 'infierno', 'jefe', 'jode', 'learn', 'leccion', 'lesson', 'manda', 'purgatorio', 'sancion',
|
||||
'shoot', 'teach', 'whip'),
|
||||
'force': ('force', 'forzar', 'fuerza'),
|
||||
'multiple_answer': ('multi', 'multi-answer', 'multiple', 'multirespuesta'),
|
||||
'poll': ('encuesta', 'quiz'),
|
||||
'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'),
|
||||
'random': ('aleatorio', 'azar', 'random'),
|
||||
'scraping': ('api', 'aqui', 'busca', 'contenido', 'content', 'descarga', 'descargar', 'download', 'envia', 'habia',
|
||||
'media', 'redes', 'scrap', 'scraping', 'search', 'send', 'social', 'sociales', 'tenia', 'video',
|
||||
'videos'),
|
||||
'song_info': ('aqui', 'cancion', 'data', 'datos', 'info', 'informacion', 'information', 'llama', 'media', 'name',
|
||||
'nombre', 'sonaba', 'sonando', 'song', 'sono', 'sound', 'suena', 'title', 'titulo',
|
||||
'video'),
|
||||
'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'),
|
||||
'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')
|
||||
'vote': ('votacion', 'votar', 'vote', 'voting', '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...',
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
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()
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
from flanabot.models.bot_action import *
|
||||
from flanabot.models.chat import *
|
||||
from flanabot.models.enums import *
|
||||
from flanabot.models.message import *
|
||||
from flanabot.models.player import *
|
||||
from flanabot.models.punishment 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,19 +1,24 @@
|
||||
__all__ = ['Chat']
|
||||
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from multibot.models import Chat as MultiBotChat
|
||||
from multibot import Chat as MultiBotChat
|
||||
|
||||
|
||||
@dataclass(eq=False)
|
||||
class Chat(MultiBotChat):
|
||||
DEFAULT_CONFIG = {'auto_covid_chart': True,
|
||||
'auto_currency_chart': True,
|
||||
'auto_delete_original': True,
|
||||
'auto_insult': True,
|
||||
'auto_scraping': True,
|
||||
'auto_weather_chart': True}
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
self.config = self.DEFAULT_CONFIG | self.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
|
||||
})
|
||||
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()
|
||||
@@ -1,22 +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.models import Message as MultiBotMessage, User
|
||||
from multibot import Message as MultiBotMessage, User
|
||||
|
||||
from flanabot.models.chat import Chat
|
||||
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
|
||||
@@ -1,32 +1,18 @@
|
||||
__all__ = ['Punishment']
|
||||
|
||||
import datetime
|
||||
from dataclasses import dataclass
|
||||
from typing import Callable
|
||||
from typing import Any
|
||||
|
||||
from multibot.models import Platform, PunishmentBase, db
|
||||
|
||||
from flanabot import constants
|
||||
from multibot import Penalty
|
||||
|
||||
|
||||
@dataclass(eq=False)
|
||||
class Punishment(PunishmentBase):
|
||||
collection = db.punishment
|
||||
class Punishment(Penalty):
|
||||
collection_name = 'punishment'
|
||||
|
||||
@classmethod
|
||||
async def check_olds(cls, unpunishment_method: Callable, platform: Platform):
|
||||
punishment_groups = cls._get_grouped_punishments(platform)
|
||||
level: int = 0
|
||||
|
||||
now = datetime.datetime.now(datetime.timezone.utc)
|
||||
for (_, _), sorted_punishments in punishment_groups:
|
||||
if not (last_punishment := sorted_punishments[-1]).until or now < last_punishment.until:
|
||||
continue
|
||||
|
||||
if last_punishment.until + constants.PUNISHMENTS_RESET <= now:
|
||||
for old_punishment in sorted_punishments:
|
||||
old_punishment.delete()
|
||||
|
||||
if last_punishment.is_active:
|
||||
await last_punishment.unpunish(unpunishment_method)
|
||||
last_punishment.is_active = False
|
||||
last_punishment.save()
|
||||
def _mongo_repr(self) -> Any:
|
||||
self_vars = super()._mongo_repr()
|
||||
self_vars['level'] = self.level
|
||||
return self_vars
|
||||
|
||||
103
requirements.txt
103
requirements.txt
@@ -1,66 +1,57 @@
|
||||
aiohttp==3.8.1
|
||||
aiosignal==1.2.0
|
||||
anyio==3.5.0
|
||||
asgiref==3.4.1
|
||||
async-generator==1.10
|
||||
aiohttp==3.8.3
|
||||
aiosignal==1.3.1
|
||||
anyio==3.6.2
|
||||
async-timeout==4.0.2
|
||||
attrs==21.4.0
|
||||
beautifulsoup4==4.10.0
|
||||
certifi==2021.10.8
|
||||
cffi==1.15.0
|
||||
chardet==4.0.0
|
||||
charset-normalizer==2.0.10
|
||||
click==8.0.3
|
||||
colorama==0.4.4
|
||||
cryptg==0.2.post4
|
||||
cryptography==36.0.1
|
||||
discord.py @ git+https://github.com/Rapptz/discord.py@b7e25645dc68bbb828bf1ede711d098c6b183237
|
||||
fastapi==0.71.0
|
||||
attrs==22.2.0
|
||||
beautifulsoup4==4.11.1
|
||||
Brotli==1.0.9
|
||||
certifi==2022.12.7
|
||||
charset-normalizer==2.1.1
|
||||
click==8.1.3
|
||||
colorama==0.4.6
|
||||
cryptg==0.4.0
|
||||
discord.py==2.2.2
|
||||
dnspython==2.2.1
|
||||
fastapi==0.89.1
|
||||
flanaapis
|
||||
flanautils
|
||||
frozenlist==1.2.0
|
||||
greenlet==1.1.2
|
||||
h11==0.12.0
|
||||
hachoir==3.1.2
|
||||
idna==3.3
|
||||
iso8601==1.0.2
|
||||
frozenlist==1.3.3
|
||||
greenlet==2.0.1
|
||||
h11==0.14.0
|
||||
hachoir==3.2.0
|
||||
idna==3.4
|
||||
iso8601==1.1.0
|
||||
jellyfish==0.9.0
|
||||
kaleido==0.2.1
|
||||
mpmath==1.2.1
|
||||
multibot
|
||||
multidict==5.2.0
|
||||
outcome==1.1.0
|
||||
Pillow==9.0.0
|
||||
multidict==6.0.4
|
||||
mutagen==1.46.0
|
||||
Pillow==9.4.0
|
||||
playwright==1.17.2
|
||||
plotly==5.5.0
|
||||
plotly==5.11.0
|
||||
pyaes==1.6.1
|
||||
pyasn1==0.4.8
|
||||
pycparser==2.21
|
||||
pydantic==1.9.0
|
||||
pyee==8.2.2
|
||||
pymongo==4.0.1
|
||||
pyOpenSSL==21.0.0
|
||||
requests==2.27.1
|
||||
rsa==4.8
|
||||
selenium==4.1.0
|
||||
selenium-stealth==1.0.6
|
||||
six==1.16.0
|
||||
sniffio==1.2.0
|
||||
sortedcontainers==2.4.0
|
||||
soupsieve==2.3.1
|
||||
starlette==0.17.1
|
||||
sympy==1.9
|
||||
Telethon==1.24.0
|
||||
tenacity==8.0.1
|
||||
TikTokApi==4.1.0
|
||||
trio==0.19.0
|
||||
trio-websocket==0.9.2
|
||||
twitchio==2.3.0
|
||||
typing_extensions==4.0.1
|
||||
ujson==5.1.0
|
||||
urllib3==1.26.8
|
||||
uvicorn==0.17.0
|
||||
websockets==10.1
|
||||
ws4py==0.5.1
|
||||
wsproto==1.0.0
|
||||
yarl==1.7.2
|
||||
pycairo==1.23.0
|
||||
pycryptodomex==3.16.0
|
||||
pydantic==1.10.4
|
||||
pyee==9.0.4
|
||||
pymongo==4.3.3
|
||||
pytube==12.1.2
|
||||
pytz==2022.7
|
||||
requests==2.28.1
|
||||
rsa==4.9
|
||||
sniffio==1.3.0
|
||||
soupsieve==2.3.2.post1
|
||||
starlette==0.22.0
|
||||
sympy==1.11.1
|
||||
Telethon==1.27.0
|
||||
tenacity==8.1.0
|
||||
twitchio==2.5.0
|
||||
typing_extensions==4.4.0
|
||||
ujson==5.7.0
|
||||
urllib3==1.26.13
|
||||
uvicorn==0.20.0
|
||||
websockets==10.4
|
||||
yarl==1.8.2
|
||||
yt-dlp==2023.3.4
|
||||
|
||||
@@ -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}*
|
||||
|
||||
@@ -26,7 +26,7 @@ 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',
|
||||
@@ -36,80 +36,12 @@ class TestParseCallbacks(unittest.TestCase):
|
||||
'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'
|
||||
]
|
||||
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'
|
||||
]
|
||||
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)
|
||||
@@ -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