LifeRPG_v2.0/modern/backend/community_features.py
TLimoges33 2b961611fd
🚀 Major Enhancement: Complete AI-Powered LifeRPG Platform with Git LFS
 New Features:
- AI-powered habit creation with natural language processing
- HuggingFace transformers integration for sentiment analysis (tracked via Git LFS)
- Advanced predictive analytics and behavioral insights
- Voice & image input capabilities for hands-free habit tracking
- Real-time notifications and community features
- Plugin system with extensible architecture

🔧 Technical Improvements:
- Comprehensive FastAPI backend with 30+ endpoints
- React frontend with PWA capabilities
- Advanced authentication with 2FA support
- RBAC authorization system
- Comprehensive security features (CSRF, rate limiting, audit logging)
- Database migrations and health monitoring
- Docker containerization support
- Git LFS configured for large AI model files (2+ GB)

📚 Documentation & DevOps:
- Complete deployment guides for multiple platforms
- Professional README with feature highlights
- GitHub Actions CI/CD workflows
- Comprehensive API documentation
- Security audit roadmap and compliance framework
- Setup scripts for development environment

🧪 Testing & Quality:
- Comprehensive test suite with 20+ test modules
- Setup verification scripts
- Working development environment with both backend and frontend
- Health checks and monitoring systems

🌟 Ready for:
- Portfolio showcasing
- Community contributions
- Production deployment
- Professional presentation
2025-09-28 21:29:19 +00:00

712 lines
24 KiB
Python

