From b07162e059aca212de1369e9394653c754599008 Mon Sep 17 00:00:00 2001 From: Zoe Moore Date: Wed, 10 Apr 2024 18:41:03 -0700 Subject: [PATCH] Initial commit, can send but not receive --- .gitignore | 160 ++++++++++++++++++++++++++++++++++++++++++++++++++++ __init__.py | 7 +++ gateway.py | 66 ++++++++++++++++++++++ session.py | 128 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 361 insertions(+) create mode 100644 .gitignore create mode 100644 __init__.py create mode 100644 gateway.py create mode 100644 session.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..68bc17f --- /dev/null +++ b/.gitignore @@ -0,0 +1,160 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..fe8bec0 --- /dev/null +++ b/__init__.py @@ -0,0 +1,7 @@ +""" +Voip ms sms/mms legacy module for slidge. +""" + +from . import gateway, session + +__all__ = "gateway", "session" diff --git a/gateway.py b/gateway.py new file mode 100644 index 0000000..2cf899d --- /dev/null +++ b/gateway.py @@ -0,0 +1,66 @@ +""" +The gateway +""" + +import aiohttp +from typing import Optional + +from slixmpp import JID + +from slidge import BaseGateway, FormField, GatewayUser +from slidge.command.register import RegistrationType +from slixmpp.exceptions import XMPPError + +from .session import API_URL +from .session import ASSETS_DIR + + +class Gateway(BaseGateway): + """ + This is instantiated once by the slidge entrypoint. + + By customizing the class attributes, we customize the registration process, + and display name of the component. + """ + + COMPONENT_NAME = "voip.ms sms gateway" + COMPONENT_AVATAR = ASSETS_DIR / "slidge-color.png" + COMPONENT_TYPE = "aim" + REGISTRATION_INSTRUCTIONS = ( + "Register with your voip.ms username and API password" + "Then you will need to enter the DID you wish to use." + ) + REGISTRATION_TYPE = RegistrationType.SINGLE_STEP_FORM + REGISTRATION_FIELDS = [ + FormField(var="username", label="User name", required=True), + FormField(var="password", label="Voip MS API password", required=True, private=True), + FormField(var="did", label="Did (phone number)", required=True), + ] + GROUPS = False + MARK_ALL_MESSAGES = False + + async def validate( + self, user_jid: JID, registration_form: dict[str, Optional[str]] + ): + """ + This function receives the values of the form defined in + :attr:`REGISTRATION_FIELDS`. Here, since we set + :attr:`REGISTRATION_TYPE` to "SINGLE_STEP_FORM", if login fails + the user will see an exception + + :param user_jid: + :param registration_form: + :return: + """ + async with aiohttp.ClientSession() as session: + async with session.get(API_URL, params={ + 'api_username': registration_form.get('username'), + 'api_password': registration_form.get('password'), + 'method': 'getDIDsInfo', + 'content_type': 'json', + }) as response: + json = await response.json() + if json['status'] == 'success': + return + else: + raise XMPPError("Received error from voipms") diff --git a/session.py b/session.py new file mode 100644 index 0000000..a7bd8a7 --- /dev/null +++ b/session.py @@ -0,0 +1,128 @@ +""" +User actions +""" + +import aiohttp +import asyncio +from datetime import datetime +from pathlib import Path +from pytz import timezone +from typing import TYPE_CHECKING, Optional, Union +from slidge import BaseSession, GatewayUser, LegacyContact, LegacyRoster + +class Contact(LegacyContact[str]): + session: "Session" + +class Roster(LegacyRoster[str, "Contact"]): + session: "Session" + + async def jid_username_to_legacy_id(self, jid_username: str) -> str: + if len(jid_username) != 10: # TODO more in depth validation + raise XMPPError("bad-request", "This is not a valid 10 digit phone number") + return jid_username + +API_URL = 'https://voip.ms/api/v1/rest.php' +EASTERN_TIME = timezone('America/New_York') +ASSETS_DIR = Path(__file__).parent / "assets" + +#@dataclass +#class VoipMsSms: +# id: str +# date: str +# type: VoipMsSmsType +# did: str +# contact: str +# message: str +# col_media1: str + +async def api_request(session, params): + async with session.get(API_URL, params=parFams) as response: + return await response.json() + +class Session(BaseSession[str, Contact]): + def __init__(self, user: GatewayUser): + self.httpsession = aiohttp.ClientSession() + super().__init__(user) + + def shutdown(self): + super().shutdown() + self.httpsession.close() + + async def login(self): + f = self.user.registration_form + async with self.httpsession.get(API_URL, params={ + 'api_username': f['username'], + 'api_password': f['password'], + 'method': 'getDIDsInfo', + 'did': f['did'], + 'content_type': 'json', + }) as response: + json = await response.json() + if json['status'] == 'success': + return f"Connected as {json['dids'][0]['did']}" + else: + return f"Failure! {json['status']}" + + async def poll_loop(): + f = self.user.registration_form + while True: + pass + + # See this issue for timezone explanation https://github.com/michaelkourlas/voipms-sms-client/issues/35 + async def get_messages(self, from_time: datetime): + f = self.user.registration_form + async with self.httpsession.get(API_URL, params={ + 'api_username': f['username'], + 'api_password': f['password'], + 'method': 'getMMS', + 'did': f['did'], + 'from': from_time.astimezone(EASTERN_TIME).strftime('%Y-%m-%d %H:%M:%S'), + 'timezone': -5, + 'type': 1, + 'all_messages': 1, + 'content_type': 'json', + }) as response: + json = await response.json() + + if json['status'] != 'success': + return [] + else: + return json['sms'] + + + async def on_file(self, chat: Contact, url: str, **_kwargs): + f = self.user.registration_form + async with self.httpsession.get(API_URL, params={ + 'api_username': f['username'], + 'api_password': f['password'], + 'method': 'sendMMS', + 'did': f['did'], + 'dst': chat.legacy_id, + 'media1': url, + 'content_type': 'json', + }) as response: + json = await response.json() + + if json['status'] != 'success': + raise XMPPError("Unable to send") + + async def on_text( + self, + chat: Contact, + text: str, + **_kwargs + ): + f = self.user.registration_form + async with self.httpsession.get(API_URL, params={ + 'api_username': f['username'], + 'api_password': f['password'], + 'method': 'sendSMS', #TODO support MMS + 'did': f['did'], + 'dst': chat.legacy_id, + 'message': text, + 'content_type': 'json', + }) as response: + json = await response.json() + + if json['status'] != 'success': + raise XMPPError("Unable to send") \ No newline at end of file