24 Commits

Author SHA1 Message Date
AlberLC
f6093ec01e Fix resource file resolution 2022-03-17 03:50:33 +01:00
AlberLC
99d84a5d35 Fix query place with coordinates 2022-02-21 00:58:56 +01:00
AlberLC
02d4965efa Fix CommonWords property name 2022-02-11 21:42:11 +01:00
AlberLC
e41338b5f6 Update workflows 2022-02-04 23:32:43 +01:00
AlberLC
47e371f270 Update README.rst 2022-02-02 01:46:16 +01:00
AlberLC
5746e99b3f Update README.rst 2022-02-02 01:43:17 +01:00
AlberLC
e1c14f7512 Update README.rst 2022-02-02 01:40:47 +01:00
AlberLC
503dfb4215 Update README.rst 2022-02-02 01:40:10 +01:00
AlberLC
a37c2ee1d7 Fix error message when inline weather 2022-01-31 06:22:02 +01:00
AlberLC
449df672b5 Update tests 2022-01-27 03:16:44 +01:00
AlberLC
d3b8c4f821 Update requirements.txt 2022-01-27 03:16:34 +01:00
AlberLC
acbd0e5ad1 Update requirements.txt 2022-01-24 05:01:57 +01:00
AlberLC
a0c693f5eb Improve string parse 2022-01-24 04:23:37 +01:00
AlberLC
5a500e71e3 Update Dockerfile 2022-01-23 01:35:42 +01:00
AlberLC
d6cdc1436f Add docker files 2022-01-21 20:07:46 +01:00
AlberLC
b9e61b296d Fix weather keywords 2022-01-18 22:05:20 +01:00
AlberLC
35583088ab Fix requirements.txt 2022-01-16 19:34:17 +01:00
AlberLC
73f2d4c485 Fix requirements.txt 2022-01-16 18:37:02 +01:00
AlberLC
a8cf321140 Fix requirements.txt encoding 2022-01-16 16:01:41 +01:00
AlberLC
944e473fac Fix .env load 2022-01-16 05:17:38 +01:00
AlberLC
f4ffa40263 Fix send scraping message when inline 2022-01-15 05:54:06 +01:00
AlberLC
296f0d625d Fix on_recover and bot_state_message in search_medias 2022-01-15 03:38:34 +01:00
AlberLC
a1858887de Update README.rst 2022-01-15 03:37:26 +01:00
AlberLC
641b36ee81 Update send_medias 2022-01-12 06:02:04 +01:00
12 changed files with 144 additions and 47 deletions

View File

@@ -22,10 +22,10 @@ jobs:
- name: Update setup.cfg
run: |
sed -i "
s/{project_name}/${{ github.event.repository.name }}/g;
s/{project_version}/${{ github.ref_name }}/g;
s/{author}/${{ github.repository_owner }}/g;
s/{description}/${{ github.event.repository.description }}/g
s|{project_name}|${{ github.event.repository.name }}|g;
s|{project_version}|${{ github.ref_name }}|g;
s|{author}|${{ github.repository_owner }}|g;
s|{description}|${{ github.event.repository.description }}|g
" setup.cfg
- name: Build package

10
Dockerfile Normal file
View File

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

View File

@@ -3,17 +3,35 @@ FlanaBot
|license| |project_version| |python_version|
Flanagan's bot.
Bot based on `github.com/AlberLC/multibot`_ to manage Discord, Telegram and Twitch chats, moderate them and add functionalities.
|
Installation
------------
Python 3.10 or higher is required.
.. code-block:: python
.. code-block::
pip install flanabot
|
Features
--------
- Talks to users.
- Delete message batches.
- It works both in groups and in private chats.
- Understands numbers and amounts of time textually expressed (useful for deleting a message batch or saying "flanabot ban john for one hour and 20 minutes").
- Shows interactive via buttons charts of past, current and forecast weather.
- Change user roles temporarily or forever.
- 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.
.. |license| image:: https://img.shields.io/github/license/AlberLC/flanabot?style=flat
:target: https://github.com/AlberLC/flanabot/blob/main/LICENSE
@@ -25,4 +43,6 @@ Python 3.10 or higher is required.
.. |python_version| image:: https://img.shields.io/pypi/pyversions/flanabot
:target: https://www.python.org/downloads/
:alt: PyPI - Python Version
:alt: PyPI - Python Version
.. _github.com/AlberLC/multibot: https://github.com/AlberLC/multibot

