From 9fec6754d999aa1979a5be2b961d53a833fb9a8c Mon Sep 17 00:00:00 2001 From: Patrick Britton Date: Wed, 4 Feb 2026 16:20:49 -0600 Subject: [PATCH] Add comprehensive social graph implementation documentation --- sojorn_docs/SOCIAL_GRAPH_IMPLEMENTATION.md | 169 +++++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 sojorn_docs/SOCIAL_GRAPH_IMPLEMENTATION.md diff --git a/sojorn_docs/SOCIAL_GRAPH_IMPLEMENTATION.md b/sojorn_docs/SOCIAL_GRAPH_IMPLEMENTATION.md new file mode 100644 index 0000000..a88e8b7 --- /dev/null +++ b/sojorn_docs/SOCIAL_GRAPH_IMPLEMENTATION.md @@ -0,0 +1,169 @@ +# Social Graph & Privacy Implementation Status + +## ✅ Completed Backend Features + +### 1. Database Schema +- **Circle Members Table**: `public.circle_members` created with user_id/member_id pairs +- **SQL Function**: `is_in_circle(owner_id, user_id)` for efficient membership checks +- **Migration**: `20260204000002_circle_privacy.up.sql` ready to apply + +### 2. Repository Methods (Go Backend) + +#### Followers & Following +- `GetFollowers(ctx, userID, limit, offset)` - Returns paginated list with harmony scores/tiers +- `GetFollowing(ctx, userID, limit, offset)` - Returns paginated list with harmony scores/tiers + +#### Circle Management +- `AddToCircle(ctx, userID, memberID)` - Add user to circle (validates following first) +- `RemoveFromCircle(ctx, userID, memberID)` - Remove from circle +- `GetCircleMembers(ctx, userID)` - List all circle members +- `IsInCircle(ctx, ownerID, userID)` - Check membership + +#### Data Export +- `ExportUserData(ctx, userID)` - Complete export (profile, posts, following list) + +### 3. API Endpoints +All routes registered in `cmd/api/main.go`: + +``` +GET /api/v1/users/:id/followers - Get user's followers list +GET /ap/v1/users/:id/following - Get list of users they follow +POST /api/v1/users/circle/:id - Add user to your circle +DELETE /api/v1/users/circle/:id - Remove user from circle +GET /api/v1/users/circle/members - Get your circle members +GET /api/v1/users/me/export - Export your complete data +``` + +## ⚠️ Remaining Implementation + +### Circle Privacy in Feed Queries + +Add this WHERE clause to `post_repository.go` in these methods: +- `GetFeed()` (line ~156) +- `GetPostsByAuthor()` (line ~243) +- `GetPostByID()` (line ~310) + +**Code to add** (after the blocking check): + +```go +AND ( + p.visibility != 'circle' -- Non-circle posts visible to all + OR p.author_id = CASE WHEN $4::text != '' THEN $4::text::uuid ELSE NULL END -- Author sees own circle posts + OR public.is_in_circle(p.author_id, CASE WHENylt:text != '' THEN $4::text::uuid ELSE NULL END) -- Circle members see circle posts +) +``` + +### Frontend Flutter Implementation + +#### 1. Update Following Screen (`lib/screens/profile/following_screen.dart`) + +Replace `_generateMockData()` with real API call: + +```dart +Future _loadFollowing() async { + setState(() { + _isLoading = true; + _error = null; + }); + + try { + final api = ref.read(apiServiceProvider); + final currentUser = ref.read(currentUserProvider); + + final data = await api.callGoApi( + '/users/${currentUser?.id}/following', + queryParams: {'limit': '20', 'offset': '${_followedUsers.length}'}, + ); + + final List users = (data['following'] as List) + .map( (json) => FollowedUser.fromJson(json)) + .toList(); + + setState(() { + _followedUsers = users; + }); + } catch (e) { + setState(() { + _error = e.toString(); + }); + } finally { + setState(() { + _isLoading = false; + }); + } +} +``` + +#### 2. Create Followers Screen + +Create `lib/screens/profile/followers_screen.dart` - similar structure to `FollowingScreen` but calling `/users/:id/followers`. + +#### 3. Add Circle Management Screen + +Create `lib/screens/settings/circle_management_screen.dart`: +- List current circle members (GET `/users/circle/members`) +- Show "Add to Circle" button on following list +- Handle add (POST `/users/circle/:id`) and remove (DELETE `/users/circle/:id`) + +#### 4. Data Export Button + +In `lib/screens/settings/profile_settings_screen.dart`: + +```dart +ListTile( + leading: Icon(Icons.download), + title: Text('Export My Data'), + subtitle: Text('Download your profile, posts, and connections'), + onTap: () async { + final api = ref.read(apiServiceProvider); + final data = await api.callGoApi('/users/me/export'); + + // Save to file + final file = File('${documentsDir}/sojorn_export.json'); + await file.writeAsString(jsonEncode(data)); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Data exported successfully!')), + ); + }, +) +``` + +#### 5. Circle Visibility in Compose + +In `lib/screens/compose/compose_screen.dart`, ensure "Circle" option in visibility dropdown: +- Options: 'public', 'friends', 'circle' +- When "Circle" is selected, post won't appear in feed for non-circle members + +## Migration Instructions + +1. **Apply Database Migration**: + ```bash + cd go-backend + migrate -path internal/database/migrations -database "your_db_url" up + ``` + +2. **Restart Go Backend** to load new routes + +3. **Test Endpoints**: + ```bash + # Get followers + curl -H "Authorization: Bearer $TOKEN" \ + "http://localhost:8080/api/v1/users/USER_ID/followers" + + # Export data + curl -H "Authorization: Bearer $TOKEN" \ + "http://localhost:8080/api/v1/users/me/export" > export.json + ``` + +4. **Update Flutter App**: + - Implement the frontend screens listed above + - Test circle visibility by creating circle-only posts + - Verify data export downloads correctly + +## Security Notes + +- Circle membership is verified via `is_in_circle()` SQL function (performant, indexed) +- Blocked users cannot see any posts (via `has_block_between()`) +- Data export only returns user's own data (enforced by JWT user_id) +- All endpoints require authentication via JWT middleware