diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..cc71ac7 --- /dev/null +++ b/AUTHORS @@ -0,0 +1 @@ +Hemna diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..5875cf8 --- /dev/null +++ b/ChangeLog @@ -0,0 +1,7 @@ +CHANGES +======= + +v0.1.0 +------ + +* Initial commit diff --git a/README.rst b/README.rst index 44fc09a..1acbd71 100644 --- a/README.rst +++ b/README.rst @@ -7,43 +7,20 @@ aprsd-telegram-plugin |pre-commit| -.. |PyPI| image:: https://img.shields.io/pypi/v/aprsd-telegram-plugin.svg - :target: https://pypi.org/project/aprsd-telegram-plugin/ - :alt: PyPI -.. |Status| image:: https://img.shields.io/pypi/status/aprsd-telegram-plugin.svg - :target: https://pypi.org/project/aprsd-telegram-plugin/ - :alt: Status -.. |Python Version| image:: https://img.shields.io/pypi/pyversions/aprsd-telegram-plugin - :target: https://pypi.org/project/aprsd-telegram-plugin - :alt: Python Version -.. |License| image:: https://img.shields.io/pypi/l/aprsd-telegram-plugin - :target: https://opensource.org/licenses/MIT - :alt: License -.. |Read the Docs| image:: https://img.shields.io/readthedocs/aprsd-telegram-plugin/latest.svg?label=Read%20the%20Docs - :target: https://aprsd-telegram-plugin.readthedocs.io/ - :alt: Read the documentation at https://aprsd-telegram-plugin.readthedocs.io/ -.. |Tests| image:: https://github.com/hemna/aprsd-telegram-plugin/workflows/Tests/badge.svg - :target: https://github.com/hemna/aprsd-telegram-plugin/actions?workflow=Tests - :alt: Tests -.. |Codecov| image:: https://codecov.io/gh/hemna/aprsd-telegram-plugin/branch/main/graph/badge.svg - :target: https://codecov.io/gh/hemna/aprsd-telegram-plugin - :alt: Codecov -.. |pre-commit| image:: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white - :target: https://github.com/pre-commit/pre-commit - :alt: pre-commit - Features -------- -* TODO - +* Have a 2 way chat with users of Telegram messenger `http://telegram.org` Requirements ------------ -* TODO - +* You have to create a telegram bot and start the bot +* Telegram users have to add that bot and then /start +* Telegram user can then message the bot +* Only after a telegram user has successfully completed the above + can you then message a telegram user from an APRS enabled HAM Radio. Installation ------------ @@ -55,6 +32,22 @@ You can install *aprsd-telegram-plugin* via pip_ from PyPI_: $ pip install aprsd-telegram-plugin +Now edit your aprsd.yml config file and add the plugin + +.. code:: yaml + + aprsd: + enabled_plugins: + - aprsd_telegram_plugin.telegram.TelegramChatPlugin + + services: + telegram: + apiKey: + shortcuts: + 'wb': hemna6969 + + + Usage ----- @@ -97,3 +90,31 @@ This project was generated from `@hemna`_'s `APRSD Plugin Python Cookiecutter`_ .. github-only .. _Contributor Guide: CONTRIBUTING.rst .. _Usage: https://aprsd-telegram-plugin.readthedocs.io/en/latest/usage.html + + +.. badges + +.. |PyPI| image:: https://img.shields.io/pypi/v/aprsd-telegram-plugin.svg + :target: https://pypi.org/project/aprsd-telegram-plugin/ + :alt: PyPI +.. |Status| image:: https://img.shields.io/pypi/status/aprsd-telegram-plugin.svg + :target: https://pypi.org/project/aprsd-telegram-plugin/ + :alt: Status +.. |Python Version| image:: https://img.shields.io/pypi/pyversions/aprsd-telegram-plugin + :target: https://pypi.org/project/aprsd-telegram-plugin + :alt: Python Version +.. |License| image:: https://img.shields.io/pypi/l/aprsd-telegram-plugin + :target: https://opensource.org/licenses/MIT + :alt: License +.. |Read the Docs| image:: https://img.shields.io/readthedocs/aprsd-telegram-plugin/latest.svg?label=Read%20the%20Docs + :target: https://aprsd-telegram-plugin.readthedocs.io/ + :alt: Read the documentation at https://aprsd-telegram-plugin.readthedocs.io/ +.. |Tests| image:: https://github.com/hemna/aprsd-telegram-plugin/workflows/Tests/badge.svg + :target: https://github.com/hemna/aprsd-telegram-plugin/actions?workflow=Tests + :alt: Tests +.. |Codecov| image:: https://codecov.io/gh/hemna/aprsd-telegram-plugin/branch/main/graph/badge.svg + :target: https://codecov.io/gh/hemna/aprsd-telegram-plugin + :alt: Codecov +.. |pre-commit| image:: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white + :target: https://github.com/pre-commit/pre-commit + :alt: pre-commit diff --git a/aprsd_telegram_plugin/aprsd_telegram_plugin.py b/aprsd_telegram_plugin/aprsd_telegram_plugin.py deleted file mode 100644 index d15ee0e..0000000 --- a/aprsd_telegram_plugin/aprsd_telegram_plugin.py +++ /dev/null @@ -1,54 +0,0 @@ -import logging - -from aprsd import messaging, plugin, trace - - -LOG = logging.getLogger("APRSD") - - -class TelegramChatPlugin(plugin.APRSDRegexCommandPluginBase): - - version = "1.0" - # Look for any command that starts with w or W - command_regex = "^[wW]" - # the command is for ? - command_name = "weather" - - enabled = False - - def setup(self): - # Do some checks here? - self.enabled = True - - def create_threads(self): - """This allows you to create and return a custom APRSDThread object. - - Create a child of the aprsd.threads.APRSDThread object and return it - It will automatically get started. - - You can see an example of one here: - https://github.com/craigerl/aprsd/blob/master/aprsd/threads.py#L141 - """ - if self.enabled: - # You can create a background APRSDThread object here - # Just return it for example: - # https://github.com/hemna/aprsd-weewx-plugin/blob/master/aprsd_weewx_plugin/aprsd_weewx_plugin.py#L42-L50 - # - return [] - - @trace.trace - def process(self, packet): - - """This is called when a received packet matches self.command_regex.""" - - LOG.info("TelegramChatPlugin Plugin") - - packet.get("from") - packet.get("message_text", None) - - if self.enabled: - # Now we can process - return "some reply message" - else: - LOG.warning("TelegramChatPlugin is disabled.") - return messaging.NULL_MESSAGE diff --git a/aprsd_telegram_plugin/telegram.py b/aprsd_telegram_plugin/telegram.py new file mode 100644 index 0000000..9ead0dc --- /dev/null +++ b/aprsd_telegram_plugin/telegram.py @@ -0,0 +1,210 @@ +import datetime +import logging +import threading + +from aprsd import messaging, objectstore, plugin, threads, trace +from telegram.ext import Filters, MessageHandler, Updater + + +LOG = logging.getLogger("APRSD") + + +class TelegramUsers(objectstore.ObjectStoreMixin): + """Class to automatically store telegram user ids between starts. + + Telegram doesn't provide an API for looking up an userid from + username, so we have to save it off for better user experience. + + Unfortunately, we can't get the userid, until the telegram user + sends a message to the bot FIRST. + """ + _instance = None + data = {} + config = None + _shortcuts = {} + + def __new__(cls, *args, **kwargs): + if cls._instance is None: + cls._instance = super().__new__(cls) + cls._instance.lock = threading.Lock() + cls._instance.config = kwargs["config"] + cls._instance.data = {} + if kwargs["config"].exists("services.telegram.shortcuts"): + cls._instance._shortcuts = kwargs["config"].get("services.telegram.shortcuts") + else: + cls._instance._shortcuts = None + cls._instance._init_store() + return cls._instance + + def __getitem__(self, item): + with self.lock: + if item in self._shortcuts: + item = self._shortcuts[item] + return self.data[item] + + def __setitem__(self, item, value): + with self.lock: + self.data[item] = value + + def __delitem__(self, item): + del self.data[item] + + def __contains__(self, item): + if item in self._shortcuts: + item = self._shortcuts[item] + + if item in self.data: + return True + else: + return False + + def get_shortcuts(self): + return self._shortcuts + + +class TelegramChatPlugin(plugin.APRSDRegexCommandPluginBase): + + version = "1.0" + # Look for any command that starts with w or W + command_regex = "^[tT][gG]" + # the command is for ? + command_name = "telegram" + + enabled = False + users = None + + def help(self): + _help = [ + "telegram: Chat with a user on telegram Messenger.", + "telegram: username has to message you first." + "tg: Send tg ", + ] + return _help + + def setup(self): + self.enabled = True + # Do some checks here? + try: + self.config.check_option(["services", "telegram", "apiKey"]) + except Exception as ex: + LOG.error(f"Failed to find config telegram:apiKey {ex}") + self.enabled = False + return + + token = self.config.get("services.telegram.apiKey") + + self.users = TelegramUsers(config=self.config) + self.users.load() + + # self.bot = telegram.Bot(token=token) + # LOG.info(self.bot.get_me()) + self.updater = Updater( + token=token, + use_context=True, + persistence=False, + ) + self.dispatcher = self.updater.dispatcher + self.dispatcher.add_handler( + MessageHandler( + Filters.text & (~Filters.command), + self.message_handler, + ), + ) + + def message_handler(self, update, context): + """This is called when a telegram users texts the bot.""" + LOG.info(f"{self.__class__.__name__}: Got message {update.message.text}") + # LOG.info(f"Text {update.message.text}") + # LOG.info(f"Chat {update.message.chat}") + # LOG.info(f"From {update.message.from.username} : ") + fromcall = self.config.get("aprs.login") + tocall = self.config.get("ham.callsign") + + if update.message.chat.type == "private": + LOG.info(f"Username {update.message.chat.username} - ID {update.message.chat.id}") + message = "Telegram({}): {}".format( + update.message.chat.username, + update.message.text, + ) + self.users[update.message.chat.username] = update.message.chat.id + # LOG.debug(self.users) + # LOG.info(f"{message}") + msg = messaging.TextMessage(fromcall, tocall, message) + msg.send() + elif update.message.chat.type == "group": + group_name = "noidea" + message = "TelegramGroup({}): {}".format( + group_name, + update.message.text, + ) + msg = messaging.TextMessage(fromcall, tocall, message) + msg.send() + + def create_threads(self): + if self.enabled: + return TelegramThread(self.config, self.updater) + + @trace.trace + def process(self, packet): + """This is called when a received packet matches self.command_regex.""" + LOG.info("TelegramChatPlugin Plugin") + + from_callsign = packet.get("from") + message = packet.get("message_text", None) + + if self.enabled: + # Now we can process + # Only allow aprsd owner to use this. + mycall = self.config["ham"]["callsign"] + + # Only allow the owner of aprsd to send a tweet + if not from_callsign.startswith(mycall): + return "Unauthorized" + + # Always should have format of + # + parts = message.split(" ") + LOG.info(parts) + + if len(parts) < 3: + return "invalid request" + # parts[0] is the command + username = parts[1] + msg = " ".join(parts[2:]) + if username not in self.users: + # Unfortunately there is no way to lookup a user ID + # from a username right now. + return f"Need a message from {username} first" + + bot = self.updater.bot + bot.sendMessage( + chat_id=self.users[username], + text=msg, + ) + + return messaging.NULL_MESSAGE + else: + LOG.warning("TelegramChatPlugin is disabled.") + return messaging.NULL_MESSAGE + + +class TelegramThread(threads.APRSDThread): + def __init__(self, config, updater): + super().__init__(self.__class__.__name__) + self.config = config + self.past = datetime.datetime.now() + self.updater = updater + + def stop(self): + self.thread_stop = True + self.updater.stop() + TelegramUsers(config=self.config).save() + + def loop(self): + """We have to loop, so we can stop the thread upon CTRL-C""" + self.updater.start_polling( + timeout=2, + drop_pending_updates=True, + ) + # so we can continue looping + return True diff --git a/requirements.txt b/requirements.txt index 3c8784e..4ed0ab4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ pbr -aprsd>=2.2.0 +aprsd>=2.4.0 +python-telegram-bot