import json
import logging
import re
import os
import emojis
from flask import Flask, abort, render_template, request, jsonify, redirect, url_for, flash, session
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user
from werkzeug.security import generate_password_hash, check_password_hash
from werkzeug.utils import secure_filename
from flask_socketio import SocketIO, emit, join_room, leave_room
import google.generativeai as genai
from dotenv import load_dotenv
from datetime import datetime, timedelta
from sqlalchemy import func, or_, case
from models import Comment, Follow, Like, Message, Notification, User, Post, db
load_dotenv()
app = Flask(__name__)
# Configure your database URI
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///social_media.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SECRET_KEY'] = os.urandom(24).hex()
app.config['UPLOAD_FOLDER'] = 'static/uploads'
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max-limit
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=30) # Set session to last for 30 days
db.init_app(app)
from cache_utils import cache
cache.init_app(app)
socketio = SocketIO(app, message_queue='memory://')
genai.configure(api_key=os.getenv('GEMINI_API_KEY'))
model = genai.GenerativeModel('gemini-pro')
migrate = Migrate(app, db)
# Import and register blueprints here
from prof import profile as profile_blueprint # Import your blueprint
app.register_blueprint(profile_blueprint)
def is_valid_input(text):
return text and len(text.strip()) > 0
def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in {'png', 'jpg', 'jpeg', 'gif', 'mp4'}
from flask_login import current_user, login_required
@app.route('/')
def index():
if current_user.is_authenticated:
all_users = User.query.all()
all_posts = Post.query.order_by(Post.timestamp.desc()).all()
# Add comment counts to each post
for post in all_posts:
post.comment_count = post.comments.count()
else:
all_users = []
all_posts = []
return render_template('index.html', all_users=all_users, all_posts=all_posts)
login_manager = LoginManager(app)
login_manager.login_view = 'login'
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
@app.before_request
def make_session_permanent():
session.permanent = True
app.permanent_session_lifetime = timedelta(days=30)
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
user = User.query.filter_by(username=request.form['username']).first()
if user and check_password_hash(user.password_hash, request.form['password']):
login_user(user, remember=True) # Set remember=True to keep the user logged in
session['user_id'] = user.id # Store user_id in session
next_page = request.args.get('next')
return redirect(next_page or url_for('index'))
flash('Invalid username or password')
return render_template('login.html')
@app.route('/logout')
def logout():
logout_user()
return redirect(url_for('login'))
@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
hashed_password = generate_password_hash(request.form['password'])
new_user = User(username=request.form['username'], email=request.form['email'], password_hash=hashed_password)
db.session.add(new_user)
db.session.commit()
flash('Account created successfully')
return redirect(url_for('login'))
return render_template('register.html')
@app.template_filter('replace_usernames')
def replace_usernames(text):
def replace_username(match):
username = match.group(1)
return f'@{username}'
return re.sub(r'@(\w+)', replace_username, text)
@app.route('/api/user_activity/')
@login_required
def user_activity(user_id):
user = User.query.get_or_404(user_id)
# Get posts count for the last 30 days
thirty_days_ago = datetime.utcnow() - timedelta(days=30)
post_counts = db.session.query(
func.date(Post.timestamp).label('date'),
func.count(Post.id).label('count')
).filter(
Post.user_id == user_id,
Post.timestamp >= thirty_days_ago
).group_by(func.date(Post.timestamp)).all()
dates = [(thirty_days_ago + timedelta(days=i)).strftime('%Y-%m-%d') for i in range(31)]
counts = [0] * 31
for date, count in post_counts:
index = (date.date() - thirty_days_ago.date()).days
counts[index] = count
return jsonify({
'labels': dates,
'posts': counts
})
@app.route('/profile/')
@login_required
def user_profile(username):
user = User.query.filter_by(username=username).first_or_404()
posts = Post.query.filter_by(user_id=user.id).all()
followers_count = user.followers.count() # Assuming you have a way to get the count of followers
return render_template('profile.html', user=user, posts=posts, followers_count=followers_count)
@app.route('/edit_profile', methods=['GET', 'POST'])
@login_required
def edit_profile():
if request.method == 'POST':
current_user.bio = request.form['bio']
if 'profile_picture' in request.files:
file = request.files['profile_picture']
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
current_user.profile_picture = filename
db.session.commit()
flash('Profile updated successfully')
return redirect(url_for('profile', username=current_user.username))
return render_template('edit_profile.html')
@app.route('/api/follow/', methods=['POST'])
@login_required
def follow(username):
user = User.query.filter_by(username=username).first()
if user is None:
return jsonify({'error': 'User not found.'}), 404
if user == current_user:
return jsonify({'error': 'You cannot follow yourself!'}), 400
if not current_user.is_following(user):
current_user.following.append(Follow(followed=user))
db.session.commit()
return jsonify({
'message': f'You are now following {username}!',
'followerCount': user.followers.count()
})
else:
return jsonify({'error': 'You are already following this user.'}), 400
@app.route('/api/unfollow/', methods=['POST'])
@login_required
def unfollow(username):
user = User.query.filter_by(username=username).first()
if user is None:
return jsonify({'error': 'User not found.'}), 404
if user == current_user:
return jsonify({'error': 'You cannot unfollow yourself!'}), 400
follow = current_user.following.filter_by(followed_id=user.id).first()
if follow:
db.session.delete(follow)
db.session.commit()
return jsonify({
'message': f'You have unfollowed {username}.',
'followerCount': user.followers.count()
})
else:
return jsonify({'error': 'You are not following this user.'}), 400
@app.route('/api/delete_account', methods=['POST'])
@login_required
def delete_account():
try:
db.session.delete(current_user)
db.session.commit()
flash('Your account has been successfully deleted.')
return jsonify({'message': 'Account deleted successfully', 'redirect': url_for('register')})
except Exception as e:
db.session.rollback()
return jsonify({'error': 'An error occurred while deleting your account.'}), 500
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
from flask import render_template, flash, redirect, url_for
@app.route('/post', methods=['POST'])
@login_required
def post():
user_input = request.form.get('content', '').strip()
if not user_input:
flash('Post content cannot be empty!', 'error')
return redirect(url_for('index'))
# Use Gemini model to check for community guideline violations
prompt = f"""
Analyze the following text for any violations of community guidelines.
If violations are found, provide a friendly explanation and suggest 3 alternative wordings.
Make the suggestions fun and engaging.
Text to analyze: "{user_input}"
Respond in the following JSON format:
{{
"violates_guidelines": boolean,
"explanation": "string",
"suggestions": ["string"]
}}
"""
try:
response = model.generate_content(prompt)
logger.debug(f"Gemini response text: {response.text}")
# Extract JSON from the response
json_match = re.search(r'\{.*\}', response.text, re.DOTALL)
if json_match:
response_json = json_match.group(0)
try:
response_data = json.loads(response_json)
except json.JSONDecodeError as e:
logger.error(f"JSONDecodeError: {str(e)} - Response: {response_json}")
flash('An error occurred while processing your post. Please try again.', 'error')
return redirect(url_for('index'))
else:
raise ValueError("No valid JSON found in the response")
logger.debug(f"Parsed response data: {response_data}")
if response_data.get('violates_guidelines', False):
flash(response_data.get('explanation', 'Your post may violate community guidelines.'), 'warning')
return render_template('index.html',
violation_suggestions=response_data.get('suggestions', []),
original_content=user_input)
# If no violations, create the post
new_post = Post(content=user_input, user_id=current_user.id, timestamp=datetime.utcnow())
if 'media' in request.files:
file = request.files['media']
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
file.save(filepath)
new_post.media_url = filename
db.session.add(new_post)
db.session.commit()
flash('Your post has been created successfully!', 'success')
return redirect(url_for('index'))
except Exception as e:
logger.error(f"Error processing or saving post: {str(e)}", exc_info=True)
flash('An error occurred while processing your post. Please try again.', 'error')
return redirect(url_for('index'))
@app.route('/submit_post', methods=['POST'])
@login_required
def submit_post():
content = request.form.get('content', '').strip()
if not content:
return jsonify({'error': 'Post content cannot be empty!'}), 400
prompt = f"""
Analyze the following text for any violations of community guidelines.
If violations are found, provide a friendly explanation and suggest 3 alternative wordings.
Make the suggestions fun and engaging.
Text to analyze: "{content}"
Respond in the following JSON format:
{{
"violates_guidelines": boolean,
"explanation": "string",
"suggestions": ["string"]
}}
"""
try:
response = model.generate_content(prompt)
logger.debug(f"Gemini response text: {response.text}")
json_match = re.search(r'\{.*\}', response.text, re.DOTALL)
if json_match:
response_data = json.loads(json_match.group(0))
else:
raise ValueError("No valid JSON found in the response")
logger.debug(f"Parsed response data: {response_data}")
if response_data.get('violates_guidelines', False):
return jsonify({
'violates_guidelines': True,
'explanation': response_data.get('explanation', 'No explanation provided.'),
'suggestions': response_data.get('suggestions', [])
}), 200
# If no violations, create the post
new_post = Post(content=content, user_id=current_user.id, timestamp=datetime.utcnow())
if 'media' in request.files:
file = request.files['media']
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
file.save(filepath)
new_post.media_url = filename
db.session.add(new_post)
db.session.commit()
return jsonify({'success': True, 'message': 'Your post has been created!'}), 200
except Exception as e:
logger.error(f"Error processing or saving post: {str(e)}", exc_info=True)
return jsonify({'error': 'An error occurred while processing your post. Please try again.'}), 500
@app.route('/like/', methods=['POST'])
@login_required
def like_post(post_id):
post = Post.query.get_or_404(post_id)
if current_user not in post.likes:
post.likes.append(current_user)
else:
post.likes.remove(current_user)
db.session.commit()
return jsonify({'likes_count': len(post.likes), 'is_liked': current_user in post.likes})
@app.route('/comment/', methods=['POST'])
@login_required
def add_comment(post_id):
post = Post.query.get_or_404(post_id)
content = request.json.get('content')
if content:
comment = Comment(content=content, author=current_user, post=post)
db.session.add(comment)
db.session.commit()
return jsonify({
'id': comment.id,
'content': comment.content,
'author': comment.author.username,
'timestamp': comment.timestamp.strftime('%Y-%m-%d %H:%M:%S')
}), 201
return jsonify({'error': 'Comment content is required'}), 400
@app.route('/comment/', methods=['DELETE'])
@login_required
def delete_comment(comment_id):
comment = Comment.query.get_or_404(comment_id)
if comment.author != current_user:
return jsonify({'error': 'Unauthorized'}), 403
db.session.delete(comment)
db.session.commit()
return jsonify({'message': 'Comment deleted successfully'}), 200
@app.route('/delete_post/', methods=['DELETE'])
@login_required
def delete_post(post_id):
post = Post.query.get_or_404(post_id)
if post.author != current_user:
return jsonify({'status': 'error', 'message': 'Unauthorized'}), 403
# Manually delete all comments associated with the post
for comment in post.comments:
db.session.delete(comment)
db.session.delete(post)
db.session.commit()
return jsonify({'status': 'success', 'message': 'Post deleted successfully'}), 200
@app.route('/conversations')
def conversations():
return redirect(url_for('messages'))
@app.route('/messages/', defaults={'recipient_id': None})
@app.route('/messages/')
@login_required
@cache.cached(timeout=60, key_prefix='messages_%s') # Cache for 1 minute
def messages(recipient_id):
available_users = get_available_users()
if recipient_id is None and available_users:
recipient_id = available_users[0]['id']
recipient = User.query.get(recipient_id) if recipient_id else None
messages = get_messages(current_user.id, recipient_id) if recipient_id else []
starters = suggest_conversation_starters(current_user.id, recipient_id) if recipient_id else []
return render_template('messages.html',
messages=messages,
starters=starters,
recipient=recipient,
available_users=available_users,
current_user=current_user)
@app.route('/api/conversation_starters/')
@login_required
def api_conversation_starters(other_user_id):
starters = suggest_conversation_starters(current_user.id, other_user_id)
return jsonify({'starters': starters})
@app.route('/send_message/', methods=['POST'])
@login_required
@cache.memoize(300)
def send_message_route(recipient_id):
content = request.form['content']
media = request.files.get('media')
media_url = None
ai_response_flag = request.form.get('ai_response', 'false').lower() == 'true'
moderation_result = moderate_content(content)
if moderation_result['violates_guidelines']:
flash('Content violates guidelines: ' + moderation_result['explanation'])
return redirect(url_for('messages', recipient_id=recipient_id))
if media and allowed_file(media.filename):
filename = secure_filename(media.filename)
media_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
media.save(media_path)
media_url = url_for('static', filename=f'uploads/{filename}')
new_message, error = send_message_helper(current_user.id, recipient_id, content, media_url)
if error:
flash('Error sending message: ' + error)
return redirect(url_for('messages', recipient_id=recipient_id))
message_data = {
'id': new_message.id,
'sender_id': current_user.id,
'recipient_id': recipient_id,
'content': content,
'media_url': media_url,
'timestamp': new_message.timestamp.isoformat()
}
socketio.emit('new_message', message_data, room=str(recipient_id))
socketio.emit('new_message', message_data, room=str(current_user.id))
# Generate AI reply if the flag is set
if ai_response_flag:
ai_reply = generate_ai_reply(content)
if ai_reply:
ai_message, _ = send_message_helper(recipient_id, current_user.id, ai_reply)
ai_message_data = {
'id': ai_message.id,
'sender_id': recipient_id,
'recipient_id': current_user.id,
'content': ai_reply,
'timestamp': ai_message.timestamp.isoformat()
}
socketio.emit('new_message', ai_message_data, room=str(current_user.id))
return redirect(url_for('messages', recipient_id=recipient_id))
def generate_ai_reply(content):
prompt = f"""
Given the following message, suggest a thoughtful and engaging reply:
"{content}"
Keep the reply concise and natural-sounding. Include appropriate emojis to make the message more engaging.
Do not use asterisks or any other formatting. The reply should be ready to send as-is.
"""
response = model.generate_content(prompt)
# Remove any remaining asterisks from the response
cleaned_response = response.text.replace('*', '')
# Use the emojis library to add emojis to the response text
return emojis.encode(cleaned_response, language='alias')
@app.route('/generate_ai_reply/', methods=['POST'])
@login_required
def api_generate_ai_reply(recipient_id):
# Fetch the latest message content from the chat with the recipient
last_message = Message.query.filter(
((Message.sender_id == current_user.id) & (Message.recipient_id == recipient_id)) |
((Message.sender_id == recipient_id) & (Message.recipient_id == current_user.id))
).order_by(Message.timestamp.desc()).first()
if last_message:
content = last_message.content
ai_reply = generate_ai_reply(content)
# Send the AI reply to the chat
new_message, error = send_message_helper(current_user.id, recipient_id, ai_reply)
if error:
return jsonify({'error': error}), 400
message_data = {
'id': new_message.id,
'sender_id': current_user.id,
'recipient_id': recipient_id,
'content': ai_reply,
'timestamp': new_message.timestamp.isoformat()
}
# Broadcast the AI message to both users
socketio.emit('new_message', message_data, room=str(recipient_id))
socketio.emit('new_message', message_data, room=str(current_user.id))
return jsonify({'reply': ai_reply}), 200
else:
return jsonify({'error': 'No previous message found to base AI reply on'}), 400
def send_message_helper(sender_id, recipient_id, content, media_url=None):
try:
if not content.strip():
return None, "Message content cannot be empty."
new_message = Message(
sender_id=sender_id,
recipient_id=recipient_id,
content=content,
media_url=media_url,
timestamp=datetime.utcnow()
)
db.session.add(new_message)
db.session.commit()
return new_message, None
except Exception as e:
db.session.rollback()
return None, f"An error occurred: {str(e)}"
def get_messages(current_user_id, recipient_id, page=1, per_page=20):
messages = db.session.query(Message, User).join(User, Message.sender_id == User.id).filter(
or_(
(Message.sender_id == current_user_id) & (Message.recipient_id == recipient_id),
(Message.sender_id == recipient_id) & (Message.recipient_id == current_user_id)
)
).order_by(Message.timestamp.asc()).paginate(page=page, per_page=per_page, error_out=False)
return messages.items
@app.route('/delete_chat_history/', methods=['POST'])
@login_required
def delete_chat_history(recipient_id):
try:
# Delete messages where the current user is either the sender or the recipient
Message.query.filter(
or_(
(Message.sender_id == current_user.id) & (Message.recipient_id == recipient_id),
(Message.sender_id == recipient_id) & (Message.recipient_id == current_user.id)
)
).delete(synchronize_session=False)
# Commit the changes to the database
db.session.commit()
return jsonify({"success": True, "message": "Chat history deleted successfully"}), 200
except Exception as e:
# If an error occurs, rollback the changes
db.session.rollback()
return jsonify({"success": False, "message": str(e)}), 500
def get_available_users():
users = User.query.filter(User.id != current_user.id).all()
return [{'id': user.id, 'username': user.username, 'profile_picture': user.profile_picture} for user in users]
def suggest_conversation_starters(user_id, other_user_id):
user = User.query.get(user_id)
other_user = User.query.get(other_user_id)
prompt = f"""
Suggest 3 conversation starters for two users based on their profiles:
User 1: {user.bio}
User 2: {other_user.bio}
Provide engaging and relevant conversation starters that could help these users connect. Include relevant emojis and exclude asterisks in your responses.
"""
response = model.generate_content(prompt)
return response.text.split('\n')
@app.route('/notifications')
@login_required
def notifications():
notifications = Notification.query.filter_by(user_id=current_user.id).order_by(Notification.timestamp.desc()).all()
return render_template('notifications.html', notifications=notifications)
from tasks import create_notification as create_notification_task
def create_notification(user_id, content):
create_notification_task.delay(user_id, content)
def get_messages(current_user_id, recipient_id, page=1, per_page=20):
messages = db.session.query(Message, User).join(User, Message.sender_id == User.id).filter(
or_(
(Message.sender_id == current_user_id) & (Message.recipient_id == recipient_id),
(Message.sender_id == recipient_id) & (Message.recipient_id == current_user_id)
)
).order_by(Message.timestamp.asc()).paginate(page=page, per_page=per_page, error_out=False)
return messages.items
def get_available_users():
users = User.query.filter(User.id != current_user.id).all()
return [{'id': user.id, 'username': user.username, 'profile_picture': user.profile_picture} for user in users]
@cache.memoize(300)
def suggest_conversation_starters(user_id, other_user_id):
user = User.query.get(user_id)
other_user = User.query.get(other_user_id)
prompt = f"""
Suggest 3 conversation starters for two users based on their profiles:
User 1: {user.bio}
User 2: {other_user.bio}
Provide engaging and relevant conversation starters that could help these users connect.
"""
response = model.generate_content(prompt)
return response.text.split('\n')
@socketio.on('typing')
def handle_typing(data):
recipient_id = data['recipient_id']
socketio.emit('typing', {'sender_id': current_user.id}, room=str(recipient_id))
@socketio.on('stop_typing')
def handle_stop_typing(data):
recipient_id = data['recipient_id']
socketio.emit('stop_typing', {'sender_id': current_user.id}, room=str(recipient_id))
@socketio.on('message_read')
def handle_message_read(data):
message_id = data['message_id']
message = Message.query.get(message_id)
if message:
message.read = True
db.session.commit()
socketio.emit('message_status_update', {'message_id': message_id, 'read': True}, room=str(message.sender_id))
@socketio.on('connect')
def handle_connect():
if current_user.is_authenticated:
join_room(str(current_user.id))
@socketio.on('disconnect')
def handle_disconnect():
if current_user.is_authenticated:
leave_room(str(current_user.id))
@app.before_request
def make_session_permanent():
session.permanent = True
app.permanent_session_lifetime = timedelta(days=30)
@cache.memoize(300)
def moderate_content(content):
prompt = f"""
Analyze the following content for appropriateness on a social media platform. Please take into account common community guidelines which may include but are not limited to: harassment, hate speech, violence, explicit content, misinformation, and spam.
Content to analyze:
"{content}"
Please provide the following in your response:
1. **Violates Guidelines**: Determine if the content violates any common social media community guidelines. Respond with `true` if it violates, otherwise `false`.
2. **Explanation**: Provide a brief explanation for your determination. Mention which specific guideline(s) are potentially violated or why the content is considered appropriate.
3. **Sentiment Analysis**: Analyze the sentiment of the content and classify it as `positive`, `neutral`, or `negative`. Provide reasoning for the sentiment classification.
4. **Suggestions for Improvement**: If the content is borderline inappropriate or has potential issues, suggest specific ways to improve it to make it more suitable for a social media platform.
Format your response as a JSON object with the following keys:
- `"violates_guidelines"`: (boolean) `true` or `false` indicating if the content violates guidelines.
- `"explanation"`: (string) A brief explanation of why the content does or does not violate guidelines.
- `"sentiment"`: (string) The sentiment analysis result, which can be `positive`, `neutral`, or `negative`.
- `"suggestions"`: (array of strings) Suggestions for improving the content if needed.
Example of a JSON response:
{{
"violates_guidelines": true,
"explanation": "The content contains explicit language which violates our community guidelines on harassment.",
"sentiment": "negative",
"suggestions": ["Remove explicit language", "Rephrase the content to be more respectful."]
}}
"""
try:
response = model.generate_content(prompt)
response_text = getattr(response, 'text', '').strip()
# Log the response text for debugging
print(f"Response text: {response_text}")
if not response_text:
raise ValueError("Received an empty response from the model")
# Try to parse the JSON response
try:
moderation_result = json.loads(response_text)
except json.JSONDecodeError as e:
print(f"Error decoding JSON: {e}")
print(f"Response text: {response_text}")
# Attempt to extract JSON from the response if it's not properly formatted
json_match = re.search(r'\{.*\}', response_text, re.DOTALL)
if json_match:
try:
moderation_result = json.loads(json_match.group())
except json.JSONDecodeError:
raise ValueError("Unable to extract valid JSON from the model's response")
else:
raise ValueError("No JSON-like structure found in the model's response")
# Validate the structure of the moderation result
required_keys = ['violates_guidelines', 'explanation', 'sentiment', 'suggestions']
if not all(key in moderation_result for key in required_keys):
raise ValueError("Moderation result is missing required keys")
# Check for vulgar language
if moderation_result.get("violates_guidelines") and "explicit" in moderation_result.get("explanation", "").lower():
moderation_result["suggestions"].append("Please avoid using vulgar language.")
return moderation_result
except Exception as e:
print(f"Error in moderate_content: {str(e)}")
# Return a default response in case of any error
return {
"violates_guidelines": False,
"explanation": "Unable to analyze content due to an error.",
"sentiment": "neutral",
"suggestions": ["Please try again later."]
}
if __name__ == '__main__':
#with app.app_context():
# db.create_all()
socketio.run(app, debug=True)