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..36e92ae0 --- /dev/null +++ b/modem/command_message_send.py @@ -0,0 +1,26 @@ +from command import TxCommand +import api_validations +import base64 +from queue import Queue +from arq_session_iss import ARQSessionISS +from message_p2p import MessageP2P +from arq_data_type_handler import ARQDataTypeHandler + +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): + data, data_type = self.arq_data_type_handler.prepare(self.message.to_payload, 'p2pmsg_lzma') + iss = ARQSessionISS(self.config, modem, + self.message.destination, + data, + self.state_manager, + data_type) + + 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..300a9c46 --- /dev/null +++ b/modem/message_p2p.py @@ -0,0 +1,71 @@ +import datetime +import api_validations +import base64 +import json + + +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(cls.__decode_attachment__(a)) + + return cls(origin, dxcall, body, attachments) + + @classmethod + def from_payload(cls, payload): + payload_message = json.loads(payload) + attachments = list(map(cls.__decode_attachment__, payload_message['attachments'])) + return cls(payload_message['origin'], payload_message['destination'], + payload_message['body'], attachments) + + def get_id(self) -> str: + return f"{self.origin}.{self.destination}.{self.timestamp}" + + def __encode_attachment__(self, binary_attachment: dict): + encoded_attachment = binary_attachment.copy() + encoded_attachment['data'] = str(base64.b64encode(binary_attachment['data']), 'utf-8') + return encoded_attachment + + def __decode_attachment__(encoded_attachment: dict): + decoded_attachment = encoded_attachment.copy() + decoded_attachment['data'] = base64.b64decode(encoded_attachment['data']) + return decoded_attachment + + def to_dict(self): + """Make a dictionary out of the message data + """ + return { + 'id': self.get_id(), + 'origin': self.origin, + 'destination': self.destination, + 'body': self.body, + 'attachments': list(map(self.__encode_attachment__, self.attachments)), + } + + def to_payload(self): + """Make a byte array ready to be sent out of the message data""" + json_string = json.dumps(self.to_dict()) + return json_string 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..d5612921 --- /dev/null +++ b/tests/test_message_p2p.py @@ -0,0 +1,43 @@ +import sys +sys.path.append('modem') +import numpy as np + +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 testToPayloadWithAttachment(self): + attachment = { + 'name': 'test.gif', + 'type': 'image/gif', + 'data': np.random.bytes(1024) + } + message = MessageP2P(self.mycall, 'DJ2LS-3', 'Hello World!', [attachment]) + payload = message.to_payload() + + received_message = MessageP2P.from_payload(payload) + self.assertEqual(message.origin, received_message.origin) + self.assertEqual(message.destination, received_message.destination) + self.assertCountEqual(message.attachments, received_message.attachments) + self.assertEqual(attachment['data'], received_message.attachments[0]['data']) + +if __name__ == '__main__': + unittest.main()