Bunch of fixes for new database model.
This commit is contained in:
@@ -8,7 +8,7 @@ import time
|
||||
from persistent.mapping import PersistentMapping
|
||||
from persistent.list import PersistentList
|
||||
from packetserver.common.util import is_valid_ax25_callsign
|
||||
from .database import ConnectionDependency
|
||||
from .database import DbDependency
|
||||
|
||||
ph = PasswordHasher()
|
||||
|
||||
@@ -52,43 +52,44 @@ class HttpUser(Persistent):
|
||||
# rf enabled checks..
|
||||
#
|
||||
|
||||
def is_rf_enabled(self, conn: ConnectionDependency) -> bool:
|
||||
def is_rf_enabled(self, db: DbDependency) -> bool:
|
||||
"""
|
||||
Check if RF gateway is enabled (i.e., callsign NOT in global blacklist).
|
||||
Requires an open ZODB connection.
|
||||
"""
|
||||
root = conn.root()
|
||||
blacklist = root.get('config', {}).get('blacklist', [])
|
||||
return self.username not in blacklist
|
||||
with db.transaction() as conn:
|
||||
root = conn.root()
|
||||
blacklist = root.get('config', {}).get('blacklist', [])
|
||||
return self.username not in blacklist
|
||||
|
||||
def set_rf_enabled(self, conn: ConnectionDependency, allow: bool):
|
||||
def set_rf_enabled(self, db: DbDependency, allow: bool):
|
||||
"""
|
||||
Enable/disable RF gateway by adding/removing from global blacklist.
|
||||
Requires an open ZODB connection (inside a transaction).
|
||||
Only allows enabling if the username is a valid AX.25 callsign.
|
||||
"""
|
||||
from packetserver.common.util import is_valid_ax25_callsign # our validator
|
||||
with db.transaction() as conn:
|
||||
root = conn.root()
|
||||
config = root.setdefault('config', PersistentMapping())
|
||||
blacklist = config.setdefault('blacklist', PersistentList())
|
||||
|
||||
root = conn.root()
|
||||
config = root.setdefault('config', PersistentMapping())
|
||||
blacklist = config.setdefault('blacklist', PersistentList())
|
||||
upper_name = self.username
|
||||
|
||||
upper_name = self.username
|
||||
if allow:
|
||||
if not is_valid_ax25_callsign(upper_name):
|
||||
raise ValueError(f"{upper_name} is not a valid AX.25 callsign – cannot enable RF access")
|
||||
if upper_name in blacklist:
|
||||
blacklist.remove(upper_name)
|
||||
blacklist._p_changed = True
|
||||
else:
|
||||
if upper_name not in blacklist:
|
||||
blacklist.append(upper_name)
|
||||
blacklist._p_changed = True
|
||||
|
||||
if allow:
|
||||
if not is_valid_ax25_callsign(upper_name):
|
||||
raise ValueError(f"{upper_name} is not a valid AX.25 callsign – cannot enable RF access")
|
||||
if upper_name in blacklist:
|
||||
blacklist.remove(upper_name)
|
||||
blacklist._p_changed = True
|
||||
else:
|
||||
if upper_name not in blacklist:
|
||||
blacklist.append(upper_name)
|
||||
blacklist._p_changed = True
|
||||
|
||||
config._p_changed = True
|
||||
root._p_changed = True
|
||||
transaction.commit()
|
||||
config._p_changed = True
|
||||
root._p_changed = True
|
||||
transaction.commit()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Password handling (unchanged)
|
||||
|
||||
@@ -46,16 +46,16 @@ def get_db() -> ZODB.DB:
|
||||
raise RuntimeError("Database not initialized – call init_db() on startup")
|
||||
return _db
|
||||
|
||||
def get_connection() -> Generator[Connection, None, None]:
|
||||
"""Per-request dependency: yields an open Connection, closes on exit."""
|
||||
db = get_db()
|
||||
conn = db.open()
|
||||
try:
|
||||
yield conn
|
||||
finally:
|
||||
#print("not closing connection")
|
||||
#conn.close()
|
||||
pass
|
||||
#def get_connection() -> Generator[Connection, None, None]:
|
||||
# """Per-request dependency: yields an open Connection, closes on exit."""
|
||||
# db = get_db()
|
||||
# conn = db.open()
|
||||
# try:
|
||||
# yield conn
|
||||
# finally:
|
||||
# #print("not closing connection")
|
||||
# #conn.close()
|
||||
# pass
|
||||
|
||||
# Optional: per-request transaction (if you want automatic commit/abort)
|
||||
def get_transaction_manager():
|
||||
@@ -63,4 +63,4 @@ def get_transaction_manager():
|
||||
|
||||
# Annotated dependencies for routers
|
||||
DbDependency = Annotated[ZODB.DB, Depends(get_db)]
|
||||
ConnectionDependency = Annotated[Connection, Depends(get_connection)]
|
||||
#ConnectionDependency = Annotated[Connection, Depends(get_connection)]
|
||||
@@ -3,49 +3,49 @@ from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
||||
|
||||
from .auth import HttpUser
|
||||
from .database import ConnectionDependency
|
||||
from .database import DbDependency
|
||||
|
||||
security = HTTPBasic()
|
||||
|
||||
|
||||
async def get_current_http_user(conn: ConnectionDependency, credentials: HTTPBasicCredentials = Depends(security)):
|
||||
async def get_current_http_user(db: DbDependency, credentials: HTTPBasicCredentials = Depends(security)):
|
||||
"""
|
||||
Authenticate via Basic Auth using HttpUser from ZODB.
|
||||
Injected by the standalone runner (get_db_connection available).
|
||||
"""
|
||||
with db.transaction() as conn:
|
||||
root = conn.root()
|
||||
|
||||
root = conn.root()
|
||||
http_users = root.get("httpUsers")
|
||||
if http_users is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid username or password",
|
||||
headers={"WWW-Authenticate": "Basic"},
|
||||
)
|
||||
|
||||
http_users = root.get("httpUsers")
|
||||
if http_users is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid username or password",
|
||||
headers={"WWW-Authenticate": "Basic"},
|
||||
)
|
||||
user: HttpUser | None = http_users.get(credentials.username.upper())
|
||||
|
||||
user: HttpUser | None = http_users.get(credentials.username.upper())
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid username or password",
|
||||
headers={"WWW-Authenticate": "Basic"},
|
||||
)
|
||||
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid username or password",
|
||||
headers={"WWW-Authenticate": "Basic"},
|
||||
)
|
||||
if not user.http_enabled:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="HTTP access disabled for this user",
|
||||
)
|
||||
|
||||
if not user.http_enabled:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="HTTP access disabled for this user",
|
||||
)
|
||||
if not user.verify_password(credentials.password):
|
||||
user.record_login_failure()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid username or password",
|
||||
headers={"WWW-Authenticate": "Basic"},
|
||||
)
|
||||
|
||||
if not user.verify_password(credentials.password):
|
||||
user.record_login_failure()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid username or password",
|
||||
headers={"WWW-Authenticate": "Basic"},
|
||||
)
|
||||
|
||||
user.record_login_success()
|
||||
return user
|
||||
user.record_login_success()
|
||||
return user
|
||||
@@ -7,7 +7,7 @@ import transaction
|
||||
from persistent.list import PersistentList
|
||||
from ZODB.Connection import Connection
|
||||
|
||||
from packetserver.http.database import DbDependency, ConnectionDependency, get_db
|
||||
from packetserver.http.database import DbDependency
|
||||
from ..dependencies import get_current_http_user
|
||||
from ..auth import HttpUser
|
||||
from ..server import templates
|
||||
|
||||
@@ -5,7 +5,7 @@ from fastapi.responses import HTMLResponse
|
||||
from packetserver.http.dependencies import get_current_http_user
|
||||
from packetserver.http.auth import HttpUser
|
||||
from packetserver.http.server import templates
|
||||
from packetserver.http.database import ConnectionDependency
|
||||
from packetserver.http.database import DbDependency
|
||||
|
||||
router = APIRouter(tags=["dashboard"])
|
||||
|
||||
@@ -16,41 +16,43 @@ from .bulletins import list_bulletins
|
||||
|
||||
@router.get("/dashboard", response_class=HTMLResponse)
|
||||
async def dashboard(
|
||||
conn: ConnectionDependency,
|
||||
db: DbDependency,
|
||||
request: Request,
|
||||
current_user: HttpUser = Depends(get_current_http_user)
|
||||
):
|
||||
# Internal call – pass explicit defaults to avoid Query object injection
|
||||
messages_resp = await api_get_messages(
|
||||
conn,
|
||||
db,
|
||||
current_user=current_user,
|
||||
type="all",
|
||||
limit=100,
|
||||
since=None # prevents Query wrapper
|
||||
)
|
||||
messages = messages_resp["messages"]
|
||||
with db.transaction() as conn:
|
||||
# Internal call – pass explicit defaults to avoid Query object injection
|
||||
|
||||
bulletins_resp = await list_bulletins(conn, limit=10, since=None)
|
||||
recent_bulletins = bulletins_resp["bulletins"]
|
||||
messages = messages_resp["messages"]
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"dashboard.html",
|
||||
{
|
||||
"request": request,
|
||||
"current_user": current_user.username,
|
||||
"messages": messages,
|
||||
"bulletins": recent_bulletins
|
||||
}
|
||||
)
|
||||
bulletins_resp = await list_bulletins(conn, limit=10, since=None)
|
||||
recent_bulletins = bulletins_resp["bulletins"]
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"dashboard.html",
|
||||
{
|
||||
"request": request,
|
||||
"current_user": current_user.username,
|
||||
"messages": messages,
|
||||
"bulletins": recent_bulletins
|
||||
}
|
||||
)
|
||||
|
||||
@router.get("/dashboard/profile", response_class=HTMLResponse)
|
||||
async def profile_page(
|
||||
conn: ConnectionDependency,
|
||||
db: DbDependency,
|
||||
request: Request,
|
||||
current_user: HttpUser = Depends(get_current_http_user)
|
||||
):
|
||||
from packetserver.http.routers.profile import profile as api_profile
|
||||
profile_data = await api_profile(conn, current_user=current_user)
|
||||
profile_data = await api_profile(db, current_user=current_user)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"profile.html",
|
||||
|
||||
@@ -4,11 +4,13 @@ from fastapi.responses import HTMLResponse
|
||||
from packetserver.http.dependencies import get_current_http_user
|
||||
from packetserver.http.auth import HttpUser
|
||||
from packetserver.http.server import templates
|
||||
from packetserver.http.database import DbDependency
|
||||
|
||||
router = APIRouter(tags=["message-detail"])
|
||||
|
||||
@router.get("/dashboard/message/{msg_id}", response_class=HTMLResponse)
|
||||
async def message_detail_page(
|
||||
db: DbDependency,
|
||||
request: Request,
|
||||
msg_id: str = Path(..., description="Message UUID as string"),
|
||||
current_user: HttpUser = Depends(get_current_http_user)
|
||||
@@ -18,6 +20,7 @@ async def message_detail_page(
|
||||
|
||||
# Call with mark_retrieved=True to auto-mark as read on view (optional—remove if you prefer manual)
|
||||
message_data = await api_get_message(
|
||||
db,
|
||||
msg_id=msg_id,
|
||||
mark_retrieved=True,
|
||||
current_user=current_user
|
||||
|
||||
@@ -10,7 +10,7 @@ from pydantic import BaseModel, Field, validator
|
||||
|
||||
from packetserver.http.dependencies import get_current_http_user
|
||||
from packetserver.http.auth import HttpUser
|
||||
from packetserver.http.database import ConnectionDependency
|
||||
from packetserver.http.database import DbDependency
|
||||
|
||||
|
||||
html_router = APIRouter(tags=["messages-html"])
|
||||
@@ -29,7 +29,7 @@ class MarkRetrievedRequest(BaseModel):
|
||||
|
||||
@router.get("/messages")
|
||||
async def get_messages(
|
||||
conn: ConnectionDependency,
|
||||
db: DbDependency,
|
||||
current_user: HttpUser = Depends(get_current_http_user),
|
||||
type: str = Query("received", description="received, sent, or all"),
|
||||
limit: Optional[int] = Query(20, le=100, description="Max messages to return (default 20, max 100)"),
|
||||
@@ -40,128 +40,131 @@ async def get_messages(
|
||||
limit = 20
|
||||
|
||||
username = current_user.username
|
||||
root = conn.root()
|
||||
with db.transaction() as conn:
|
||||
root = conn.root()
|
||||
|
||||
if 'messages' not in root:
|
||||
root['messages'] = PersistentMapping()
|
||||
if username not in root['messages']:
|
||||
root['messages'][username] = persistent.list.PersistentList()
|
||||
if 'messages' not in root:
|
||||
root['messages'] = PersistentMapping()
|
||||
if username not in root['messages']:
|
||||
root['messages'][username] = persistent.list.PersistentList()
|
||||
|
||||
mailbox = root['messages'][username]
|
||||
mailbox = root['messages'][username]
|
||||
|
||||
since_dt = None
|
||||
if since:
|
||||
try:
|
||||
since_dt = datetime.fromisoformat(since.replace("Z", "+00:00"))
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Invalid 'since' format")
|
||||
since_dt = None
|
||||
if since:
|
||||
try:
|
||||
since_dt = datetime.fromisoformat(since.replace("Z", "+00:00"))
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Invalid 'since' format")
|
||||
|
||||
messages = []
|
||||
for msg in mailbox:
|
||||
if type == "received" and msg.msg_from == username:
|
||||
continue
|
||||
if type == "sent" and msg.msg_from != username:
|
||||
continue
|
||||
if since_dt and msg.sent_at < since_dt:
|
||||
continue
|
||||
messages = []
|
||||
for msg in mailbox:
|
||||
if type == "received" and msg.msg_from == username:
|
||||
continue
|
||||
if type == "sent" and msg.msg_from != username:
|
||||
continue
|
||||
if since_dt and msg.sent_at < since_dt:
|
||||
continue
|
||||
|
||||
messages.append({
|
||||
"id": str(msg.msg_id),
|
||||
"from": msg.msg_from,
|
||||
"to": list(msg.msg_to) if isinstance(msg.msg_to, tuple) else [msg.msg_to],
|
||||
"sent_at": msg.sent_at.isoformat() + "Z",
|
||||
"text": msg.text,
|
||||
"has_attachments": len(msg.attachments) > 0,
|
||||
"retrieved": msg.retrieved,
|
||||
})
|
||||
messages.append({
|
||||
"id": str(msg.msg_id),
|
||||
"from": msg.msg_from,
|
||||
"to": list(msg.msg_to) if isinstance(msg.msg_to, tuple) else [msg.msg_to],
|
||||
"sent_at": msg.sent_at.isoformat() + "Z",
|
||||
"text": msg.text,
|
||||
"has_attachments": len(msg.attachments) > 0,
|
||||
"retrieved": msg.retrieved,
|
||||
})
|
||||
|
||||
messages.sort(key=lambda m: m["sent_at"], reverse=True)
|
||||
messages.sort(key=lambda m: m["sent_at"], reverse=True)
|
||||
|
||||
return {"messages": messages[:limit], "total_returned": len(messages[:limit])}
|
||||
return {"messages": messages[:limit], "total_returned": len(messages[:limit])}
|
||||
|
||||
@router.get("/messages/{msg_id}")
|
||||
async def get_message(
|
||||
conn: ConnectionDependency,
|
||||
db: DbDependency,
|
||||
msg_id: str = Path(..., description="UUID of the message (as string)"),
|
||||
mark_retrieved: bool = Query(False, description="If true, mark message as retrieved/read"),
|
||||
current_user: HttpUser = Depends(get_current_http_user)
|
||||
):
|
||||
root = conn.root()
|
||||
with db.transaction() as conn:
|
||||
root = conn.root()
|
||||
|
||||
username = current_user.username
|
||||
username = current_user.username
|
||||
|
||||
messages_root = root.get('messages', {})
|
||||
mailbox = messages_root.get(username)
|
||||
if not mailbox:
|
||||
raise HTTPException(status_code=404, detail="Mailbox not found")
|
||||
messages_root = root.get('messages', {})
|
||||
mailbox = messages_root.get(username)
|
||||
if not mailbox:
|
||||
raise HTTPException(status_code=404, detail="Mailbox not found")
|
||||
|
||||
# Find message by ID
|
||||
target_msg = None
|
||||
for msg in mailbox:
|
||||
if str(msg.msg_id) == msg_id:
|
||||
target_msg = msg
|
||||
break
|
||||
# Find message by ID
|
||||
target_msg = None
|
||||
for msg in mailbox:
|
||||
if str(msg.msg_id) == msg_id:
|
||||
target_msg = msg
|
||||
break
|
||||
|
||||
if not target_msg:
|
||||
raise HTTPException(status_code=404, detail="Message not found")
|
||||
if not target_msg:
|
||||
raise HTTPException(status_code=404, detail="Message not found")
|
||||
|
||||
# Optionally mark as retrieved
|
||||
if mark_retrieved and not target_msg.retrieved:
|
||||
target_msg.retrieved = True
|
||||
target_msg._p_changed = True
|
||||
mailbox._p_changed = True
|
||||
# Explicit transaction for the write
|
||||
transaction.get().commit()
|
||||
# Optionally mark as retrieved
|
||||
if mark_retrieved and not target_msg.retrieved:
|
||||
target_msg.retrieved = True
|
||||
target_msg._p_changed = True
|
||||
mailbox._p_changed = True
|
||||
# Explicit transaction for the write
|
||||
transaction.get().commit()
|
||||
|
||||
return {
|
||||
"id": str(target_msg.msg_id),
|
||||
"from": target_msg.msg_from or "UNKNOWN",
|
||||
"to": list(target_msg.msg_to),
|
||||
"sent_at": target_msg.sent_at.isoformat() + "Z",
|
||||
"text": target_msg.text,
|
||||
"retrieved": target_msg.retrieved,
|
||||
"has_attachments": len(target_msg.attachments) > 0,
|
||||
# Future: "attachments": [...] metadata
|
||||
}
|
||||
return {
|
||||
"id": str(target_msg.msg_id),
|
||||
"from": target_msg.msg_from or "UNKNOWN",
|
||||
"to": list(target_msg.msg_to),
|
||||
"sent_at": target_msg.sent_at.isoformat() + "Z",
|
||||
"text": target_msg.text,
|
||||
"retrieved": target_msg.retrieved,
|
||||
"has_attachments": len(target_msg.attachments) > 0,
|
||||
# Future: "attachments": [...] metadata
|
||||
}
|
||||
|
||||
@router.patch("/messages/{msg_id}")
|
||||
async def mark_message_retrieved(
|
||||
conn: ConnectionDependency,
|
||||
db: DbDependency,
|
||||
msg_id: str = Path(..., description="Message UUID as string"),
|
||||
payload: MarkRetrievedRequest = None,
|
||||
current_user: HttpUser = Depends(get_current_http_user)
|
||||
):
|
||||
root = conn.root()
|
||||
with db.transaction() as conn:
|
||||
root = conn.root()
|
||||
|
||||
username = current_user.username
|
||||
mailbox = root.get('messages', {}).get(username)
|
||||
username = current_user.username
|
||||
mailbox = root.get('messages', {}).get(username)
|
||||
|
||||
if not mailbox:
|
||||
raise HTTPException(status_code=404, detail="Mailbox not found")
|
||||
if not mailbox:
|
||||
raise HTTPException(status_code=404, detail="Mailbox not found")
|
||||
|
||||
target_msg = None
|
||||
for msg in mailbox:
|
||||
if str(msg.msg_id) == msg_id:
|
||||
target_msg = msg
|
||||
break
|
||||
target_msg = None
|
||||
for msg in mailbox:
|
||||
if str(msg.msg_id) == msg_id:
|
||||
target_msg = msg
|
||||
break
|
||||
|
||||
if not target_msg:
|
||||
raise HTTPException(status_code=404, detail="Message not found")
|
||||
if not target_msg:
|
||||
raise HTTPException(status_code=404, detail="Message not found")
|
||||
|
||||
if target_msg.retrieved:
|
||||
# Already marked – idempotent success
|
||||
return {"status": "already_retrieved", "id": msg_id}
|
||||
if target_msg.retrieved:
|
||||
# Already marked – idempotent success
|
||||
return {"status": "already_retrieved", "id": msg_id}
|
||||
|
||||
target_msg.retrieved = True
|
||||
target_msg._p_changed = True
|
||||
mailbox._p_changed = True
|
||||
transaction.get().commit()
|
||||
target_msg.retrieved = True
|
||||
target_msg._p_changed = True
|
||||
mailbox._p_changed = True
|
||||
transaction.get().commit()
|
||||
|
||||
return {"status": "marked_retrieved", "id": msg_id}
|
||||
return {"status": "marked_retrieved", "id": msg_id}
|
||||
|
||||
@html_router.get("/messages", response_class=HTMLResponse)
|
||||
async def message_list_page(
|
||||
conn: ConnectionDependency,
|
||||
db: DbDependency,
|
||||
request: Request,
|
||||
type: str = Query("received", alias="msg_type"), # matches your filter links
|
||||
limit: Optional[int] = Query(50, le=100),
|
||||
@@ -169,7 +172,7 @@ async def message_list_page(
|
||||
):
|
||||
from packetserver.http.server import templates
|
||||
# Directly call the existing API endpoint function
|
||||
api_resp = await get_messages(conn, current_user=current_user, type=type, limit=limit, since=None)
|
||||
api_resp = await get_messages(db, current_user=current_user, type=type, limit=limit, since=None)
|
||||
messages = api_resp["messages"]
|
||||
|
||||
return templates.TemplateResponse(
|
||||
|
||||
@@ -6,7 +6,7 @@ import mimetypes
|
||||
|
||||
from packetserver.http.dependencies import get_current_http_user
|
||||
from packetserver.http.auth import HttpUser
|
||||
from packetserver.http.database import DbDependency, ConnectionDependency
|
||||
from packetserver.http.database import DbDependency
|
||||
from packetserver.server.objects import Object
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
@@ -3,26 +3,28 @@ from fastapi import APIRouter, Depends
|
||||
|
||||
from packetserver.http.dependencies import get_current_http_user
|
||||
from packetserver.http.auth import HttpUser
|
||||
from packetserver.http.database import ConnectionDependency
|
||||
from packetserver.http.database import DbDependency
|
||||
|
||||
router = APIRouter(prefix="/api/v1", tags=["auth"])
|
||||
|
||||
|
||||
@router.get("/profile")
|
||||
async def profile(conn: ConnectionDependency,current_user: HttpUser = Depends(get_current_http_user)):
|
||||
async def profile(db: DbDependency, current_user: HttpUser = Depends(get_current_http_user)):
|
||||
username = current_user.username
|
||||
root = conn.root()
|
||||
rf_enabled = current_user.is_rf_enabled(db)
|
||||
|
||||
# Get main BBS User and safe dict
|
||||
main_users = root.get('users', {})
|
||||
bbs_user = main_users.get(username)
|
||||
safe_profile = bbs_user.to_safe_dict() if bbs_user else {}
|
||||
rf_enabled = current_user.is_rf_enabled(conn)
|
||||
with db.transaction() as conn:
|
||||
root = conn.root()
|
||||
main_users = root.get('users', {})
|
||||
bbs_user = main_users.get(username)
|
||||
safe_profile = bbs_user.to_safe_dict() if bbs_user else {}
|
||||
|
||||
return {
|
||||
**safe_profile,
|
||||
"http_enabled": current_user.http_enabled,
|
||||
"rf_enabled": rf_enabled,
|
||||
"http_created_at": current_user.created_at,
|
||||
"http_last_login": current_user.last_login,
|
||||
}
|
||||
|
||||
return {
|
||||
**safe_profile,
|
||||
"http_enabled": current_user.http_enabled,
|
||||
"rf_enabled": rf_enabled,
|
||||
"http_created_at": current_user.created_at,
|
||||
"http_last_login": current_user.last_login,
|
||||
}
|
||||
@@ -11,7 +11,7 @@ from packetserver.http.dependencies import get_current_http_user
|
||||
from packetserver.http.auth import HttpUser
|
||||
from packetserver.server.messages import Message
|
||||
from packetserver.common.util import is_valid_ax25_callsign
|
||||
from packetserver.http.database import ConnectionDependency
|
||||
from packetserver.http.database import DbDependency
|
||||
|
||||
router = APIRouter(prefix="/api/v1", tags=["messages"])
|
||||
|
||||
@@ -40,55 +40,57 @@ class SendMessageRequest(BaseModel):
|
||||
|
||||
@router.post("/messages")
|
||||
async def send_message(
|
||||
conn: ConnectionDependency,
|
||||
db: DbDependency,
|
||||
payload: SendMessageRequest,
|
||||
current_user: HttpUser = Depends(get_current_http_user)
|
||||
):
|
||||
root = conn.root()
|
||||
is_rf_enabled = current_user.is_rf_enabled(db)
|
||||
with db.transaction() as conn:
|
||||
root = conn.root()
|
||||
|
||||
if not current_user.is_rf_enabled(conn):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="RF gateway access required to send messages"
|
||||
if not is_rf_enabled:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="RF gateway access required to send messages"
|
||||
)
|
||||
|
||||
username = current_user.username
|
||||
|
||||
# Prepare recipients
|
||||
to_list = payload.to
|
||||
to_tuple = tuple(to_list)
|
||||
if "ALL" in to_list:
|
||||
to_tuple = ("ALL",)
|
||||
|
||||
is_bulletin = "ALL" in to_list
|
||||
recipients = to_list if not is_bulletin else list(root.get('users', {}).keys())
|
||||
|
||||
# Create message using only supported core params
|
||||
new_msg = Message(
|
||||
text=payload.text,
|
||||
msg_from=username,
|
||||
msg_to=to_tuple,
|
||||
attachments=()
|
||||
)
|
||||
|
||||
username = current_user.username
|
||||
# Deliver to recipients + always sender (sent folder)
|
||||
messages_root = root.setdefault('messages', PersistentMapping())
|
||||
delivered_to = set()
|
||||
|
||||
# Prepare recipients
|
||||
to_list = payload.to
|
||||
to_tuple = tuple(to_list)
|
||||
if "ALL" in to_list:
|
||||
to_tuple = ("ALL",)
|
||||
for recip in set(recipients) | {username}:
|
||||
mailbox = messages_root.setdefault(recip, PersistentList())
|
||||
mailbox.append(new_msg)
|
||||
mailbox._p_changed = True
|
||||
delivered_to.add(recip)
|
||||
|
||||
is_bulletin = "ALL" in to_list
|
||||
recipients = to_list if not is_bulletin else list(root.get('users', {}).keys())
|
||||
messages_root._p_changed = True
|
||||
transaction.commit()
|
||||
|
||||
# Create message using only supported core params
|
||||
new_msg = Message(
|
||||
text=payload.text,
|
||||
msg_from=username,
|
||||
msg_to=to_tuple,
|
||||
attachments=()
|
||||
)
|
||||
|
||||
# Deliver to recipients + always sender (sent folder)
|
||||
messages_root = root.setdefault('messages', PersistentMapping())
|
||||
delivered_to = set()
|
||||
|
||||
for recip in set(recipients) | {username}:
|
||||
mailbox = messages_root.setdefault(recip, PersistentList())
|
||||
mailbox.append(new_msg)
|
||||
mailbox._p_changed = True
|
||||
delivered_to.add(recip)
|
||||
|
||||
messages_root._p_changed = True
|
||||
transaction.commit()
|
||||
|
||||
return {
|
||||
"status": "sent",
|
||||
"message_id": str(new_msg.msg_id),
|
||||
"from": username,
|
||||
"to": list(to_tuple),
|
||||
"sent_at": new_msg.sent_at.isoformat() + "Z",
|
||||
"recipients_delivered": len(delivered_to)
|
||||
}
|
||||
return {
|
||||
"status": "sent",
|
||||
"message_id": str(new_msg.msg_id),
|
||||
"from": username,
|
||||
"to": list(to_tuple),
|
||||
"sent_at": new_msg.sent_at.isoformat() + "Z",
|
||||
"recipients_delivered": len(delivered_to)
|
||||
}
|
||||
Reference in New Issue
Block a user