diff --git a/tildes/tildes/models/user/user.py b/tildes/tildes/models/user/user.py index b6221c7..0b23339 100644 --- a/tildes/tildes/models/user/user.py +++ b/tildes/tildes/models/user/user.py @@ -226,6 +226,7 @@ class User(DatabaseModel): "generate_invite", "search_posts", "view_removed_posts", + "send_message", ): acl.append((Allow, self.user_id, permission)) diff --git a/tildes/tildes/routes.py b/tildes/tildes/routes.py index bd8b8f4..1641a17 100644 --- a/tildes/tildes/routes.py +++ b/tildes/tildes/routes.py @@ -85,6 +85,8 @@ def includeme(config: Configurator) -> None: "/conversations/{conversation_id36}", factory=message_conversation_by_id36, ) + # Send new PM to user + config.add_route("new_message_from_inbox", "/new", factory=LoggedInFactory) config.add_route("settings", "/settings", factory=LoggedInFactory) with config.route_prefix_context("/settings"): @@ -189,14 +191,16 @@ def add_intercooler_routes(config: Configurator) -> None: class LoggedInFactory: - """Simple class to use as `factory` to restrict routes to logged-in users. - - This class can be used when a route should only be accessible to logged-in users but - doesn't already have another factory that would handle that by checking access to a - specific resource (such as a topic or message). - """ - - __acl__ = ((Allow, Authenticated, "view"),) + """Simple class to use as `factory` to restrict routes to logged-in users.""" + + __acl__ = [ + (Allow, Authenticated, "view"), + ( + Allow, + Authenticated, + "message", + ), # Add this line to give the 'message' permission to authenticated users + ] def __init__(self, request: Request): """Initialize - no-op, but needs to take the request as an arg.""" diff --git a/tildes/tildes/schemas/message.py b/tildes/tildes/schemas/message.py index 2f16075..2a758b8 100644 --- a/tildes/tildes/schemas/message.py +++ b/tildes/tildes/schemas/message.py @@ -29,3 +29,9 @@ class MessageReplySchema(Schema): markdown = Markdown() rendered_html = String(dump_only=True) created_time = DateTime(dump_only=True) + + +class NewMessageConversationSchema(MessageConversationSchema): + """Marshmallow schema for starting a new message conversation.""" + + recipient = String(required=True) diff --git a/tildes/tildes/templates/macros/user_menu.jinja2 b/tildes/tildes/templates/macros/user_menu.jinja2 index 5c36150..bf06b21 100644 --- a/tildes/tildes/templates/macros/user_menu.jinja2 +++ b/tildes/tildes/templates/macros/user_menu.jinja2 @@ -65,6 +65,11 @@ Sent messages +
#}
+{# SPDX-License-Identifier: AGPL-3.0-or-later #}
+
+{% extends 'base_no_sidebar.jinja2' %}
+
+{% from 'macros/forms.jinja2' import markdown_textarea %}
+
+{% block title %}New private message conversation{% endblock %}
+
+{% block main_heading %}
+ Send a new private message
+{% endblock %}
+
+{% block content %}
+
+{% endblock %}
diff --git a/tildes/tildes/views/api/web/exceptions.py b/tildes/tildes/views/api/web/exceptions.py
index 827ba61..060150b 100644
--- a/tildes/tildes/views/api/web/exceptions.py
+++ b/tildes/tildes/views/api/web/exceptions.py
@@ -75,6 +75,8 @@ def error_to_text_response(request: Request) -> Response:
elif isinstance(request.exception, HTTPBadRequest):
if response.title == "Bad CSRF Token":
response.text = "Page expired, reload and try again"
+ elif "Recipient user not found" in str(response):
+ response.text = "Username not found"
else:
response.text = "Unknown error"
diff --git a/tildes/tildes/views/api/web/user.py b/tildes/tildes/views/api/web/user.py
index a9b70f3..f287b12 100644
--- a/tildes/tildes/views/api/web/user.py
+++ b/tildes/tildes/views/api/web/user.py
@@ -13,6 +13,9 @@ from pyramid.httpexceptions import (
HTTPForbidden,
HTTPUnauthorized,
HTTPUnprocessableEntity,
+ HTTPNotFound,
+ HTTPFound,
+ HTTPBadRequest
)
from pyramid.request import Request
from pyramid.response import Response
@@ -28,6 +31,13 @@ from tildes.schemas.topic import TopicSchema
from tildes.schemas.user import UserSchema
from tildes.views import IC_NOOP
from tildes.views.decorators import ic_view_config, use_kwargs
+from tildes.models.message import MessageConversation, MessageReply
+from tildes.schemas.message import NewMessageConversationSchema
+
+
+
+from pyramid.view import view_config
+#from pyramid.security import authenticated_userid
PASSWORD_FIELD = UserSchema(only=("password",)).fields["password"]
@@ -423,3 +433,47 @@ def delete_user_ban(request: Request) -> Response:
request.context.is_banned = False
return Response("Unbanned")
+
+
+
+
+
+@view_config(
+ route_name="new_message_from_inbox",
+ renderer="new_message_from_inbox.jinja2",
+ request_method="GET",
+ permission="view",
+)
+def get_new_message_form(request: Request) -> dict:
+ """Render form for entering a new private message to send."""
+ return {}
+
+
+# This is the view function for handling the form submission
+@view_config(
+ route_name="new_message_from_inbox", request_method="POST", permission="message"
+)
+@use_kwargs(
+ NewMessageConversationSchema(only=("subject", "markdown", "recipient")),
+ location="form",
+)
+def post_new_message(
+ request: Request, subject: str, markdown: str, recipient: str
+) -> HTTPFound:
+ """Start a new message conversation with a user."""
+ recipient_user = (
+ request.db_session.query(User).filter_by(username=recipient).one_or_none()
+ )
+
+ if recipient_user is None:
+ raise HTTPBadRequest("Recipient user not found")
+
+ new_conversation = MessageConversation(
+ sender=request.user,
+ recipient=recipient_user,
+ subject=subject,
+ markdown=markdown,
+ )
+ request.db_session.add(new_conversation)
+
+ raise HTTPFound(request.route_url("messages_sent"))
diff --git a/tildes/tildes/views/exceptions.py b/tildes/tildes/views/exceptions.py
index f119f28..bdbf201 100644
--- a/tildes/tildes/views/exceptions.py
+++ b/tildes/tildes/views/exceptions.py
@@ -83,10 +83,11 @@ def generic_error_page(request: Request) -> dict:
request.response.status_int = request.exception.status_int
error = f"Error {request.exception.status_code} ({request.exception.title})"
+ description = ""
if isinstance(request.exception, HTTPForbidden):
- description = "You don't have access to this page"
- if isinstance(request.exception, HTTPUnprocessableEntity) and isinstance(
+ description = request.exception
+ elif isinstance(request.exception, HTTPUnprocessableEntity) and isinstance(
request.exception.__context__, ValidationError
):
errors = errors_from_validationerror(request.exception.__context__)
@@ -94,7 +95,14 @@ def generic_error_page(request: Request) -> dict:
else:
description = request.exception.explanation
- return {"error": error, "description": description}
+ # For debugging, add the exception details
+ debug_info = {
+ "message": str(request.exception),
+ "type": str(type(request.exception)),
+ "args": request.exception.args,
+ }
+
+ return {"error": error, "description": description, "debug_info": debug_info}
@forbidden_view_config(xhr=False)