comments for js8call

This commit is contained in:
tekstrand 2024-07-17 21:22:12 -05:00
parent 87add16151
commit 571df87c69

View File

@ -13,18 +13,131 @@ from utils import send_message, update_user_state
config_file = 'config.ini'
def from_message(content):
"""
Converts a JSON-formatted string into a dictionary.
This method attempts to parse a JSON-formatted string and convert it into a Python dictionary.
If the content is not valid JSON, it returns an empty dictionary.
Parameters:
-----------
content : str
The JSON-formatted string to be converted into a dictionary.
Returns:
--------
dict
A dictionary representation of the JSON content. If the content is not valid JSON, an empty
dictionary is returned.
"""
try:
return json.loads(content)
except ValueError:
return {}
def to_message(typ, value='', params=None):
"""
Converts data into a JSON-formatted string for messaging.
This method creates a dictionary with the provided type, value, and parameters, and then converts it
into a JSON-formatted string. The resulting string is suitable for sending as a message to the JS8Call server.
Parameters:
-----------
type : str
The type of the message. This is a required field.
value : str, optional
The value or content of the message. Default is an empty string.
params : dict, optional
Additional parameters for the message. Default is an empty dictionary if not provided.
Returns:
--------
str
A JSON-formatted string representing the message.
"""
if params is None:
params = {}
return json.dumps({'type': typ, 'value': value, 'params': params})
class JS8CallClient:
"""
JS8CallClient integrates with the JS8Call server to handle messaging.
This class establishes a connection with the JS8Call server, processes incoming messages,
and provides methods to send messages and store them in a SQLite database. It handles different
types of messages, such as individual, group, and urgent messages.
Attributes:
-----------
interface : object
The communication interface used to interact with the user.
logger : logging.Logger
The logger for the client, used to log information, warnings, and errors.
config : configparser.ConfigParser
The configuration parser to read settings from the config file.
server : tuple
The server address and port for the JS8Call server.
db_file : str
The file path for the SQLite database.
js8groups : list
The list of group names for JS8Call group messages.
store_messages : bool
Flag indicating whether to store regular messages in the database.
js8urgent : list
The list of group names for JS8Call urgent messages.
connected : bool
Flag indicating whether the client is connected to the JS8Call server.
sock : socket.socket
The socket used for the connection to the JS8Call server.
db_conn : sqlite3.Connection
The SQLite database connection.
Methods:
--------
from_message(content):
Converts message content from JSON format.
to_message(type, value='', params=None):
Converts data to JSON message format.
create_tables():
Creates necessary tables in the database if they do not already exist.
insert_message(sender, receiver, message):
Inserts a message into the 'messages' table in the database.
insert_group(sender, groupname, message):
Inserts a group message into the 'groups' table in the database.
insert_urgent(sender, groupname, message):
Inserts an urgent message into the 'urgent' table in the database.
process(message):
Processes a received message from the JS8Call server.
send(*args, **kwargs):
Sends a message to the JS8Call server.
connect():
Establishes a connection to the JS8Call server.
close():
Closes the connection to the JS8Call server.
"""
def __init__(self, interface, logger=None):
self.logger = logger or logging.getLogger('js8call')
self.logger.setLevel(logging.INFO)
@ -56,6 +169,19 @@ class JS8CallClient:
self.logger.info("JS8Call configuration not found. Skipping JS8Call integration.")
def create_tables(self):
"""
Creates necessary tables in the database if they do not already exist.
This method sets up the 'messages', 'groups', and 'urgent' tables in the database.
Each table is created with columns for storing relevant information about messages.
If the database connection is not available, it logs an error message.
Tables:
-------
- messages: Stores individual messages with sender, receiver, and message content.
- groups: Stores group messages with sender, group name, and message content.
- urgent: Stores urgent messages with sender, group name, and message content.
"""
if not self.db_conn:
return
@ -90,6 +216,23 @@ class JS8CallClient:
self.logger.info("Database tables created or verified.")
def insert_message(self, sender, receiver, message):
"""
Inserts a message into the 'messages' table in the database.
This method saves a message along with its sender and receiver into the 'messages'
table. If the database connection is not available, it logs an error message.
Parameters:
-----------
sender : str
The meshtastic node identifier of the sender who issued the command
receiver : str
The identifier of the receiver of the message. This is typically the user's node id
message : str
The content of the urgent message.
"""
if not self.db_conn:
self.logger.error("Database connection is not available.")
return
@ -104,6 +247,20 @@ class JS8CallClient:
self.logger.error(f"Failed to insert message into database: {e}")
def insert_group(self, sender, groupname, message):
"""
Inserts a group message into the 'groups' table in the database.
Parameters:
-----------
sender : str
The meshtastic node identifier of the sender who issued the command
groupname : str
The name of the group to which the urgent message belongs.
message : str
The content of the urgent message.
"""
if not self.db_conn:
self.logger.error("Database connection is not available.")
return
@ -118,6 +275,20 @@ class JS8CallClient:
self.logger.error(f"Failed to insert group message into database: {e}")
def insert_urgent(self, sender, groupname, message):
"""
Inserts an urgent message into the 'urgent' table in the database.
Parameters:
-----------
sender : str
The meshtastic node identifier of the sender who issued the command
groupname : str
The name of the group to which the urgent message belongs.
message : str
The content of the urgent message.
"""
if not self.db_conn:
self.logger.error("Database connection is not available.")
return
@ -131,7 +302,34 @@ class JS8CallClient:
except sqlite3.Error as e:
self.logger.error(f"Failed to insert urgent message into database: {e}")
def process(self, message):
"""
Processes a received message from the JS8Call server.
This method handles various types of messages received from the JS8Call server.
It categorizes messages based on their type and performs the necessary actions
such as logging, storing in the database, or sending notifications.
Parameters:
-----------
message : dict
The message dictionary received from the JS8Call server. It should contain
the following message portions:
- 'type' (str): The type of the message (e.g., 'RX.DIRECTED').
- 'value' (str): The content of the message, which may include the sender,
receiver, and the message body.
- 'params' (dict): Additional parameters associated with the message (optional).
For 'RX.DIRECTED' messages, the method extracts the sender, receiver, and message
content, and performs specific actions based on the receiver's role:
- If the receiver is in the urgent list, the message is stored in the urgent messages
table and a notification is sent.
- If the receiver is in the group list, the message is stored in the group messages table.
- If message storage is enabled, the message is stored in the regular messages table.
"""
# Extract 'type', 'value', and 'params' from the message
typ = message.get('type', '')
value = message.get('value', '')
params = message.get('params', {})
@ -139,17 +337,19 @@ class JS8CallClient:
if not typ:
return
# Only handle these message types
rx_types = [
'RX.ACTIVITY', 'RX.DIRECTED', 'RX.SPOT', 'RX.CALL_ACTIVITY',
'RX.CALL_SELECTED', 'RX.DIRECTED_ME', 'RX.ECHO', 'RX.DIRECTED_GROUP',
'RX.META', 'RX.MSG', 'RX.PING', 'RX.PONG', 'RX.STREAM'
]
if typ not in rx_types:
return
if typ == 'RX.DIRECTED' and value:
# Split the message string into an array
parts = value.split(' ')
# Make sure we have at least 3 elements in the array
if len(parts) < 3:
self.logger.warning(f"Unexpected message format: {value}")
return
@ -160,26 +360,70 @@ class JS8CallClient:
self.logger.info(f"Received JS8Call message: {sender} to {receiver} - {msg}")
# Receiver is in the urgent list, insert the message into the urgent table and notify
if receiver in self.js8urgent:
self.insert_urgent(sender, receiver, msg)
notification_message = f"💥 URGENT JS8Call Message Received 💥\nFrom: {sender}\nCheck BBS for message"
send_message(notification_message, BROADCAST_NUM, self.interface)
# Receiver is in the groups list, insert the message into the groups table
elif receiver in self.js8groups:
self.insert_group(sender, receiver, msg)
# If storing messages is enabled, insert the message into the messages table
elif self.store_messages:
self.insert_message(sender, receiver, msg)
else:
pass
def send(self, *args, **kwargs):
"""
Sends a message to the JS8Call server.
This method formats the given arguments into a JSON message and sends it to the JS8Call server
over a socket connection. Each message is assigned a unique ID if not provided.
Parameters:
*args : tuple
Positional arguments that represent the components of the message to be sent.
These typically include the message type and value.
**kwargs : dict
Keyword arguments that provide additional parameters for the message. The 'params' key
is expected to be a dictionary of parameters to include in the message. If the '_ID' key
is not present in 'params', it is generated automatically.
- 'params' (dict): Optional dictionary of additional parameters to include in the message.
If not provided, an empty dictionary is used.
Example: {'TO': 'CALLSIGN'}
"""
# Retrieve the 'params' dictionary from the keyword arguments, or initialize it as an empty dictionary if not provided
params = kwargs.get('params', {})
# If '_ID' is not in the params dictionary, generate a unique ID based on the current time in milliseconds
if '_ID' not in params:
params['_ID'] = '{}'.format(int(time.time() * 1000))
kwargs['params'] = params
# Convert the provided arguments and keyword arguments to a JSON message
message = to_message(*args, **kwargs)
self.sock.send((message + '\n').encode('utf-8')) # Convert to bytes
# Send the JSON message to the JS8Call server by adding a newline character and encoding it to UTF-8
self.sock.send((message + '\n').encode('utf-8'))
def connect(self):
"""
Establishes a connection to the JS8Call server.
This method attempts to connect to the JS8Call server using the host and port
specified in the configuration file. Once connected, it sends a status request
to the server and continuously listens for incoming messages. If the connection
is refused or an error occurs, it logs the appropriate message.
The method will keep the connection open and process any received messages until
the connection is closed or an error occurs.
"""
if not self.server[0] or not self.server[1]:
self.logger.info("JS8Call server configuration not found. Skipping JS8Call connection.")
return
@ -191,6 +435,7 @@ class JS8CallClient:
self.connected = True
self.send("STATION.GET_STATUS")
# Continuously listen for incoming messages while connected
while self.connected:
content = self.sock.recv(65500).decode('utf-8') # Decode received bytes to string
if not content:
@ -216,11 +461,51 @@ class JS8CallClient:
def handle_js8call_command(sender_id, interface):
"""
Handles the initial JS8Call command.
This method sends a JS8Call menu to the specified sender, providing options for
group messages, station messages, urgent messages, and all messages. It also updates
the user state to indicate that the JS8Call menu has been presented.
Parameters:
-----------
sender_id : str
The meshtastic node identifier of the sender who issued the command
interface : object
The interface through which messages are sent. This is typically an instance
of the communication interface used to interact with the user.
"""
response = "JS8Call Menu:\n[G]roup Messages\n[S]tation Messages\n[U]rgent Messages\nE[X]IT"
send_message(response, sender_id, interface)
update_user_state(sender_id, {'command': 'JS8CALL_MENU', 'step': 1})
def handle_js8call_steps(sender_id, message, step, interface, state):
"""
Handles the steps for the JS8Call command.
Processes the user's input at each step of the JS8Call menu. Based on the user's choice,
it directs them to the appropriate sub-menu or handles invalid options
Parameters:
-----------
sender_id : str
Meshtastic node id
message : str
The message or input received from the user, representing their choice in the JS8Call menu.
step : int
The current step in the JS8Call menu navigation process.
interface : object
Instance of meshtastic interface of type specified by the configuration
see get_interface for more details
state : dict
The current state of the user's interaction
"""
if step == 1:
choice = message.lower()
if choice == 'x':
@ -237,6 +522,22 @@ def handle_js8call_steps(sender_id, message, step, interface, state):
handle_js8call_command(sender_id, interface)
def handle_group_messages_command(sender_id, interface):
"""
Handles the command to display group messages.
This method retrieves distinct group names from the group messages database and presents
a menu to the user with the available groups. If no group messages are available, it notifies
the user and returns to the main JS8Call menu.
Parameters:
-----------
sender_id : str
Meshtastic node id
interface : object
Instance of meshtastic interface of type specified by the configuration
see get_interface for more details
"""
conn = sqlite3.connect('js8call.db')
c = conn.cursor()
c.execute("SELECT DISTINCT groupname FROM groups")
@ -250,6 +551,22 @@ def handle_group_messages_command(sender_id, interface):
handle_js8call_command(sender_id, interface)
def handle_station_messages_command(sender_id, interface):
"""
Handles the command to display station messages.
This method retrieves all messages from the 'messages' table in the database and presents
them to the user. Each message includes the sender, receiver, and the message content.
If no station messages are available, it notifies the user and returns to the main JS8Call menu.
Parameters:
-----------
sender_id : str
Meshtastic node id
interface : object
Instance of meshtastic interface of type specified by the configuration
see get_interface for more details
"""
conn = sqlite3.connect('js8call.db')
c = conn.cursor()
c.execute("SELECT sender, receiver, message, timestamp FROM messages")
@ -262,6 +579,22 @@ def handle_station_messages_command(sender_id, interface):
handle_js8call_command(sender_id, interface)
def handle_urgent_messages_command(sender_id, interface):
"""
Handles the command to display urgent messages.
This method retrieves all urgent messages from the 'urgent' table in the database and presents
them to the user. Each message includes the sender, group name, and the message content.
If no urgent messages are available, it notifies the user and returns to the main JS8Call menu.
Parameters:
-----------
sender_id : str
Meshtastic node id
interface : object
Instance of meshtastic interface of type specified by the configuration
see get_interface for more details
"""
conn = sqlite3.connect('js8call.db')
c = conn.cursor()
c.execute("SELECT sender, groupname, message, timestamp FROM urgent")
@ -274,6 +607,31 @@ def handle_urgent_messages_command(sender_id, interface):
handle_js8call_command(sender_id, interface)
def handle_group_message_selection(sender_id, message, step, state, interface):
"""
Handles the selection of a group from the group messages menu.
This method processes the user's selection of a group and retrieves the messages
for the selected group from the database. It then presents the messages to the user.
If the selection is invalid, it prompts the user to choose again.
Parameters:
-----------
sender_id : str
Meshtastic node id
message : str
The message or input received from the user, representing their choice in the JS8Call menu.
step : int
The current step in the JS8Call menu navigation process.
interface : object
Instance of meshtastic interface of type specified by the configuration
see get_interface for more details
state : dict
The current state of the user's interaction
"""
groups = state['groups']
try:
group_index = int(message)