messaging system uses userid straight from the DB isntead of tokens

This commit is contained in:
386a51 committed 2026-04-07 07:07:50 +00:00
1 parent 6fdecaaff9
commit 4975fc71e3
6 files changed
+500 -3

No files matched your search

+119 -1
View File
@@ -509,4 +509,122 @@ def update_user_settings(user_id, username, email, account_type=None):
"UPDATE users SET username = ?, email = ? WHERE user_id = ?",
(username, email, user_id)
)
return True, "Settings updated successfully"
return True, "Settings updated successfully"
# ── messaging system ─────────────────────────────────────────────────────────────────
def send_message(sender_id, receiver_id, subject, body, parent_message_id=None):
"""Send a new message"""
with get_db() as conn:
cursor = conn.execute("""
INSERT INTO messages (sender_id, receiver_id, subject, body, parent_message_id)
VALUES (?, ?, ?, ?, ?)
""", (sender_id, receiver_id, subject, body, parent_message_id))
return cursor.lastrowid
def get_user_conversations(user_id):
"""Get all conversations for a user (unique correspondents)"""
with get_db() as conn:
rows = conn.execute("""
SELECT DISTINCT
u.user_id,
u.username,
u.email,
MAX(m.created_at) as last_message,
COUNT(CASE WHEN m.is_read = 0 AND m.receiver_id = ? THEN 1 END) as unread_count
FROM messages m
JOIN users u ON (u.user_id = m.sender_id OR u.user_id = m.receiver_id)
WHERE (m.sender_id = ? OR m.receiver_id = ?)
AND u.user_id != ?
AND ((m.sender_id = ? AND m.sender_deleted = 0) OR (m.receiver_id = ? AND m.receiver_deleted = 0))
GROUP BY u.user_id
ORDER BY last_message DESC
""", (user_id, user_id, user_id, user_id, user_id, user_id))
return [dict(row) for row in rows]
def get_messages_between_users(user1_id, user2_id, limit=50, offset=0):
"""Get message history between two users"""
with get_db() as conn:
rows = conn.execute("""
SELECT m.*,
u1.username as sender_name,
u2.username as receiver_name
FROM messages m
JOIN users u1 ON m.sender_id = u1.user_id
JOIN users u2 ON m.receiver_id = u2.user_id
WHERE ((m.sender_id = ? AND m.receiver_id = ?) OR (m.sender_id = ? AND m.receiver_id = ?))
AND ((m.sender_id = ? AND m.sender_deleted = 0) OR (m.receiver_id = ? AND m.receiver_deleted = 0))
ORDER BY m.created_at DESC
LIMIT ? OFFSET ?
""", (user1_id, user2_id, user2_id, user1_id, user1_id, user2_id, limit, offset))
return [dict(row) for row in rows]
def mark_message_as_read(message_id, user_id):
"""Mark a specific message as read"""
with get_db() as conn:
conn.execute("""
UPDATE messages
SET is_read = 1
WHERE message_id = ? AND receiver_id = ?
""", (message_id, user_id))
def mark_conversation_as_read(user_id, other_user_id):
"""Mark all messages from a specific user as read"""
with get_db() as conn:
conn.execute("""
UPDATE messages
SET is_read = 1
WHERE sender_id = ? AND receiver_id = ? AND is_read = 0
""", (other_user_id, user_id))
def delete_message_for_user(message_id, user_id):
"""Soft delete a message for a specific user"""
with get_db() as conn:
conn.execute("""
UPDATE messages
SET sender_deleted = CASE WHEN sender_id = ? THEN 1 ELSE sender_deleted END,
receiver_deleted = CASE WHEN receiver_id = ? THEN 1 ELSE receiver_deleted END
WHERE message_id = ?
""", (user_id, user_id, message_id))
def get_unread_message_count(user_id):
"""Get total unread messages for a user"""
with get_db() as conn:
row = conn.execute("""
SELECT COUNT(*) as count
FROM messages
WHERE receiver_id = ? AND is_read = 0 AND receiver_deleted = 0
""", (user_id,)).fetchone()
return row['count'] if row else 0
def search_users(query, current_user_id, limit=10):
"""Search for users to message"""
with get_db() as conn:
rows = conn.execute("""
SELECT user_id, username, email, account_type
FROM users
WHERE (username LIKE ? OR email LIKE ?)
AND user_id != ?
LIMIT ?
""", (f'%{query}%', f'%{query}%', current_user_id, limit))
return [dict(row) for row in rows]
def get_user_by_id(user_id):
"""Get user by ID - returns user info for messaging"""
with get_db() as conn:
row = conn.execute(
"SELECT user_id, username, email, account_type FROM users WHERE user_id = ? LIMIT 1",
(user_id,)
).fetchone()
return dict(row) if row else None
+118
View File
@@ -0,0 +1,118 @@
from flask import render_template, Blueprint, request, redirect, url_for, flash, g, jsonify
from database.db_commands import (
send_message, get_user_conversations, get_messages_between_users,
mark_conversation_as_read, delete_message_for_user,
get_unread_message_count, search_users, get_user_by_id
)
page = Blueprint("messages", __name__, url_prefix="/messages")
@page.before_request
def require_login():
if not g.user:
flash("You must be logged in to access messages.", "info")
return redirect(url_for("login.login"))
@page.route("/")
def inbox():
conversations = get_user_conversations(g.user["user_id"])
unread_total = get_unread_message_count(g.user["user_id"])
return render_template(
"messages/inbox.jinja",
conversations=conversations,
unread_total=unread_total
)
@page.route("/conversation/<int:user_id>")
def conversation(user_id):
# Mark messages as read
mark_conversation_as_read(g.user["user_id"], user_id)
messages = get_messages_between_users(g.user["user_id"], user_id)
other_user = get_user_by_id(user_id)
if not other_user:
flash("User not found.", "info")
return redirect(url_for("messages.inbox"))
return render_template(
"messages/conversation.jinja",
messages=messages,
other_user=other_user,
recipient_id=user_id
)
@page.route("/send", methods=["POST"])
def send():
recipient_id = request.form.get("recipient_id")
subject = request.form.get("subject", "").strip()
body = request.form.get("body", "").strip()
parent_id = request.form.get("parent_message_id")
if not body:
flash("Message cannot be empty.", "info")
return redirect(request.referrer or url_for("messages.inbox"))
if not recipient_id:
flash("user cannot be empty.", "info")
return redirect(request.referrer or url_for("messages.inbox"))
send_message(
g.user["user_id"],
int(recipient_id),
subject[:200] if subject else None,
body,
int(parent_id) if parent_id else None
)
flash("Message sent!", "info")
if parent_id:
return redirect(url_for("messages.conversation", user_id=recipient_id))
return redirect(url_for("messages.inbox"))
@page.route("/delete/<int:message_id>", methods=["POST"])
def delete(message_id):
delete_message_for_user(message_id, g.user["user_id"])
flash("Message deleted.", "info")
return redirect(request.referrer or url_for("messages.inbox"))
@page.route("/unread-count")
def unread_count():
if not g.user:
return jsonify({"count": 0})
count = get_unread_message_count(g.user["user_id"])
return jsonify({"count": count})
@page.route("/search")
def search():
query = request.args.get("q", "").strip()
if len(query) < 2:
return jsonify({"users": []})
users = search_users(query, g.user["user_id"])
return jsonify({"users": users})
@page.route("/compose")
def compose():
recipient_id = request.args.get("to")
recipient_username = None
if recipient_id:
user = get_user_by_id(recipient_id)
if user:
recipient_username = user["username"]
return render_template(
"messages/compose.jinja",
recipient_id=recipient_id,
recipient_username=recipient_username
)
+101
View File
@@ -0,0 +1,101 @@
{% extends "base.jinja" %}
{% block title %}New Message{% endblock %}
{% block content %}
{% from 'button.jinja' import button %}
<section id="hero" style="padding: 40px 20px; text-align: center;">
<h1>New Message</h1>
</section>
<section style="padding: 0 20px 40px 20px; max-width: 600px; margin: 0 auto;">
<div class="container-full">
<form action="{{ url_for('messages.send') }}" method="POST" class="form">
<div class="form-group" style="position: relative;">
<label for="recipient_search">To:</label>
<input type="text" id="recipient_search"
placeholder="Search for a user by username..."
value="{{ recipient_username if recipient_username else '' }}">
<input type="hidden" name="recipient_id" id="recipient_id"
value="{{ recipient_id if recipient_id else '' }}">
<div id="search-results" style="
position: absolute;
background: var(--background);
border: 1px solid var(--primary);
border-radius: 8px;
max-height: 200px;
overflow-y: auto;
width: calc(100% - 40px);
display: none;
z-index: 100;
"></div>
</div>
<div class="form-group">
<label for="subject">Subject (optional):</label>
<input type="text" id="subject" name="subject" maxlength="200">
</div>
<div class="form-group">
<label for="body">Message:</label>
<textarea id="body" name="body" rows="6" required></textarea>
</div>
<button type="submit" class="submit-button">Send Message</button>
</form>
</div>
<div style="margin-top: 20px; text-align: center;">
{{ button(url='/messages', button_text='Cancel', extra_classes='homepage-btn') }}
</div>
</section>
<script>
const searchInput = document.getElementById('recipient_search');
const recipientIdInput = document.getElementById('recipient_id');
const resultsDiv = document.getElementById('search-results');
let searchTimeout;
searchInput.addEventListener('input', function() {
clearTimeout(searchTimeout);
const query = this.value.trim();
if (query.length < 2) {
resultsDiv.style.display = 'none';
return;
}
searchTimeout = setTimeout(() => {
fetch(`/messages/search?q=${encodeURIComponent(query)}`)
.then(response => response.json())
.then(data => {
if (data.users.length > 0) {
resultsDiv.innerHTML = data.users.map(user => `
<div style="padding: 10px; cursor: pointer; border-bottom: 1px solid var(--primary);"
onclick="selectUser(${user.user_id}, '${user.username.replace(/'/g, "\\'")}')">
<strong>${user.username}</strong><br>
<small>${user.email}</small>
</div>
`).join('');
resultsDiv.style.display = 'block';
} else {
resultsDiv.innerHTML = '<div style="padding: 10px;">No users found</div>';
resultsDiv.style.display = 'block';
}
});
}, 300);
});
function selectUser(id, username) {
searchInput.value = username;
recipientIdInput.value = id;
resultsDiv.style.display = 'none';
}
// Close results when clicking outside
document.addEventListener('click', function(e) {
if (!searchInput.contains(e.target) && !resultsDiv.contains(e.target)) {
resultsDiv.style.display = 'none';
}
});
</script>
{% endblock %}
+60
View File
@@ -0,0 +1,60 @@
{% extends "base.jinja" %}
{% block title %}Conversation with {{ other_user.username }}{% endblock %}
{% block content %}
{% from 'button.jinja' import button %}
<section id="hero" style="padding: 40px 20px; text-align: center;">
<h1>Conversation with {{ other_user.username }}</h1>
<p style="opacity: 0.7;">{{ other_user.email }}</p>
</section>
<section style="padding: 0 20px 40px 20px; max-width: 700px; margin: 0 auto;">
{# Message thread #}
<div class="container-full" style="margin-bottom: 20px; max-height: 500px; overflow-y: auto;">
{% if messages %}
{% for msg in messages|reverse %}
<div style="
margin-bottom: 20px;
text-align: {% if msg.sender_id == user.user_id %}right{% else %}left{% endif %};
">
<div style="
display: inline-block;
max-width: 80%;
background: {% if msg.sender_id == user.user_id %}var(--primary_darker){% else %}var(--background){% endif %};
border: 1.5px solid var(--primary);
border-radius: 12px;
padding: 12px 16px;
">
{% if msg.subject %}
<strong style="display: block; margin-bottom: 4px;">{{ msg.subject }}</strong>
{% endif %}
<p style="margin: 0; white-space: pre-wrap;">{{ msg.body }}</p>
<small style="opacity: 0.5; display: block; margin-top: 6px;">
{{ msg.created_at[:16] }}
</small>
</div>
</div>
{% endfor %}
{% else %}
<p style="text-align: center; opacity: 0.6;">No messages yet. Start the conversation!</p>
{% endif %}
</div>
{# Reply form #}
<div class="container-full">
<h3>Send a reply</h3>
<form action="{{ url_for('messages.send') }}" method="POST" class="form">
<input type="hidden" name="recipient_id" value="{{ other_user.user_id }}">
<div class="form-group">
<textarea name="body" rows="4" placeholder="Type your message..." required></textarea>
</div>
<button type="submit" class="submit-button">Send Message</button>
</form>
</div>
<div style="margin-top: 20px; text-align: center;">
{{ button(url='/messages', button_text='Back to Inbox', extra_classes='homepage-btn') }}
</div>
</section>
{% endblock %}
+61
View File
@@ -0,0 +1,61 @@
{% extends "base.jinja" %}
{% block title %}Messages{% endblock %}
{% block content %}
{% from 'button.jinja' import button %}
<section id="hero" style="padding: 40px 20px; text-align: center;">
<h1>Messages</h1>
{% if unread_total > 0 %}
<p style="color: var(--accent);">You have {{ unread_total }} unread message{{ 's' if unread_total != 1 }}</p>
{% endif %}
</section>
<section style="padding: 0 20px 40px 20px; max-width: 800px; margin: 0 auto;">
<div style="margin-bottom: 20px; text-align: right;">
{{ button(url='/messages/compose', button_text='+ New Message', extra_classes='homepage-btn') }}
</div>
{% if conversations %}
<div class="container-full">
{% for conv in conversations %}
<a href="{{ url_for('messages.conversation', user_id=conv.user_id) }}"
style="text-decoration: none; color: var(--text);">
<div style="
background: var(--background);
border: 1.5px solid var(--primary);
border-radius: 12px;
padding: 15px 20px;
margin-bottom: 10px;
display: flex;
justify-content: space-between;
align-items: center;
transition: border-color 0.2s;
">
<div>
<strong>{{ conv.username }}</strong>
{% if conv.unread_count > 0 %}
<span style="
background: var(--accent);
border-radius: 10px;
padding: 2px 8px;
font-size: 0.75rem;
margin-left: 8px;
">{{ conv.unread_count }} new</span>
{% endif %}
<p style="opacity: 0.6; font-size: 0.85rem; margin-top: 4px;">
{{ conv.last_message[:16] if conv.last_message else 'No messages' }}
</p>
</div>
<span style="opacity: 0.5;">→</span>
</div>
</a>
{% endfor %}
</div>
{% else %}
<div class="container-full" style="text-align: center;">
<p style="opacity: 0.6;">No messages yet.</p>
{{ button(url='/messages/compose', button_text='Send message', extra_classes='homepage-btn') }}
</div>
{% endif %}
</section>
{% endblock %}
+41 -2
View File
@@ -24,6 +24,23 @@
</div>
<a href="/admin">Admin</a>
{% endif %}
<!-- Messages link with unread badge -->
<a href="/messages" class="messages-link" style="position: relative;">
Messages
<span id="unread-badge" style="
background: #e74c3c;
color: white;
border-radius: 10px;
padding: 2px 6px;
font-size: 0.7rem;
margin-left: 5px;
display: none;
">0</span>
</a>
<a href="/about">About</a>
<div class="dropdown">
<a class="dropbtn">{{user["username"]}} ▾</a>
<div class="dropdown-content">
@@ -32,10 +49,32 @@
<a href="/logout">Logout</a>
</div>
</div>
<a href="/about">About</a>
{% else %}
{{ button(url='/login', button_text='Login', extra_classes='') }}
{{ button(url='/register', button_text='Register', extra_classes='') }}
{% endif %}
</div>
</div>
</div>
<script>
function updateUnreadCount() {
fetch('/messages/unread-count')
.then(response => response.json())
.then(data => {
const badge = document.getElementById('unread-badge');
if (data.count > 0) {
badge.textContent = data.count;
badge.style.display = 'inline-block';
} else {
badge.style.display = 'none';
}
})
.catch(error => console.error('Error fetching unread count:', error));
}
// Update immediately on page load
updateUnreadCount();
// Then update every 30 seconds
setInterval(updateUnreadCount, 30000);
</script>