Added a user database and added a few automatic features. Users are created for everyone who connects. User database is publicly searchable for users not marked hidden. Need to add a few more fields for users.

This commit is contained in:
Michael Woods
2025-01-05 01:11:59 -05:00
parent 754a1c580f
commit 7a38f4aa6b
4 changed files with 206 additions and 13 deletions

View File

@@ -1,6 +1,7 @@
import pe.app import pe.app
from packetserver.common import Response, Message, Request, PacketServerConnection, send_response, send_blank_response from packetserver.common import Response, Message, Request, PacketServerConnection, send_response, send_blank_response
from packetserver.server.constants import default_server_config from packetserver.server.constants import default_server_config
from packetserver.server.users import User
from copy import deepcopy from copy import deepcopy
import ax25 import ax25
from pathlib import Path from pathlib import Path
@@ -49,10 +50,18 @@ class Server:
self.db = ZODB.DB(self.storage) self.db = ZODB.DB(self.storage)
with self.db.transaction() as conn: with self.db.transaction() as conn:
if 'config' not in conn.root(): if 'config' not in conn.root():
logging.debug("no config, writing blank default config")
conn.root.config = PersistentMapping(deepcopy(default_server_config)) conn.root.config = PersistentMapping(deepcopy(default_server_config))
conn.root.config['blacklist'] = PersistentList() conn.root.config['blacklist'] = PersistentList()
if 'SYSTEM' not in conn.root.config['blacklist']:
logging.debug("Adding 'SYSTEM' to blacklist in case someone feels like violating FCC rules.")
conn.root.config['blacklist'].append('SYSTEM')
if 'users' not in conn.root(): if 'users' not in conn.root():
conn.root.users = OOBTree() logging.debug("users missing, creating bucket")
conn.root.users = PersistentMapping()
if 'SYSTEM' not in conn.root.users:
logging.debug("Creating system user for first time.")
User('SYSTEM', hidden=True, enabled=False).write_new(conn.root())
init_bulletins(conn.root()) init_bulletins(conn.root())
self.app = pe.app.Application() self.app = pe.app.Application()
PacketServerConnection.receive_subscribers.append(lambda x: self.server_receiver(x)) PacketServerConnection.receive_subscribers.append(lambda x: self.server_receiver(x))
@@ -66,19 +75,29 @@ class Server:
return str(Path(self.home_dir).joinpath('data.zopedb')) return str(Path(self.home_dir).joinpath('data.zopedb'))
def server_connection_bouncer(self, conn: PacketServerConnection): def server_connection_bouncer(self, conn: PacketServerConnection):
logging.debug("new connection bouncer checking for blacklist") logging.debug("new connection bouncer checking user status")
# blacklist check # blacklist check
blacklisted = False blacklisted = False
base = ax25.Address(conn.remote_callsign).call
with self.db.transaction() as storage: with self.db.transaction() as storage:
if 'blacklist' in storage.root.config: if 'blacklist' in storage.root.config:
bl = storage.root.config['blacklist'] bl = storage.root.config['blacklist']
logging.debug(f"A blacklist exists: {bl}") logging.debug(f"A blacklist exists: {bl}")
base = ax25.Address(conn.remote_callsign).call
logging.debug(f"Checking callsign {base.upper()}") logging.debug(f"Checking callsign {base.upper()}")
if base.upper() in bl: if base.upper() in bl:
logging.debug(f"Connection from blacklisted callsign {base}") logging.debug(f"Connection from blacklisted callsign {base}")
conn.closing = True conn.closing = True
blacklisted = True blacklisted = True
# user object check
if base in storage.root.users:
logging.debug(f"User {base} exists in db.")
u = storage.root.users[base]
u.seen()
else:
logging.info(f"Creating new user {base}")
u = User(base.upper().strip())
u.write_new(storage.root())
if blacklisted: if blacklisted:
count = 0 count = 0
while count < 10: while count < 10:

View File

