diff --git a/modern/backend/app.py b/modern/backend/app.py index 3b342d6..883bbf7 100644 --- a/modern/backend/app.py +++ b/modern/backend/app.py @@ -5,6 +5,7 @@ from .oauth import router as oauth_router import os import requests import time +from fastapi import Body app = FastAPI(title='LifeRPG Modern Backend') @@ -74,3 +75,153 @@ def google_events(integration_id: int): return resp.json() finally: db.close() + + +@app.post('/api/v1/guilds') +def create_guild(payload: dict = Body({})): + name = payload.get('name') + owner_id = payload.get('owner_id', 1) + if not name: + raise HTTPException(status_code=400, detail='name required') + db = models.SessionLocal() + try: + g = models.Guild(name=name, description=payload.get('description'), owner_id=owner_id) + db.add(g) + db.commit() + db.refresh(g) + return {'id': g.id, 'name': g.name} + finally: + db.close() + + +@app.get('/api/v1/guilds') +def list_guilds(): + db = models.SessionLocal() + try: + rows = db.query(models.Guild).all() + return [{'id': r.id, 'name': r.name, 'owner_id': r.owner_id} for r in rows] + finally: + db.close() + + +@app.post('/api/v1/guilds/{guild_id}/members') +def add_guild_member(guild_id: int, payload: dict = Body({})): + user_id = payload.get('user_id') + role = payload.get('role', 'member') + if not user_id: + raise HTTPException(status_code=400, detail='user_id required') + db = models.SessionLocal() + try: + gm = models.GuildMember(guild_id=guild_id, user_id=user_id, role=role) + db.add(gm) + db.commit() + db.refresh(gm) + return {'id': gm.id, 'guild_id': gm.guild_id, 'user_id': gm.user_id} + finally: + db.close() + + +@app.get('/api/v1/guilds/{guild_id}/members') +def list_guild_members(guild_id: int): + db = models.SessionLocal() + try: + rows = db.query(models.GuildMember).filter_by(guild_id=guild_id).all() + return [{'id': r.id, 'user_id': r.user_id, 'role': r.role} for r in rows] + finally: + db.close() + + +@app.get('/api/v1/users/{user_id}/integrations') +def list_user_integrations(user_id: int): + db = models.SessionLocal() + try: + rows = db.query(models.Integration).filter_by(user_id=user_id).all() + out = [ + {"id": r.id, "provider": r.provider, "external_id": r.external_id, "created_at": r.created_at.isoformat() if r.created_at else None} + for r in rows + ] + return out + finally: + db.close() + + +@app.get('/api/v1/integrations') +def list_integrations(): + db = models.SessionLocal() + try: + rows = db.query(models.Integration).all() + out = [ + {"id": r.id, "user_id": r.user_id, "provider": r.provider, "external_id": r.external_id, "created_at": r.created_at.isoformat() if r.created_at else None} + for r in rows + ] + return out + finally: + db.close() + + +@app.delete('/api/v1/integrations/{integration_id}') +def delete_integration(integration_id: int): + db = models.SessionLocal() + try: + row = db.query(models.Integration).filter_by(id=integration_id).first() + if not row: + raise HTTPException(status_code=404, detail='integration not found') + db.delete(row) + db.commit() + return {'ok': True} + finally: + db.close() + + +@app.post('/api/v1/integrations/{integration_id}/sync_to_habits') +def sync_integration_to_habits(integration_id: int, payload: dict = Body({})): + """Fetch events from the integration and create Habit + Log entries. + + Demo mapping: create a Habit per event with title 'Event: ' and a Log entry. + """ + db = models.SessionLocal() + try: + integration = db.query(models.Integration).filter_by(id=integration_id).first() + if not integration: + raise HTTPException(status_code=404, detail='integration not found') + + # Fetch events via existing events endpoint logic + # Reuse token refresh + decrypt logic from oauth module + token_row = db.query(models.OAuthToken).filter_by(integration_id=integration_id).order_by(models.OAuthToken.id.desc()).first() + if not token_row: + raise HTTPException(status_code=404, detail='no token found for integration') + + from .oauth import refresh_google_token_if_needed + refreshed = refresh_google_token_if_needed(token_row) + if refreshed: + token_row = refreshed + + from .crypto import decrypt_text + access = decrypt_text(token_row.access_token) + if not access: + raise HTTPException(status_code=500, detail='unable to decrypt access token') + + headers = {'Authorization': f'Bearer {access}'} + params = {'maxResults': 25, 'singleEvents': True, 'orderBy': 'startTime', 'timeMin': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())} + resp = requests.get('https://www.googleapis.com/calendar/v3/calendars/primary/events', headers=headers, params=params, timeout=10) + if resp.status_code != 200: + raise HTTPException(status_code=502, detail='google api error') + events = resp.json().get('items', []) + + created = [] + for ev in events: + title = ev.get('summary') or 'Untitled Event' + # Create habit and log + habit = models.Habit(project_id=None, user_id=integration.user_id, title=f'Event: {title}', notes=str(ev), cadence='once') + db.add(habit) + db.commit() + db.refresh(habit) + + log = models.Log(habit_id=habit.id, user_id=integration.user_id, action='imported_event') + db.add(log) + db.commit() + created.append({'habit_id': habit.id, 'title': habit.title}) + + return {'created': created, 'count': len(created)} + finally: + db.close() diff --git a/modern/backend/models.py b/modern/backend/models.py index e546f57..b414cea 100644 --- a/modern/backend/models.py +++ b/modern/backend/models.py @@ -102,6 +102,23 @@ class ChangeLog(Base): created_at = Column(DateTime, server_default=func.current_timestamp()) +class Guild(Base): + __tablename__ = 'guilds' + id = Column(Integer, primary_key=True) + name = Column(String, nullable=False) + description = Column(Text) + owner_id = Column(Integer, ForeignKey('users.id')) + created_at = Column(DateTime, server_default=func.current_timestamp()) + + +class GuildMember(Base): + __tablename__ = 'guild_members' + id = Column(Integer, primary_key=True) + guild_id = Column(Integer, ForeignKey('guilds.id')) + user_id = Column(Integer, ForeignKey('users.id')) + role = Column(String, default='member') + + def init_db(): Base.metadata.create_all(bind=engine)