import socket
|
|
import re
|
|
from flask import Flask, request, jsonify
|
|
from twilio.rest import Client
|
|
import time
|
|
|
|
app = Flask(__name__)
|
|
|
|
# Set this variable to enable/disable private mode
|
|
private_mode = False # Change this value as needed
|
|
|
|
# List of callsigns allowed to send messages if private_mode is TRUE. Accepts ALL SSIDs for a CALLSIGN listed.
|
|
allowed_callsigns = ['CALLSIGN0', 'CALLSIGN1', 'CALLSIGN2'] # Add more callsigns as needed
|
|
|
|
# Twilio credentials
|
|
TWILIO_ACCOUNT_SID = 'SID'
|
|
TWILIO_AUTH_TOKEN = 'TOKEN'
|
|
TWILIO_PHONE_NUMBER = '+NUMBER' # Your Twilio phone number
|
|
|
|
# APRS credentials
|
|
APRS_CALLSIGN = 'CALLSIGN'
|
|
APRS_PASSCODE = 'PASSCODE'
|
|
APRS_SERVER = 'rotate.aprs2.net'
|
|
APRS_PORT = 14580
|
|
|
|
# Initialize the socket
|
|
aprs_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
|
|
# Dictionary to store the last received APRS message ID for each user
|
|
user_last_message_id = {}
|
|
|
|
processed_message_ids = set()
|
|
|
|
# Outside the main loop, initialize a dictionary to store message history
|
|
received_aprs_messages = {}
|
|
|
|
received_acks = {}
|
|
|
|
RETRY_INTERVAL = 60 # Adjust this as needed
|
|
|
|
MAX_RETRIES = 3 # Adjust this as needed
|
|
|
|
|
|
def send_ack_message(sender, message_id):
|
|
ack_message = 'ack{}'.format(message_id)
|
|
sender_length = len(sender)
|
|
spaces_after_sender = ' ' * max(0, 9 - sender_length)
|
|
ack_packet_format = '{}>APRS::{}{}:{}\r\n'.format(APRS_CALLSIGN, sender, spaces_after_sender, ack_message)
|
|
ack_packet = ack_packet_format.encode()
|
|
aprs_socket.sendall(ack_packet)
|
|
print("Sent ACK to {}: {}".format(sender, ack_message))
|
|
print("Outgoing ACK packet: {}".format(ack_packet.decode()))
|
|
|
|
def send_rej_message(sender, message_id):
|
|
rej_message = 'rej{}'.format(message_id)
|
|
sender_length = len(sender)
|
|
spaces_after_sender = ' ' * max(0, 9 - sender_length)
|
|
rej_packet_format = '{}>APRS::{}{}:{}\r\n'.format(APRS_CALLSIGN, sender, spaces_after_sender, rej_message)
|
|
rej_packet = rej_packet_format.encode()
|
|
aprs_socket.sendall(rej_packet)
|
|
print("Sent REJ to {}: {}".format(sender, rej_message))
|
|
print("Outgoing REJ packet: {}".format(rej_packet.decode()))
|
|
|
|
|
|
def send_sms(twilio_phone_number, to_phone_number, from_callsign, body_message):
|
|
# Initialize the Twilio client
|
|
client = Client(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN)
|
|
|
|
try:
|
|
# Send SMS using the Twilio API
|
|
message = client.messages.create(
|
|
body="@{} {}".format(from_callsign, body_message),
|
|
from_=twilio_phone_number,
|
|
to=to_phone_number
|
|
)
|
|
print("SMS sent successfully.")
|
|
print("SMS SID:", message.sid)
|
|
except Exception as e:
|
|
print("Error sending SMS:", str(e))
|
|
|
|
|
|
def format_aprs_packet(callsign, message):
|
|
sender_length = len(callsign)
|
|
spaces_after_sender = ' ' * max(1, 9 - sender_length)
|
|
aprs_packet_format = '{}>APRS::{}{}:{}\r\n'.format(APRS_CALLSIGN, callsign, spaces_after_sender, message)
|
|
return aprs_packet_format
|
|
|
|
# Dictionary to store the mapping of aliases (callsigns) to phone numbers
|
|
alias_map = {
|
|
'laura': '5032985265', # Replace 'alias1' with the desired alias and '1234567890' with the corresponding phone number.
|
|
'alias2': '9876543210', # Add more entries as needed for other aliases and phone numbers.
|
|
# Add more entries as needed.
|
|
}
|
|
|
|
def find_phone_number_from_alias(alias):
|
|
return alias_map.get(alias.lower())
|
|
|
|
# Create a new dictionary to store the reverse mapping of phone numbers to aliases
|
|
reverse_alias_map = {v: k for k, v in alias_map.items()}
|
|
|
|
|
|
@app.route('/sms', methods=['POST'])
|
|
def receive_sms():
|
|
# Parse the incoming SMS message
|
|
data = request.form
|
|
from_phone_number = data['From']
|
|
body_message = data['Body']
|
|
|
|
# If the message is in the correct format, the function extracts the callsign and APRS message content from the SMS body.
|
|
if body_message.startswith('@'):
|
|
parts = body_message.split(' ', 1)
|
|
if len(parts) == 2:
|
|
# Extract the 10-digit phone number from the sender's phone number
|
|
sender_phone_number = from_phone_number[-10:]
|
|
callsign = parts[0][1:].upper() #Convert to UPPERCASE
|
|
aprs_message = parts[1]
|
|
|
|
# Get the last APRS message ID sent to this user
|
|
last_message_id = user_last_message_id.get(from_phone_number, 0)
|
|
|
|
# Increment the message ID to avoid duplicate messages
|
|
last_message_id += 1
|
|
user_last_message_id[from_phone_number] = last_message_id
|
|
|
|
# Use the reverse alias mapping to check if the sender's phone number has an associated alias
|
|
alias = reverse_alias_map.get(sender_phone_number.lower())
|
|
if alias:
|
|
sender_phone_number = alias
|
|
# If an alias is found, use it; otherwise, use the phone number itself as the alias
|
|
if alias:
|
|
sender_phone_number = alias
|
|
|
|
# Format the APRS packet and send it to the APRS server
|
|
aprs_packet = format_aprs_packet(callsign, "@{} {}".format(sender_phone_number, aprs_message + "{" + str(last_message_id)))
|
|
aprs_socket.sendall(aprs_packet.encode())
|
|
#print(user_last_message_id)
|
|
#print(last_message_id)
|
|
print("Sent APRS message to {}: {}".format(callsign, aprs_message))
|
|
print("Outgoing APRS packet: {}".format(aprs_packet.strip()))
|
|
|
|
time.sleep(5) # Sleeping here allows time for incoming ack before retry
|
|
|
|
# Retry sending the message if ACK is not received
|
|
retry_count = 0
|
|
ack_received = False # Flag to track whether ACK is received
|
|
|
|
while retry_count < MAX_RETRIES and not ack_received:
|
|
if str(last_message_id) in received_acks.get(callsign, set()):
|
|
print("Message ACK received. No further retries needed.")
|
|
ack_received = True
|
|
# Reset retry count and remove the ACK ID
|
|
retry_count = 0
|
|
received_acks.get(callsign, set()).discard(str(last_message_id))
|
|
else:
|
|
print("ACK not received. Retrying in {} seconds.".format(RETRY_INTERVAL))
|
|
aprs_socket.sendall(aprs_packet.encode())
|
|
retry_count += 1
|
|
time.sleep(RETRY_INTERVAL) # Pause for the defined interval
|
|
|
|
if ack_received:
|
|
print("ACK received during retries. No further retries needed.")
|
|
elif retry_count >= MAX_RETRIES:
|
|
print("Max retries reached. No ACK received for the message.")
|
|
|
|
return jsonify({'status': 'success'})
|
|
else:
|
|
return jsonify({'status': 'error', 'message': 'Invalid SMS format'})
|
|
else:
|
|
return jsonify({'status': 'error', 'message': 'SMS does not start with "@" symbol'})
|
|
|
|
|
|
def receive_aprs_messages():
|
|
# Connect to the APRS server
|
|
aprs_socket.connect((APRS_SERVER, APRS_PORT))
|
|
print("Connected to APRS server with callsign: {}".format(APRS_CALLSIGN))
|
|
|
|
# Send login information with APRS callsign and passcode
|
|
login_str = 'user {} pass {} vers SMS-Gateway 0.1b\r\n'.format(APRS_CALLSIGN, APRS_PASSCODE)
|
|
aprs_socket.sendall(login_str.encode())
|
|
print("Sent login information.")
|
|
|
|
|
|
buffer = ""
|
|
try:
|
|
while True:
|
|
data = aprs_socket.recv(1024)
|
|
if not data:
|
|
break
|
|
|
|
# Add received data to the buffer
|
|
buffer += data.decode()
|
|
|
|
# Split buffer into lines
|
|
lines = buffer.split('\n')
|
|
|
|
# Process each line
|
|
for line in lines[:-1]:
|
|
if line.startswith('#'):
|
|
continue
|
|
|
|
# Process APRS message
|
|
print("Received raw APRS packet: {}".format(line.strip()))
|
|
parts = line.strip().split(':')
|
|
if len(parts) >= 2:
|
|
from_callsign = parts[0].split('>')[0].strip()
|
|
message_text = ':'.join(parts[1:]).strip()
|
|
|
|
# Extract and process ACK ID if present
|
|
if "ack" in message_text:
|
|
parts = message_text.split("ack", 1)
|
|
if len(parts) == 2 and parts[1].isdigit():
|
|
ack_id = parts[1]
|
|
process_ack_id(from_callsign, ack_id)
|
|
# End RXd ACK ID for MSG Retries
|
|
|
|
|
|
# Check if the message contains "{"
|
|
if "{" in message_text:
|
|
message_id = message_text.split('{')[1].strip('}')
|
|
|
|
|
|
# Remove the first 11 characters from the message to exclude the "Callsign :" prefix
|
|
verbose_message = message_text[11:].split('{')[0].strip()
|
|
|
|
# Inside the receive_aprs_messages function
|
|
if private_mode:
|
|
# Use regular expression to match main callsign and accept all SSIDs
|
|
callsign_pattern = re.compile(r'^({})(-\d+)?$'.format('|'.join(map(re.escape, allowed_callsigns))))
|
|
if not callsign_pattern.match(from_callsign):
|
|
print("Unauthorized sender:", from_callsign)
|
|
send_rej_message(from_callsign, message_id)
|
|
continue # Skip processing messages from unauthorized senders
|
|
|
|
# Display verbose message content
|
|
print("From: {}".format(from_callsign))
|
|
print("Message: {}".format(verbose_message))
|
|
print("Message ID: {}".format(message_id))
|
|
print(user_last_message_id)
|
|
|
|
|
|
# Check if the verbose message contains the desired format with a number or an alias
|
|
pattern = r'@(\d{10}|\w+) (.+)'
|
|
match = re.match(pattern, verbose_message)
|
|
|
|
# Send ACK
|
|
send_ack_message(from_callsign, message_id)
|
|
|
|
|
|
if match:
|
|
recipient = match.group(1)
|
|
aprs_message = match.group(2)
|
|
|
|
# Check if the recipient is a 10-digit number or an alias
|
|
if recipient.isdigit():
|
|
# Recipient is a 10-digit number
|
|
phone_number = recipient
|
|
else:
|
|
# Recipient is an alias
|
|
phone_number = find_phone_number_from_alias(recipient)
|
|
|
|
|
|
if phone_number:
|
|
# Get the last APRS message ID sent to this user
|
|
last_message_id = user_last_message_id.get(from_callsign, 0)
|
|
last_message_id += 1
|
|
user_last_message_id[from_callsign] = last_message_id
|
|
|
|
|
|
# Check for duplicate messages
|
|
if (aprs_message, message_id) in received_aprs_messages.get(from_callsign, set()):
|
|
print("Duplicate message detected. Skipping SMS sending.")
|
|
send_ack_message(from_callsign, message_id)
|
|
|
|
else:
|
|
# Mark the message as received
|
|
received_aprs_messages.setdefault(from_callsign, set()).add((aprs_message, message_id))
|
|
|
|
# Send SMS
|
|
send_sms(TWILIO_PHONE_NUMBER, phone_number, from_callsign, aprs_message)
|
|
|
|
# Add this line to mark the message ID as processed
|
|
processed_message_ids.add(message_id)
|
|
|
|
# else:
|
|
print("Recipient not found in alias map or not a 10-digit number: {}".format(recipient))
|
|
|
|
|
|
# Extract and process ACK ID if present
|
|
if message_text.startswith("ack"):
|
|
ack_id = message_text[3:] # Remove the "ack" prefix
|
|
process_ack_id(from_callsign, ack_id)
|
|
|
|
|
|
pass
|
|
# Send ACK
|
|
# The last line might be an incomplete packet, so keep it in the buffer
|
|
buffer = lines[-1]
|
|
|
|
except Exception as e:
|
|
print("Error receiving APRS messages: {}".format(e))
|
|
|
|
finally:
|
|
# Close the socket connection when done
|
|
aprs_socket.close()
|
|
|
|
#Implementation for ack check with Message Retries #TODO
|
|
def process_ack_id(from_callsign, ack_id):
|
|
print("Received ACK from {}: {}".format(from_callsign, ack_id))
|
|
received_acks.setdefault(from_callsign, set()).add(ack_id)
|
|
|
|
if __name__ == '__main__':
|
|
print("APRS bot is running. Waiting for APRS messages...")
|
|
|
|
# Run the Flask web application in a separate thread to handle incoming SMS messages
|
|
from threading import Thread
|
|
webhook_thread = Thread(target=app.run, kwargs={'host': '23.138.32.57', 'port': 5000})
|
|
webhook_thread.start()
|
|
|
|
# Start listening for APRS messages
|
|
receive_aprs_messages()
|