Coverage for integrations / social / api_gamification.py: 25.6%
1278 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-12 04:49 +0000
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-12 04:49 +0000
1"""
2HevolveSocial - Gamification API Blueprint
3~76 endpoints for Resonance, Achievements, Challenges, Seasons, Regions,
4Encounters, Agent Evolution, Ratings, Distribution, Onboarding, Campaigns.
5"""
6import logging
7from datetime import datetime, timedelta
8from flask import Blueprint, request, jsonify, g
10from sqlalchemy.orm import Session as SASession
11from .auth import require_auth, optional_auth, require_admin
12from .rate_limiter import rate_limit
13from .models import (
14 get_db, User, Post, ResonanceWallet, ResonanceTransaction,
15 Achievement, UserAchievement, Season, Challenge, UserChallenge,
16 Region, RegionMembership, Encounter, Rating, TrustScore,
17 AgentEvolution, AgentCollaboration, Referral, ReferralCode,
18 Boost, OnboardingProgress, Campaign, CampaignAction,
19 AdUnit, AdPlacement, AdImpression, PeerNode, HostingReward,
20)
21from .resonance_engine import ResonanceService
23logger = logging.getLogger('hevolve_social')
25gamification_bp = Blueprint('gamification', __name__, url_prefix='/api/social')
28def _ok(data=None, meta=None, status=200):
29 r = {'success': True}
30 if data is not None:
31 r['data'] = data
32 if meta is not None:
33 r['meta'] = meta
34 return jsonify(r), status
37def _err(msg, status=400):
38 return jsonify({'success': False, 'error': msg}), status
41def _paginate(total, limit, offset):
42 return {'total': total, 'limit': limit, 'offset': offset,
43 'has_more': offset + limit < total}
46def _get_json():
47 return request.get_json(force=True, silent=True) or {}
50# ═══════════════════════════════════════════════════════════════
51# RESONANCE (10 endpoints)
52# ═══════════════════════════════════════════════════════════════
54@gamification_bp.route('/resonance/wallet', methods=['GET'])
55@require_auth
56def resonance_wallet_self():
57 db = get_db()
58 try:
59 wallet = ResonanceService.get_wallet(db, g.user_id)
60 if not wallet:
61 wallet = ResonanceService.get_or_create_wallet(db, g.user_id).to_dict()
62 db.commit()
63 return _ok(wallet)
64 finally:
65 db.close()
68@gamification_bp.route('/resonance/wallet/<user_id>', methods=['GET'])
69@optional_auth
70def resonance_wallet_user(user_id):
71 db = get_db()
72 try:
73 wallet = ResonanceService.get_wallet(db, user_id)
74 if not wallet:
75 return _err('Wallet not found', 404)
76 return _ok(wallet)
77 finally:
78 db.close()
81@gamification_bp.route('/resonance/transactions', methods=['GET'])
82@require_auth
83def resonance_transactions():
84 db = get_db()
85 try:
86 currency = request.args.get('currency')
87 limit = min(int(request.args.get('limit', 50)), 100)
88 offset = int(request.args.get('offset', 0))
89 txns = ResonanceService.get_transactions(db, g.user_id, currency, limit, offset)
90 return _ok(txns)
91 finally:
92 db.close()
95@gamification_bp.route('/resonance/leaderboard', methods=['GET'])
96@optional_auth
97def resonance_leaderboard():
98 db = get_db()
99 try:
100 currency = request.args.get('currency', 'pulse')
101 region_id = request.args.get('region')
102 limit = min(int(request.args.get('limit', 50)), 100)
103 offset = int(request.args.get('offset', 0))
104 board = ResonanceService.get_leaderboard(db, currency, limit, offset, region_id)
105 return _ok(board)
106 finally:
107 db.close()
110@gamification_bp.route('/resonance/boost', methods=['POST'])
111@require_auth
112def resonance_boost():
113 db = get_db()
114 try:
115 data = _get_json()
116 target_type = data.get('target_type')
117 target_id = data.get('target_id')
118 spark_amount = int(data.get('spark_amount', 10))
119 if not target_type or not target_id:
120 return _err('target_type and target_id required')
121 if spark_amount < 1:
122 return _err('spark_amount must be positive')
124 ok, remaining = ResonanceService.spend_spark(
125 db, g.user_id, spark_amount, 'boost', target_id,
126 f'Boost {target_type} {target_id}')
127 if not ok:
128 return _err(f'Insufficient Spark (have {remaining})')
130 multiplier = min(1.0 + spark_amount * 0.01, 2.0)
131 hours = spark_amount
132 boost = Boost(
133 user_id=g.user_id, target_type=target_type, target_id=target_id,
134 spark_spent=spark_amount, boost_multiplier=multiplier,
135 expires_at=datetime.utcnow() + timedelta(hours=hours),
136 )
137 db.add(boost)
139 if target_type == 'post':
140 post = db.query(Post).filter_by(id=target_id).first()
141 if post:
142 post.boost_score = (post.boost_score or 0) + multiplier
144 db.commit()
145 return _ok(boost.to_dict())
146 except Exception as e:
147 db.rollback()
148 return _err(str(e))
149 finally:
150 db.close()
153@gamification_bp.route('/resonance/boosts/<target_type>/<target_id>', methods=['GET'])
154@optional_auth
155def resonance_boosts_for_target(target_type, target_id):
156 db = get_db()
157 try:
158 boosts = db.query(Boost).filter_by(
159 target_type=target_type, target_id=target_id
160 ).filter(Boost.expires_at > datetime.utcnow()).all()
161 return _ok([b.to_dict() for b in boosts])
162 finally:
163 db.close()
166@gamification_bp.route('/resonance/level-info', methods=['GET'])
167@require_auth
168def resonance_level_info():
169 db = get_db()
170 try:
171 wallet = ResonanceService.get_or_create_wallet(db, g.user_id)
172 from .resonance_engine import xp_for_level, title_for_level, LEVEL_TITLES
173 info = {
174 'level': wallet.level, 'title': wallet.level_title,
175 'xp': wallet.xp, 'xp_next': wallet.xp_next_level,
176 'progress_pct': round(wallet.xp / max(wallet.xp_next_level, 1) * 100, 1),
177 'all_titles': {str(k): v for k, v in LEVEL_TITLES.items()},
178 }
179 db.commit()
180 return _ok(info)
181 finally:
182 db.close()
185@gamification_bp.route('/resonance/streak', methods=['GET'])
186@require_auth
187def resonance_streak():
188 db = get_db()
189 try:
190 wallet = ResonanceService.get_or_create_wallet(db, g.user_id)
191 db.commit()
192 return _ok({
193 'streak_days': wallet.streak_days,
194 'streak_best': wallet.streak_best,
195 'last_active_date': wallet.last_active_date,
196 })
197 finally:
198 db.close()
201@gamification_bp.route('/resonance/daily-checkin', methods=['POST'])
202@require_auth
203def resonance_daily_checkin():
204 db = get_db()
205 try:
206 result = ResonanceService.process_streak(db, g.user_id)
207 db.commit()
208 return _ok(result)
209 except Exception as e:
210 db.rollback()
211 return _err(str(e))
212 finally:
213 db.close()
216@gamification_bp.route('/resonance/breakdown/<user_id>', methods=['GET'])
217@optional_auth
218def resonance_breakdown(user_id):
219 db = get_db()
220 try:
221 breakdown = ResonanceService.get_breakdown(db, user_id)
222 if not breakdown:
223 return _err('User not found', 404)
224 return _ok(breakdown)
225 finally:
226 db.close()
229# ═══════════════════════════════════════════════════════════════
230# ACHIEVEMENTS (5 endpoints)
231# ═══════════════════════════════════════════════════════════════
233@gamification_bp.route('/achievements', methods=['GET'])
234@optional_auth
235def list_achievements():
236 from .gamification_service import GamificationService
237 db = get_db()
238 try:
239 achievements = GamificationService.get_all_achievements(db)
240 return _ok(achievements)
241 finally:
242 db.close()
245@gamification_bp.route('/achievements/<user_id>', methods=['GET'])
246@optional_auth
247def user_achievements(user_id):
248 from .gamification_service import GamificationService
249 db = get_db()
250 try:
251 achievements = GamificationService.get_user_achievements(db, user_id)
252 return _ok(achievements)
253 finally:
254 db.close()
257@gamification_bp.route('/achievements/<achievement_id>/showcase', methods=['POST'])
258@require_auth
259def toggle_showcase(achievement_id):
260 from .gamification_service import GamificationService
261 db = get_db()
262 try:
263 data = _get_json()
264 result = GamificationService.toggle_showcase(db, g.user_id, achievement_id)
265 if result is None:
266 return _err("Achievement not found or not unlocked", 404)
267 db.commit()
268 return _ok({'is_showcased': result})
269 except Exception as e:
270 db.rollback()
271 return _err(str(e))
272 finally:
273 db.close()
276# ═══════════════════════════════════════════════════════════════
277# CHALLENGES (5 endpoints)
278# ═══════════════════════════════════════════════════════════════
280@gamification_bp.route('/challenges', methods=['GET'])
281@optional_auth
282def list_challenges():
283 from .gamification_service import GamificationService
284 db = get_db()
285 try:
286 user_id = g.user_id if hasattr(g, 'user_id') and g.user_id else None
287 challenges = GamificationService.get_active_challenges(db, user_id)
288 return _ok(challenges)
289 finally:
290 db.close()
293@gamification_bp.route('/challenges/<challenge_id>', methods=['GET'])
294@optional_auth
295def get_challenge(challenge_id):
296 from .gamification_service import GamificationService
297 db = get_db()
298 try:
299 user_id = g.user_id if hasattr(g, 'user_id') and g.user_id else None
300 challenge = GamificationService.get_challenge(db, challenge_id, user_id)
301 if not challenge:
302 return _err("Challenge not found", 404)
303 return _ok(challenge)
304 finally:
305 db.close()
308@gamification_bp.route('/challenges/<challenge_id>/progress', methods=['POST'])
309@require_auth
310def update_challenge_progress(challenge_id):
311 from .gamification_service import GamificationService
312 db = get_db()
313 try:
314 data = _get_json()
315 increment = data.get('increment', 1)
316 result = GamificationService.update_challenge_progress(db, g.user_id, challenge_id, increment)
317 if not result:
318 return _err("Challenge not found", 404)
319 db.commit()
320 return _ok(result)
321 except Exception as e:
322 db.rollback()
323 return _err(str(e))
324 finally:
325 db.close()
328@gamification_bp.route('/challenges/<challenge_id>/claim', methods=['POST'])
329@require_auth
330def claim_challenge_reward(challenge_id):
331 from .gamification_service import GamificationService
332 db = get_db()
333 try:
334 result = GamificationService.claim_challenge_reward(db, g.user_id, challenge_id)
335 if not result:
336 return _err("Challenge not completed or not found", 404)
337 db.commit()
338 return _ok(result)
339 except Exception as e:
340 db.rollback()
341 return _err(str(e))
342 finally:
343 db.close()
346# ═══════════════════════════════════════════════════════════════
347# SEASONS (4 endpoints)
348# ═══════════════════════════════════════════════════════════════
350@gamification_bp.route('/seasons/current', methods=['GET'])
351@optional_auth
352def current_season():
353 from .gamification_service import GamificationService
354 db = get_db()
355 try:
356 season = GamificationService.get_current_season(db)
357 return _ok(season)
358 finally:
359 db.close()
362@gamification_bp.route('/seasons/<season_id>/leaderboard', methods=['GET'])
363@optional_auth
364def season_leaderboard(season_id):
365 from .gamification_service import GamificationService
366 db = get_db()
367 try:
368 limit = min(int(request.args.get('limit', 50)), 100)
369 offset = int(request.args.get('offset', 0))
370 result = GamificationService.get_season_leaderboard(db, season_id, limit, offset)
371 return _ok(result)
372 finally:
373 db.close()
376@gamification_bp.route('/seasons/<season_id>/achievements', methods=['GET'])
377@optional_auth
378def season_achievements(season_id):
379 from .gamification_service import GamificationService
380 db = get_db()
381 try:
382 result = GamificationService.get_season_achievements(db, season_id)
383 return _ok(result)
384 finally:
385 db.close()
388@gamification_bp.route('/collectibles/<user_id>', methods=['GET'])
389@optional_auth
390def user_collectibles(user_id):
391 db = get_db()
392 try:
393 showcased = db.query(UserAchievement).filter_by(
394 user_id=user_id, is_showcased=True).all()
395 wallet = ResonanceService.get_wallet(db, user_id)
396 return _ok({
397 'showcased_achievements': [ua.to_dict() for ua in showcased],
398 'level': wallet.get('level', 1) if wallet else 1,
399 'level_title': wallet.get('level_title', 'Newcomer') if wallet else 'Newcomer',
400 })
401 finally:
402 db.close()
405# ═══════════════════════════════════════════════════════════════
406# REGIONS & GOVERNANCE (14 endpoints)
407# ═══════════════════════════════════════════════════════════════
409@gamification_bp.route('/regions', methods=['GET'])
410@optional_auth
411def list_regions():
412 from .region_service import RegionService
413 db = get_db()
414 try:
415 limit = min(int(request.args.get('limit', 50)), 100)
416 offset = int(request.args.get('offset', 0))
417 regions = RegionService.list_regions(db, limit=limit, offset=offset)
418 return _ok(regions)
419 finally:
420 db.close()
423@gamification_bp.route('/regions', methods=['POST'])
424@require_auth
425def create_region():
426 from .region_service import RegionService
427 db = get_db()
428 try:
429 data = _get_json()
430 result = RegionService.create_region(db, g.user_id, data)
431 db.commit()
432 return _ok(result, status=201)
433 except Exception as e:
434 db.rollback()
435 return _err(str(e))
436 finally:
437 db.close()
440@gamification_bp.route('/regions/<region_id>', methods=['GET'])
441@optional_auth
442def get_region(region_id):
443 from .region_service import RegionService
444 db = get_db()
445 try:
446 region = RegionService.get_region(db, region_id)
447 if not region:
448 return _err('Region not found', 404)
449 return _ok(region)
450 finally:
451 db.close()
454@gamification_bp.route('/regions/<region_id>', methods=['PATCH'])
455@require_auth
456def update_region(region_id):
457 db = get_db()
458 try:
459 mem = db.query(RegionMembership).filter_by(
460 user_id=g.user_id, region_id=region_id).first()
461 if not mem or mem.role not in ('admin', 'steward'):
462 return _err('Insufficient privileges', 403)
463 region = db.query(Region).filter_by(id=region_id).first()
464 if not region:
465 return _err('Region not found', 404)
466 data = _get_json()
467 for field in ('display_name', 'description', 'global_server_url', 'settings_json'):
468 if field in data:
469 setattr(region, field, data[field])
470 db.commit()
471 return _ok(region.to_dict())
472 except Exception as e:
473 db.rollback()
474 return _err(str(e))
475 finally:
476 db.close()
479@gamification_bp.route('/regions/<region_id>/join', methods=['POST'])
480@require_auth
481def join_region(region_id):
482 from .region_service import RegionService
483 db = get_db()
484 try:
485 result = RegionService.join_region(db, g.user_id, region_id)
486 db.commit()
487 return _ok(result)
488 except Exception as e:
489 db.rollback()
490 return _err(str(e))
491 finally:
492 db.close()
495@gamification_bp.route('/regions/<region_id>/leave', methods=['DELETE'])
496@require_auth
497def leave_region(region_id):
498 from .region_service import RegionService
499 db = get_db()
500 try:
501 result = RegionService.leave_region(db, g.user_id, region_id)
502 db.commit()
503 return _ok(result)
504 except Exception as e:
505 db.rollback()
506 return _err(str(e))
507 finally:
508 db.close()
511@gamification_bp.route('/regions/<region_id>/members', methods=['GET'])
512@optional_auth
513def region_members(region_id):
514 from .region_service import RegionService
515 db = get_db()
516 try:
517 limit = min(int(request.args.get('limit', 50)), 100)
518 offset = int(request.args.get('offset', 0))
519 result = RegionService.get_members(db, region_id, limit=limit, offset=offset)
520 return _ok(result)
521 finally:
522 db.close()
525@gamification_bp.route('/regions/<region_id>/feed', methods=['GET'])
526@optional_auth
527def region_feed(region_id):
528 from .region_service import RegionService
529 db = get_db()
530 try:
531 limit = min(int(request.args.get('limit', 20)), 50)
532 offset = int(request.args.get('offset', 0))
533 result = RegionService.get_regional_feed(db, region_id, limit=limit, offset=offset)
534 return _ok(result)
535 finally:
536 db.close()
539@gamification_bp.route('/regions/<region_id>/leaderboard', methods=['GET'])
540@optional_auth
541def region_leaderboard(region_id):
542 from .region_service import RegionService
543 db = get_db()
544 try:
545 limit = min(int(request.args.get('limit', 50)), 100)
546 offset = int(request.args.get('offset', 0))
547 result = RegionService.get_regional_leaderboard(db, region_id, limit=limit, offset=offset)
548 return _ok(result)
549 finally:
550 db.close()
553@gamification_bp.route('/regions/<region_id>/governance', methods=['GET'])
554@optional_auth
555def region_governance(region_id):
556 from .region_service import RegionService
557 db = get_db()
558 try:
559 result = RegionService.get_governance_info(db, region_id)
560 if not result:
561 return _err('Region not found', 404)
562 return _ok(result)
563 finally:
564 db.close()
567@gamification_bp.route('/regions/<region_id>/promote', methods=['POST'])
568@require_auth
569def promote_member(region_id):
570 from .region_service import RegionService
571 db = get_db()
572 try:
573 data = _get_json()
574 target_user_id = data.get('user_id')
575 new_role = data.get('role')
576 result = RegionService.promote_member(db, g.user_id, region_id, target_user_id, new_role)
577 db.commit()
578 return _ok(result)
579 except Exception as e:
580 db.rollback()
581 return _err(str(e))
582 finally:
583 db.close()
586@gamification_bp.route('/regions/<region_id>/demote', methods=['POST'])
587@require_auth
588def demote_member(region_id):
589 from .region_service import RegionService
590 db = get_db()
591 try:
592 data = _get_json()
593 target_user_id = data.get('user_id')
594 result = RegionService.demote_member(db, g.user_id, region_id, target_user_id)
595 db.commit()
596 return _ok(result)
597 except Exception as e:
598 db.rollback()
599 return _err(str(e))
600 finally:
601 db.close()
604@gamification_bp.route('/regions/nearby', methods=['GET'])
605@optional_auth
606def nearby_regions():
607 from .region_service import RegionService
608 db = get_db()
609 try:
610 lat = float(request.args.get('lat', 0))
611 lon = float(request.args.get('lon', 0))
612 radius = float(request.args.get('radius', 100))
613 result = RegionService.nearby_regions(db, lat, lon, radius)
614 return _ok(result)
615 finally:
616 db.close()
619@gamification_bp.route('/regions/<region_id>/sync', methods=['POST'])
620@require_auth
621def sync_region(region_id):
622 db = get_db()
623 try:
624 mem = db.query(RegionMembership).filter_by(
625 user_id=g.user_id, region_id=region_id).first()
626 if not mem or mem.role not in ('admin', 'steward'):
627 return _err('Insufficient privileges', 403)
628 region = db.query(Region).filter_by(id=region_id).first()
629 if not region or not region.global_server_url:
630 return _err('No global server configured')
631 # Sync with global server (placeholder - actual federation call)
632 return _ok({'synced': True, 'global_server': region.global_server_url})
633 finally:
634 db.close()
637# ═══════════════════════════════════════════════════════════════
638# ENCOUNTERS (6 endpoints)
639# ═══════════════════════════════════════════════════════════════
641@gamification_bp.route('/encounters', methods=['GET'])
642@require_auth
643def list_encounters():
644 from .encounter_service import EncounterService
645 db = get_db()
646 try:
647 limit = min(int(request.args.get('limit', 20)), 50)
648 encounters = EncounterService.get_encounters(db, g.user_id, limit=limit)
649 return _ok(encounters)
650 finally:
651 db.close()
654@gamification_bp.route('/encounters/<user_id>', methods=['GET'])
655@require_auth
656def shared_encounters(user_id):
657 from .encounter_service import EncounterService
658 db = get_db()
659 try:
660 encounters = EncounterService.get_encounters_with(db, g.user_id, user_id)
661 return _ok(encounters)
662 finally:
663 db.close()
666@gamification_bp.route('/encounters/<encounter_id>/acknowledge', methods=['POST'])
667@require_auth
668def acknowledge_encounter(encounter_id):
669 from .encounter_service import EncounterService
670 db = get_db()
671 try:
672 result = EncounterService.acknowledge_encounter(db, encounter_id, g.user_id)
673 if not result:
674 return _err('Encounter not found', 404)
675 db.commit()
676 return _ok(result)
677 except Exception as e:
678 db.rollback()
679 return _err(str(e))
680 finally:
681 db.close()
684@gamification_bp.route('/encounters/suggestions', methods=['GET'])
685@require_auth
686def encounter_suggestions():
687 from .encounter_service import EncounterService
688 db = get_db()
689 try:
690 suggestions = EncounterService.get_suggestions(db, g.user_id)
691 return _ok(suggestions)
692 finally:
693 db.close()
696@gamification_bp.route('/encounters/bonds', methods=['GET'])
697@require_auth
698def encounter_bonds():
699 from .encounter_service import EncounterService
700 db = get_db()
701 try:
702 bonds = EncounterService.get_bonds(db, g.user_id)
703 return _ok(bonds)
704 finally:
705 db.close()
708@gamification_bp.route('/encounters/nearby', methods=['GET'])
709@require_auth
710def encounters_nearby():
711 from .encounter_service import EncounterService
712 db = get_db()
713 try:
714 result = EncounterService.get_nearby_active(db, g.user_id)
715 return _ok(result)
716 finally:
717 db.close()
720# ═══════════════════════════════════════════════════════════════
721# AGENT EVOLUTION (8 endpoints)
722# ═══════════════════════════════════════════════════════════════
724@gamification_bp.route('/agents/<agent_id>/evolution', methods=['GET'])
725@optional_auth
726def agent_evolution(agent_id):
727 from .agent_evolution_service import AgentEvolutionService
728 db = get_db()
729 try:
730 result = AgentEvolutionService.get_evolution(db, agent_id)
731 return _ok(result)
732 finally:
733 db.close()
736@gamification_bp.route('/agents/<agent_id>/specialize', methods=['POST'])
737@require_auth
738def agent_specialize(agent_id):
739 from .agent_evolution_service import AgentEvolutionService
740 db = get_db()
741 try:
742 data = _get_json()
743 path = data.get('path')
744 result = AgentEvolutionService.specialize(db, agent_id, path)
745 db.commit()
746 return _ok(result)
747 except Exception as e:
748 db.rollback()
749 return _err(str(e))
750 finally:
751 db.close()
754@gamification_bp.route('/agents/leaderboard', methods=['GET'])
755@optional_auth
756def agent_leaderboard():
757 from .agent_evolution_service import AgentEvolutionService
758 db = get_db()
759 try:
760 limit = min(int(request.args.get('limit', 50)), 100)
761 result = AgentEvolutionService.get_agent_leaderboard(db, limit=limit)
762 return _ok(result)
763 finally:
764 db.close()
767SPECIALIZATION_TREES = {
768 'analyst': {'base': 'Analyst', 'advanced': 'Oracle',
769 'description': 'Data analysis, pattern recognition, insights'},
770 'creator': {'base': 'Creator', 'advanced': 'Visionary',
771 'description': 'Content generation, creative problem solving'},
772 'executor': {'base': 'Executor', 'advanced': 'Automaton',
773 'description': 'Task execution, workflow automation'},
774 'communicator': {'base': 'Communicator', 'advanced': 'Ambassador',
775 'description': 'Inter-agent communication, negotiation'},
776}
779@gamification_bp.route('/agents/specialization-trees', methods=['GET'])
780@optional_auth
781def specialization_trees():
782 return _ok(SPECIALIZATION_TREES)
785@gamification_bp.route('/agents/<agent_id>/collaborations', methods=['GET'])
786@optional_auth
787def agent_collaborations(agent_id):
788 from .agent_evolution_service import AgentEvolutionService
789 db = get_db()
790 try:
791 limit = min(int(request.args.get('limit', 50)), 100)
792 result = AgentEvolutionService.get_collaborations(db, agent_id, limit=limit)
793 return _ok(result)
794 finally:
795 db.close()
798@gamification_bp.route('/agents/<agent_id>/collaborate', methods=['POST'])
799@require_auth
800def record_collaboration(agent_id):
801 from .agent_evolution_service import AgentEvolutionService
802 db = get_db()
803 try:
804 data = _get_json()
805 other_agent_id = data.get('other_agent_id')
806 collab_type = data.get('type', 'co_task')
807 quality = float(data.get('quality_score', 0.5))
808 task_id = data.get('task_id')
809 result = AgentEvolutionService.record_collaboration(
810 db, agent_id, other_agent_id, collab_type, quality, task_id)
811 db.commit()
812 return _ok(result)
813 except Exception as e:
814 db.rollback()
815 return _err(str(e))
816 finally:
817 db.close()
820@gamification_bp.route('/agents/showcase', methods=['GET'])
821@optional_auth
822def agent_showcase():
823 from .agent_evolution_service import AgentEvolutionService
824 db = get_db()
825 try:
826 limit = min(int(request.args.get('limit', 20)), 50)
827 result = AgentEvolutionService.get_showcase(db, limit=limit)
828 return _ok(result)
829 finally:
830 db.close()
833@gamification_bp.route('/agents/<agent_id>/evolution-history', methods=['GET'])
834@optional_auth
835def agent_evolution_history(agent_id):
836 db = get_db()
837 try:
838 # XP timeline from transactions
839 txns = db.query(ResonanceTransaction).filter_by(
840 user_id=agent_id, currency='xp'
841 ).order_by(ResonanceTransaction.created_at).all()
842 return _ok([t.to_dict() for t in txns])
843 finally:
844 db.close()
847# ═══════════════════════════════════════════════════════════════
848# RATINGS & TRUST (6 endpoints)
849# ═══════════════════════════════════════════════════════════════
851@gamification_bp.route('/ratings', methods=['POST'])
852@require_auth
853def submit_rating():
854 from .rating_service import RatingService
855 db = get_db()
856 try:
857 data = _get_json()
858 result = RatingService.submit_rating(db, g.user_id, data)
859 db.commit()
860 return _ok(result)
861 except Exception as e:
862 db.rollback()
863 return _err(str(e))
864 finally:
865 db.close()
868def _recalculate_trust(db: SASession, user_id: str):
869 """Recalculate composite trust score for a user."""
870 from sqlalchemy import func as sqlfunc
871 avgs = db.query(
872 Rating.dimension, sqlfunc.avg(Rating.score), sqlfunc.count(Rating.id)
873 ).filter_by(rated_id=user_id).group_by(Rating.dimension).all()
875 ts = db.query(TrustScore).filter_by(user_id=user_id).first()
876 if not ts:
877 ts = TrustScore(user_id=user_id)
878 db.add(ts)
880 total = 0
881 for dim, avg_score, count in avgs:
882 setattr(ts, f'avg_{dim}', float(avg_score))
883 total += count
885 ts.total_ratings_received = total
886 # Weighted composite: skill 0.25, usefulness 0.30, reliability 0.30, creativity 0.15
887 ts.composite_trust = (
888 ts.avg_skill * 0.25 + ts.avg_usefulness * 0.30 +
889 ts.avg_reliability * 0.30 + ts.avg_creativity * 0.15
890 )
893@gamification_bp.route('/ratings/<user_id>', methods=['GET'])
894@optional_auth
895def get_trust_scores(user_id):
896 from .rating_service import RatingService
897 db = get_db()
898 try:
899 result = RatingService.get_aggregated(db, user_id)
900 return _ok(result)
901 finally:
902 db.close()
905@gamification_bp.route('/ratings/<user_id>/received', methods=['GET'])
906@optional_auth
907def ratings_received(user_id):
908 from .rating_service import RatingService
909 db = get_db()
910 try:
911 limit = min(int(request.args.get('limit', 50)), 100)
912 offset = int(request.args.get('offset', 0))
913 result = RatingService.get_ratings_received(db, user_id, limit=limit, offset=offset)
914 return _ok(result)
915 finally:
916 db.close()
919@gamification_bp.route('/ratings/<user_id>/given', methods=['GET'])
920@require_auth
921def ratings_given(user_id):
922 from .rating_service import RatingService
923 db = get_db()
924 try:
925 if user_id != g.user_id:
926 return _err('Can only view your own given ratings', 403)
927 limit = min(int(request.args.get('limit', 50)), 100)
928 offset = int(request.args.get('offset', 0))
929 result = RatingService.get_ratings_given(db, user_id, limit=limit, offset=offset)
930 return _ok(result)
931 finally:
932 db.close()
935@gamification_bp.route('/ratings/context/<context_type>/<context_id>', methods=['GET'])
936@optional_auth
937def ratings_for_context(context_type, context_id):
938 db = get_db()
939 try:
940 ratings = db.query(Rating).filter_by(
941 context_type=context_type, context_id=context_id).all()
942 return _ok([r.to_dict() for r in ratings])
943 finally:
944 db.close()
947@gamification_bp.route('/trust/<user_id>', methods=['GET'])
948@optional_auth
949def trust_card(user_id):
950 from .rating_service import RatingService
951 db = get_db()
952 try:
953 result = RatingService.get_trust_score(db, user_id)
954 if not result:
955 return _err('User not found', 404)
956 return _ok(result)
957 finally:
958 db.close()
961# ═══════════════════════════════════════════════════════════════
962# DISTRIBUTION (8 endpoints)
963# ═══════════════════════════════════════════════════════════════
965@gamification_bp.route('/referral/code', methods=['GET'])
966@require_auth
967def get_referral_code():
968 from .distribution_service import DistributionService
969 db = get_db()
970 try:
971 result = DistributionService.get_or_create_referral_code(db, g.user_id)
972 # Trigger onboarding step when user first shares their referral code
973 try:
974 from .onboarding_service import OnboardingService
975 OnboardingService.auto_advance(db, g.user_id, 'share_referral')
976 except Exception:
977 pass
978 db.commit()
979 return _ok(result)
980 except Exception as e:
981 db.rollback()
982 return _err(str(e))
983 finally:
984 db.close()
987@gamification_bp.route('/referral/use', methods=['POST'])
988@require_auth
989def use_referral_code():
990 from .distribution_service import DistributionService
991 db = get_db()
992 try:
993 data = _get_json()
994 code = data.get('code', '')
995 result = DistributionService.use_referral_code(db, g.user_id, code)
996 db.commit()
997 return _ok(result)
998 except Exception as e:
999 db.rollback()
1000 return _err(str(e))
1001 finally:
1002 db.close()
1005@gamification_bp.route('/referral/stats', methods=['GET'])
1006@require_auth
1007def referral_stats():
1008 from .distribution_service import DistributionService
1009 db = get_db()
1010 try:
1011 result = DistributionService.get_referral_stats(db, g.user_id)
1012 return _ok(result)
1013 finally:
1014 db.close()
1017@gamification_bp.route('/marketplace/recipes', methods=['GET'])
1018@optional_auth
1019def marketplace_recipes():
1020 db = get_db()
1021 try:
1022 from .models import RecipeShare
1023 limit = min(int(request.args.get('limit', 20)), 50)
1024 recipes = db.query(RecipeShare).order_by(
1025 RecipeShare.fork_count.desc()).limit(limit).all()
1026 return _ok([r.to_dict() for r in recipes])
1027 finally:
1028 db.close()
1031@gamification_bp.route('/marketplace/agents', methods=['GET'])
1032@optional_auth
1033def marketplace_agents():
1034 db = get_db()
1035 try:
1036 limit = min(int(request.args.get('limit', 20)), 50)
1037 agents = db.query(User).filter_by(
1038 user_type='agent', is_banned=False
1039 ).order_by(User.karma_score.desc()).limit(limit).all()
1040 return _ok([a.to_dict() for a in agents])
1041 finally:
1042 db.close()
1045# ── Marketplace HART Listings (CRUD + hire + reviews) ──
1047MARKETPLACE_CATEGORIES = [
1048 'content_creation', 'analysis_research', 'learning_tutoring',
1049 'game_design', 'creative', 'custom',
1050]
1053@gamification_bp.route('/marketplace/listings', methods=['GET'])
1054@optional_auth
1055def list_listings():
1056 """List active marketplace listings with optional filters."""
1057 from .models import MarketplaceListing
1058 db = get_db()
1059 try:
1060 limit = min(int(request.args.get('limit', 20)), 50)
1061 offset = int(request.args.get('offset', 0))
1062 category = request.args.get('category')
1063 q = request.args.get('q', '').strip()
1065 query = db.query(MarketplaceListing).filter_by(is_active=True)
1066 if category:
1067 query = query.filter_by(category=category)
1068 if q:
1069 query = query.filter(
1070 MarketplaceListing.title.ilike(f'%{q}%') |
1071 MarketplaceListing.description.ilike(f'%{q}%')
1072 )
1073 total = query.count()
1074 listings = query.order_by(
1075 MarketplaceListing.hire_count.desc(),
1076 MarketplaceListing.rating_avg.desc(),
1077 ).offset(offset).limit(limit).all()
1078 return _ok([l.to_dict() for l in listings], _paginate(total, limit, offset))
1079 except Exception as e:
1080 logger.error(f"marketplace/listings GET error: {e}")
1081 return _err(str(e), 500)
1082 finally:
1083 db.close()
1086@gamification_bp.route('/marketplace/listings/<listing_id>', methods=['GET'])
1087@optional_auth
1088def get_listing(listing_id):
1089 """Get a single marketplace listing by ID."""
1090 from .models import MarketplaceListing
1091 db = get_db()
1092 try:
1093 listing = db.query(MarketplaceListing).filter_by(id=listing_id).first()
1094 if not listing:
1095 return _err('Listing not found', 404)
1096 return _ok(listing.to_dict())
1097 except Exception as e:
1098 logger.error(f"marketplace/listings/{listing_id} GET error: {e}")
1099 return _err(str(e), 500)
1100 finally:
1101 db.close()
1104@gamification_bp.route('/marketplace/listings', methods=['POST'])
1105@require_auth
1106def create_listing():
1107 """Create a new marketplace listing for an agent owned by the current user."""
1108 from .models import MarketplaceListing
1109 db = get_db()
1110 try:
1111 data = _get_json()
1112 agent_id = data.get('agent_id')
1113 if not agent_id:
1114 return _err('agent_id required')
1116 agent = db.query(User).filter_by(id=agent_id, user_type='agent').first()
1117 if not agent:
1118 return _err('Agent not found', 404)
1119 if agent.owner_id and agent.owner_id != g.user_id:
1120 return _err('Not your agent', 403)
1122 title = data.get('title', '').strip()
1123 if not title:
1124 return _err('title required')
1126 listing = MarketplaceListing(
1127 agent_id=agent_id,
1128 title=title,
1129 description=data.get('description', ''),
1130 category=data.get('category', 'custom'),
1131 price_spark=max(0, int(data.get('price_spark', 0))),
1132 )
1133 db.add(listing)
1134 db.commit()
1135 db.refresh(listing)
1136 return _ok(listing.to_dict(), status=201)
1137 except Exception as e:
1138 db.rollback()
1139 logger.error(f"marketplace/listings POST error: {e}")
1140 return _err(str(e), 500)
1141 finally:
1142 db.close()
1145@gamification_bp.route('/marketplace/listings/<listing_id>/hire', methods=['POST'])
1146@require_auth
1147def hire_listing(listing_id):
1148 """Hire a HART via a marketplace listing. Increments hire_count."""
1149 from .models import MarketplaceListing
1150 db = get_db()
1151 try:
1152 listing = db.query(MarketplaceListing).filter_by(id=listing_id, is_active=True).first()
1153 if not listing:
1154 return _err('Listing not found or inactive', 404)
1156 listing.hire_count = (listing.hire_count or 0) + 1
1157 db.commit()
1158 return _ok({
1159 'listing_id': listing.id,
1160 'agent_id': listing.agent_id,
1161 'hire_count': listing.hire_count,
1162 })
1163 except Exception as e:
1164 db.rollback()
1165 logger.error(f"marketplace/listings/{listing_id}/hire error: {e}")
1166 return _err(str(e), 500)
1167 finally:
1168 db.close()
1171@gamification_bp.route('/marketplace/listings/<listing_id>/reviews', methods=['GET'])
1172@optional_auth
1173def list_reviews(listing_id):
1174 """Get reviews for a marketplace listing."""
1175 from .models import ListingReview
1176 db = get_db()
1177 try:
1178 limit = min(int(request.args.get('limit', 20)), 50)
1179 offset = int(request.args.get('offset', 0))
1180 query = db.query(ListingReview).filter_by(listing_id=listing_id)
1181 total = query.count()
1182 reviews = query.order_by(ListingReview.created_at.desc()).offset(offset).limit(limit).all()
1183 return _ok([r.to_dict() for r in reviews], _paginate(total, limit, offset))
1184 except Exception as e:
1185 logger.error(f"reviews GET error: {e}")
1186 return _err(str(e), 500)
1187 finally:
1188 db.close()
1191@gamification_bp.route('/marketplace/listings/<listing_id>/reviews', methods=['POST'])
1192@require_auth
1193def add_review(listing_id):
1194 """Add a review to a marketplace listing. One review per user per listing."""
1195 from .models import MarketplaceListing, ListingReview
1196 from sqlalchemy import func as sa_func
1197 db = get_db()
1198 try:
1199 listing = db.query(MarketplaceListing).filter_by(id=listing_id).first()
1200 if not listing:
1201 return _err('Listing not found', 404)
1203 data = _get_json()
1204 rating = int(data.get('rating', 0))
1205 if rating < 1 or rating > 5:
1206 return _err('rating must be 1-5')
1208 existing = db.query(ListingReview).filter_by(
1209 listing_id=listing_id, user_id=g.user_id
1210 ).first()
1211 if existing:
1212 return _err('Already reviewed this listing', 409)
1214 review = ListingReview(
1215 listing_id=listing_id,
1216 user_id=g.user_id,
1217 rating=rating,
1218 text=data.get('text', ''),
1219 )
1220 db.add(review)
1222 listing.review_count = (listing.review_count or 0) + 1
1223 all_ratings = db.query(sa_func.avg(ListingReview.rating)).filter_by(
1224 listing_id=listing_id
1225 ).scalar()
1226 listing.rating_avg = round(float(all_ratings or rating), 2)
1228 db.commit()
1229 db.refresh(review)
1230 return _ok(review.to_dict(), status=201)
1231 except Exception as e:
1232 db.rollback()
1233 logger.error(f"reviews POST error: {e}")
1234 return _err(str(e), 500)
1235 finally:
1236 db.close()
1239@gamification_bp.route('/marketplace/categories', methods=['GET'])
1240@optional_auth
1241def list_marketplace_categories():
1242 """List available marketplace categories."""
1243 return _ok(MARKETPLACE_CATEGORIES)
1246@gamification_bp.route('/share/generate-link', methods=['POST'])
1247@require_auth
1248def generate_share_link():
1249 db = get_db()
1250 try:
1251 data = _get_json()
1252 target_type = data.get('target_type')
1253 target_id = data.get('target_id')
1254 # Get user's referral code
1255 user = db.query(User).filter_by(id=g.user_id).first()
1256 ref_code = user.referral_code if user else ''
1257 base_url = data.get('base_url', 'https://hevolve.ai')
1258 link = f"{base_url}/{target_type}/{target_id}?ref={ref_code}"
1259 return _ok({'link': link, 'referral_code': ref_code})
1260 finally:
1261 db.close()
1264@gamification_bp.route('/federation/contribution', methods=['GET'])
1265@optional_auth
1266def federation_contribution():
1267 db = get_db()
1268 try:
1269 from .models import PeerNode
1270 nodes = db.query(PeerNode).filter_by(
1271 status='active'
1272 ).order_by(PeerNode.contribution_score.desc()).limit(50).all()
1273 return _ok([n.to_dict() for n in nodes])
1274 finally:
1275 db.close()
1278@gamification_bp.route('/growth/stats', methods=['GET'])
1279@require_admin
1280def growth_stats():
1281 db = get_db()
1282 try:
1283 total_users = db.query(User).count()
1284 total_agents = db.query(User).filter_by(user_type='agent').count()
1285 total_wallets = db.query(ResonanceWallet).count()
1286 total_regions = db.query(Region).count()
1287 total_campaigns = db.query(Campaign).count()
1288 return _ok({
1289 'total_users': total_users,
1290 'total_agents': total_agents,
1291 'total_wallets': total_wallets,
1292 'total_regions': total_regions,
1293 'total_campaigns': total_campaigns,
1294 })
1295 finally:
1296 db.close()
1299# ═══════════════════════════════════════════════════════════════
1300# CAMPAIGNS — "Make Me Viral" (8 endpoints)
1301# ═══════════════════════════════════════════════════════════════
1303@gamification_bp.route('/campaigns', methods=['POST'])
1304@require_auth
1305def create_campaign():
1306 from .campaign_service import CampaignService
1307 db = get_db()
1308 try:
1309 data = _get_json()
1310 result = CampaignService.create_campaign(db, g.user_id, data)
1311 db.commit()
1312 return _ok(result, status=201)
1313 except Exception as e:
1314 db.rollback()
1315 return _err(str(e))
1316 finally:
1317 db.close()
1320@gamification_bp.route('/campaigns', methods=['GET'])
1321@require_auth
1322def list_campaigns():
1323 from .campaign_service import CampaignService
1324 db = get_db()
1325 try:
1326 limit = min(int(request.args.get('limit', 50)), 100)
1327 offset = int(request.args.get('offset', 0))
1328 result = CampaignService.list_campaigns(db, owner_id=g.user_id, limit=limit, offset=offset)
1329 return _ok(result)
1330 finally:
1331 db.close()
1334@gamification_bp.route('/campaigns/<campaign_id>', methods=['GET'])
1335@require_auth
1336def get_campaign(campaign_id):
1337 from .campaign_service import CampaignService
1338 db = get_db()
1339 try:
1340 result = CampaignService.get_campaign(db, campaign_id)
1341 if not result:
1342 return _err('Campaign not found', 404)
1343 return _ok(result)
1344 finally:
1345 db.close()
1348@gamification_bp.route('/campaigns/<campaign_id>', methods=['PATCH'])
1349@require_auth
1350def update_campaign(campaign_id):
1351 from .campaign_service import CampaignService
1352 db = get_db()
1353 try:
1354 data = _get_json()
1355 result = CampaignService.update_campaign(db, campaign_id, g.user_id, data)
1356 db.commit()
1357 return _ok(result)
1358 except Exception as e:
1359 db.rollback()
1360 return _err(str(e))
1361 finally:
1362 db.close()
1365@gamification_bp.route('/campaigns/<campaign_id>/generate-strategy', methods=['POST'])
1366@require_auth
1367def generate_campaign_strategy(campaign_id):
1368 from .campaign_service import CampaignService
1369 db = get_db()
1370 try:
1371 result = CampaignService.generate_strategy(db, campaign_id, g.user_id)
1372 db.commit()
1373 return _ok(result)
1374 except Exception as e:
1375 db.rollback()
1376 return _err(str(e))
1377 finally:
1378 db.close()
1381@gamification_bp.route('/campaigns/<campaign_id>/execute-step', methods=['POST'])
1382@require_auth
1383def execute_campaign_step(campaign_id):
1384 from .campaign_service import CampaignService
1385 db = get_db()
1386 try:
1387 result = CampaignService.execute_campaign_step(db, campaign_id, g.user_id)
1388 db.commit()
1389 return _ok(result)
1390 except Exception as e:
1391 db.rollback()
1392 return _err(str(e))
1393 finally:
1394 db.close()
1397@gamification_bp.route('/campaigns/leaderboard', methods=['GET'])
1398@optional_auth
1399def campaign_leaderboard():
1400 from .campaign_service import CampaignService
1401 db = get_db()
1402 try:
1403 limit = min(int(request.args.get('limit', 20)), 50)
1404 offset = int(request.args.get('offset', 0))
1405 result = CampaignService.get_leaderboard(db, limit=limit, offset=offset)
1406 return _ok(result)
1407 finally:
1408 db.close()
1411@gamification_bp.route('/campaigns/<campaign_id>', methods=['DELETE'])
1412@require_auth
1413def delete_campaign(campaign_id):
1414 from .campaign_service import CampaignService
1415 db = get_db()
1416 try:
1417 result = CampaignService.delete_campaign(db, campaign_id, g.user_id)
1418 db.commit()
1419 return _ok(result)
1420 except Exception as e:
1421 db.rollback()
1422 return _err(str(e))
1423 finally:
1424 db.close()
1427# ═══════════════════════════════════════════════════════════════
1428# ONBOARDING (4 endpoints)
1429# ═══════════════════════════════════════════════════════════════
1431ONBOARDING_STEPS = [
1432 'welcome', 'pick_interests', 'claim_handle', 'follow_friends',
1433 'first_interaction', 'join_community', 'create_something',
1434]
1437@gamification_bp.route('/onboarding/progress', methods=['GET'])
1438@require_auth
1439def onboarding_progress():
1440 from .onboarding_service import OnboardingService
1441 db = get_db()
1442 try:
1443 result = OnboardingService.get_progress(db, g.user_id)
1444 db.commit()
1445 return _ok(result)
1446 finally:
1447 db.close()
1450@gamification_bp.route('/onboarding/complete-step', methods=['POST'])
1451@require_auth
1452def complete_onboarding_step():
1453 from .onboarding_service import OnboardingService
1454 db = get_db()
1455 try:
1456 data = _get_json()
1457 step = data.get('step')
1458 result = OnboardingService.complete_step(db, g.user_id, step)
1459 db.commit()
1460 return _ok(result)
1461 except Exception as e:
1462 db.rollback()
1463 return _err(str(e))
1464 finally:
1465 db.close()
1468@gamification_bp.route('/onboarding/dismiss', methods=['POST'])
1469@require_auth
1470def dismiss_onboarding():
1471 from .onboarding_service import OnboardingService
1472 db = get_db()
1473 try:
1474 result = OnboardingService.dismiss(db, g.user_id)
1475 db.commit()
1476 return _ok(result)
1477 except Exception as e:
1478 db.rollback()
1479 return _err(str(e))
1480 finally:
1481 db.close()
1484@gamification_bp.route('/onboarding/suggestion', methods=['GET'])
1485@require_auth
1486def onboarding_suggestion():
1487 from .onboarding_service import OnboardingService
1488 db = get_db()
1489 try:
1490 result = OnboardingService.get_suggestion(db, g.user_id)
1491 return _ok(result)
1492 finally:
1493 db.close()
1496# ── Proximity & Missed Connections ──────────────────────────────
1498@gamification_bp.route('/encounters/location-ping', methods=['POST'])
1499@require_auth
1500def location_ping():
1501 from .proximity_service import ProximityService
1502 data = request.get_json(force=True, silent=True) or {}
1503 lat = data.get('lat')
1504 lon = data.get('lon')
1505 accuracy = data.get('accuracy', 0)
1506 if lat is None or lon is None:
1507 return _err("lat and lon required", 400)
1508 try:
1509 lat, lon = float(lat), float(lon)
1510 except (TypeError, ValueError):
1511 return _err("Invalid coordinates", 400)
1512 if not (-90 <= lat <= 90 and -180 <= lon <= 180):
1513 return _err("Coordinates out of range", 400)
1514 db = get_db()
1515 try:
1516 device_id = data.get('device_id')
1517 result = ProximityService.update_location(db, g.user_id, lat, lon, accuracy, device_id=device_id)
1518 db.commit()
1519 return _ok(result)
1520 except Exception as e:
1521 db.rollback()
1522 return _err(str(e))
1523 finally:
1524 db.close()
1527@gamification_bp.route('/encounters/nearby-now', methods=['GET'])
1528@require_auth
1529def nearby_now():
1530 from .proximity_service import ProximityService
1531 db = get_db()
1532 try:
1533 count = ProximityService.get_nearby_count(db, g.user_id)
1534 return _ok({'nearby_count': count})
1535 finally:
1536 db.close()
1539@gamification_bp.route('/encounters/proximity-matches', methods=['GET'])
1540@require_auth
1541def proximity_matches():
1542 from .proximity_service import ProximityService
1543 db = get_db()
1544 try:
1545 status = request.args.get('status')
1546 matches = ProximityService.get_matches(db, g.user_id, status=status)
1547 return _ok(matches)
1548 finally:
1549 db.close()
1552@gamification_bp.route('/encounters/proximity/<match_id>/reveal', methods=['POST'])
1553@require_auth
1554def proximity_reveal(match_id):
1555 from .proximity_service import ProximityService
1556 db = get_db()
1557 try:
1558 result = ProximityService.reveal_self(db, match_id, g.user_id)
1559 db.commit()
1560 return _ok(result)
1561 except ValueError as e:
1562 db.rollback()
1563 return _err(str(e), 400)
1564 except Exception as e:
1565 db.rollback()
1566 return _err(str(e))
1567 finally:
1568 db.close()
1571@gamification_bp.route('/encounters/location-settings', methods=['GET', 'PATCH'])
1572@require_auth
1573def location_settings():
1574 from .proximity_service import ProximityService
1575 db = get_db()
1576 try:
1577 if request.method == 'GET':
1578 result = ProximityService.get_location_settings(db, g.user_id)
1579 return _ok(result)
1580 else:
1581 data = request.get_json(force=True, silent=True) or {}
1582 enabled = data.get('location_sharing_enabled', False)
1583 result = ProximityService.update_location_settings(db, g.user_id, enabled)
1584 db.commit()
1585 return _ok(result)
1586 except ValueError as e:
1587 db.rollback()
1588 return _err(str(e), 400)
1589 except Exception as e:
1590 db.rollback()
1591 return _err(str(e))
1592 finally:
1593 db.close()
1596@gamification_bp.route('/encounters/missed-connections', methods=['GET', 'POST'])
1597@require_auth
1598def missed_connections():
1599 from .proximity_service import ProximityService
1600 db = get_db()
1601 try:
1602 if request.method == 'POST':
1603 data = request.get_json(force=True, silent=True) or {}
1604 result = ProximityService.create_missed_connection(
1605 db, g.user_id,
1606 lat=float(data.get('lat', 0)),
1607 lon=float(data.get('lon', 0)),
1608 location_name=data.get('location_name', ''),
1609 description=data.get('description', ''),
1610 was_at_iso=data.get('was_at', ''),
1611 )
1612 db.commit()
1613 return _ok(result), 201
1614 else:
1615 lat = request.args.get('lat', type=float)
1616 lon = request.args.get('lon', type=float)
1617 radius = request.args.get('radius', 1000, type=float)
1618 limit = request.args.get('limit', 20, type=int)
1619 offset = request.args.get('offset', 0, type=int)
1620 sort = request.args.get('sort', 'recent')
1621 if lat is None or lon is None:
1622 return _err("lat and lon required for search", 400)
1623 result = ProximityService.search_missed_connections(
1624 db, lat, lon, radius, limit=min(limit, 50), offset=offset,
1625 exclude_user_id=None, sort=sort)
1626 return _ok(result['data'], meta=result['meta'])
1627 except ValueError as e:
1628 db.rollback()
1629 return _err(str(e), 400)
1630 except Exception as e:
1631 db.rollback()
1632 return _err(str(e))
1633 finally:
1634 db.close()
1637@gamification_bp.route('/encounters/missed-connections/mine', methods=['GET'])
1638@require_auth
1639def my_missed_connections():
1640 from .proximity_service import ProximityService
1641 db = get_db()
1642 try:
1643 limit = request.args.get('limit', 20, type=int)
1644 offset = request.args.get('offset', 0, type=int)
1645 result = ProximityService.get_my_missed_connections(db, g.user_id, limit=min(limit, 50), offset=offset)
1646 return _ok(result['data'], meta=result['meta'])
1647 finally:
1648 db.close()
1651@gamification_bp.route('/encounters/missed-connections/<missed_id>', methods=['GET', 'DELETE'])
1652@require_auth
1653def missed_connection_detail(missed_id):
1654 from .proximity_service import ProximityService
1655 db = get_db()
1656 try:
1657 if request.method == 'DELETE':
1658 result = ProximityService.delete_missed_connection(db, missed_id, g.user_id)
1659 db.commit()
1660 return _ok(result)
1661 else:
1662 result = ProximityService.get_missed_with_responses(db, missed_id, viewer_id=g.user_id)
1663 return _ok(result)
1664 except ValueError as e:
1665 db.rollback()
1666 return _err(str(e), 400)
1667 except Exception as e:
1668 db.rollback()
1669 return _err(str(e))
1670 finally:
1671 db.close()
1674@gamification_bp.route('/encounters/missed-connections/<missed_id>/respond', methods=['POST'])
1675@require_auth
1676def respond_missed_connection(missed_id):
1677 from .proximity_service import ProximityService
1678 data = request.get_json(force=True, silent=True) or {}
1679 db = get_db()
1680 try:
1681 result = ProximityService.respond_to_missed(db, missed_id, g.user_id, data.get('message', ''))
1682 db.commit()
1683 return _ok(result), 201
1684 except ValueError as e:
1685 db.rollback()
1686 return _err(str(e), 400)
1687 except Exception as e:
1688 db.rollback()
1689 return _err(str(e))
1690 finally:
1691 db.close()
1694@gamification_bp.route('/encounters/missed-connections/<missed_id>/accept/<response_id>', methods=['POST'])
1695@require_auth
1696def accept_missed_response(missed_id, response_id):
1697 from .proximity_service import ProximityService
1698 db = get_db()
1699 try:
1700 result = ProximityService.accept_missed_response(db, missed_id, response_id, g.user_id)
1701 db.commit()
1702 return _ok(result)
1703 except ValueError as e:
1704 db.rollback()
1705 return _err(str(e), 400)
1706 except Exception as e:
1707 db.rollback()
1708 return _err(str(e))
1709 finally:
1710 db.close()
1713@gamification_bp.route('/encounters/missed-connections/suggest-locations', methods=['GET'])
1714@require_auth
1715def suggest_locations():
1716 from .proximity_service import ProximityService
1717 lat = request.args.get('lat', type=float)
1718 lon = request.args.get('lon', type=float)
1719 if lat is None or lon is None:
1720 return _err("lat and lon required", 400)
1721 db = get_db()
1722 try:
1723 result = ProximityService.auto_suggest_locations(db, lat, lon)
1724 return _ok(result)
1725 finally:
1726 db.close()
1729# ═══════════════════════════════════════════════════════════════
1730# ADS (7 endpoints)
1731# ═══════════════════════════════════════════════════════════════
1733@gamification_bp.route('/ads', methods=['POST'])
1734@require_auth
1735def create_ad():
1736 from .ad_service import AdService
1737 db = get_db()
1738 try:
1739 data = _get_json()
1740 result = AdService.create_ad(
1741 db, g.user_id,
1742 title=data.get('title', ''),
1743 click_url=data.get('click_url', ''),
1744 content=data.get('content', ''),
1745 image_url=data.get('image_url', ''),
1746 ad_type=data.get('ad_type', 'banner'),
1747 targeting=data.get('targeting'),
1748 budget_spark=int(data.get('budget_spark', 100)),
1749 cost_per_impression=float(data.get('cost_per_impression', 0.1)),
1750 cost_per_click=float(data.get('cost_per_click', 1.0)),
1751 starts_at=data.get('starts_at'),
1752 ends_at=data.get('ends_at'),
1753 )
1754 if 'error' in result:
1755 return _err(result['error'])
1756 db.commit()
1757 return _ok(result, status=201)
1758 except Exception as e:
1759 db.rollback()
1760 return _err(str(e))
1761 finally:
1762 db.close()
1765@gamification_bp.route('/ads/serve', methods=['GET'])
1766@optional_auth
1767def serve_ad():
1768 from .ad_service import AdService
1769 db = get_db()
1770 try:
1771 user_id = g.user_id if hasattr(g, 'user_id') and g.user_id else None
1772 region_id = request.args.get('region')
1773 placement = request.args.get('placement', 'feed_top')
1774 node_id = request.args.get('node_id')
1775 result = AdService.serve_ad(db, user_id, region_id, placement, node_id)
1776 if not result:
1777 return _ok(None)
1778 return _ok(result)
1779 finally:
1780 db.close()
1783@gamification_bp.route('/ads/<ad_id>/impression', methods=['POST'])
1784@require_auth
1785def record_ad_impression(ad_id):
1786 from .ad_service import AdService
1787 import hashlib
1788 db = get_db()
1789 try:
1790 data = _get_json()
1791 user_id = g.user_id
1792 node_id = data.get('node_id')
1793 region_id = data.get('region_id')
1794 placement_id = data.get('placement_id')
1795 ip_raw = request.remote_addr or ''
1796 ip_hash = hashlib.sha256(ip_raw.encode()).hexdigest()[:16]
1797 result = AdService.record_impression(
1798 db, ad_id, user_id, node_id, region_id, placement_id, ip_hash)
1799 if not result:
1800 return _err('Ad not found', 404)
1801 if 'error' in result:
1802 return _err(result['error'], 429)
1803 db.commit()
1804 return _ok(result)
1805 except Exception as e:
1806 db.rollback()
1807 return _err(str(e))
1808 finally:
1809 db.close()
1812@gamification_bp.route('/ads/<ad_id>/click', methods=['POST'])
1813@require_auth
1814def record_ad_click(ad_id):
1815 from .ad_service import AdService
1816 import hashlib
1817 db = get_db()
1818 try:
1819 data = _get_json()
1820 user_id = g.user_id
1821 node_id = data.get('node_id')
1822 ip_raw = request.remote_addr or ''
1823 ip_hash = hashlib.sha256(ip_raw.encode()).hexdigest()[:16]
1824 result = AdService.record_click(db, ad_id, user_id, node_id, ip_hash)
1825 if not result:
1826 return _err('Ad not found', 404)
1827 if 'error' in result:
1828 return _err(result['error'], 429)
1829 db.commit()
1830 return _ok(result)
1831 except Exception as e:
1832 db.rollback()
1833 return _err(str(e))
1834 finally:
1835 db.close()
1838@gamification_bp.route('/ads/mine', methods=['GET'])
1839@require_auth
1840def list_my_ads():
1841 from .ad_service import AdService
1842 db = get_db()
1843 try:
1844 status = request.args.get('status')
1845 limit = min(int(request.args.get('limit', 25)), 100)
1846 offset = int(request.args.get('offset', 0))
1847 ads = AdService.list_my_ads(db, g.user_id, status, limit, offset)
1848 return _ok(ads)
1849 finally:
1850 db.close()
1853@gamification_bp.route('/ads/<ad_id>/analytics', methods=['GET'])
1854@require_auth
1855def ad_analytics(ad_id):
1856 from .ad_service import AdService
1857 db = get_db()
1858 try:
1859 result = AdService.get_analytics(db, ad_id, g.user_id)
1860 if not result:
1861 return _err('Ad not found', 404)
1862 return _ok(result)
1863 finally:
1864 db.close()
1867@gamification_bp.route('/ads/<ad_id>', methods=['DELETE'])
1868@require_auth
1869def delete_ad(ad_id):
1870 from .ad_service import AdService
1871 db = get_db()
1872 try:
1873 result = AdService.delete_ad(db, ad_id, g.user_id)
1874 if not result:
1875 return _err('Ad not found', 404)
1876 db.commit()
1877 return _ok(result)
1878 except Exception as e:
1879 db.rollback()
1880 return _err(str(e))
1881 finally:
1882 db.close()
1885# ═══════════════════════════════════════════════════════════════
1886# HOSTING REWARDS (3 endpoints)
1887# ═══════════════════════════════════════════════════════════════
1889@gamification_bp.route('/hosting/rewards', methods=['GET'])
1890@require_auth
1891def hosting_rewards():
1892 from .hosting_reward_service import HostingRewardService
1893 db = get_db()
1894 try:
1895 node_id = request.args.get('node_id')
1896 limit = min(int(request.args.get('limit', 50)), 100)
1897 offset = int(request.args.get('offset', 0))
1898 rewards = HostingRewardService.get_rewards(
1899 db, node_id=node_id, operator_id=g.user_id, limit=limit, offset=offset)
1900 summary = None
1901 if node_id:
1902 summary = HostingRewardService.get_reward_summary(db, node_id)
1903 return _ok({'rewards': rewards, 'summary': summary})
1904 finally:
1905 db.close()
1908@gamification_bp.route('/hosting/leaderboard', methods=['GET'])
1909@optional_auth
1910def hosting_leaderboard():
1911 from .hosting_reward_service import HostingRewardService
1912 db = get_db()
1913 try:
1914 limit = min(int(request.args.get('limit', 50)), 100)
1915 offset = int(request.args.get('offset', 0))
1916 result = HostingRewardService.get_leaderboard(db, limit, offset)
1917 return _ok(result)
1918 finally:
1919 db.close()
1922@gamification_bp.route('/hosting/compute-rewards', methods=['POST'])
1923@require_admin
1924def compute_hosting_rewards():
1925 from .hosting_reward_service import HostingRewardService
1926 db = get_db()
1927 try:
1928 period_days = int(request.args.get('period_days', 7))
1929 scores = HostingRewardService.compute_all_scores(db, period_days)
1930 # Distribute uptime bonuses and check milestones for each node
1931 bonuses = []
1932 milestones = []
1933 for s in scores:
1934 bonus = HostingRewardService.distribute_uptime_bonus(db, s['node_id'])
1935 if bonus:
1936 bonuses.append(bonus)
1937 milestone = HostingRewardService.check_milestones(db, s['node_id'])
1938 if milestone:
1939 milestones.append(milestone)
1940 db.commit()
1941 return _ok({
1942 'scores_computed': len(scores),
1943 'uptime_bonuses': len(bonuses),
1944 'milestones_awarded': len(milestones),
1945 'details': scores,
1946 })
1947 except Exception as e:
1948 db.rollback()
1949 return _err(str(e))
1950 finally:
1951 db.close()