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:
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
167
src/packetserver/server/users.py
Normal file
167
src/packetserver/server/users.py
Normal 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)
|
||||||
Reference in New Issue
Block a user