diff --git a/modem/api_validations.py b/modem/api_validations.py index 81f9d2d3..e3b50021 100644 --- a/modem/api_validations.py +++ b/modem/api_validations.py @@ -3,3 +3,10 @@ import re def validate_freedata_callsign(callsign): regexp = "^[a-zA-Z]+\d+\w+-\d{1,2}$" return re.compile(regexp).match(callsign) is not None + +def validate_message_attachment(attachment): + for field in ['name', 'type', 'data']: + if field not in attachment: + raise ValueError(f"Attachment missing '{field}'") + if len(attachment[field]) < 1: + raise ValueError(f"Attachment has empty '{field}'") diff --git a/modem/command_message_send.py b/modem/command_message_send.py new file mode 100644 index 00000000..4915cf5e --- /dev/null +++ b/modem/command_message_send.py @@ -0,0 +1,23 @@ +from command import TxCommand +import api_validations +import base64 +from queue import Queue +from arq_session_iss import ARQSessionISS +from message_p2p import MessageP2P + +class SendMessageCommand(TxCommand): + """Command to send a P2P message using an ARQ transfer session + """ + + def set_params_from_api(self, apiParams): + origin = f"{self.config['STATION']['mycall']}-{self.config['STATION']['myssid']}" + self.message = MessageP2P.from_api_params(origin, apiParams) + + def transmit(self, modem): + iss = ARQSessionISS(self.config, modem, + self.message.destination, + self.message.to_payload(), + self.state_manager) + + self.state_manager.register_arq_iss_session(iss) + iss.start() diff --git a/modem/message_p2p.py b/modem/message_p2p.py new file mode 100644 index 00000000..30b7d7a7 --- /dev/null +++ b/modem/message_p2p.py @@ -0,0 +1,62 @@ +import datetime +import api_validations +import base64 +import json +import lzma + + +class MessageP2P: + def __init__(self, origin: str, destination: str, body: str, attachments: list) -> None: + self.timestamp = datetime.datetime.now().isoformat() + self.origin = origin + self.destination = destination + self.body = body + self.attachments = attachments + + @classmethod + def from_api_params(cls, origin: str, params: dict): + + dxcall = params['dxcall'] + if not api_validations.validate_freedata_callsign(dxcall): + dxcall = f"{dxcall}-0" + + if not api_validations.validate_freedata_callsign(dxcall): + raise ValueError(f"Invalid dxcall given ({params['dxcall']})") + + body = params['body'] + if len(body) < 1: + raise ValueError(f"Body cannot be empty") + + attachments = [] + if 'attachments' in params: + for a in params['attachments']: + api_validations.validate_message_attachment(a) + attachments.append({ + 'name': a['name'], + 'type': a['type'], + 'data': base64.decode(a['data']), + }) + + return cls(origin, dxcall, body, attachments) + + def get_id(self) -> str: + return f"{self.origin}.{self.destination}.{self.timestamp}" + + def to_dict(self): + """Make a dictionary out of the message data + """ + message = { + 'id': self.get_id(), + 'origin': self.origin, + 'destination': self.destination, + 'body': self.body, + 'attachments': self.attachments, + } + return message + + def to_payload(self): + """Make a byte array ready to be sent out of the message data""" + json_string = json.dumps(self.to_dict()) + json_bytes = bytes(json_string, 'utf-8') + final_payload = lzma.compress(json_bytes) + return final_payload diff --git a/modem/server.py b/modem/server.py index 3a5e0971..7e55723b 100644 --- a/modem/server.py +++ b/modem/server.py @@ -16,6 +16,7 @@ import command_ping import command_feq import command_test import command_arq_raw +import command_message_send import event_manager app = Flask(__name__) @@ -234,6 +235,13 @@ def get_post_radio(): elif request.method == 'GET': return api_response(app.state_manager.get_radio_status()) +@app.route('/freedata/messages', methods=['POST']) +def post_freedata_message(): + if enqueue_tx_command(command_message_send.SendMessageCommand, request.json): + return api_response(request.json) + else: + api_abort('Error executing command...', 500) + # @app.route('/modem/arq_connect', methods=['POST']) # @app.route('/modem/arq_disconnect', methods=['POST']) # @app.route('/modem/send_raw', methods=['POST']) diff --git a/tests/test_message_p2p.py b/tests/test_message_p2p.py new file mode 100755 index 00000000..f1341e52 --- /dev/null +++ b/tests/test_message_p2p.py @@ -0,0 +1,37 @@ +import sys +sys.path.append('modem') + +import unittest +from config import CONFIG +from message_p2p import MessageP2P + +class TestDataFrameFactory(unittest.TestCase): + + @classmethod + def setUpClass(cls): + config_manager = CONFIG('modem/config.ini.example') + cls.config = config_manager.read() + cls.mycall = f"{cls.config['STATION']['mycall']}-{cls.config['STATION']['myssid']}" + + + def testFromApiParams(self): + api_params = { + 'dxcall': 'DJ2LS-3', + 'body': 'Hello World!', + } + message = MessageP2P.from_api_params(self.mycall, api_params) + self.assertEqual(message.destination, api_params['dxcall']) + self.assertEqual(message.body, api_params['body']) + + def testToPayload(self): + api_params = { + 'dxcall': 'DJ2LS-3', + 'body': 'Hello World!', + } + message = MessageP2P.from_api_params(self.mycall, api_params) + payload = message.to_payload() + self.assertGreater(len(payload), 0) + self.assertIsInstance(payload, bytes) + +if __name__ == '__main__': + unittest.main()