Skip to content

Commit

Permalink
Merge pull request #21 from tblanarik/cleanup
Browse files Browse the repository at this point in the history
Major cleanup and refactoring
  • Loading branch information
tblanarik authored Oct 19, 2024
2 parents a5c0bc9 + 5b7cd70 commit 52eb5df
Show file tree
Hide file tree
Showing 6 changed files with 168 additions and 113 deletions.
24 changes: 24 additions & 0 deletions discord_http.py
Original file line number Diff line number Diff line change
@@ -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']
4 changes: 3 additions & 1 deletion function_app.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
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)

@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)
Expand Down
24 changes: 24 additions & 0 deletions hamalertmessage.py
Original file line number Diff line number Diff line change
@@ -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}'
122 changes: 41 additions & 81 deletions spotbot.py
Original file line number Diff line number Diff line change
@@ -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']
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
42 changes: 24 additions & 18 deletions tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
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)
65 changes: 52 additions & 13 deletions test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

0 comments on commit 52eb5df

Please sign in to comment.