"""
Community Features System - Social Engagement and Habit Buddies
Enables users to connect, share progress, and motivate each other
"""
import asyncio
import json
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Any
from dataclasses import dataclass, asdict
from enum import Enum
from sqlalchemy.orm import Session
from sqlalchemy import text, and_, or_
from fastapi import HTTPException
from .models import User, Habit, Log
from .db import get_db
class ChallengeStatus(Enum):
DRAFT = "draft"
ACTIVE = "active"
COMPLETED = "completed"
CANCELLED = "cancelled"
class ChallengeType(Enum):
INDIVIDUAL = "individual" # Personal challenge
GROUP = "group" # Multiple participants
COMMUNITY = "community" # Open to all users
@dataclass
class Community:
"""Represents a community/group of users"""
id: int
name: str
description: str
category: str # fitness, productivity, wellness, etc.
is_public: bool
member_count: int
created_by: int
created_at: datetime
tags: List[str]
rules: Dict[str, Any]
@dataclass
class HabitBuddy:
"""Represents a habit accountability partnership"""
id: int
user1_id: int
user2_id: int
shared_habits: List[int] # habit IDs they're tracking together
status: str # active, paused, completed
created_at: datetime
motivation_message: str
check_in_frequency: str # daily, weekly
@dataclass
class Challenge:
"""Represents a habit challenge"""
id: int
title: str
description: str
challenge_type: ChallengeType
status: ChallengeStatus
start_date: datetime
end_date: datetime
creator_id: int
participants: List[int]
habit_template: Dict[str, Any]
rewards: Dict[str, Any]
rules: Dict[str, Any]
progress: Dict[int, Any] # user_id -> progress data
@dataclass
class Achievement:
"""Community achievement/badge"""
id: int
title: str
description: str
icon: str
category: str
requirements: Dict[str, Any]
rarity: str # common, rare, epic, legendary
points: int
@dataclass
class SocialPost:
"""Social media style post about habits"""
id: int
user_id: int
content: str
post_type: str # milestone, motivation, question, celebration
habit_id: Optional[int]
media_urls: List[str]
likes: int
comments: List[Dict]
created_at: datetime
visibility: str # public, friends, private
class CommunityManager:
"""Manages community features and social interactions"""
def __init__(self, db_session: Session):
self.db = db_session
async def create_community(self, creator_id: int, community_data: Dict) -> Community:
"""Create a new community"""
# Validate community data
required_fields = ['name', 'description', 'category']
for field in required_fields:
if field not in community_data:
raise ValueError(f"Missing required field: {field}")
# Insert into database
query = """
INSERT INTO communities (name, description, category, is_public,
created_by, created_at, tags, rules)
VALUES (:name, :description, :category, :is_public,
:created_by, :created_at, :tags, :rules)
RETURNING id
"""
result = await self.db.execute(text(query), {
'name': community_data['name'],
'description': community_data['description'],
'category': community_data['category'],
'is_public': community_data.get('is_public', True),
'created_by': creator_id,
'created_at': datetime.now(),
'tags': json.dumps(community_data.get('tags', [])),
'rules': json.dumps(community_data.get('rules', {}))
})
community_id = result.scalar()
# Add creator as first member
await self._add_community_member(community_id, creator_id, role='admin')
# Return the created community
return await self.get_community(community_id)
async def get_community(self, community_id: int) -> Optional[Community]:
"""Get community details"""
query = """
SELECT c.*, COUNT(cm.user_id) as member_count
FROM communities c
LEFT JOIN community_members cm ON c.id = cm.community_id
WHERE c.id = :community_id
GROUP BY c.id
"""
result = await self.db.execute(text(query), {'community_id': community_id})
row = result.first()
if not row:
return None
return Community(
id=row.id,
name=row.name,
description=row.description,
category=row.category,
is_public=row.is_public,
member_count=row.member_count or 0,
created_by=row.created_by,
created_at=row.created_at,
tags=json.loads(row.tags or '[]'),
rules=json.loads(row.rules or '{}')
)
async def join_community(self, community_id: int, user_id: int) -> bool:
"""Join a community"""
# Check if community exists and is public or user is invited
community = await self.get_community(community_id)
if not community:
raise HTTPException(status_code=404, detail="Community not found")
# Check if already a member
existing_member = await self._is_community_member(community_id, user_id)
if existing_member:
return False # Already a member
# Add as member
await self._add_community_member(community_id, user_id, role='member')
return True
async def _add_community_member(self, community_id: int, user_id: int, role: str = 'member'):
"""Add a member to a community"""
query = """
INSERT INTO community_members (community_id, user_id, role, joined_at)
VALUES (:community_id, :user_id, :role, :joined_at)
ON CONFLICT (community_id, user_id) DO NOTHING
"""
await self.db.execute(text(query), {
'community_id': community_id,
'user_id': user_id,
'role': role,
'joined_at': datetime.now()
})
async def _is_community_member(self, community_id: int, user_id: int) -> bool:
"""Check if user is a community member"""
query = """
SELECT 1 FROM community_members
WHERE community_id = :community_id AND user_id = :user_id
"""
result = await self.db.execute(text(query), {
'community_id': community_id,
'user_id': user_id
})
return result.first() is not None
async def create_habit_buddy_partnership(self, user1_id: int, user2_id: int,
shared_habits: List[int]) -> HabitBuddy:
"""Create a habit buddy partnership"""
# Validate that both users exist and habits belong to one of them
# Implementation depends on your user validation logic
query = """
INSERT INTO habit_buddies (user1_id, user2_id, shared_habits, status,
created_at, check_in_frequency)
VALUES (:user1_id, :user2_id, :shared_habits, :status,
:created_at, :check_in_frequency)
RETURNING id
"""
result = await self.db.execute(text(query), {
'user1_id': user1_id,
'user2_id': user2_id,
'shared_habits': json.dumps(shared_habits),
'status': 'active',
'created_at': datetime.now(),
'check_in_frequency': 'daily'
})
buddy_id = result.scalar()
return HabitBuddy(
id=buddy_id,
user1_id=user1_id,
user2_id=user2_id,
shared_habits=shared_habits,
status='active',
created_at=datetime.now(),
motivation_message='',
check_in_frequency='daily'
)
async def get_user_habit_buddies(self, user_id: int) -> List[HabitBuddy]:
"""Get all habit buddies for a user"""
query = """
SELECT hb.*, u1.username as user1_name, u2.username as user2_name
FROM habit_buddies hb
JOIN users u1 ON hb.user1_id = u1.id
JOIN users u2 ON hb.user2_id = u2.id
WHERE (hb.user1_id = :user_id OR hb.user2_id = :user_id)
AND hb.status = 'active'
ORDER BY hb.created_at DESC
"""
result = await self.db.execute(text(query), {'user_id': user_id})
buddies = []
for row in result:
buddies.append(HabitBuddy(
id=row.id,
user1_id=row.user1_id,
user2_id=row.user2_id,
shared_habits=json.loads(row.shared_habits or '[]'),
status=row.status,
created_at=row.created_at,
motivation_message=row.motivation_message or '',
check_in_frequency=row.check_in_frequency or 'daily'
))
return buddies
class ChallengeManager:
"""Manages habit challenges and competitions"""
def __init__(self, db_session: Session):
self.db = db_session
async def create_challenge(self, creator_id: int, challenge_data: Dict) -> Challenge:
"""Create a new challenge"""
# Validate challenge data
required_fields = ['title', 'description', 'challenge_type', 'start_date', 'end_date']
for field in required_fields:
if field not in challenge_data:
raise ValueError(f"Missing required field: {field}")
query = """
INSERT INTO challenges (title, description, challenge_type, status,
start_date, end_date, creator_id, created_at,
habit_template, rewards, rules)
VALUES (:title, :description, :challenge_type, :status,
:start_date, :end_date, :creator_id, :created_at,
:habit_template, :rewards, :rules)
RETURNING id
"""
result = await self.db.execute(text(query), {
'title': challenge_data['title'],
'description': challenge_data['description'],
'challenge_type': challenge_data['challenge_type'],
'status': ChallengeStatus.DRAFT.value,
'start_date': challenge_data['start_date'],
'end_date': challenge_data['end_date'],
'creator_id': creator_id,
'created_at': datetime.now(),
'habit_template': json.dumps(challenge_data.get('habit_template', {})),
'rewards': json.dumps(challenge_data.get('rewards', {})),
'rules': json.dumps(challenge_data.get('rules', {}))
})
challenge_id = result.scalar()
# Auto-join creator to their own challenge
await self.join_challenge(challenge_id, creator_id)
return await self.get_challenge(challenge_id)
async def get_challenge(self, challenge_id: int) -> Optional[Challenge]:
"""Get challenge details"""
query = """
SELECT c.*,
COALESCE(
json_agg(
json_build_object('user_id', cp.user_id, 'joined_at', cp.joined_at)
) FILTER (WHERE cp.user_id IS NOT NULL),
'[]'
) as participants_data
FROM challenges c
LEFT JOIN challenge_participants cp ON c.id = cp.challenge_id
WHERE c.id = :challenge_id
GROUP BY c.id
"""
result = await self.db.execute(text(query), {'challenge_id': challenge_id})
row = result.first()
if not row:
return None
participants_data = json.loads(row.participants_data)
participants = [p['user_id'] for p in participants_data]
return Challenge(
id=row.id,
title=row.title,
description=row.description,
challenge_type=ChallengeType(row.challenge_type),
status=ChallengeStatus(row.status),
start_date=row.start_date,
end_date=row.end_date,
creator_id=row.creator_id,
participants=participants,
habit_template=json.loads(row.habit_template or '{}'),
rewards=json.loads(row.rewards or '{}'),
rules=json.loads(row.rules or '{}'),
progress={} # Will be populated separately if needed
)
async def join_challenge(self, challenge_id: int, user_id: int) -> bool:
"""Join a challenge"""
# Check if challenge exists and is joinable
challenge = await self.get_challenge(challenge_id)
if not challenge:
raise HTTPException(status_code=404, detail="Challenge not found")
if challenge.status not in [ChallengeStatus.DRAFT, ChallengeStatus.ACTIVE]:
raise HTTPException(status_code=400, detail="Challenge not joinable")
# Check if already participating
if user_id in challenge.participants:
return False # Already participating
# Add participant
query = """
INSERT INTO challenge_participants (challenge_id, user_id, joined_at)
VALUES (:challenge_id, :user_id, :joined_at)
ON CONFLICT (challenge_id, user_id) DO NOTHING
"""
await self.db.execute(text(query), {
'challenge_id': challenge_id,
'user_id': user_id,
'joined_at': datetime.now()
})
return True
async def get_active_challenges(self, user_id: Optional[int] = None,
limit: int = 20) -> List[Challenge]:
"""Get active challenges, optionally filtered by user participation"""
base_query = """
SELECT c.*,
COUNT(cp.user_id) as participant_count,
CASE WHEN :user_id IS NULL THEN FALSE
ELSE EXISTS(
SELECT 1 FROM challenge_participants cp2
WHERE cp2.challenge_id = c.id AND cp2.user_id = :user_id
) END as user_participating
FROM challenges c
LEFT JOIN challenge_participants cp ON c.id = cp.challenge_id
WHERE c.status = 'active'
AND c.start_date <= :now
AND c.end_date > :now
"""
if user_id:
base_query += """
AND (c.challenge_type = 'community'
OR EXISTS(
SELECT 1 FROM challenge_participants cp3
WHERE cp3.challenge_id = c.id AND cp3.user_id = :user_id
))
"""
base_query += """
GROUP BY c.id
ORDER BY c.start_date DESC
LIMIT :limit
"""
result = await self.db.execute(text(base_query), {
'user_id': user_id,
'now': datetime.now(),
'limit': limit
})
challenges = []
for row in result:
# Get participants for this challenge
participants = await self._get_challenge_participants(row.id)
challenges.append(Challenge(
id=row.id,
title=row.title,
description=row.description,
challenge_type=ChallengeType(row.challenge_type),
status=ChallengeStatus(row.status),
start_date=row.start_date,
end_date=row.end_date,
creator_id=row.creator_id,
participants=participants,
habit_template=json.loads(row.habit_template or '{}'),
rewards=json.loads(row.rewards or '{}'),
rules=json.loads(row.rules or '{}'),
progress={}
))
return challenges
async def _get_challenge_participants(self, challenge_id: int) -> List[int]:
"""Get list of participant user IDs for a challenge"""
query = """
SELECT user_id FROM challenge_participants
WHERE challenge_id = :challenge_id
"""
result = await self.db.execute(text(query), {'challenge_id': challenge_id})
return [row.user_id for row in result]
async def update_challenge_progress(self, challenge_id: int, user_id: int,
progress_data: Dict):
"""Update a user's progress in a challenge"""
query = """
INSERT INTO challenge_progress (challenge_id, user_id, progress_data, updated_at)
VALUES (:challenge_id, :user_id, :progress_data, :updated_at)
ON CONFLICT (challenge_id, user_id)
DO UPDATE SET
progress_data = :progress_data,
updated_at = :updated_at
"""
await self.db.execute(text(query), {
'challenge_id': challenge_id,
'user_id': user_id,
'progress_data': json.dumps(progress_data),
'updated_at': datetime.now()
})
async def get_challenge_leaderboard(self, challenge_id: int) -> List[Dict]:
"""Get leaderboard for a challenge"""
query = """
SELECT
cp.user_id,
u.username,
cp.progress_data,
cp.updated_at,
ROW_NUMBER() OVER (ORDER BY
CAST(cp.progress_data->>'score' AS INTEGER) DESC,
cp.updated_at ASC
) as rank
FROM challenge_progress cp
JOIN users u ON cp.user_id = u.id
WHERE cp.challenge_id = :challenge_id
ORDER BY rank
LIMIT 50
"""
result = await self.db.execute(text(query), {'challenge_id': challenge_id})
leaderboard = []
for row in result:
leaderboard.append({
'rank': row.rank,
'user_id': row.user_id,
'username': row.username,
'progress': json.loads(row.progress_data or '{}'),
'last_updated': row.updated_at
})
return leaderboard
class SocialFeedManager:
"""Manages social feed and posts"""
def __init__(self, db_session: Session):
self.db = db_session
async def create_post(self, user_id: int, post_data: Dict) -> SocialPost:
"""Create a social post"""
query = """
INSERT INTO social_posts (user_id, content, post_type, habit_id,
media_urls, created_at, visibility)
VALUES (:user_id, :content, :post_type, :habit_id,
:media_urls, :created_at, :visibility)
RETURNING id
"""
result = await self.db.execute(text(query), {
'user_id': user_id,
'content': post_data['content'],
'post_type': post_data.get('post_type', 'general'),
'habit_id': post_data.get('habit_id'),
'media_urls': json.dumps(post_data.get('media_urls', [])),
'created_at': datetime.now(),
'visibility': post_data.get('visibility', 'public')
})
post_id = result.scalar()
return SocialPost(
id=post_id,
user_id=user_id,
content=post_data['content'],
post_type=post_data.get('post_type', 'general'),
habit_id=post_data.get('habit_id'),
media_urls=post_data.get('media_urls', []),
likes=0,
comments=[],
created_at=datetime.now(),
visibility=post_data.get('visibility', 'public')
)
async def get_user_feed(self, user_id: int, limit: int = 50) -> List[Dict]:
"""Get social feed for a user"""
query = """
SELECT
sp.*,
u.username,
u.avatar_url,
COUNT(spl.id) as likes_count,
COUNT(spc.id) as comments_count
FROM social_posts sp
JOIN users u ON sp.user_id = u.id
LEFT JOIN social_post_likes spl ON sp.id = spl.post_id
LEFT JOIN social_post_comments spc ON sp.id = spc.post_id
WHERE sp.visibility = 'public'
OR sp.user_id = :user_id
OR sp.user_id IN (
SELECT user2_id FROM habit_buddies WHERE user1_id = :user_id
UNION
SELECT user1_id FROM habit_buddies WHERE user2_id = :user_id
)
GROUP BY sp.id, u.username, u.avatar_url
ORDER BY sp.created_at DESC
LIMIT :limit
"""
result = await self.db.execute(text(query), {
'user_id': user_id,
'limit': limit
})
feed = []
for row in result:
feed.append({
'id': row.id,
'user_id': row.user_id,
'username': row.username,
'avatar_url': row.avatar_url,
'content': row.content,
'post_type': row.post_type,
'habit_id': row.habit_id,
'media_urls': json.loads(row.media_urls or '[]'),
'likes': row.likes_count,
'comments': row.comments_count,
'created_at': row.created_at,
'visibility': row.visibility
})
return feed
async def like_post(self, post_id: int, user_id: int) -> bool:
"""Like or unlike a post"""
# Check if already liked
query = """
SELECT 1 FROM social_post_likes
WHERE post_id = :post_id AND user_id = :user_id
"""
result = await self.db.execute(text(query), {
'post_id': post_id,
'user_id': user_id
})
if result.first():
# Unlike
delete_query = """
DELETE FROM social_post_likes
WHERE post_id = :post_id AND user_id = :user_id
"""
await self.db.execute(text(delete_query), {
'post_id': post_id,
'user_id': user_id
})
return False
else:
# Like
insert_query = """
INSERT INTO social_post_likes (post_id, user_id, created_at)
VALUES (:post_id, :user_id, :created_at)
"""
await self.db.execute(text(insert_query), {
'post_id': post_id,
'user_id': user_id,
'created_at': datetime.now()
})
return True
# FastAPI endpoints for community features
async def create_community_endpoint(creator_id: int, community_data: Dict,
db: Session) -> Dict:
"""Create a new community"""
manager = CommunityManager(db)
community = await manager.create_community(creator_id, community_data)
return asdict(community)
async def get_user_communities(user_id: int, db: Session) -> List[Dict]:
"""Get communities for a user"""
query = """
SELECT c.*, cm.role, cm.joined_at
FROM communities c
JOIN community_members cm ON c.id = cm.community_id
WHERE cm.user_id = :user_id
ORDER BY cm.joined_at DESC
"""
result = await db.execute(text(query), {'user_id': user_id})
communities = []
for row in result:
communities.append({
'id': row.id,
'name': row.name,
'description': row.description,
'category': row.category,
'is_public': row.is_public,
'created_by': row.created_by,
'created_at': row.created_at,
'tags': json.loads(row.tags or '[]'),
'user_role': row.role,
'joined_at': row.joined_at
})
return communities