You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

695 lines
32 KiB

import socket
import re
from flask import Flask, request, jsonify
from twilio.rest import Client
import time
import threading
import setproctitle
import json
# Set the custom process name
setproctitle.setproctitle("sms")
app = Flask(__name__)
# Set the expiration time in seconds (e.g., 90 seconds)
MESSAGE_EXPIRATION_TIME = 3600
# 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
tocall = 'APOSMS'
user_callsign = 'YOUR_CALLSIGN'
# Twilio credentials
TWILIO_ACCOUNT_SID = 'SID'
TWILIO_AUTH_TOKEN = 'AUTH'
TWILIO_PHONE_NUMBER = '+NUMBER' # Your Twilio phone number
TWILIO_PHONE_NUMBER_UK = '+UKNUMBER' #UK SUPPORT
# APRS credentials
APRS_CALLSIGN = 'CALL'
APRS_PASSCODE = 'PASS'
APRS_SERVER = 'rotate.aprs2.net'
APRS_PORT = 14580
# Initialize the socket
aprs_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# Declare socket_ready as a global variable
socket_ready = False
# Dictionary to store the last number an APRS user messaged (callsign: last_number)
last_message_number = {}
# 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 = 90 # Adjust this as needed
MAX_RETRIES = 4 # Adjust this as needed
def handle_alias_update(from_callsign, verbose_message):
global alias_map, reverse_alias_map # Access the global dictionaries
# Extract alias information from the received message
alias_info = verbose_message.split('#alias', 1)[1].strip() # Remove the '#alias' prefix and trim whitespace
# Split the alias information into parts
alias_parts = alias_info.split()
if len(alias_parts) == 3:
action, alias_name, alias_phone = alias_parts
alias_name = alias_name.lower()
# Ensure the action is valid
if action in ("#add", "#remove"):
# Check if the alias phone is either 10 or 12 digits long #UK Support
if len(alias_phone) not in {10, 12} or not alias_phone.isdigit():
print("Invalid alias phone number. It must be either exactly 10 or 12 digits long.")
return
# Extract the SSID from the from_callsign
from_callsign_parts = from_callsign.split('-')
if len(from_callsign_parts) == 2:
from_callsign_strip = from_callsign_parts[0]
else:
from_callsign_strip = from_callsign # No SSID found
if from_callsign_strip in alias_map:
existing_aliases = alias_map[from_callsign_strip]
if action == "#add":
# Adding an alias
if alias_name in existing_aliases:
existing_phone = existing_aliases[alias_name]
if existing_phone != alias_phone:
# Alias name is found, but with a different phone number, update both name and phone
existing_aliases[alias_name] = alias_phone
elif alias_phone in existing_aliases.values():
# Alias phone number is found, update the associated alias name
for name, phone in existing_aliases.items():
if phone == alias_phone:
existing_aliases[alias_name] = alias_phone
del existing_aliases[name] # Remove the old alias name
break # Stop searching after the first match is found
else:
# Neither the alias name nor the alias phone is found, add the new alias
existing_aliases[alias_name] = alias_phone
elif action == "#remove":
# Removing an alias
if alias_name in existing_aliases:
if existing_aliases[alias_name] == alias_phone:
# Check if the provided alias and phone match the existing alias
alias_map[from_callsign_strip].pop(alias_name)
if not alias_map[from_callsign_strip]:
# If there are no more aliases for this callsign, remove the callsign entry
alias_map.pop(from_callsign_strip)
else:
print("Alias not found for removal:", alias_name, alias_phone)
else:
# If the callsign is not in the alias map, create a new entry
if action == "#add":
alias_map[from_callsign_strip] = {alias_name: alias_phone}
# Update the reverse_alias_map to reflect the changes
reverse_alias_map = generate_reverse_alias_map(alias_map)
# Save the updated alias map to a file
save_alias_map_to_file(alias_map, alias_map_filename)
# Print a message indicating the update
print("Alias map updated:")
print(alias_map)
else:
print("Invalid action:", action)
else:
print("Invalid alias information:", alias_info)
def save_alias_map_to_file(alias_map, filename):
# Save the alias map to the specified file in JSON format with proper formatting
with open(filename, 'w') as file:
formatted_json = json.dumps(alias_map, indent=4)
file.write(formatted_json)
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 = '{}>{}::{}{}:{}\r\n'.format(APRS_CALLSIGN, tocall, 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 = '{}>{}::{}{}:{}\r\n'.format(APRS_CALLSIGN, tocall, 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_aprs_messages(callsign, from_phone_number, sender_phone_number, aprs_message, last_message_id):
#New Chunk Method
chunk_size = 67
# Additional characters for portion information
portion_info_chars = 4
# Calculate the constant value considering portion information
constant_value = chunk_size - len(sender_phone_number) - 2 - portion_info_chars
# Split the APRS message into chunks of 67 characters
message_chunks = [aprs_message[i:i + constant_value] for i in range(0, len(aprs_message), constant_value)]
#End Chunk Method
#New Portion Calc
total_chunks = len(message_chunks)
# Get the last APRS message ID sent to this user
last_message_id = user_last_message_id.get(from_phone_number, 0)
user_last_message_id[from_phone_number] = last_message_id
# Initialize a separate counter variable for the message ID within the loop
current_message_id = last_message_id
for i, chunk in enumerate(message_chunks):
# Calculate portion information
portion_info = " {}/{}".format(i + 1, total_chunks) if total_chunks > 1 else ""
print("Chunks", len(message_chunks))
# Format the APRS packet and send it to the APRS server
aprs_packet = format_aprs_packet(callsign, "@{}{} {}{}".format(sender_phone_number, portion_info, chunk, "{" + str(current_message_id)))
print("chunks ID: ", current_message_id)
print(aprs_packet)
aprs_socket.sendall(aprs_packet.encode())
# Not a good delay, maybe seek alternative options.
time.sleep(10) # Sleeping here allows time for incoming ack before retry
print("sleep ID: ", current_message_id)
retry_count = 0
ack_received = False
while retry_count < MAX_RETRIES and not ack_received:
if str(current_message_id) in received_acks.get(callsign, set()):
print("Message ACK received. No further retries needed.")
ack_received = True
retry_count = 0
received_acks.get(callsign, set()).discard(str(current_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)
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.")
# Increment the counter variable for the next chunk
current_message_id += 1
# Update the last_message_id outside the loop
last_message_id += len(message_chunks) - 1
user_last_message_id[from_phone_number] = last_message_id
print("Last ID: ", last_message_id)
print("Next ID: ", current_message_id)
print(user_last_message_id)
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 send_sms_uk(twilio_phone_number_uk, to_phone_number, from_callsign, body_message): #UK SUPPORT
# 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_uk,
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(0, 9 - sender_length) #1,9 - Changed 9-16
aprs_packet_format = '{}>{}::{}{}:{}\r\n'.format(APRS_CALLSIGN, tocall, callsign, spaces_after_sender, message)
return aprs_packet_format
def load_alias_map_from_file(filename):
try:
with open(filename, 'r') as file:
alias_map = json.load(file)
return alias_map
except (FileNotFoundError, json.JSONDecodeError):
return {} # Return an empty dictionary if the file is not found or cannot be parsed
# Define the filename for the alias map file
alias_map_filename = '/root/app/sms_map.json'
# Load the alias map from the file
alias_map = load_alias_map_from_file(alias_map_filename)
# Create a new dictionary to store the reverse mapping of phone numbers and aliases to callsigns
def generate_reverse_alias_map(alias_map):
reverse_alias_map = {}
for callsign, aliases_and_numbers in alias_map.items():
reverse_alias_map[callsign] = {}
for alias, phone_number in aliases_and_numbers.items():
reverse_alias_map[callsign][phone_number] = alias
return reverse_alias_map
# Whenever you update the alias map, you can regenerate the reverse_alias_map
# For example, after updating the alias map with new data, call this function
reverse_alias_map = generate_reverse_alias_map(alias_map)
def extract_sender_phone_number(from_phone_number):
# Extract the phone number from the sender's phone number based on the prefix
if from_phone_number.startswith('+1'):
return from_phone_number[2:]
elif from_phone_number.startswith('+44'):
return from_phone_number[1:]
else:
return from_phone_number[-10:]
def is_message_expired(timestamp):
return time.time() - timestamp > MESSAGE_EXPIRATION_TIME
@app.route('/sms', methods=['POST'])
def receive_sms():
global last_message_number #Questioning this. Consider options later.
# Parse the incoming SMS message
data = request.form
from_phone_number = data['From']
body_message = data['Body']
print (body_message)
if body_message.startswith('@'):
parts = body_message.split(' ', 1)
if len(parts) == 2:
# Extract the phone number from the sender's phone number
sender_phone_number = extract_sender_phone_number(from_phone_number)
callsign = parts[0][1:].upper()
aprs_message = parts[1]
print(callsign)
last_message_number[sender_phone_number] = callsign #Questioning this. Consider options later.
print(last_message_number)
# Get the last APRS message ID sent to this user
last_message_id = user_last_message_id.get(from_phone_number, 0)
last_message_id += 1
user_last_message_id[from_phone_number] = last_message_id
print("RX SMS ID: ", last_message_id)
print("RX USR ID: ", user_last_message_id)
# Extract the SSID from the from_callsign
from_callsign_parts = callsign.split('-')
if len(from_callsign_parts) == 2:
from_callsign_strip = from_callsign_parts[0]
else:
from_callsign_strip = callsign # No SSID found
print("No SSID Found")
# Use the reverse alias mapping to check if the sender's phone number has an associated alias
alias = reverse_alias_map.get(from_callsign_strip, {}).get(sender_phone_number.lower())
if alias:
sender_phone_number = alias
# Format and send APRS packets
send_aprs_messages(callsign, from_phone_number, sender_phone_number, aprs_message, last_message_id)
return jsonify({'status': 'success'})
else:
return jsonify({'status': 'error', 'message': 'Invalid SMS format'})
else:
print ("no callsign found")
sender_phone_number = extract_sender_phone_number(from_phone_number)
callsign = last_message_number.get(sender_phone_number, None)
print("From:", from_phone_number[-10:])
print("From Full:", from_phone_number)
print("Dictionary:", last_message_number)
print("Callsign Found:", callsign)
if callsign:
# Extract the APRS message content
aprs_message = body_message
# Get the last APRS message ID sent to this user
last_message_id = user_last_message_id.get(from_phone_number, 0)
last_message_id += 1
user_last_message_id[from_phone_number] = last_message_id
# Extract the SSID from the from_callsign
from_callsign_parts = callsign.split('-')
if len(from_callsign_parts) == 2:
from_callsign_strip = from_callsign_parts[0]
else:
from_callsign_strip = callsign # No SSID found
# Use the reverse alias mapping to check if the sender's phone number has an associated alias
alias = reverse_alias_map.get(from_callsign_strip, {}).get(sender_phone_number.lower())
if alias:
sender_phone_number = alias
# Format and send APRS packets
send_aprs_messages(callsign, from_phone_number, sender_phone_number, aprs_message, last_message_id)
return jsonify({'status': 'success'})
else:
return jsonify({'status': 'error', 'message': 'No associated callsign found for the sender\'s phone number'})
def establish_aprs_connection():
global aprs_socket, socket_ready
while True:
try:
# Initialize the socket and connect to the APRS server
aprs_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
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 1.4 Beta\r\n'.format(APRS_CALLSIGN, APRS_PASSCODE)
aprs_socket.sendall(login_str.encode())
print("Sent login information.")
# Set the socket_ready flag to indicate that the socket is ready for keepalives
socket_ready = True
# If the connection was successful, break out of the loop
break
except socket.error as e:
print("Socket error:", str(e))
socket_ready = False
time.sleep(1) # Wait for a while before attempting to reconnect
except Exception as e:
print("Error connecting to APRS server: {}".format(e))
socket_ready = False
time.sleep(1) # Wait for a while before attempting to reconnect
def receive_aprs_messages():
global socket_ready, last_message_number, alias_map # Declare that you're using the global variable
while True:
try:
if not socket_ready:
# Attempt to establish a new connection to the APRS server
establish_aprs_connection()
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 verbose_message starts with ":SMS:" (or the appropriate APRS_CALLSIGN)
if message_text.startswith(":{}".format(APRS_CALLSIGN)):
# Check if the message contains "{"
if "{" in message_text[-6:]:
message_id = message_text.split('{')[1]
# 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)
# Initialize match
match = None
# Check if the verbose message contains the desired format with a number or an alias
#alias_pattern = r'#alias (.+)'
alias_pattern = re.compile(r'#alias (.+)', re.IGNORECASE)
alias_match = re.search(alias_pattern, verbose_message)
print("alias pattern")
if alias_match:
print("did we make it to 1")
# Call a function to handle alias updates
handle_alias_update(from_callsign, verbose_message.lower()) #added lower
else:
pattern = r'@(\d{10}|\w+) (.+)'
match = re.match(pattern, verbose_message)
# Send ACK
send_ack_message(from_callsign, message_id)
print("did we send this ack?")
if match:
recipient = match.group(1)
# Update the dictionary with the last message number for the callsign
# Extract the SSID from the from_callsign
from_callsign_parts = from_callsign.split('-')
if len(from_callsign_parts) == 2:
from_callsign_strip = from_callsign_parts[0]
else:
from_callsign_strip = from_callsign # No SSID found
# Use the reverse alias mapping to check if the sender's phone number has an associated alias
alias = alias_map.get(from_callsign_strip, {}).get(recipient.lower())
if alias:
recipient = alias
print("Recipient:", recipient)
#last_message_number[recipient.lower()] = from_callsign
print ("To #", recipient)
print ("From", from_callsign)
print ("Dictionary", last_message_number)
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
last_message_number[recipient.lower()] = from_callsign
else:
# Recipient is an alias
phone_number = alias_map.get(from_callsign, {}).get(recipient)
print("phone", phone_number)
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
if not phone_number:
print("Recipient not found in alias map or not a 10 or 12 digit number: {}".format(recipient))
continue # Skip processing the current message and move on to the next one in the loop
# Check for duplicate messages
messages_for_callsign = received_aprs_messages.get(from_callsign, [])
current_time = time.time()
# Check if the message has been received in the last 90 seconds
if any(not is_message_expired(stored_timestamp) and stored_message_id == message_id for stored_message, stored_message_id, stored_timestamp in messages_for_callsign):
print("Message received in the last 3600 seconds. Skipping further processing.")
#UK SUPPORT
else:
# Mark the message as received
received_aprs_messages.setdefault(from_callsign, []).append((aprs_message, message_id, current_time))
print(received_aprs_messages)
if len(phone_number) == 12 and phone_number.startswith("44"):
# UK phone number format: 12 digits and starts with "44"
send_sms_uk(TWILIO_PHONE_NUMBER_UK, phone_number, from_callsign, aprs_message)
else:
# Default behavior
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)
print("Process MSG ID")
# 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)
print("Process 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))
socket_ready = False # Reset the socket_ready flag to indicate a disconnected state
# Close the socket to release system resources
if aprs_socket:
aprs_socket.close()
time.sleep(1) # Wait for a while before attempting to reconnect
except Exception as e:
print("Error in receive_aprs_messages:", str(e))
#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)
# Update your records or take any other necessary action
def send_keepalive():
global socket_ready # Declare that you're using the global variable
while True:
try:
if socket_ready:
# Send a keepalive packet to the APRS server
keepalive_packet = '#\r\n'
aprs_socket.sendall(keepalive_packet.encode())
print("Sent keepalive packet.")
except socket.error as e:
print("Socket error while sending keepalive:", str(e))
# Reestablish the connection to the APRS server
establish_aprs_connection()
except Exception as e:
print("Error while sending keepalive:", str(e))
# Reestablish the connection to the APRS server
establish_aprs_connection()
time.sleep(30) # Send keepalive every 10 seconds
def send_beacon():
global socket_ready # Declare that you're using the global variable
while True:
try:
if socket_ready:
# Send a keepalive packet to the APRS server
beacon_packet = '{}>{}:Your Position {}\r\n'.format(APRS_CALLSIGN, tocall, user_callsign)
status_beacon = '{}>{}:>Status Beacon\r\n'.format(APRS_CALLSIGN, tocall)
aprs_socket.sendall(beacon_packet.encode())
aprs_socket.sendall(status_beacon.encode())
print("Sent Beacon Packet.")
except Exception as e:
print("Error sending beacon:", str(e))
time.sleep(600) # Send beacon every 10 minutes
if __name__ == '__main__':
print("APRS bot is running. Waiting for APRS messages...")
# Establish the initial connection to the APRS server
establish_aprs_connection()
# Start a separate thread for sending keepalive packets
keepalive_thread = threading.Thread(target=send_keepalive)
keepalive_thread.start()
# Start a separate thread for sending keepalive packets
beacon_thread = threading.Thread(target=send_beacon)
beacon_thread.start()
# 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': '0.0.0.0', 'port': 12345})
webhook_thread.start()
# Start listening for APRS messages
receive_aprs_messages()