Files
moto-adv-website/app/routes/chat_api.py
ske087 1661f5f588 feat: Complete chat system implementation and password reset enhancement
- 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
2025-08-09 20:44:25 +03:00

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