- Add comprehensive chat system with modern UI design - Implement admin-based password reset system - Fix template syntax errors and 500 server errors - Add chat routes, API endpoints, and database models - Enhance user interface with Tailwind CSS card-based design - Implement community guidelines and quick action features - Add responsive design for mobile and desktop compatibility - Create support chat functionality with admin integration - Fix JavaScript inheritance in base template - Add database migration for chat system tables Features: ✅ Modern chat interface with room management ✅ Admin-based password reset workflow ✅ Real-time chat with mobile app support ✅ Professional UI with gradient cards and hover effects ✅ Community guidelines and safety features ✅ Responsive design for all devices ✅ Error-free template rendering
561 lines
17 KiB
Python
561 lines
17 KiB
Python
"""
|
|
Chat API routes for mobile app compatibility
|
|
Provides RESTful endpoints for chat functionality
|
|
"""
|
|
from flask import Blueprint, request, jsonify, current_app
|
|
from flask_login import login_required, current_user
|
|
from sqlalchemy import desc, and_, or_
|
|
from datetime import datetime, timedelta
|
|
import re
|
|
|
|
from app.extensions import db
|
|
from app.models import ChatRoom, ChatMessage, ChatParticipant, Post, User
|
|
|
|
# Create blueprint
|
|
chat_api = Blueprint('chat_api', __name__)
|
|
|
|
# Chat rules and guidelines
|
|
CHAT_RULES = [
|
|
"Be respectful and courteous to all community members",
|
|
"No offensive language, harassment, or personal attacks",
|
|
"Stay on topic - use post-specific chats for discussions about routes",
|
|
"No spam or promotional content without permission",
|
|
"Share useful tips and experiences about motorcycle adventures",
|
|
"Help newcomers and answer questions when you can",
|
|
"Report inappropriate behavior to administrators",
|
|
"Keep conversations constructive and helpful"
|
|
]
|
|
|
|
# Profanity filter (basic implementation)
|
|
BLOCKED_WORDS = [
|
|
'spam', 'scam', 'fake', 'stupid', 'idiot', 'hate'
|
|
# Add more words as needed
|
|
]
|
|
|
|
def contains_blocked_content(text):
|
|
"""Check if text contains blocked words"""
|
|
text_lower = text.lower()
|
|
return any(word in text_lower for word in BLOCKED_WORDS)
|
|
|
|
@chat_api.route('/rules', methods=['GET'])
|
|
def get_chat_rules():
|
|
"""Get chat rules and guidelines"""
|
|
return jsonify({
|
|
'success': True,
|
|
'rules': CHAT_RULES
|
|
})
|
|
|
|
@chat_api.route('/rooms', methods=['GET'])
|
|
@login_required
|
|
def get_chat_rooms():
|
|
"""Get list of available chat rooms for the user"""
|
|
page = request.args.get('page', 1, type=int)
|
|
per_page = request.args.get('per_page', 20, type=int)
|
|
room_type = request.args.get('type', None)
|
|
|
|
# Base query - only active rooms the user can access
|
|
query = ChatRoom.query.filter(ChatRoom.is_active == True)
|
|
|
|
# Filter by type if specified
|
|
if room_type:
|
|
query = query.filter(ChatRoom.room_type == room_type)
|
|
|
|
# Order by last activity
|
|
query = query.order_by(desc(ChatRoom.last_activity))
|
|
|
|
# Paginate
|
|
rooms = query.paginate(
|
|
page=page, per_page=per_page, error_out=False
|
|
)
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'rooms': [room.to_dict() for room in rooms.items],
|
|
'pagination': {
|
|
'page': page,
|
|
'pages': rooms.pages,
|
|
'per_page': per_page,
|
|
'total': rooms.total,
|
|
'has_next': rooms.has_next,
|
|
'has_prev': rooms.has_prev
|
|
}
|
|
})
|
|
|
|
@chat_api.route('/rooms', methods=['POST'])
|
|
@login_required
|
|
def create_chat_room():
|
|
"""Create a new chat room"""
|
|
data = request.get_json()
|
|
|
|
if not data or not data.get('name'):
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Room name is required'
|
|
}), 400
|
|
|
|
# Validate input
|
|
name = data.get('name', '').strip()
|
|
description = data.get('description', '').strip()
|
|
room_type = data.get('room_type', 'general')
|
|
related_post_id = data.get('related_post_id')
|
|
|
|
if len(name) < 3 or len(name) > 100:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Room name must be between 3 and 100 characters'
|
|
}), 400
|
|
|
|
# Check if room already exists
|
|
existing_room = ChatRoom.query.filter_by(name=name).first()
|
|
if existing_room:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'A room with this name already exists'
|
|
}), 400
|
|
|
|
# Validate related post if specified
|
|
if related_post_id:
|
|
post = Post.query.get(related_post_id)
|
|
if not post:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Related post not found'
|
|
}), 404
|
|
|
|
try:
|
|
# Create room
|
|
room = ChatRoom(
|
|
name=name,
|
|
description=description,
|
|
room_type=room_type,
|
|
created_by_id=current_user.id,
|
|
related_post_id=related_post_id
|
|
)
|
|
db.session.add(room)
|
|
db.session.flush() # Get the room ID
|
|
|
|
# Add creator as participant with admin role
|
|
participant = ChatParticipant(
|
|
room_id=room.id,
|
|
user_id=current_user.id,
|
|
role='admin'
|
|
)
|
|
db.session.add(participant)
|
|
|
|
# Add system message
|
|
system_message = ChatMessage(
|
|
room_id=room.id,
|
|
user_id=current_user.id,
|
|
content=f"Welcome to {name}! This chat room was created for motorcycle adventure discussions.",
|
|
message_type='system'
|
|
)
|
|
db.session.add(system_message)
|
|
|
|
db.session.commit()
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'room': room.to_dict()
|
|
}), 201
|
|
|
|
except Exception as e:
|
|
db.session.rollback()
|
|
current_app.logger.error(f"Error creating chat room: {str(e)}")
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Failed to create chat room'
|
|
}), 500
|
|
|
|
@chat_api.route('/rooms/<int:room_id>', methods=['GET'])
|
|
@login_required
|
|
def get_chat_room(room_id):
|
|
"""Get chat room details"""
|
|
room = ChatRoom.query.get_or_404(room_id)
|
|
|
|
# Check if user has access
|
|
if room.is_private:
|
|
participant = ChatParticipant.query.filter_by(
|
|
room_id=room_id,
|
|
user_id=current_user.id
|
|
).first()
|
|
if not participant:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Access denied'
|
|
}), 403
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'room': room.to_dict()
|
|
})
|
|
|
|
@chat_api.route('/rooms/<int:room_id>/join', methods=['POST'])
|
|
@login_required
|
|
def join_chat_room(room_id):
|
|
"""Join a chat room"""
|
|
room = ChatRoom.query.get_or_404(room_id)
|
|
|
|
# Check if already a participant
|
|
existing_participant = ChatParticipant.query.filter_by(
|
|
room_id=room_id,
|
|
user_id=current_user.id
|
|
).first()
|
|
|
|
if existing_participant:
|
|
return jsonify({
|
|
'success': True,
|
|
'message': 'Already a member of this room'
|
|
})
|
|
|
|
try:
|
|
# Add user as participant
|
|
participant = ChatParticipant(
|
|
room_id=room_id,
|
|
user_id=current_user.id,
|
|
role='member'
|
|
)
|
|
db.session.add(participant)
|
|
|
|
# Add system message
|
|
system_message = ChatMessage(
|
|
room_id=room_id,
|
|
user_id=current_user.id,
|
|
content=f"{current_user.nickname} joined the chat",
|
|
message_type='system'
|
|
)
|
|
db.session.add(system_message)
|
|
|
|
# Update room activity
|
|
room.last_activity = datetime.utcnow()
|
|
|
|
db.session.commit()
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'message': 'Successfully joined the room'
|
|
})
|
|
|
|
except Exception as e:
|
|
db.session.rollback()
|
|
current_app.logger.error(f"Error joining chat room: {str(e)}")
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Failed to join room'
|
|
}), 500
|
|
|
|
@chat_api.route('/rooms/<int:room_id>/messages', methods=['GET'])
|
|
@login_required
|
|
def get_chat_messages(room_id):
|
|
"""Get messages from a chat room"""
|
|
room = ChatRoom.query.get_or_404(room_id)
|
|
|
|
# Check access
|
|
if room.is_private:
|
|
participant = ChatParticipant.query.filter_by(
|
|
room_id=room_id,
|
|
user_id=current_user.id
|
|
).first()
|
|
if not participant:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Access denied'
|
|
}), 403
|
|
|
|
page = request.args.get('page', 1, type=int)
|
|
per_page = request.args.get('per_page', 50, type=int)
|
|
|
|
# Get messages (newest first for mobile scrolling)
|
|
messages = ChatMessage.query.filter_by(
|
|
room_id=room_id,
|
|
is_deleted=False
|
|
).order_by(desc(ChatMessage.created_at)).paginate(
|
|
page=page, per_page=per_page, error_out=False
|
|
)
|
|
|
|
# Update user's last seen
|
|
participant = ChatParticipant.query.filter_by(
|
|
room_id=room_id,
|
|
user_id=current_user.id
|
|
).first()
|
|
if participant:
|
|
participant.last_seen = datetime.utcnow()
|
|
db.session.commit()
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'messages': [msg.to_dict() for msg in reversed(messages.items)],
|
|
'pagination': {
|
|
'page': page,
|
|
'pages': messages.pages,
|
|
'per_page': per_page,
|
|
'total': messages.total,
|
|
'has_next': messages.has_next,
|
|
'has_prev': messages.has_prev
|
|
}
|
|
})
|
|
|
|
@chat_api.route('/rooms/<int:room_id>/messages', methods=['POST'])
|
|
@login_required
|
|
def send_message(room_id):
|
|
"""Send a message to a chat room"""
|
|
room = ChatRoom.query.get_or_404(room_id)
|
|
data = request.get_json()
|
|
|
|
if not data or not data.get('content'):
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Message content is required'
|
|
}), 400
|
|
|
|
content = data.get('content', '').strip()
|
|
message_type = data.get('message_type', 'text')
|
|
reply_to_id = data.get('reply_to_id')
|
|
|
|
# Validate content
|
|
if len(content) < 1 or len(content) > 2000:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Message must be between 1 and 2000 characters'
|
|
}), 400
|
|
|
|
# Check for blocked content
|
|
if contains_blocked_content(content):
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Message contains inappropriate content'
|
|
}), 400
|
|
|
|
# Check if user is participant
|
|
participant = ChatParticipant.query.filter_by(
|
|
room_id=room_id,
|
|
user_id=current_user.id
|
|
).first()
|
|
|
|
if not participant:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'You must join the room first'
|
|
}), 403
|
|
|
|
if participant.is_muted:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'You are muted in this room'
|
|
}), 403
|
|
|
|
try:
|
|
# Create message
|
|
message = ChatMessage(
|
|
room_id=room_id,
|
|
user_id=current_user.id,
|
|
content=content,
|
|
message_type=message_type,
|
|
reply_to_id=reply_to_id
|
|
)
|
|
db.session.add(message)
|
|
|
|
# Update room activity
|
|
room.last_activity = datetime.utcnow()
|
|
|
|
# Update participant last seen
|
|
participant.last_seen = datetime.utcnow()
|
|
|
|
db.session.commit()
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'message': message.to_dict()
|
|
}), 201
|
|
|
|
except Exception as e:
|
|
db.session.rollback()
|
|
current_app.logger.error(f"Error sending message: {str(e)}")
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Failed to send message'
|
|
}), 500
|
|
|
|
@chat_api.route('/rooms/<int:room_id>/participants', methods=['GET'])
|
|
@login_required
|
|
def get_room_participants(room_id):
|
|
"""Get participants of a chat room"""
|
|
room = ChatRoom.query.get_or_404(room_id)
|
|
|
|
participants = ChatParticipant.query.filter_by(room_id=room_id).all()
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'participants': [p.to_dict() for p in participants]
|
|
})
|
|
|
|
@chat_api.route('/admin-support', methods=['POST'])
|
|
@login_required
|
|
def create_admin_support_chat():
|
|
"""Create a chat room for admin support (e.g., password reset)"""
|
|
data = request.get_json()
|
|
reason = data.get('reason', 'general_support')
|
|
description = data.get('description', '')
|
|
|
|
# Check if user already has an active admin support chat
|
|
existing_room = ChatRoom.query.filter(
|
|
and_(
|
|
ChatRoom.room_type == 'admin_support',
|
|
ChatRoom.created_by_id == current_user.id,
|
|
ChatRoom.is_active == True
|
|
)
|
|
).first()
|
|
|
|
if existing_room:
|
|
return jsonify({
|
|
'success': True,
|
|
'room': existing_room.to_dict(),
|
|
'message': 'Using existing support chat'
|
|
})
|
|
|
|
try:
|
|
# Create admin support room
|
|
room_name = f"Support - {current_user.nickname} - {reason}"
|
|
room = ChatRoom(
|
|
name=room_name,
|
|
description=f"Admin support chat for {current_user.nickname}. Reason: {reason}",
|
|
room_type='admin_support',
|
|
is_private=True,
|
|
created_by_id=current_user.id
|
|
)
|
|
db.session.add(room)
|
|
db.session.flush()
|
|
|
|
# Add user as participant
|
|
participant = ChatParticipant(
|
|
room_id=room.id,
|
|
user_id=current_user.id,
|
|
role='member'
|
|
)
|
|
db.session.add(participant)
|
|
|
|
# Add all admins as participants
|
|
admins = User.query.filter_by(is_admin=True).all()
|
|
for admin in admins:
|
|
if admin.id != current_user.id:
|
|
admin_participant = ChatParticipant(
|
|
room_id=room.id,
|
|
user_id=admin.id,
|
|
role='admin'
|
|
)
|
|
db.session.add(admin_participant)
|
|
|
|
# Add initial message
|
|
initial_message = ChatMessage(
|
|
room_id=room.id,
|
|
user_id=current_user.id,
|
|
content=f"Hello! I need help with: {reason}. {description}",
|
|
message_type='text'
|
|
)
|
|
db.session.add(initial_message)
|
|
|
|
db.session.commit()
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'room': room.to_dict()
|
|
}), 201
|
|
|
|
except Exception as e:
|
|
db.session.rollback()
|
|
current_app.logger.error(f"Error creating admin support chat: {str(e)}")
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Failed to create support chat'
|
|
}), 500
|
|
|
|
@chat_api.route('/post/<int:post_id>/discussion', methods=['POST'])
|
|
@login_required
|
|
def create_post_discussion(post_id):
|
|
"""Create or get discussion chat for a specific post"""
|
|
post = Post.query.get_or_404(post_id)
|
|
|
|
# Check if discussion already exists
|
|
existing_room = ChatRoom.query.filter(
|
|
and_(
|
|
ChatRoom.room_type == 'post_discussion',
|
|
ChatRoom.related_post_id == post_id,
|
|
ChatRoom.is_active == True
|
|
)
|
|
).first()
|
|
|
|
if existing_room:
|
|
# Join the existing room if not already a participant
|
|
participant = ChatParticipant.query.filter_by(
|
|
room_id=existing_room.id,
|
|
user_id=current_user.id
|
|
).first()
|
|
|
|
if not participant:
|
|
participant = ChatParticipant(
|
|
room_id=existing_room.id,
|
|
user_id=current_user.id,
|
|
role='member'
|
|
)
|
|
db.session.add(participant)
|
|
db.session.commit()
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'room': existing_room.to_dict(),
|
|
'message': 'Joined existing discussion'
|
|
})
|
|
|
|
try:
|
|
# Create new discussion room
|
|
room_name = f"Discussion: {post.title}"
|
|
room = ChatRoom(
|
|
name=room_name,
|
|
description=f"Discussion about the post: {post.title}",
|
|
room_type='post_discussion',
|
|
created_by_id=current_user.id,
|
|
related_post_id=post_id
|
|
)
|
|
db.session.add(room)
|
|
db.session.flush()
|
|
|
|
# Add creator as participant
|
|
participant = ChatParticipant(
|
|
room_id=room.id,
|
|
user_id=current_user.id,
|
|
role='moderator'
|
|
)
|
|
db.session.add(participant)
|
|
|
|
# Add post author as participant if different
|
|
if post.author_id != current_user.id:
|
|
author_participant = ChatParticipant(
|
|
room_id=room.id,
|
|
user_id=post.author_id,
|
|
role='moderator'
|
|
)
|
|
db.session.add(author_participant)
|
|
|
|
# Add initial message
|
|
initial_message = ChatMessage(
|
|
room_id=room.id,
|
|
user_id=current_user.id,
|
|
content=f"Started discussion about: {post.title}",
|
|
message_type='system'
|
|
)
|
|
db.session.add(initial_message)
|
|
|
|
db.session.commit()
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'room': room.to_dict()
|
|
}), 201
|
|
|
|
except Exception as e:
|
|
db.session.rollback()
|
|
current_app.logger.error(f"Error creating post discussion: {str(e)}")
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Failed to create discussion'
|
|
}), 500
|