diff --git a/pybliotecario.ini.example b/pybliotecario.ini.example index abd74cb..c7c6e7d 100644 --- a/pybliotecario.ini.example +++ b/pybliotecario.ini.example @@ -1,8 +1,12 @@ [DEFAULT] TOKEN = -chat_id = -main_folder = /home//.pybliotecario - +# chat_id accepts also a list of IDs separated by comma, the first one is considered the main one +chat_id = +main_folder = /home//.local/share/pybliotecario +# quiet defaults to false, whether the bot should always reply back +quiet = false +# when chivato is true, the bot will write to the main chat_id whenever an user not in chat_id writes to the bot +chivato = false # [FACEBOOK] verify = diff --git a/src/pybliotecario/argument_parser.py b/src/pybliotecario/argument_parser.py index e1a1ec7..f36341c 100644 --- a/src/pybliotecario/argument_parser.py +++ b/src/pybliotecario/argument_parser.py @@ -1,6 +1,7 @@ """ Wrapper for the argument parser and the initialization """ + from argparse import Action, ArgumentParser, ArgumentTypeError import configparser import glob @@ -93,7 +94,7 @@ def configure_telegram(): token = input("Authorization token: ") # Try to fire up the bot with the given token - telegram_API = TelegramUtil(token, timeout=20) + telegram_API = TelegramUtil(token=token, timeout=20) print("Thanks, let's test this out. Say something (anything!) to your bot in telegram") for _ in range(20): # Allow for 20 tries diff --git a/src/pybliotecario/backend/basic_backend.py b/src/pybliotecario/backend/basic_backend.py index 8bb001a..43ae94d 100644 --- a/src/pybliotecario/backend/basic_backend.py +++ b/src/pybliotecario/backend/basic_backend.py @@ -8,6 +8,7 @@ """ from abc import ABC, abstractmethod +from configparser import ConfigParser import json import logging import urllib @@ -160,7 +161,14 @@ class Backend(ABC): """ - _max_size = 99999 + def __init__(self, config=None, debug=False, **kwargs): + if config is None: + # If no config is passed, generate and empty one + config = ConfigParser() + self._max_size = 99999 + self._quiet = config.getboolean("DEFAULT", "quiet", fallback=False) + self._config = config + self._debug = debug @abstractmethod def _get_updates(self, not_empty=False): @@ -175,6 +183,12 @@ def raw_updates(self): def send_message(self, text, chat, **kwargs): """Sends a message to the chat""" + def send_quiet_message(self, text, chat, **kwargs): + """Like ``send_message`` but only sends the message if quiet is set to False + otherwise, do nothing""" + if not self._quiet: + return self.send_message(text, chat, **kwargs) + @property @abstractmethod def _message_class(self): diff --git a/src/pybliotecario/backend/facebook_util.py b/src/pybliotecario/backend/facebook_util.py index 65c2f93..f8da466 100644 --- a/src/pybliotecario/backend/facebook_util.py +++ b/src/pybliotecario/backend/facebook_util.py @@ -79,20 +79,31 @@ class FacebookUtil(Backend): _message_class = FacebookMessage _max_size = MAX_SIZE - def __init__(self, PAGE_TOKEN, VERIFY_TOKEN, host="0.0.0.0", port=3000, debug=False): + def __init__(self, config=None, host="0.0.0.0", port=3000, **kwargs): + super().__init__(config, **kwargs) + if config is None: + raise ValueError("A configuration with a FACEBOOK section must be given") + + try: + fb_config = config["FACEBOOK"] + except KeyError: + raise ValueError("No facebook section found for facebook in pybliotecario.ini") + if not _HAS_FLASK: # Raise the error now - raise ModuleNotFoundError("No module named 'flask'") + raise ModuleNotFoundError("No module named 'flask', needed for Facebook backend") - self.page_access_token = PAGE_TOKEN - self.verify_token = VERIFY_TOKEN + verify_token = fb_config.get("verify") + page_token = fb_config.get("app_token") + + self.page_access_token = page_token + self.verify_token = verify_token self.port = port self.host = host app = Flask(__name__) # Load the listener into the webhook endpoint app.add_url_rule("/webhook", "webhook", self.listener, methods=["POST", "GET"]) self.flask_app = app - self.debug = debug self.action_function = None self.auth = {"access_token": self.page_access_token} @@ -121,7 +132,7 @@ def act_on_updates(self, action_function, not_empty=False): opens the webhook to wait ofr updates and act on them """ self.action_function = action_function - self.flask_app.run(host=self.host, port=self.port, debug=self.debug) + self.flask_app.run(host=self.host, port=self.port, debug=self._debug) def _get_updates(self, not_empty=False): """This class skips get_updates and uses act_on_updates directly""" @@ -184,8 +195,12 @@ def send_file(self, filepath, chat): if __name__ == "__main__": + from configparser import ConfigParser + logger.info("Testing FB Util") verify = "your_verify_token" app_token = "your_app_key" - fb_util = FacebookUtil(app_token, verify, debug=True) + config = ConfigParser() + config["FACEBOOK"] = {"verify": verify, "app_token": app_token} + fb_util = FacebookUtil(config, debug=True) fb_util.act_on_updates(lambda x: print(x)) diff --git a/src/pybliotecario/backend/telegram_util.py b/src/pybliotecario/backend/telegram_util.py index 1cb804d..9ec2ab3 100644 --- a/src/pybliotecario/backend/telegram_util.py +++ b/src/pybliotecario/backend/telegram_util.py @@ -106,13 +106,18 @@ class TelegramUtil(Backend): _message_class = TelegramMessage - def __init__(self, TOKEN, debug=False, timeout=300): + def __init__(self, config=None, token=None, timeout=300, **kwargs): + super().__init__(config, **kwargs) + if token is None: + if config is None: + raise ValueError("Either a config or a token must be provided for Telegram") + token = config.defaults().get("token") + self.offset = None - self.debug = debug self.timeout = timeout # Build app the API urls - base_URL = TELEGRAM_URL + f"bot{TOKEN}/" - self.base_fileURL = TELEGRAM_URL + f"file/bot{TOKEN}/" + base_URL = TELEGRAM_URL + f"bot{token}/" + self.base_fileURL = TELEGRAM_URL + f"file/bot{token}/" self.send_msg = base_URL + "sendMessage" self.send_img = base_URL + "sendPhoto" self.send_doc = base_URL + "sendDocument" @@ -185,7 +190,7 @@ def _get_updates(self, not_empty=False): if not updates and not_empty: return self._get_updates(not_empty=True) - if self.debug: + if self._debug: logger.info("Request url: %s", url) logger.info("Obtained updates: %s", updates) @@ -254,7 +259,7 @@ def download_file(self, file_id, file_path): if __name__ == "__main__": logger.info("Testing TelegramUtil") token = "must put a token here to test" - ut = TelegramUtil(token, debug=True) + ut = TelegramUtil(token=token, debug=True) # noinspection PyProtectedMember results = ut._get_updates() for res in results: diff --git a/src/pybliotecario/components/component_core.py b/src/pybliotecario/components/component_core.py index 45b07b8..f78b38d 100644 --- a/src/pybliotecario/components/component_core.py +++ b/src/pybliotecario/components/component_core.py @@ -11,6 +11,7 @@ the class Component will just pass the text of the msg (or the command) to the `act_on_command` or `act_on_message` methods. """ + import logging import os import sys @@ -138,11 +139,15 @@ def act_on_command(self, content=None): self.telegram.send_message("Command line argument invoked", self.chat_id) # Some useful wrappers - def send_msg(self, msg, chat_id=None, markdown=False): + def send_msg(self, msg, chat_id=None, markdown=False, quiet=False): """Wrapper around API send_msg, if chat_id is not defined - it will use the chat_id this class was instantiated to""" + it will use the chat_id this class was instantiated to. + If ``quiet`` == True, use `send_quiet_message` + """ if chat_id is None: chat_id = self.interaction_chat + if quiet: + return self.telegram.send_quiet_message(msg, chat_id, markdown=markdown) return self.telegram.send_message(msg, chat_id, markdown=markdown) def send_img(self, imgpath, chat_id=None, delete=False): @@ -168,3 +173,7 @@ def send_file(self, filepath, chat_id=None, delete=False): self.telegram.send_file(filepath, chat_id) if delete: os.remove(filepath) + + def _not_allowed_msg(self, chat_id=None): + """Tell the calling ID they are not allowed to use this component""" + return self.send_msg("You are not allowed to use this", quiet=True, chat_id=chat_id) diff --git a/src/pybliotecario/components/dnd.py b/src/pybliotecario/components/dnd.py index ed66968..1dedb1f 100644 --- a/src/pybliotecario/components/dnd.py +++ b/src/pybliotecario/components/dnd.py @@ -1,6 +1,7 @@ """ Module implementing some functions useful for playing DnD over the internet """ + import logging from random import randint import re diff --git a/src/pybliotecario/components/github_component.py b/src/pybliotecario/components/github_component.py index 7b301a9..1458423 100644 --- a/src/pybliotecario/components/github_component.py +++ b/src/pybliotecario/components/github_component.py @@ -5,6 +5,7 @@ https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token and follow the instructions """ + import datetime import logging diff --git a/src/pybliotecario/components/ip_lookup.py b/src/pybliotecario/components/ip_lookup.py index 0a45b30..d31d269 100644 --- a/src/pybliotecario/components/ip_lookup.py +++ b/src/pybliotecario/components/ip_lookup.py @@ -1,4 +1,5 @@ """ Server-helper function to look up the current IP of the program """ + import logging import urllib.request @@ -24,10 +25,9 @@ class IpLookup(Component): def telegram_message(self, msg): """If the chat id asking is the correct one sends a msg with the current ip, otherwise fails""" - if self.check_identity(msg): - send_msg = ip_lookup() - else: - send_msg = "You are not allowed to see this" + if not self.check_identity(msg): + return self._not_allowed_msg() + send_msg = ip_lookup() self.telegram.send_message(send_msg, msg.chat_id) def cmdline_command(self, args): diff --git a/src/pybliotecario/components/photocol.py b/src/pybliotecario/components/photocol.py index f7a871e..30231a1 100644 --- a/src/pybliotecario/components/photocol.py +++ b/src/pybliotecario/components/photocol.py @@ -4,6 +4,7 @@ It's a companion to https://github.com/scarlehoff/websito/blob/master/views/foto.pug """ + from datetime import datetime import json import logging @@ -106,8 +107,7 @@ def telegram_message(self, msg): return self.send_msg("Command not understood") if not self.check_identity(msg): - self.send_msg("You are not allowed to interact with this command!") - return + return self._not_allowed_msg() if msg.command == "photocol_remove": return self._remove_from_db(msg.text) diff --git a/src/pybliotecario/components/pid.py b/src/pybliotecario/components/pid.py index 32b92ca..23db064 100644 --- a/src/pybliotecario/components/pid.py +++ b/src/pybliotecario/components/pid.py @@ -101,17 +101,16 @@ def alive(pid): return is_it_alive(pid) def telegram_message(self, msg): - if self.check_identity(msg): - pid_string = msg.text.strip() - if msg.command == "kill_pid": - if pid_string.isdigit(): - return_msg = self.kill(int(pid_string)) - else: - return_msg = f"{pid_string} is not a PID?" - elif msg.command == "is_pid_alive": - return_msg = self.alive(pid_string) + if not self.check_identity(msg): + return self._not_allowed_msg + pid_string = msg.text.strip() + if msg.command == "kill_pid": + if pid_string.isdigit(): + return_msg = self.kill(int(pid_string)) else: - return_msg = f"Command {msg.command} not understood?" + return_msg = f"{pid_string} is not a PID?" + elif msg.command == "is_pid_alive": + return_msg = self.alive(pid_string) else: - return_msg = "You are not allowed to use this" + return_msg = f"Command {msg.command} not understood?" self.send_msg(return_msg) diff --git a/src/pybliotecario/components/scripts.py b/src/pybliotecario/components/scripts.py index aea3781..6a2335e 100644 --- a/src/pybliotecario/components/scripts.py +++ b/src/pybliotecario/components/scripts.py @@ -3,6 +3,7 @@ For instant, good_morning will call the command defined in /script good_morning will call the command defined in [SCRIPTS] good_morning """ + import logging import pathlib import shlex @@ -121,7 +122,7 @@ def configure_me(cls): def telegram_message(self, msg): if not self.check_identity(msg) and not self._allow_everyone: self.blocked = True - self.send_msg("You are not allowed to run scripts here") + self._not_allowed_msg() if self.blocked: return @@ -148,7 +149,7 @@ def telegram_message(self, msg): else: cmd_list = [f"./{command_path.name}"] + script_args sp.run(cmd_list, check=True, cwd=command_path.parent) - self.send_msg("Command ran") + self.send_msg("Command ran", quiet=True) except sp.CalledProcessError: self.send_msg("Command ran but failed") else: diff --git a/src/pybliotecario/components/stocks.py b/src/pybliotecario/components/stocks.py index 4e9745d..10b953c 100644 --- a/src/pybliotecario/components/stocks.py +++ b/src/pybliotecario/components/stocks.py @@ -10,6 +10,7 @@ }, """ + import json import logging diff --git a/src/pybliotecario/components/system.py b/src/pybliotecario/components/system.py index 0afed49..16d2b93 100644 --- a/src/pybliotecario/components/system.py +++ b/src/pybliotecario/components/system.py @@ -2,6 +2,7 @@ This component contains system-commands to be run remotely """ + import subprocess as sp from pybliotecario.components.component_core import Component @@ -32,8 +33,7 @@ def __init__(self, telegram_object, **kwargs): def telegram_message(self, msg): # Allow only the main user to use system if not self.check_identity(msg): - self.send_msg("You are not allowed to run scripts here") - return + return self._not_allowed_msg() command_key = msg.text.strip() command_name = ACCEPTED_COMMANDS.get(command_key) # Check whether the command is in the accepted_commands dictionary diff --git a/src/pybliotecario/components/twitter.py b/src/pybliotecario/components/twitter.py index e858e18..5d1f109 100644 --- a/src/pybliotecario/components/twitter.py +++ b/src/pybliotecario/components/twitter.py @@ -13,6 +13,7 @@ consumer_secret = ``` """ + import logging import tweepy as tw diff --git a/src/pybliotecario/core_loop.py b/src/pybliotecario/core_loop.py index 42076e1..b8347ce 100644 --- a/src/pybliotecario/core_loop.py +++ b/src/pybliotecario/core_loop.py @@ -2,6 +2,7 @@ This module manages the core loop of the pybliotecario when it is called with daemon mode -d """ + from datetime import datetime import logging from pathlib import Path @@ -109,7 +110,7 @@ def act_on_message(message): file_path = _monthly_folder(main_folder) / file_name result = tele_api.download_file(message.file_id, file_path) if result: - tele_api.send_message("¡Archivo recibido y guardado!", chat_id) + tele_api.send_quiet_message("¡Archivo recibido y guardado!", chat_id) logger.info("File saved to %s", file_path) else: tele_api.send_message("There was some problem with this, sorry", chat_id) @@ -119,7 +120,7 @@ def act_on_message(message): # Otherwise just save the msg to the log and send a funny reply _write_to_daily_log(main_folder, message.text) random_msg = still_alive() - tele_api.send_message(random_msg, chat_id) + tele_api.send_quiet_message(random_msg, chat_id) except_counter = 0 except Exception as e: logger.error(f"This message produced an exception: {e}") diff --git a/src/pybliotecario/customconf.py b/src/pybliotecario/customconf.py index 99cc354..be56e8b 100644 --- a/src/pybliotecario/customconf.py +++ b/src/pybliotecario/customconf.py @@ -2,6 +2,7 @@ Define custom parsers for the config reader and default data/config locations """ + from configparser import ConfigParser from copy import copy from os import environ diff --git a/src/pybliotecario/on_cmd_message.py b/src/pybliotecario/on_cmd_message.py index 5b2666b..598023b 100644 --- a/src/pybliotecario/on_cmd_message.py +++ b/src/pybliotecario/on_cmd_message.py @@ -6,6 +6,7 @@ This is a design choice as this way it is not necessary to have all dependencies if you want to run only some submodules of the pybliotecario. """ + import importlib import logging diff --git a/src/pybliotecario/pybliotecario.py b/src/pybliotecario/pybliotecario.py index d722086..a092468 100755 --- a/src/pybliotecario/pybliotecario.py +++ b/src/pybliotecario/pybliotecario.py @@ -18,7 +18,7 @@ def read_config(config_file=None): - """Reads the pybliotecario config file and uploads the global configuration + """Reads the pybliotecario config file and updates the global configuration By default looks always in the default file path (in XDG_CONFIG_HOME) and the current folder """ default_file_path = default_config_path() @@ -107,19 +107,13 @@ def main(cmdline_arg=None, tele_api=None, config=None): ) sys.exit(-1) - tele_api = TelegramUtil(api_token, debug=args.debug) + tele_api = TelegramUtil(config=config, debug=args.debug) elif args.backend.lower() == "test": tele_api = TestUtil("/tmp/test_file.txt") elif args.backend.lower() == "facebook": - try: - fb_config = config["FACEBOOK"] - except KeyError: - raise ValueError("No facebook section found for facebook in pybliotecario.ini") - verify_token = fb_config.get("verify") - app_token = fb_config.get("app_token") - tele_api = FacebookUtil(app_token, verify_token, debug=args.debug) + tele_api = FacebookUtil(config=config, debug=args.debug) # Check whether we have chat id - chat_id = fb_config.get("chat_id") + chat_id = config["FACEBOOK"].get("chat_id") if chat_id is not None: config.set("DEFAULT", "chat_id", chat_id)