17
docker-compose.yaml Normal file
View File

@@ -0,0 +1,17 @@
services:
flanabot:
image: flanaganvaquero/flanabot
build: .
env_file:
- .env
mongodb:
image: mongo
environment:
- MONGO_INITDB_ROOT_USERNAME=${MONGO_USER}
- MONGO_INITDB_ROOT_PASSWORD=${MONGO_PASSWORD}
volumes:
- mongodata:/data/db
volumes:
mongodata:

View File

@@ -3,7 +3,9 @@ import datetime
import itertools
import pprint
import random
import time as time_module
from abc import ABC
from asyncio import Future
from typing import Iterable, Iterator, Type
import flanaapis.geolocation.functions
@@ -34,7 +36,7 @@ class FlanaBot(MultiBot, ABC):
def _add_handlers(self):
super()._add_handlers()
self.register(self._on_bye, constants.KEYWORDS['bye'], min_ratio=1)
self.register(self._on_bye, constants.KEYWORDS['bye'])
self.register(self._on_config_list_show, constants.KEYWORDS['config'])
self.register(self._on_config_list_show, constants.KEYWORDS['help'])
@@ -85,6 +87,7 @@ class FlanaBot(MultiBot, ABC):
self.register(self._on_hello, constants.KEYWORDS['hello'])
self.register(self._on_mute, constants.KEYWORDS['mute'])
self.register(self._on_mute, (('haz', 'se'), constants.KEYWORDS['mute']))
self.register(self._on_mute, (constants.KEYWORDS['deactivate'], constants.KEYWORDS['unmute']))
self.register(self._on_mute, (constants.KEYWORDS['deactivate'], constants.KEYWORDS['sound']))
@@ -252,7 +255,8 @@ class FlanaBot(MultiBot, ABC):
Source.TWITTER: {MediaType.IMAGE: 0, MediaType.AUDIO: 0, MediaType.GIF: 0, MediaType.VIDEO: 0, None: 0, MediaType.ERROR: 0},
Source.INSTAGRAM: {MediaType.IMAGE: 0, MediaType.AUDIO: 0, MediaType.GIF: 0, MediaType.VIDEO: 0, None: 0, MediaType.ERROR: 0},
Source.TIKTOK: {MediaType.IMAGE: 0, MediaType.AUDIO: 0, MediaType.GIF: 0, MediaType.VIDEO: 0, None: 0, MediaType.ERROR: 0},
Source.REDDIT: {MediaType.IMAGE: 0, MediaType.AUDIO: 0, MediaType.GIF: 0, MediaType.VIDEO: 0, None: 0, MediaType.ERROR: 0}
Source.REDDIT: {MediaType.IMAGE: 0, MediaType.AUDIO: 0, MediaType.GIF: 0, MediaType.VIDEO: 0, None: 0, MediaType.ERROR: 0},
None: {MediaType.IMAGE: 0, MediaType.AUDIO: 0, MediaType.GIF: 0, MediaType.VIDEO: 0, None: 0, MediaType.ERROR: 0}
}
for media in medias:
medias_count[media.source][media.type_] += 1
@@ -278,7 +282,7 @@ class FlanaBot(MultiBot, ABC):
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.name}")
medias_sended_info.append(f"{flanautils.join_last_separator(source_medias_sended_info, ', ', ' y ')} de {source.name 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'
@@ -292,19 +296,35 @@ class FlanaBot(MultiBot, ABC):
async def _search_and_send_medias(self, message: Message, send_song_info=False) -> list[Message]:
sended_media_messages = []
if medias := await self._search_medias(message):
sended_media_messages, _ = await self.send_medias(medias, message, send_song_info)
return sended_media_messages
async def _search_medias(self, message: Message) -> OrderedSet[Media]:
results: tuple[OrderedSet[Media] | BaseException, ...] = await asyncio.gather(
bot_state_message: Message | None = None
start_time = time_module.perf_counter()
results: Future = asyncio.gather(
twitter.get_medias(message.text),
instagram.get_medias(message.text),
tiktok.get_medias(message.text),
return_exceptions=True
)
results, exceptions = flanautils.filter_exceptions(results)
if not message.is_inline and (self.is_bot_mentioned(message) or not message.chat.is_group):
while not results.done():
if constants.SCRAPING_MESSAGE_WAITING_TIME <= time_module.perf_counter() - start_time:
bot_state_message = await self.send(random.choice(constants.SCRAPING_PHRASES), message)
break
await asyncio.sleep(0.1)
await results
if bot_state_message:
await self.delete_message(bot_state_message)
results, exceptions = flanautils.filter_exceptions(results.result())
await self._manage_exceptions(exceptions, message)
@@ -445,6 +465,7 @@ class FlanaBot(MultiBot, ABC):
return
if not message_deleted_bot_action:
await self.send_error(random.choice(constants.RECOVER_PHRASES), message)
return
affected_object_ids = [affected_message_object_id for affected_message_object_id in message_deleted_bot_action.affected_objects]
@@ -452,6 +473,8 @@ class FlanaBot(MultiBot, ABC):
for deleted_message in deleted_messages:
await self.send(deleted_message.text, message)
message_deleted_bot_action.delete()
async def _on_scraping(self, message: Message, delete_original: bool = None) -> OrderedSet[Media]:
sended_media_messages = await self._search_and_send_medias(message.replied_message) if message.replied_message else OrderedSet()
sended_media_messages += await self._search_and_send_medias(message)
@@ -545,29 +568,32 @@ class FlanaBot(MultiBot, ABC):
user_names_with_at_sign = {user.name.lower() for user in message.chat.users}
user_names_without_at_sign = {user.name.lower().replace('@', '') for user in message.chat.users}
original_text_words = flanautils.remove_accents(message.text.lower())
original_text_words = original_text_words.replace(',', ' ').replace(';', ' ').replace('-', ' -')
original_text_words = flanautils.translate(
original_text_words,
{symbol: None for symbol in set(flanautils.SYMBOLS) - {'-', '.'}}
).split()
place_words = (
OrderedSet(original_text_words)
- flanautils.cartesian_product_string_matching(original_text_words, constants.KEYWORDS['show'], min_ratio=0.8).keys()
- flanautils.cartesian_product_string_matching(original_text_words, constants.KEYWORDS['weather_chart'], min_ratio=0.8).keys()
- flanautils.cartesian_product_string_matching(original_text_words, constants.KEYWORDS['date'], min_ratio=0.8).keys()
- flanautils.cartesian_product_string_matching(original_text_words, constants.KEYWORDS['thanks'], min_ratio=0.8).keys()
- flanautils.cartesian_product_string_matching(original_text_words, constants.KEYWORDS['show'], min_ratio=0.85).keys()
- flanautils.cartesian_product_string_matching(original_text_words, constants.KEYWORDS['weather_chart'], min_ratio=0.85).keys()
- flanautils.cartesian_product_string_matching(original_text_words, constants.KEYWORDS['date'], min_ratio=0.85).keys()
- flanautils.cartesian_product_string_matching(original_text_words, constants.KEYWORDS['thanks'], min_ratio=0.85).keys()
- user_names_with_at_sign
- user_names_without_at_sign
- flanautils.CommonWords.words
- flanautils.CommonWords.all_words
)
if not place_words:
await self.send_error(random.choice(('¿Tiempo dónde?', 'Indica el sitio.', 'Y el sitio?', 'y el sitio? me lo invento?')), message)
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:
await self.send_error(Media('resources/mucho_texto.png'), message)
if not message.is_inline:
await self.send_error(Media(str(flanautils.resolve_path('resources/mucho_texto.png'))), message, send_as_file=False)
return
if show_progress_state:
bot_state_message = await self.send(f'Buscando "{place_query}" en el mapa 🧐...', message)
@@ -758,6 +784,10 @@ class FlanaBot(MultiBot, ABC):
sended_info_message = await self.send(f'{message.author.name} compartió{self._medias_sended_info(medias)}', 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()

View File

@@ -13,6 +13,7 @@ PUNISHMENTS_RESET = datetime.timedelta(weeks=6 * flanautils.WEEKS_IN_A_MONTH)
RECOVERY_DELETED_MESSAGE_BEFORE = datetime.timedelta(hours=1)
TIME_THRESHOLD_TO_MANUAL_UNMUTE = datetime.timedelta(days=3)
TIME_THRESHOLD_TO_MANUAL_UNPUNISH = datetime.timedelta(days=3)
SCRAPING_MESSAGE_WAITING_TIME = 0.1
BAD_PHRASES = (
'Cállate ya anda.',
@@ -51,8 +52,8 @@ HELLO_PHRASES = ('alo', 'aloh', 'buenas', 'Hola.', 'hello', 'hey', 'hi', 'hola',
KEYWORDS = {
'activate': ('activa', 'activar', 'activate', 'deja', 'dejale', 'devuelve', 'devuelvele', 'enable', 'encender',
'enciende', 'habilita', 'habilitar'),
'bye': ('adieu', 'adio', 'adiooooo', 'adios', 'agur', 'buenas', 'bye', 'cama', 'chao', 'dias', 'farewell',
'goodbye', 'hasta', 'luego', 'noches', 'pronto', 'taluego', 'taluegorl', 'tenga', 'vemos', 'vista', 'voy'),
'bye': ('adieu', 'adio', 'adiooooo', 'adios', 'agur', 'buenas', 'bye', 'cama', 'chao', 'farewell', 'goodbye',
'hasta', 'luego', 'noches', 'pronto', 'taluego', 'taluegorl', 'tenga', 'vemos', 'vista', 'voy'),
'change': ('alter', 'alternar', 'alternate', 'cambiar', 'change', 'default', 'defecto', 'edit', 'editar',
'exchange', 'modificar', 'modify', 'permutar', 'predeterminado', 'shift', 'swap', 'switch', 'turn',
'vary'),
@@ -83,7 +84,7 @@ KEYWORDS = {
'scraping': ('api', 'aqui', 'busca', 'contenido', 'content', 'descarga', 'descargar', 'download', 'envia', 'habia',
'media', 'redes', 'scrap', 'scraping', 'search', 'send', 'social', 'sociales', 'tenia', 'video',
'videos'),
'show': ('actual', 'enseña', 'estado', 'how', 'is', 'muestra', 'show', 'como'),
'show': ('actual', 'enseña', 'estado', 'how', 'muestra', 'show', 'como'),
'song_info': ('aqui', 'cancion', 'data', 'datos', 'info', 'informacion', 'information', 'llama', 'media', 'name',
'nombre', 'sonaba', 'sonando', 'song', 'sono', 'sound', 'suena', 'title', 'titulo',
'video'),
@@ -95,6 +96,18 @@ KEYWORDS = {
'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', 've', 'ventisca',
'weather', 'wetter')
'solano', 'storm', 'sun', 'temperatura', 'tempo', 'tiempo', 'tormenta', 'ventisca', 'weather',
'wetter')
}
RECOVER_PHRASES = (
'No hay nada que recuperar.',
'Ya lo he recuperado y enviado, así que callate ya.',
'Ya lo he recuperado y enviado, así que mejor estás antento antes de dar por culo.',
'Ya lo he recuperado y enviado, no lo voy a hacer dos veces.',
'Ya lo he recuperado y enviado. A ver si leemos más y jodemos menos.',
'Ya lo he reenviado.'
)
SCRAPING_PHRASES = ('Analizando...', 'Buscando...', 'Hackeando internet... 👀', 'Rebuscando en la web...',
'Robando cosas...', 'Scrapeando...', 'Scraping...')

View File

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

View File

@@ -1,7 +1,9 @@
from dataclasses import dataclass
from dataclasses import dataclass, field
from multibot import Chat as MultiBotChat
from flanabot.models.user import User
@dataclass(eq=False)
class Chat(MultiBotChat):
@@ -11,6 +13,7 @@ class Chat(MultiBotChat):
'auto_delete_original': True,
'auto_scraping': True,
'auto_weather_chart': True}
users: list[User] = field(default_factory=list)
def __post_init__(self):
super().__post_init__()

View File

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

Binary file not shown.

0
tests/unit/__init__.py Normal file
View File

View File

@@ -1,16 +1,21 @@
import os
import flanautils
os.environ |= flanautils.find_environment_variables('../.env')
import unittest
from typing import Iterable
from flanabot import constants
import flanautils
from flanabot.bots.flana_bots.flana_tele_bot import FlanaTeleBot
from multibot import constants as multibot_constants
from flanabot.bots.flana_tele_bot import FlanaTeleBot
class TestParseCallbacks(unittest.TestCase):
def _test_no_always_callbacks(self, phrases: Iterable[str], callback: callable):
for i, phrase in enumerate(phrases):
with self.subTest(phrase):
callbacks = [registered_callback.callback for registered_callback in self.flana_tele_bot._parse_callbacks(phrase, constants.RATIO_REWARD_EXPONENT, constants.KEYWORDS_LENGHT_PENALTY, constants.MINIMUM_RATIO_TO_MATCH)
callbacks = [registered_callback.callback for registered_callback in self.flana_tele_bot._parse_callbacks(phrase, multibot_constants.RATIO_REWARD_EXPONENT, multibot_constants.KEYWORDS_LENGHT_PENALTY, multibot_constants.MINIMUM_RATIO_TO_MATCH)
if not registered_callback.always]
self.assertEqual(1, len(callbacks))
self.assertEqual(callback, callbacks[0], f'\n\nExpected: {callback.__name__}\nActual: {callbacks[0].__name__}')
@@ -111,9 +116,9 @@ class TestParseCallbacks(unittest.TestCase):
def test_on_mute(self):
phrases = [
'silencia',
'silencia al pavo ese',
'calla a ese pesao',
# 'silencia',
# 'silencia al pavo ese',
# 'calla a ese pesao',
'haz que se calle',
'quitale el microfono a ese',
'quitale el micro',
@@ -124,13 +129,6 @@ class TestParseCallbacks(unittest.TestCase):
]
self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_mute)
def test_on_new_message(self):
for i in range(10):
phrase = flanautils.random_string(0, 30, n_spaces=20)
with self.subTest(phrase):
callbacks = [registered_callback.callback for registered_callback in self.flana_tele_bot._parse_callbacks(phrase)]
self.assertIn(self.flana_tele_bot._on_new_message, callbacks, f'\n\nExpected: {self.flana_tele_bot._on_new_message.__name__} in {callbacks}')
def test_on_new_message_default(self):
phrases = [
'asdqwergf',
@@ -157,7 +155,6 @@ class TestParseCallbacks(unittest.TestCase):
def test_on_punish(self):
phrases = [
'acabo con el',
'acaba con el',
'destrozalo',
'ataca',
@@ -169,8 +166,6 @@ class TestParseCallbacks(unittest.TestCase):
'castigalo',
'castiga a',
'castiga',
'banealo',
'banea',
'enseña quien manda'
]
self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_punish)
@@ -249,8 +244,7 @@ class TestParseCallbacks(unittest.TestCase):
'perdona a',
'illo quitale a @flanagan el castigo',
'quita castigo',
'devuelve los permisos',
'desbanea'
'devuelve los permisos'
]
self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_unpunish)
@@ -265,6 +259,8 @@ class TestParseCallbacks(unittest.TestCase):
'sol',
'temperatura',
'humedad',
'que tiempo hara mañana',
'que tiempo hara manana',
'que tiempo hace en malaga',
'que tiempo hace en calle larios',
'tiempo rusia',