@@ -4,8 +4,7 @@ import persistent.list
from persistent.mapping import PersistentMapping from persistent.mapping import PersistentMapping
import datetime import datetime
from typing import Self,Union,Optional from typing import Self,Union,Optional
from packetserver.common import PacketServerConnection, Request, Response, Message from packetserver.common import PacketServerConnection, Request, Response, Message, send_response, send_blank_response
from packetserver.server.requests import send_response, send_blank_response
import ZODB import ZODB
import logging import logging
@@ -41,8 +40,8 @@ class Bulletin(persistent.Persistent):
self.author = author self.author = author
self.subject = subject self.subject = subject
self.body = text self.body = text
self.created_at = None self.created_at = datetime.datetime.now(datetime.UTC)
self.updated_at = None self.updated_at = datetime.datetime.now(datetime.UTC)
self.id = None self.id = None
@classmethod @classmethod
@@ -78,6 +77,7 @@ class Bulletin(persistent.Persistent):
def handle_bulletin_get(req: Request, conn: PacketServerConnection, db: ZODB.DB): def handle_bulletin_get(req: Request, conn: PacketServerConnection, db: ZODB.DB):
response = Response.blank() response = Response.blank()
sp = req.path.split("/") sp = req.path.split("/")
logging.debug(f"bulletin get path: {sp}")
bid = None bid = None
limit = None limit = None
if 'limit' in req.vars: if 'limit' in req.vars:
@@ -90,14 +90,18 @@ def handle_bulletin_get(req: Request, conn: PacketServerConnection, db: ZODB.DB)
bid = int(req.vars['id']) bid = int(req.vars['id'])
except ValueError: except ValueError:
pass pass
if len(sp) > 2: if len(sp) > 1:
logging.debug(f"checking path for bulletin id")
try: try:
bid = int(sp[2].strip()) logging.debug(f"{sp[1]}")
bid = int(sp[1].strip())
except ValueError: except ValueError:
pass pass
logging.debug(f"bid is {bid}")
with db.transaction() as db: with db.transaction() as db:
if bid: if bid is not None:
logging.debug(f"retrieving bulletin: {bid}")
bull = Bulletin.get_bulletin_by_id(bid, db.root()) bull = Bulletin.get_bulletin_by_id(bid, db.root())
if bull: if bull:
response.payload = bull.to_dict() response.payload = bull.to_dict()
@@ -105,6 +109,7 @@ def handle_bulletin_get(req: Request, conn: PacketServerConnection, db: ZODB.DB)
else: else:
response.status_code = 404 response.status_code = 404
else: else:
logging.debug(f"retrieving all bulletins")
bulls = Bulletin.get_recent_bulletins(db.root(), limit=limit) bulls = Bulletin.get_recent_bulletins(db.root(), limit=limit)
response.payload = [bulletin.to_dict() for bulletin in bulls] response.payload = [bulletin.to_dict() for bulletin in bulls]
response.status_code = 200 response.status_code = 200
@@ -125,13 +130,13 @@ def handle_bulletin_post(req: Request, conn: PacketServerConnection, db: ZODB.DB
b.write_new(db.root()) b.write_new(db.root())
send_blank_response(conn, req, status_code=201) send_blank_response(conn, req, status_code=201)
def handle_bulletin_update(req: Request, conn: PacketServerConnection, db: ZODB.DB): def handle_bulletin_update(req: Request, conn: PacketServerConnection, db: ZODB.DB): # TODO
response = Response.blank() response = Response.blank()
with db.transaction() as db: with db.transaction() as db:
pass pass
send_response(conn, response, req) send_response(conn, response, req)
def handle_bulletin_delete(req: Request, conn: PacketServerConnection, db: ZODB.DB): def handle_bulletin_delete(req: Request, conn: PacketServerConnection, db: ZODB.DB): # TODO
response = Response.blank() response = Response.blank()
with db.transaction() as db: with db.transaction() as db:
pass pass

View File

@@ -3,6 +3,7 @@
from msgpack.exceptions import OutOfData from msgpack.exceptions import OutOfData
from packetserver.common import Message, Request, Response, PacketServerConnection, send_response, send_blank_response from packetserver.common import Message, Request, Response, PacketServerConnection, send_response, send_blank_response
from .bulletin import bulletin_root_handler from .bulletin import bulletin_root_handler
from .users import user_root_handler
import logging import logging
from typing import Union from typing import Union
import ZODB import ZODB
@@ -38,7 +39,8 @@ def root_root_handler(req: Request, conn: PacketServerConnection,
standard_handlers = { standard_handlers = {
"": root_root_handler, "": root_root_handler,
"bulletin": bulletin_root_handler "bulletin": bulletin_root_handler,
"user": user_root_handler
} }

View File

@@ -0,0 +1,167 @@
"""Module containing code related to users."""
import ax25
import persistent
import persistent.list
from persistent.mapping import PersistentMapping
import datetime
from typing import Self,Union,Optional
from packetserver.common import PacketServerConnection, Request, Response, Message, send_response, send_blank_response
import ZODB
import logging
import uuid
from uuid import UUID
class User(persistent.Persistent):
def __init__(self, username: str, enabled: bool = True, hidden: bool = False, bio: str = "", status: str = ""):
self._username = username.upper().strip()
self.enabled = enabled
self.hidden = hidden
self.created_at = datetime.datetime.now(datetime.UTC)
self.last_seen = self.created_at
self._uuid = None
if len(bio) > 4000:
self._bio = bio[:4000]
else:
self._bio = bio
if len(status) > 300:
self._status = status[:300]
else:
self._status = status
def write_new(self, db_root: PersistentMapping):
all_uuids = [db_root['users'][x].uuid for x in db_root['users']]
self._uuid = uuid.uuid4()
while self.uuid in all_uuids:
self._uuid = uuid.uuid4()
logging.debug(f"Creating new user account {self.username} - {self.uuid}")
if self.username not in db_root['users']:
db_root['users'][self.username] = self
@property
def uuid(self):
return self._uuid
@classmethod
def get_user_by_username(cls, username: str, db_root: PersistentMapping) -> Self:
try:
if username.upper().strip() in db_root['users']:
return db_root['users'][username.upper().strip()]
except Exception:
return None
return None
@classmethod
def get_user_by_uuid(cls, user_uuid: Union[UUID, bytes, int, str], db_root: PersistentMapping) -> Self:
try:
if type(uuid) is uuid.UUID:
uid = user_uuid
elif type(uuid) is bytes:
uid = uuid.UUID(bytes=user_uuid)
elif type(uuid) is int:
uid = uuid.UUID(int=user_uuid)
else:
uid = uuid.UUID(str(user_uuid))
for user in db_root['users']:
if uid == db_root['users'][user].uuid:
return db_root['users'][user].uuid
except Exception:
return None
return None
@classmethod
def get_all_users(cls, db_root: PersistentMapping, limit: int = None) -> list:
all_users = sorted(db_root['users'].values(), key=lambda user: user.username)
if not limit:
return all_users
else:
if len(all_users) < limit:
return all_users
else:
return all_users[:limit]
def seen(self):
self.last_seen = datetime.datetime.now(datetime.UTC)
@property
def username(self) -> str:
return self._username.upper().strip()
@property
def bio(self) -> str:
return self._bio
@bio.setter
def bio(self, bio: str):
if len(bio) > 4000:
self._bio = bio[:4000]
else:
self._bio = bio
@property
def status(self) -> str:
return self._status
@status.setter
def status(self, status: str):
if len(status) > 300:
self._status = status[:300]
else:
self._status = status
def to_safe_dict(self) -> dict:
return {
"username": self.username,
"status": self.status,
"bio": self.bio,
"last_seen": self.last_seen.isoformat(),
"created_at": self.created_at.isoformat()
}
def handle_user_get(req: Request, conn: PacketServerConnection, db: ZODB.DB):
sp = req.path.split("/")
logging.debug("handle_user_get working")
user = None
user_var = req.vars.get('username')
response = Response.blank()
response.status_code = 404
limit = None
if 'limit' in req.vars:
try:
limit = int(req.vars['limit'])
except ValueError:
pass
with db.transaction() as db:
if len(sp) > 1:
logging.debug(f"trying to get the username from the path {sp[1].strip().upper()}")
user = User.get_user_by_username(sp[1].strip().upper(), db.root())
logging.debug(f"user holds: {user}")
if user and not user.hidden:
response.status_code = 200
response.payload = user.to_safe_dict()
else:
if user_var:
user = User.get_user_by_username(user_var.upper().strip(), db.root())
if user and not user.hidden:
response.status_code = 200
response.payload = user.to_safe_dict()
else:
if user_var:
user = User.get_user_by_username(user_var.upper().strip(), db.root())
if user and not user.hidden:
response.status_code = 200
response.payload = user.to_safe_dict()
else:
response.status_code = 200
response.payload = [x.to_safe_dict() for x in User.get_all_users(db.root(), limit=limit) if not x.hidden]
send_response(conn, response, req)
def handle_user_update(req: Request, conn: PacketServerConnection, db: ZODB.DB): # TODO
pass
def user_root_handler(req: Request, conn: PacketServerConnection, db: ZODB.DB):
logging.debug(f"{req} being processed by user_root_handler")
if req.method is Request.Method.GET:
handle_user_get(req, conn, db)
else:
send_blank_response(conn, req, status_code=404)