diff --git a/discord_http.py b/discord_http.py new file mode 100644 index 0000000..5768af6 --- /dev/null +++ b/discord_http.py @@ -0,0 +1,24 @@ +import requests +import os + +class DiscordHttp: + def __init__(self): + self.session = requests.Session() + + def post_message(self, content, messageId=None): + # flags = 4 means it will suppress embeds: https://discord.com/developers/docs/resources/message#message-object-message-flags + content_payload = {"content": content, "flags": 4} + target_url = os.getenv('TARGET_URL') + verb = "POST" + if messageId is not None: + target_url = target_url + f"/messages/{messageId}" + verb = "PATCH" + response = self.session.request(verb, url=target_url, params={"wait": "true"}, json=content_payload) + return response.json()['id'] + + def get_message_from_id(self, messageId): + target_url = os.getenv('TARGET_URL') + verb = "GET" + target_url = target_url + f"/messages/{messageId}" + response = self.session.request(verb, url=target_url) + return response.json()['content'] \ No newline at end of file diff --git a/function_app.py b/function_app.py index 41d743c..897b555 100644 --- a/function_app.py +++ b/function_app.py @@ -1,6 +1,8 @@ import logging import azure.functions as func import spotbot as sb +import tables +import discord_http import cleanup app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION) @@ -8,7 +10,7 @@ @app.route(route="spotbot", methods=[func.HttpMethod.POST]) def spotbot(req: func.HttpRequest) -> func.HttpResponse: try: - sb.run(req) + sb.SpotBot(req, tables.HamAlertTable(), discord_http.DiscordHttp()).process() except Exception as _excpt: logging.error(f"Exception occurred: {_excpt}") return func.HttpResponse(body=f"Exception occurred: {_excpt}", status_code=500) diff --git a/hamalertmessage.py b/hamalertmessage.py new file mode 100644 index 0000000..235bcf8 --- /dev/null +++ b/hamalertmessage.py @@ -0,0 +1,24 @@ +import datetime +from pytz import timezone + +class HamAlertMessage: + def __init__(self, req_body): + self.callsign = req_body.get('callsign', 'Unknown') + self.source = req_body.get('source', 'Unknown') + self.frequency = req_body.get('frequency', 'Unknown') + self.mode = req_body.get('mode', 'Unknown') + self.summitRef = req_body.get('summitRef', '') + self.wwffRef = req_body.get('wwffRef', '') + self.received_time_pt = datetime.datetime.now(timezone('US/Pacific')) + + def spot_deeplink(self): + match self.source: + case "sotawatch": + return f"[{self.source}](https://sotl.as/activators/{self.callsign})" + case "pota": + return f"[{self.source}](https://api.pota.app/spot/comments/{self.callsign}/{self.wwffRef})" + case _: + return "" + + def __str__(self): + return f'{self.received_time_pt.strftime("%H:%M")} | {self.callsign} | {self.spot_deeplink()} | freq: {self.frequency} | mode: {self.mode} | loc: {self.summitRef}{self.wwffRef}' \ No newline at end of file diff --git a/spotbot.py b/spotbot.py index 41c7b02..d85fbf4 100644 --- a/spotbot.py +++ b/spotbot.py @@ -1,84 +1,44 @@ import logging import os import datetime -from pytz import timezone -import requests -import tables - - -def run(req): - logging.info('Python HTTP trigger function processed a request.') - dd = datetime.datetime.now(timezone('US/Pacific')) - - req_body = req.get_json() - logging.info(f"Received JSON: {req_body}") - - callsign = req_body.get('callsign') - - content = create_content(req_body, dd) - - table = tables.get_table() - entity = tables.query_for_entity(table, callsign) - messageId = None - existingMessage = None - - if is_entity_recent(entity): - messageId = entity['MessageId'] - existingMessage = get_previous_message(messageId).replace("~", "") - content = "~~" + existingMessage + "~~\n" + content - - # flags = 4 means it will suppress embeds: https://discord.com/developers/docs/resources/message#message-object-message-flags - content_payload = {"content":content, "flags": 4} - - messageId = post_message(content_payload, messageId) - tables.upsert_entity(table, callsign, messageId) - -def create_content(req_body, dd): - callsign = req_body.get('callsign', 'Unknown') - source = req_body.get('source', 'Unknown') - frequency = req_body.get('frequency', 'Unknown') - mode = req_body.get('mode', 'Unknown') - summitRef = req_body.get('summitRef', '') - wwffRef = req_body.get('wwffRef', '') - - spot_deeplink = create_spot_deeplink(source, callsign, wwffRef) - formatted_time = dd.strftime("%H:%M") - - content = f"{formatted_time} | {callsign} | {spot_deeplink} | freq: {frequency} | mode: {mode} | loc: {summitRef}{wwffRef}" - return content - -def create_spot_deeplink(source, callsign, wwffRef): - match source: - case "sotawatch": - return f"[{source}](https://sotl.as/activators/{callsign})" - case "pota": - return f"[{source}](https://api.pota.app/spot/comments/{callsign}/{wwffRef})" - case _: - return "" - -def is_entity_recent(entity): - if entity is None: - return False - ent_time = entity.metadata['timestamp'] - cur_time = datetime.datetime.now(datetime.timezone.utc) - lookback_seconds = int(os.getenv('LOOKBACK_SECONDS', 7200)) - return (cur_time - ent_time).total_seconds() < lookback_seconds - -def post_message(content, messageId=None): - target_url = os.getenv('TARGET_URL') - verb = "POST" - if messageId is not None: - target_url = target_url + f"/messages/{messageId}" - verb = "PATCH" - response = requests.request(verb, url=target_url, params={"wait": "true"}, json=content) - return extract_message_id(response) - -def get_previous_message(messageId): - target_url = os.getenv('TARGET_URL') - verb = "GET" - target_url = target_url + f"/messages/{messageId}" - response = requests.request(verb, url=target_url) - return response.json()['content'] - -def extract_message_id(response): - return response.json()['id'] \ No newline at end of file +from hamalertmessage import HamAlertMessage + +class SpotBot: + def __init__(self, http_req, table, discord_http): + self.http_req = http_req + self.ham = HamAlertMessage(http_req.get_json()) + self.table = table + self.discord_http = discord_http + + def process(self): + logging.info('Processing HamAlert message') + previous_message, message_id = self.get_last_message() + if previous_message: + previous_message = self.strikethrough_mesage(previous_message) + content = self.combine_messages(previous_message, self.ham) + else: + content = str(self.ham) + message_id = self.discord_http.post_message(content, message_id) + self.table.upsert_entity(self.ham.callsign, message_id) + + def strikethrough_mesage(self, message): + return f"~~{message}~~" + + def combine_messages(self, m1, m2): + return f"{m1}\n{m2}" + + def get_last_message(self): + last_message_entity = self.table.query_for_entity(self.ham.callsign) + if self.is_entity_recent(last_message_entity): + messageId = last_message_entity['MessageId'] + existing_message = self.discord_http.get_message_from_id(messageId) + return existing_message.replace("~", ""), messageId + return "", None + + def is_entity_recent(self, entity): + if entity is None: + return False + ent_time = entity.metadata['timestamp'] + cur_time = datetime.datetime.now(datetime.timezone.utc) + lookback_seconds = int(os.getenv('LOOKBACK_SECONDS', 7200)) + return (cur_time - ent_time).total_seconds() < lookback_seconds \ No newline at end of file diff --git a/tables.py b/tables.py index c03cf94..83ce344 100644 --- a/tables.py +++ b/tables.py @@ -3,23 +3,29 @@ from azure.data.tables import TableServiceClient from azure.data.tables import UpdateMode -def get_table(): - connection_string = os.getenv('AzureWebJobsStorage') - table_name = os.getenv('TABLE_NAME') - table_service_client = TableServiceClient.from_connection_string(conn_str=connection_string) - table_client = table_service_client.get_table_client(table_name=table_name) - return table_client +class HamAlertTable: + def __init__(self): + connection_string = os.getenv('AzureWebJobsStorage') + table_name = os.getenv('TABLE_NAME') + table_service_client = TableServiceClient.from_connection_string(conn_str=connection_string) + self.table_client = table_service_client.get_table_client(table_name=table_name) -def query_for_entity(table_client, callsign): - entities = [ent for ent in table_client.query_entities(f"PartitionKey eq '{callsign}' and RowKey eq '{callsign}'")] - if len(entities) > 0: - logging.info(f"Entity already exists for {callsign}") - return entities[0] if len(entities) > 0 else None + def initialize_table(self): + connection_string = os.getenv('AzureWebJobsStorage') + table_name = os.getenv('TABLE_NAME') + table_service_client = TableServiceClient.from_connection_string(conn_str=connection_string) + self.table_client = table_service_client.get_table_client(table_name=table_name) -def upsert_entity(table_client, callsign, messageId): - entity = { - u'PartitionKey': callsign, - u'RowKey': callsign, - u'MessageId': messageId - } - table_client.upsert_entity(mode=UpdateMode.REPLACE, entity=entity) \ No newline at end of file + def query_for_entity(self, callsign): + entities = [ent for ent in self.table_client.query_entities(f"PartitionKey eq '{callsign}' and RowKey eq '{callsign}'")] + if len(entities) > 0: + logging.info(f"Entity already exists for {callsign}") + return entities[0] if len(entities) > 0 else None + + def upsert_entity(self, callsign, messageId): + entity = { + u'PartitionKey': callsign, + u'RowKey': callsign, + u'MessageId': messageId + } + self.table_client.upsert_entity(mode=UpdateMode.REPLACE, entity=entity) \ No newline at end of file diff --git a/test.py b/test.py index 51348e1..9b7e97e 100644 --- a/test.py +++ b/test.py @@ -5,19 +5,58 @@ class TestSpotBot(unittest.TestCase): - def test_function_app_basic(self): - dd = datetime.strptime("2024-10-13T01:05:03", "%Y-%m-%dT%H:%M:%S") - req_body = {"callsign":"KI7HSG", "source": "pota", "frequency": "14.074", "mode": "FT8", "wwffRef":"US-0052"} - content = spotbot.create_content(req_body, dd) - expected = '01:05 | KI7HSG | [pota](https://api.pota.app/spot/comments/KI7HSG/US-0052) | freq: 14.074 | mode: FT8 | loc: US-0052' - self.assertEqual(content, expected) - - def test_function_app(self): - dd = datetime.strptime("2024-10-13T01:05:03", "%Y-%m-%dT%H:%M:%S") - req_body = {"callsign":"KI7HSG", "source": "sotawatch", "frequency": "14.074", "mode": "FT8", "summitRef": "ABCD"} - content = spotbot.create_content(req_body, dd) - expected = '01:05 | KI7HSG | [sotawatch](https://sotl.as/activators/KI7HSG) | freq: 14.074 | mode: FT8 | loc: ABCD' - self.assertEqual(content, expected) + def test_spotbot(self): + sb = spotbot.SpotBot(FakeHttpRequest(), table=FakeHamAlertTable(FakeEntity("1234", "KI7HSG")), discord_http=FakeDiscordHttp()) + sb.process() + self.assertEqual(sb.table.saved_callsign, "KI7HSG") + self.assertEqual(sb.table.saved_messageId, "9876") + dt = sb.ham.received_time_pt.strftime("%H:%M") + self.assertEqual(sb.discord_http.posted_message, f"~~01:05 | KI7HSG | [pota](https://api.pota.app/spot/comments/KI7HSG/US-0052) | freq: 14.074 | mode: FT8 | loc: US-0052~~\n{dt} | KI7HSG | [sotawatch](https://sotl.as/activators/KI7HSG) | freq: 14.074 | mode: FT8 | loc: ABCD") + + def test_spotbot_2(self): + sb = spotbot.SpotBot(FakeHttpRequest(), table=FakeHamAlertTable(None), discord_http=FakeDiscordHttp()) + sb.process() + self.assertEqual(sb.table.saved_callsign, "KI7HSG") + self.assertEqual(sb.table.saved_messageId, "9876") + dt = sb.ham.received_time_pt.strftime("%H:%M") + self.assertEqual(sb.discord_http.posted_message, f"{dt} | KI7HSG | [sotawatch](https://sotl.as/activators/KI7HSG) | freq: 14.074 | mode: FT8 | loc: ABCD") + + +''' +Fake classes for testing +''' + +class FakeHttpRequest: + def get_json(self): + return {"callsign":"KI7HSG", "source": "sotawatch", "frequency": "14.074", "mode": "FT8", "summitRef": "ABCD"} + +class FakeHamAlertTable: + def __init__(self, fake_entity): + self.saved_callsign = None + self.saved_messageId = None + self.fake_entity = fake_entity + def query_for_entity(self, callsign): + return self.fake_entity + def upsert_entity(self, callsign, messageId): + self.saved_callsign = callsign + self.saved_messageId = messageId + +class FakeEntity(dict): + def __init__(self, messageId, callsign): + self['MessageId'] = messageId + self['PartitionKey'] = callsign + self['RowKey'] = callsign + self.metadata = {"timestamp": datetime.now(timezone('US/Pacific'))} + +class FakeDiscordHttp: + def __init__(self): + self.posted_message = None + def post_message(self, content, messageId=None): + self.posted_message = content + return "9876" + def get_message_from_id(self, messageId): + return '01:05 | KI7HSG | [pota](https://api.pota.app/spot/comments/KI7HSG/US-0052) | freq: 14.074 | mode: FT8 | loc: US-0052' + if __name__ == '__main__': unittest.main()