diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..59c8e7a
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,8 @@
+TELEGRAM_BOT_TOKEN=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+VERBOSITY=0.33
+LOG_LEVEL=WARNING
+POLL_INTERVAL=3
+BOT_GREETING="Hi! I'm a friendly, crazy slightly psychopath robot"
+MAX_HUMAN_USERNAME_LENGTH=100
+MAX_CHINESE_CHARS_PERCENT=0.15
+WELCOME_DELAY=330
diff --git a/.gitignore b/.gitignore
index 88aec55..e94c2fe 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,11 +1,3 @@
-*.pyc
-pycache
-myconfig.py
-virtualenv
-.idea
-.vscode
-.DS_Store
-.env
-.pytest_cache
-.python-version
bot.log
+.atico/
+.env
diff --git a/.travis.yml b/.travis.yml
index 964d029..8f1b069 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,8 +1,11 @@
dist: bionic
language: python
python:
- - 3.6
- - 3.7
+python:
+ - "3.9"
+ - "3.10"
+ - "3.11"
+ - "3.12"
install:
- pip install pipenv
- pipenv install -d
diff --git a/README.md b/README.md
index b5dc7da..532fecf 100644
--- a/README.md
+++ b/README.md
@@ -10,18 +10,19 @@ Telegram Bot made in Python to automate different tasks of [Python Canarias](htt
## Installation
Create a virtualenv for Python3 and install dependencies. In this
-example we are using pyenv:
+example we are using python -m venv:
~~~console
-$ pyenv virtualenv 3.12.4 pydeckard
-$ pyenv activate pydeckard
-$ pip install -r requirements.txt
+$ python -m venv pydeckard
+$ cd pydeckard
+$ source ./bin/activate
+$ ./bin/pip install git+https://github.com/misanram/pydeckard.git@Instalar-desde-GitHub
~~~
A developer needs to install a few more packages:
~~~console
-$ pip install -r dev-requirements.txt
+$ ./bin/pip install git+https://github.com/misanram/pydeckard.git@Instalar-desde-GitHub[dev]
~~~
Next step is to set your bot token for development:
@@ -33,13 +34,19 @@ $ echo 'TELEGRAM_BOT_TOKEN = ""' > .env
Now you can launch the bot with:
~~~console
-$ python bot.py
+$ python3 bot.py
~~~
-You can use the flag `--verbose` (or `-v') to get more information in rhe console:
+~~~systemd
+
+
+
+
+
+You can use the flag `--verbose` (or `-v') to get more information in the console:
~~~console
-$ python bot.py --verbose
+$ python3 bot.py --verbose
~~~
@@ -48,5 +55,5 @@ $ python bot.py --verbose
Use pytest:
~~~console
-$ python -m pytest
+$ python3 -m pytest
~~~
diff --git a/fabfile.py b/fabfile.py
deleted file mode 100644
index 92c8966..0000000
--- a/fabfile.py
+++ /dev/null
@@ -1,11 +0,0 @@
-from fabric.api import env, local, cd, run
-
-env.hosts = ["pythoncanarias.es"]
-
-
-def deploy():
- local("git push")
- with cd("~/pydeckard"):
- run("git pull")
- run("pipenv install")
- run("supervisorctl restart pydeckard")
diff --git a/pydeckard.service b/pydeckard.service
deleted file mode 100644
index 50a6d5c..0000000
--- a/pydeckard.service
+++ /dev/null
@@ -1,11 +0,0 @@
-[Unit]
-Description=PyDeckard
-
-[Service]
-Restart=always
-WorkingDirectory=/home/jileon/pydeckard/
-ExecStart=/home/jileon/.pyenv/versions/3.12.4/envs/pydeckard/bin/python bot.py
-
-[Install]
-WantedBy=multi-user.target
-Alias=PyDeckard.service
diff --git a/pydeckard.service.example b/pydeckard.service.example
new file mode 100644
index 0000000..c64515a
--- /dev/null
+++ b/pydeckard.service.example
@@ -0,0 +1,18 @@
+[Unit]
+Description=PyDeckard
+After=network.target
+
+[Service]
+User=el_que_ejecute_el_bot
+Group=el_que_ejecute_el_bot
+Restart=always
+WorkingDirectory=/ruta_al_entorno_virtual
+ExecStart=/ruta_al_entorno_virtual/bin/bot
+# La onfiguración del bot puede hacerse:
+ A) Colocando el archivo .env en el WorkingDirectory
+ B) Colocando el archivo .env en cualquier directorio que User pueda leer y declarandolo en
+ EnvironmentFile=/cualquier_directorio/.env
+
+[Install]
+WantedBy=multi-user.target
+Alias=PyDeckard.service
diff --git a/pydeckard/__init__.py b/pydeckard/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/bot.py b/pydeckard/bot.py
similarity index 92%
rename from bot.py
rename to pydeckard/bot.py
index 4f6a433..6607694 100644
--- a/bot.py
+++ b/pydeckard/bot.py
@@ -1,20 +1,19 @@
#!/usr/bin/enb python3
-
from datetime import datetime as DateTime
import itertools
import argparse
import logging
import sys
import time
-from logging.handlers import RotatingFileHandler
import telegram
from telegram import Update
from telegram.ext import ApplicationBuilder, filters, MessageHandler, CommandHandler, ContextTypes
from telegram.constants import ParseMode
-import config
-import utils
+
+from pydeckard import utils
+from pydeckard import config
class DeckardBot():
@@ -22,7 +21,6 @@ class DeckardBot():
def __init__(self):
self.get_options()
self.set_logger()
- self.verbose = False
self.started_at = DateTime.now()
def get_options(self):
@@ -31,22 +29,20 @@ def get_options(self):
description='PyDeckard Bot',
epilog='Text at the bottom of help',
)
- parser.add_argument('-v', '--verbose', action='store_true')
+ parser.add_argument('--setup', action='store_true', help='Start the setup wizard')
args = parser.parse_args()
- self.verbose = args.verbose
+ if args.setup:
+ utils.setup_bot()
+
def set_logger(self):
self.logger = logging.getLogger('bot')
- file_handler = RotatingFileHandler('bot.log', maxBytes=1_000_000, backupCount=5)
- console_handler = logging.NullHandler()
- if self.verbose:
- console_handler = logging.StreamHandler()
-
+ console_handler = logging.StreamHandler()
logging.basicConfig(
- level=logging.WARNING, # Pone el nivel de todos los logger a WARNING
+ level=logging.DEBUG, # Pone el nivel de todos los logger a WARNING
format='%(asctime)s [%(name)s] %(levelname)s: %(message)s',
- handlers=[file_handler,console_handler],
+ handlers=[console_handler],
force=True
)
@@ -57,6 +53,7 @@ def set_logger(self):
def trace(self, msg):
self.logger.info(msg)
+
async def command_status(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
self.trace('Received command: /status')
python_version = sys.version.split(maxsplit=1)[0]
@@ -188,6 +185,9 @@ def run(self):
application.run_polling(poll_interval=config.POLL_INTERVAL)
-if __name__ == "__main__":
+def main():
bot = DeckardBot()
bot.run()
+
+if __name__ == "__main__":
+ main()
diff --git a/config.py b/pydeckard/config.py
similarity index 97%
rename from config.py
rename to pydeckard/config.py
index e8f3ced..b15cd1c 100644
--- a/config.py
+++ b/pydeckard/config.py
@@ -103,7 +103,7 @@ def bot_replies_enabled() -> bool:
("elixir",): "BIBA ELICSÍR!! ¥",
("cobol",): "BIBA KOBOL!! 💾",
("fortran",): "BIBA FORRRTRÁN!! √",
- ("c\+\+",): "BIBA CEMASMÁS!! ⊕",
+ (r"c\+\+",): "BIBA CEMASMÁS!! ⊕",
("javascript",): "BIBA JABAESCRIP!! 🔮",
("php",): "BIBA PEACHEPÉ!.! ⛱",
("perl",): "BIBA PERRRRRL! 🐫",
@@ -121,7 +121,3 @@ def bot_replies_enabled() -> bool:
"tiempo... como lágrimas en la lluvia. Es hora de morir. 🔫",
("python", "pitón", "piton"): THE_ZEN_OF_PYTHON,
}
-
-MAXLEN_FOR_USERNAME_TO_TREAT_AS_HUMAN = 100
-
-CHINESE_CHARS_MAX_PERCENT = 0.15
diff --git a/pydeckard/utils.py b/pydeckard/utils.py
new file mode 100644
index 0000000..45a0b60
--- /dev/null
+++ b/pydeckard/utils.py
@@ -0,0 +1,288 @@
+import functools
+import datetime
+import grp
+import platform
+import pwd
+import random
+import re
+import sys
+from pathlib import Path
+from typing import Tuple, Optional, NamedTuple
+
+from telegram import User
+from pydeckard import config
+
+
+def is_chinese(c):
+ """
+ Returns True if the character passed as parameter is a Chinese one
+ """
+ num = ord(c)
+ return any(
+ (
+ 0x2E80 <= num <= 0x2FD5,
+ 0x3190 <= num <= 0x319F,
+ 0x3400 <= num <= 0x4DBF,
+ 0x4E00 <= num <= 0x9FCC,
+ 0x6300 <= num <= 0x77FF,
+ )
+ )
+
+
+def too_much_chinese_chars(s):
+ letters = list(s)
+ num_chinese_chars = sum([is_chinese(c) for c in letters])
+ percent = num_chinese_chars / len(letters)
+ # More than allowed chars are Chinese
+ return percent > config.MAX_CHINESE_CHARS_PERCENT
+
+
+def is_valid_name(user: User):
+ return len(user.first_name) <= config.MAX_HUMAN_USERNAME_LENGTH
+
+
+def is_tgmember_sect(first_name: str):
+ return "tgmember.com" in first_name.lower()
+
+
+def is_bot(user: User):
+ """
+ Returns True if a new user is a bot. So far only the length of the
+ username is checked. In the future, we can add more conditions and use a
+ score/weight of the probability of being a bot.
+
+ :param user: The new User
+ :typus user: User
+ :return: True if the new user is considered a bot (according to our rules)
+ :rtype: bool
+ """
+ # Add all the checks that you consider necessary
+ return any(
+ (
+ not is_valid_name(user),
+ too_much_chinese_chars(user.first_name),
+ is_tgmember_sect(user.first_name),
+ )
+ )
+
+
+@functools.lru_cache()
+def get_reply_regex(trigger_words: Tuple[str]):
+ """
+ Build a regex to match on the trigger words
+ """
+ pattern = "|".join([fr"\b{word}\b" for word in trigger_words])
+ return re.compile(pattern, re.IGNORECASE)
+
+
+def bot_wants_to_reply() -> bool:
+ return random.random() < config.VERBOSITY
+
+
+class BotReplySpec(NamedTuple):
+ message: str
+ trigger: str
+ reply: str
+
+
+def triggers_reply(message: str) -> Optional[BotReplySpec]:
+ for trigger_words, bot_reply in config.REPLIES.items():
+ regex = get_reply_regex(trigger_words)
+ match = regex.search(message)
+ if match is not None and bot_wants_to_reply():
+ # When a match is found, check if the bot will reply based on its
+ # reply likelihood
+ if not isinstance(bot_reply, str):
+ # If value is a list then pick random string from
+ # multiple values:
+ bot_reply = random.choice(bot_reply)
+ return BotReplySpec(message, match.group(0), bot_reply)
+ return None
+
+
+def pluralise(number: int, singular: str, plural: Optional[str] = None) -> str:
+ if plural is None:
+ plural = f"{singular}s"
+ return singular if number == 1 else plural
+
+
+def since(reference) -> str:
+ """Returns a textual description of time passed.
+
+ Parameter:
+ - reference: datetime is the datetime used to get the difference
+ ir delta.
+ """
+
+ dt = datetime.datetime.now()
+ delta = dt - reference
+ buff = []
+ days = delta.days
+ if days:
+ buff.append(f"{days} {pluralise(days, 'day')}")
+ seconds = delta.seconds
+ if seconds > 3600:
+ hours = seconds // 3600
+ buff.append(f"{hours} {pluralise(hours, 'hour')}")
+ seconds %= 3600
+ minutes = seconds // 60
+ if minutes > 0:
+ buff.append(f"{minutes} {pluralise(minutes, 'minute')}")
+ seconds %= 60
+ buff.append(f"{seconds} {pluralise(seconds, 'second')}")
+ return " ".join(buff)
+
+
+def validate_input(prompt_head, acceptable=None, typus=None):
+ """
+ This function is designed to capture the parameters that will be used to configure the bot.
+ It captures input and validates the data obtained.
+ Steps:
+ Create a text string to use as a prompt.
+ Request the input.
+ Validate the received data.
+ Return the validated data or None.
+ Parameter capture can be interrupted with Ctrl+C
+
+ Arguments
+ prompt_head (str): Start of the message to be displayed to the user.
+ acceptable (list/tuple, optional): Whitelist of values or range (min, max).
+ typus (callable): Data type to convert the input to.
+
+ Return
+ The data validated and converted to type 'typus' or None
+ """
+
+ prompt_tail = ''
+
+ if isinstance(acceptable, list):
+ prompt_tail = f" ({'/'.join(map(str, acceptable))})"
+ elif isinstance(acceptable, tuple):
+ prompt_tail = f' ({acceptable[0]}-{acceptable[1]})'
+
+ prompt = f'{prompt_head}{prompt_tail}: '
+
+ while True:
+ try:
+ data = input(prompt).strip()
+ except KeyboardInterrupt:
+ raise
+
+ if not (data and callable(typus)):
+ return None
+
+ try:
+ if typus is int:
+ data = int(data, 0)
+ elif typus is str and acceptable and all(x.isupper() for x in acceptable):
+ data = data.upper()
+ elif typus is str or typus is float:
+ data = typus(data)
+ else:
+ return None
+ except ValueError:
+ print(f'El valor debe ser de tipo {typus.__name__}')
+ continue
+
+ if isinstance(acceptable, list) and data not in acceptable:
+ print(f'El valor debe ser una de estas opciones: {'/'.join(map(str, acceptable))}')
+ continue
+
+ if isinstance(acceptable, tuple):
+ if not (acceptable[0] <= data <= acceptable[1]):
+ print(f'El valor debe estar entre {acceptable[0]} y {acceptable[1]}.')
+ continue
+
+ return data
+
+
+def setup_bot():
+ """
+ A wizard starts to configure the bot and create an automatic startup system based on the operating system.
+ It performs an input for each required configuration parameter.
+ The "parameters" list contains all the defined parameters, each as a tuple of four elements:
+ parameter name,
+ prompt for input,
+ a tuple with two values to indicate a range of allowed values OR a list with values to indicate the
+ different allowed options OR None,
+ a class to cast the value to the allowed type
+ """
+
+ root_path = Path(sys.prefix)
+ bin_path = Path(sys.executable).parent
+ bot_executable = bin_path / 'bot'
+ env_path = root_path / '.env'
+ system_name = platform.system()
+
+ print(f'\n--- Asistente de configuración para PyDeckard (SO: {system_name}) ---\n\n')
+
+ parameters = [('TELEGRAM_BOT_TOKEN', 'Introduzca el Token del Bot', None, str),
+ ('VERBOSITY', 'Nivel de verbosidad', (0.0, 1.0), float),
+ ('LOG_LEVEL', 'Nivel de registro de logs', ['DEBUG', 'INFO', 'WARNING', 'ERROR'], str),
+ ('POLL_INTERVAL', 'Intervalo de polling para la API de Telegram', (1, 10), int),
+ ('BOT_GREETING', 'Saludo del bot', None, str),
+ ('MAX_HUMAN_USERNAME_LENGTH', 'Longitud máxima del username', None, int),
+ ('MAX_CHINESE_CHARS_PERCENT', 'Máximo porcentaje de caracteres chinos en username', (0.0,
+ 1.0), float),
+ ('WELCOME_DELAY', 'Tiempo de retardo para la bienvenida (seg)', None, int),
+ ]
+
+ try:
+ items_env = {key: validate_input(*args) for key, *args in parameters}
+ except KeyboardInterrupt:
+ print('Asistente cancelado por el usuario.')
+ sys.exit(1)
+
+ with open(env_path, 'w') as fout:
+ lines = [f'{key}={value}\n' for key, value in items_env.items() if value]
+ fout.writelines(lines)
+
+ print(f'\n\nArchivo .env creado en {root_path}')
+
+ if system_name == 'Linux':
+ stat_info = root_path.stat()
+
+ user_name = pwd.getpwuid(stat_info.st_uid).pw_name
+ group_name = grp.getgrgid(stat_info.st_gid).gr_name
+
+ service_path = root_path / 'pydeckard.service'
+
+ service_content = f"""[Unit]
+ Description=PyDeckard
+ After=network.target
+
+ [Service]
+ Type=simple
+ User={user_name}
+ Group={group_name}
+ WorkingDirectory={root_path}
+ ExecStart={bot_executable}
+ Environment=PYTHONUNBUFFERED=1
+ Restart=always
+
+ [Install]
+ WantedBy=multi-user.target
+ Alias=PyDeckard.service
+ """
+
+ with open(service_path, 'w') as f:
+ f.write(service_content)
+
+ print(f'\nArchivo pydeckard.service creado en {root_path}')
+ print(f'\nsudo cp {service_path} /etc/systemd/system/')
+ print('sudo systemctl daemon-reload')
+ print('sudo systemctl enable --now pydeckard')
+
+ sys.exit(0)
+
+ elif system_name == 'Darwin':
+ print('Entorno macOS detectado, configuración realizada, pregúntele a Apple® como arrancarlo.')
+ sys.exit(1)
+
+ elif system_name == 'Windows':
+ print('Entorno Windows detectado, configuración realizada, pregúntele a Microsoft® como arrancarlo.')
+ sys.exit(1)
+
+ elif system_name == 'Java':
+ print('Entorno Jython detectado. Usted mismo.')
+ sys.exit(1)
diff --git a/pyproject.toml b/pyproject.toml
index ecba4b1..f058c9f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,3 +1,44 @@
+[build-system]
+requires = ['setuptools>=61.0']
+build-backend = 'setuptools.build_meta'
+
+[project]
+name = 'pydeckard'
+description = 'Un bot de Telegram para el canal de Python Canarias'
+requires-python = '>=3.10'
+authors = [
+ { name = 'Python Canarias', email = 'info@pythoncanarias.es' },
+]
+classifiers = [
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.12",
+ "Operating System :: OS Independent",
+ "Intended Audience :: Developers",
+ "Topic :: Communications :: Chat",
+]
+license = "GPL-3.0-or-later"
+license-files = ["LICENSE"]
+dynamic = ['dependencies', 'readme', 'optional-dependencies']
+version="0.1.7"
+
+[project.urls]
+Homepage = "https://github.com/pythoncanarias/pydeckard.git"
+
+[tool.setuptools.dynamic]
+readme = { file = ['README.md', ] }
+dependencies = { file = 'requirements.txt' }
+
+[tool.setuptools.dynamic.optional-dependencies]
+dev = { file = ["dev-requirements.txt"] }
+
+[tool.setuptools.packages.find]
+include = ["pydeckard*"]
+where = ["."]
+
+[project.scripts]
+bot = "pydeckard.bot:main"
+
+
[tool.ruff]
line-length = 120
target-version = "py312"
diff --git a/run.sh b/run.sh
deleted file mode 100755
index 144be68..0000000
--- a/run.sh
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/bin/bash
-# Master script.
-
-cd "$(dirname "$0")"
-source ~/.pyenv/versions/3.12.4/envs/pydeckard/bin/activate
-exec python bot.py
diff --git a/test/test_bot_detection.py b/test/test_bot_detection.py
index b74cd12..51f35c0 100644
--- a/test/test_bot_detection.py
+++ b/test/test_bot_detection.py
@@ -1,6 +1,6 @@
from unittest.mock import Mock
import pytest
-import utils
+from pydeckard import utils
# testing is_chinese
diff --git a/test/test_replies.py b/test/test_replies.py
index e893a7a..2868b3d 100644
--- a/test/test_replies.py
+++ b/test/test_replies.py
@@ -1,7 +1,6 @@
import pytest
-import config
-import utils
+from pydeckard import config, utils
@pytest.fixture()
diff --git a/test/test_utils.py b/test/test_utils.py
index 54d7450..86687b8 100644
--- a/test/test_utils.py
+++ b/test/test_utils.py
@@ -1,60 +1,492 @@
#!/usr/bin/env python
-# -*- coding: utf-8 -*-
import datetime
import pytest
from freezegun import freeze_time
-import utils
+from pydeckard.utils import validate_input, since
@freeze_time("2019-05-16 13:35:16")
def test_since_second():
ref = datetime.datetime(2019, 5, 16, 13, 35, 15)
- assert utils.since(ref) == "1 second"
+ assert since(ref) == "1 second"
@freeze_time("2019-05-16 13:35:21")
def test_since_seconds():
ref = datetime.datetime(2019, 5, 16, 13, 35, 15)
- assert utils.since(ref) == "6 seconds"
+ assert since(ref) == "6 seconds"
@freeze_time("2019-05-16 13:36:21")
def test_since_minute_and_seconds():
ref = datetime.datetime(2019, 5, 16, 13, 35, 15)
- assert utils.since(ref) == "1 minute 6 seconds"
+ assert since(ref) == "1 minute 6 seconds"
@freeze_time("2019-05-16 13:37:21")
def test_since_minutes_and_seconds():
ref = datetime.datetime(2019, 5, 16, 13, 35, 15)
- assert utils.since(ref) == "2 minutes 6 seconds"
+ assert since(ref) == "2 minutes 6 seconds"
@freeze_time("2019-05-16 14:37:21")
def test_since_hour_and_minutes_and_seconds():
ref = datetime.datetime(2019, 5, 16, 13, 35, 15)
- assert utils.since(ref) == "1 hour 2 minutes 6 seconds"
+ assert since(ref) == "1 hour 2 minutes 6 seconds"
@freeze_time("2019-05-16 15:37:21")
def test_since_hours_and_minutes_and_seconds():
ref = datetime.datetime(2019, 5, 16, 13, 35, 15)
- assert utils.since(ref) == "2 hours 2 minutes 6 seconds"
+ assert since(ref) == "2 hours 2 minutes 6 seconds"
@freeze_time("2019-05-17 15:37:21")
def test_since_day_hours_and_minutes_and_seconds():
ref = datetime.datetime(2019, 5, 16, 13, 35, 15)
- assert utils.since(ref) == "1 day 2 hours 2 minutes 6 seconds"
+ assert since(ref) == "1 day 2 hours 2 minutes 6 seconds"
@freeze_time("2019-05-19 15:37:21")
def test_since_days_hours_and_minutes_and_seconds():
ref = datetime.datetime(2019, 5, 16, 13, 35, 15)
- assert utils.since(ref) == "3 days 2 hours 2 minutes 6 seconds"
+ assert since(ref) == "3 days 2 hours 2 minutes 6 seconds"
+
+
+def test_tipo_none0(monkeypatch):
+ monkeypatch.setattr('builtins.input', lambda _: '')
+
+ assert validate_input('Dato', None, None) is None
+
+
+def test_tipo_none1(monkeypatch):
+ monkeypatch.setattr('builtins.input', lambda _: '1')
+
+ assert validate_input('Dato', None, None) is None
+
+
+def test_tipo_none2(monkeypatch):
+ monkeypatch.setattr('builtins.input', lambda _: '1.0')
+
+ assert validate_input('Dato', None, None) is None
+
+
+def test_tipo_none3(monkeypatch):
+ monkeypatch.setattr('builtins.input', lambda _: 'Texto')
+
+ assert validate_input('Dato', None, None) is None
+
+
+def test_tipo_texto0(monkeypatch):
+ monkeypatch.setattr('builtins.input', lambda _: '')
+
+ assert validate_input('Dato', None, str) is None
+
+
+def test_tipo_texto1(monkeypatch):
+ monkeypatch.setattr('builtins.input', lambda _: '1')
+
+ assert validate_input('Dato', None, str) == '1'
+
+
+def test_tipo_texto2(monkeypatch):
+ monkeypatch.setattr('builtins.input', lambda _: '1.0')
+
+ assert validate_input('Dato', None, str) == '1.0'
+
+
+def test_tipo_texto3(monkeypatch):
+ monkeypatch.setattr('builtins.input', lambda _: 'Texto')
+
+ assert validate_input('Dato', None, str) == 'Texto'
+
+
+def test_tipo_int0(monkeypatch):
+ monkeypatch.setattr('builtins.input', lambda _: '')
+
+ assert validate_input('Dato', None, int) is None
+
+
+def test_tipo_int1(monkeypatch):
+ # Decimal
+ monkeypatch.setattr('builtins.input', lambda _: '1')
+
+ assert validate_input('Dato', None, int) == 1
+
+
+def test_tipo_int2(monkeypatch):
+ # Octal
+ monkeypatch.setattr('builtins.input', lambda _: '0o10')
+
+ assert validate_input('Dato', None, int) == 8
+
+
+def test_tipo_int3(monkeypatch):
+ # Hexadecimal
+ monkeypatch.setattr('builtins.input', lambda _: '0xff')
+
+ assert validate_input('Dato', None, int) == 255
+
+
+def test_tipo_int4(monkeypatch):
+ # Binario
+ monkeypatch.setattr('builtins.input', lambda _: '0b11')
+
+ assert validate_input('Dato', None, int) == 3
+
+def test_tipo_int5(monkeypatch):
+ # Separador _
+ monkeypatch.setattr('builtins.input', lambda _: '1_000_000')
+
+ assert validate_input('Dato', None, int) == 1000000
+
+
+def test_tipo_int6(monkeypatch):
+ # Cero negativo
+ monkeypatch.setattr('builtins.input', lambda _: '-0')
+
+ assert validate_input('Dato', None, int) == 0
+
+
+def test_tipo_int7(monkeypatch):
+ # Decimal con espacios
+ monkeypatch.setattr('builtins.input', lambda _: ' 10 ')
+
+ assert validate_input('Dato', None, int) == 10
+
+def test_tipo_int8(monkeypatch):
+ # Hexadecimal con espacios
+ monkeypatch.setattr('builtins.input', lambda _: ' 0xff ')
+
+ assert validate_input('Dato', None, int) == 255
+
+
+def test_tipo_int15(monkeypatch, capsys):
+ # Float
+ entradas = iter(['1.0', '1'])
+ monkeypatch.setattr("builtins.input", lambda _: next(entradas))
+
+ resultado = validate_input('Dato', None, int)
+ imprime = capsys.readouterr().out
+
+ assert 'El valor debe ser de tipo int' in imprime
+ assert resultado == 1
+
+
+def test_tipo_int16(monkeypatch, capsys):
+ # Texto
+ entradas = iter(['Texto', '1'])
+ monkeypatch.setattr("builtins.input", lambda _: next(entradas))
+
+ resultado = validate_input('Dato', None, int)
+ imprime = capsys.readouterr().out
+
+ assert 'El valor debe ser de tipo int' in imprime
+ assert resultado == 1
+
+
+def test_tipo_float0(monkeypatch):
+ monkeypatch.setattr('builtins.input', lambda _: '')
+
+ assert validate_input('Dato', None, float) is None
+
+
+def test_tipo_float1(monkeypatch):
+ monkeypatch.setattr('builtins.input', lambda _: '1')
+
+ assert validate_input('Dato', None, float) == 1.0
+
+
+def test_tipo_float2(monkeypatch):
+ monkeypatch.setattr('builtins.input', lambda _: '1.0')
+
+ assert validate_input('Dato', None, float) == 1.0
+
+
+def test_tipo_float3(monkeypatch, capsys):
+ entradas = iter(['Texto', '1.0'])
+ monkeypatch.setattr("builtins.input", lambda _: next(entradas))
+
+ resultado = validate_input('Dato', None, float)
+ imprime = capsys.readouterr().out
+
+ assert 'El valor debe ser de tipo float' in imprime
+ assert resultado == 1.0
+
+
+def test_tipo_float4(monkeypatch):
+ monkeypatch.setattr('builtins.input', lambda _: '-0.0')
+
+ assert validate_input('Dato', None, float) == -0.0
+
+
+def test_rango_tipo_int0(monkeypatch):
+ monkeypatch.setattr('builtins.input', lambda _: '')
+
+ assert validate_input('Dato', (0, 1), int) is None
+
+
+def test_rango_tipo_int1(monkeypatch):
+ # En rango entero
+ monkeypatch.setattr('builtins.input', lambda _: '1')
+
+ assert validate_input('Dato', (0, 10), int) == 1
+
+
+def test_rango_tipo_int2(monkeypatch, capsys):
+ # Fuera de rango entero
+ entradas = iter(['20', '-2', '1'])
+ monkeypatch.setattr("builtins.input", lambda _: next(entradas))
+
+ resultado = validate_input('Dato', (0, 10), int)
+ imprime = capsys.readouterr().out
+
+ assert 'Error: El valor debe estar entre 0 y 10' in imprime
+ assert resultado == 1
+
+
+def test_rango_tipo_int3(monkeypatch, capsys):
+ # En rango float
+ entradas = iter(['5.0', '1'])
+ monkeypatch.setattr("builtins.input", lambda _: next(entradas))
+
+ resultado = validate_input('Dato', (0, 10), int)
+ imprime = capsys.readouterr().out
+
+ assert 'El valor debe ser de tipo int' in imprime
+ assert resultado == 1
+
+
+def test_rango_tipo_int4(monkeypatch, capsys):
+ # Fuera de rango float
+ entradas = iter(['20.0', '-1.0', '1'])
+ monkeypatch.setattr("builtins.input", lambda _: next(entradas))
+
+ resultado = validate_input('Dato', (0, 10), int)
+ imprime = capsys.readouterr().out
+
+ assert 'El valor debe ser de tipo int' in imprime
+ assert resultado == 1
+
+
+def test_rango_tipo_int5(monkeypatch, capsys):
+ # Texto
+ entradas = iter(['texto', '1'])
+ monkeypatch.setattr("builtins.input", lambda _: next(entradas))
+
+ resultado = validate_input('Dato', (0, 10), int)
+ imprime = capsys.readouterr().out
+
+ assert 'El valor debe ser de tipo int' in imprime
+ assert resultado == 1
+
+
+def test_rango_tipo_float0(monkeypatch):
+ monkeypatch.setattr('builtins.input', lambda _: '')
+
+ assert validate_input('Dato', (0.0, 1.0), float) is None
+
+
+def test_rango_tipo_float1(monkeypatch):
+ # En rango entero
+ monkeypatch.setattr('builtins.input', lambda _: '1')
+
+ assert validate_input('Dato', (0.0, 1.0), float) == 1.0
+
+
+def test_rango_tipo_float2(monkeypatch, capsys):
+ # Fuera de rango entero
+ entradas = iter(['2', '-2','1.0'])
+ monkeypatch.setattr("builtins.input", lambda _: next(entradas))
+
+ resultado = validate_input('Dato', (0.0, 1.0), float)
+ imprime = capsys.readouterr().out
+
+ assert 'Error: El valor debe estar entre 0.0 y 1.0' in imprime
+ assert resultado == 1.0
+
+
+def test_rango_tipo_float3(monkeypatch):
+ # En rango float
+ monkeypatch.setattr('builtins.input', lambda _: '1.0')
+
+ assert validate_input('Dato', (0.0, 1.0), float) == 1.0
+
+
+def test_rango_tipo_float4(monkeypatch, capsys):
+ # Fuera de rango float
+ entradas = iter(['2.0', '-1.0', '1.0'])
+ monkeypatch.setattr("builtins.input", lambda _: next(entradas))
+
+ resultado = validate_input('Dato', (0.0, 1.0), float)
+ imprime = capsys.readouterr().out
+
+ assert 'Error: El valor debe estar entre 0.0 y 1.0' in imprime
+ assert resultado == 1.0
+
+
+def test_rango_tipo_float5(monkeypatch, capsys):
+ # Texto
+ entradas = iter(['texto', '1.0'])
+ monkeypatch.setattr("builtins.input", lambda _: next(entradas))
+
+ resultado = validate_input('Dato', (0.0, 1.0), float)
+ imprime = capsys.readouterr().out
+
+ assert 'El valor debe ser de tipo float' in imprime
+ assert resultado == 1.0
+
+
+def test_option_tipo_int0(monkeypatch):
+ monkeypatch.setattr('builtins.input', lambda _: '')
+
+ assert validate_input('Dato', [-2, -1, 0, 1, 2, 3], int) is None
+
+
+def test_option_tipo_int1(monkeypatch):
+ # En lista entero
+ monkeypatch.setattr('builtins.input', lambda _: '1')
+
+ assert validate_input('Dato', [-2, -1, 0, 1, 2, 3], int) == 1
+
+
+def test_option_tipo_int2(monkeypatch, capsys):
+ # Fuera de lista entero
+ entradas = iter(['20', '-23', '1'])
+ monkeypatch.setattr("builtins.input", lambda _: next(entradas))
+
+ resultado = validate_input('Dato', [-2, -1, 0, 1, 2, 3], int)
+ imprime = capsys.readouterr().out
+
+ assert 'Error: El valor debe ser una de estas opciones: -2/-1/0/1/2/3' in imprime
+ assert resultado == 1
+
+
+def test_option_tipo_int4(monkeypatch, capsys):
+ # Fuera de lista float
+ entradas = iter(['20.0', '-10.0', '1'])
+ monkeypatch.setattr("builtins.input", lambda _: next(entradas))
+
+ resultado = validate_input('Dato', [-2, -1, 0, 1, 2, 3], int)
+ imprime = capsys.readouterr().out
+
+ assert 'El valor debe ser de tipo int' in imprime
+ assert resultado == 1
+
+
+def test_option_tipo_int5(monkeypatch, capsys):
+ # Texto
+ entradas = iter(['texto', '1'])
+ monkeypatch.setattr("builtins.input", lambda _: next(entradas))
+
+ resultado = validate_input('Dato', [-2, -1, 0, 1, 2, 3], int)
+ imprime = capsys.readouterr().out
+
+ assert 'El valor debe ser de tipo int' in imprime
+ assert resultado == 1
+
+
+def test_option_tipo_int6(monkeypatch, capsys):
+ # En lista vacia entero
+ entradas = iter(['texto', ''])
+ monkeypatch.setattr("builtins.input", lambda _: next(entradas))
+
+ resultado = validate_input('Dato', [], int)
+ imprime = capsys.readouterr().out
+
+ assert 'El valor debe ser de tipo int' in imprime
+ assert resultado is None
+
+
+def test_option_tipo_texto0(monkeypatch):
+ monkeypatch.setattr('builtins.input', lambda _: '')
+
+ assert validate_input('Dato', ['DEBUG', 'INFO', 'WARNING', 'ERROR'], str) is None
+
+
+def test_option_tipo_texto1(monkeypatch, capsys):
+ # Entero
+ entradas = iter(['20', 'DEBUG'])
+ monkeypatch.setattr("builtins.input", lambda _: next(entradas))
+
+ resultado = validate_input('Dato', ['DEBUG', 'INFO', 'WARNING', 'ERROR'], str)
+ imprime = capsys.readouterr().out
+
+ assert 'Error: El valor debe ser una de estas opciones: DEBUG/INFO/WARNING/ERROR' in imprime
+ assert resultado == 'DEBUG'
+
+
+def test_option_tipo_texto2(monkeypatch, capsys):
+ # float
+ entradas = iter(['20.0', 'DEBUG'])
+ monkeypatch.setattr("builtins.input", lambda _: next(entradas))
+
+ resultado = validate_input('Dato', ['DEBUG', 'INFO', 'WARNING', 'ERROR'], str)
+ imprime = capsys.readouterr().out
+
+ assert 'Error: El valor debe ser una de estas opciones: DEBUG/INFO/WARNING/ERROR' in imprime
+ assert resultado == 'DEBUG'
+
+
+def test_option_tipo_texto3(monkeypatch, capsys):
+ # Fuera de lista texto
+ entradas = iter(['HOLA', 'DEBUG'])
+ monkeypatch.setattr("builtins.input", lambda _: next(entradas))
+
+ resultado = validate_input('Dato', ['DEBUG', 'INFO', 'WARNING', 'ERROR'], str)
+ imprime = capsys.readouterr().out
+
+ assert 'Error: El valor debe ser una de estas opciones: DEBUG/INFO/WARNING/ERROR' in imprime
+ assert resultado == 'DEBUG'
+
+
+def test_option_tipo_texto4(monkeypatch):
+ # Texto
+ monkeypatch.setattr('builtins.input', lambda _: 'DEBUG')
+
+ assert validate_input('Dato', ['DEBUG', 'INFO', 'WARNING', 'ERROR'], str) == 'DEBUG'
+
+
+def test_option_tipo_texto5(monkeypatch):
+ # Texto minúscula
+ monkeypatch.setattr('builtins.input', lambda _: 'debug')
+
+ assert validate_input('Dato', ['DEBUG', 'INFO', 'WARNING', 'ERROR'], str) == 'DEBUG'
+
+
+def test_option_tipo_texto6(monkeypatch):
+ # Texto mezcla minúsculas
+ monkeypatch.setattr('builtins.input', lambda _: 'DebuG')
+
+ assert validate_input('Dato', ['DEBUG', 'INFO', 'WARNING', 'ERROR'], str) == 'DEBUG'
+
+
+def test_option_tipo_texto7(monkeypatch, capsys):
+ # En lista vacia texto
+ entradas = iter(['texto', ''])
+ monkeypatch.setattr("builtins.input", lambda _: next(entradas))
+
+ resultado = validate_input('Dato', [], str)
+ imprime = capsys.readouterr().out
+
+ assert 'Error: El valor debe ser una de estas opciones: ' in imprime
+ assert resultado is None
+
+
+def test_tipo_raro0(monkeypatch):
+ monkeypatch.setattr('builtins.input', lambda _: 'texto')
+
+ assert validate_input('Dato', None, bool) is None
+
+
+def test_tipo_raro1(monkeypatch):
+ # Tipo no callable
+ monkeypatch.setattr('builtins.input', lambda _: 'texto')
+
+ assert validate_input('Dato', None, object()) is None
if __name__ == "__main__":
diff --git a/utils.py b/utils.py
deleted file mode 100644
index d0c8e4f..0000000
--- a/utils.py
+++ /dev/null
@@ -1,128 +0,0 @@
-import functools
-import datetime
-import random
-import re
-from typing import Tuple, Optional, NamedTuple
-
-from telegram import User
-import config
-
-
-def is_chinese(c):
- """
- Returns True if the character passed as parameter is a Chinese one
- """
- num = ord(c)
- return any(
- (
- 0x2E80 <= num <= 0x2FD5,
- 0x3190 <= num <= 0x319F,
- 0x3400 <= num <= 0x4DBF,
- 0x4E00 <= num <= 0x9FCC,
- 0x6300 <= num <= 0x77FF,
- )
- )
-
-
-def too_much_chinese_chars(s):
- letters = list(s)
- num_chinese_chars = sum([is_chinese(c) for c in letters])
- percent = num_chinese_chars / len(letters)
- # More than allowed chars are Chinese
- return percent > config.MAX_CHINESE_CHARS_PERCENT
-
-
-def is_valid_name(user: User):
- return len(user.first_name) <= config.MAX_HUMAN_USERNAME_LENGTH
-
-
-def is_tgmember_sect(first_name: str):
- return "tgmember.com" in first_name.lower()
-
-
-def is_bot(user: User):
- """
- Returns True if a new user is a bot. So far only the length of the
- username is checked. In the future, we can add more conditions and use a
- score/weight of the probability of being a bot.
-
- :param user: The new User
- :type user: User
- :return: True if the new user is considered a bot (according to our rules)
- :rtype: bool
- """
- # Add all the checks that you consider necessary
- return any(
- (
- not is_valid_name(user),
- too_much_chinese_chars(user.first_name),
- is_tgmember_sect(user.first_name),
- )
- )
-
-
-@functools.lru_cache()
-def get_reply_regex(trigger_words: Tuple[str]):
- """
- Build a regex to match on the trigger words
- """
- pattern = "|".join([fr"\b{word}\b" for word in trigger_words])
- return re.compile(pattern, re.IGNORECASE)
-
-
-def bot_wants_to_reply() -> bool:
- return random.random() < config.VERBOSITY
-
-
-class BotReplySpec(NamedTuple):
- message: str
- trigger: str
- reply: str
-
-
-def triggers_reply(message: str) -> Optional[BotReplySpec]:
- for trigger_words, bot_reply in config.REPLIES.items():
- regex = get_reply_regex(trigger_words)
- match = regex.search(message)
- if match is not None and bot_wants_to_reply():
- # When a match is found, check if the bot will reply based on its
- # reply likelihood
- if not isinstance(bot_reply, str):
- # If value is a list then pick random string from
- # multiple values:
- bot_reply = random.choice(bot_reply)
- return BotReplySpec(message, match.group(0), bot_reply)
- return None
-
-
-def pluralise(number: int, singular: str, plural: Optional[str] = None) -> str:
- if plural is None:
- plural = f"{singular}s"
- return singular if number == 1 else plural
-
-
-def since(reference) -> str:
- """Returns a textual description of time passed.
-
- Parameter:
- - reference: datetime is the datetime used to get the difference
- ir delta.
- """
-
- dt = datetime.datetime.now()
- delta = dt - reference
- buff = []
- days = delta.days
- if days:
- buff.append(f"{days} {pluralise(days, 'day')}")
- seconds = delta.seconds
- if seconds > 3600:
- hours = seconds // 3600
- buff.append(f"{hours} {pluralise(hours, 'hour')}")
- seconds %= 3600
- minutes = seconds // 60
- if minutes > 0:
- buff.append(f"{minutes} {pluralise(minutes, 'minute')}")
- seconds %= 60
- buff.append(f"{seconds} {pluralise(seconds, 'second')}")
- return " ".join(buff)