Grain flutter app
1import 'dart:convert';
2import 'dart:io';
3
4import 'package:grain/app_logger.dart';
5import 'package:grain/main.dart';
6import 'package:grain/models/session.dart';
7import 'package:http/http.dart' as http;
8import 'package:mime/mime.dart';
9
10import './auth.dart';
11import 'models/followers_result.dart';
12import 'models/follows_result.dart';
13import 'models/gallery.dart';
14import 'models/gallery_photo.dart';
15import 'models/gallery_thread.dart';
16import 'models/notification.dart' as grain;
17import 'models/procedures/procedures.dart';
18import 'models/profile.dart';
19
20class ApiService {
21 Profile? currentUser;
22 Profile? loadedProfile;
23 List<Gallery> galleries = [];
24
25 String get _apiUrl => AppConfig.apiUrl;
26
27 Future<Session?> refreshSession(Session session) async {
28 final url = Uri.parse('$_apiUrl/api/token/refresh');
29 final headers = {'Content-Type': 'application/json'};
30 try {
31 final response = await http.post(
32 url,
33 headers: headers,
34 body: jsonEncode({'refreshToken': session.refreshToken}),
35 );
36 if (response.statusCode == 200) {
37 appLogger.i('Session refreshed successfully');
38 return Session.fromJson(jsonDecode(response.body));
39 } else {
40 appLogger.w('Failed to refresh session: ${response.statusCode} ${response.body}');
41 return null;
42 }
43 } catch (e) {
44 appLogger.e('Error refreshing session: $e');
45 return null;
46 }
47 }
48
49 Future<bool> revokeSession(Session session) async {
50 final url = Uri.parse('$_apiUrl/api/token/revoke');
51 final headers = {'Content-Type': 'application/json'};
52 try {
53 final response = await http.post(
54 url,
55 headers: headers,
56 body: jsonEncode({'refreshToken': session.refreshToken}),
57 );
58 if (response.statusCode == 200) {
59 appLogger.i('Session revoked successfully');
60 return true;
61 } else {
62 appLogger.w('Failed to revoke session: ${response.statusCode} ${response.body}');
63 return false;
64 }
65 } catch (e) {
66 appLogger.e('Error revoking session: $e');
67 return false;
68 }
69 }
70
71 Future<Profile?> fetchCurrentUser() async {
72 final session = await auth.getValidSession();
73
74 if (session == null || session.did.isEmpty) {
75 return null;
76 }
77
78 final user = await fetchProfile(did: session.did);
79
80 currentUser = user;
81
82 return user;
83 }
84
85 Future<Profile?> fetchProfile({required String did}) async {
86 final session = await auth.getValidSession();
87 final token = session?.token;
88 appLogger.i('Fetching profile for did: $did');
89 final response = await http.get(
90 Uri.parse('$_apiUrl/xrpc/social.grain.actor.getProfile?actor=$did'),
91 headers: {'Content-Type': 'application/json', 'Authorization': "Bearer $token"},
92 );
93 if (response.statusCode != 200) {
94 appLogger.w('Failed to fetch profile: ${response.statusCode} ${response.body}');
95 return null;
96 }
97 return Profile.fromJson(jsonDecode(response.body));
98 }
99
100 Future<List<Gallery>> fetchActorGalleries({required String did}) async {
101 appLogger.i('Fetching galleries for actor did: $did');
102 final response = await http.get(
103 Uri.parse('$_apiUrl/xrpc/social.grain.gallery.getActorGalleries?actor=$did'),
104 headers: {'Content-Type': 'application/json'},
105 );
106 if (response.statusCode != 200) {
107 appLogger.w('Failed to fetch galleries: ${response.statusCode} ${response.body}');
108 return [];
109 }
110 final json = jsonDecode(response.body);
111 galleries =
112 (json['items'] as List<dynamic>?)?.map((item) => Gallery.fromJson(item)).toList() ?? [];
113 return galleries;
114 }
115
116 Future<List<GalleryPhoto>> fetchActorPhotos({required String did}) async {
117 appLogger.i('Fetching photos for actor did: $did');
118 final response = await http.get(
119 Uri.parse('$_apiUrl/xrpc/social.grain.photo.getActorPhotos?actor=$did'),
120 headers: {'Content-Type': 'application/json'},
121 );
122 if (response.statusCode != 200) {
123 appLogger.w('Failed to fetch photos: ${response.statusCode} ${response.body}');
124 return [];
125 }
126 final json = jsonDecode(response.body);
127 return (json['items'] as List<dynamic>?)?.map((item) => GalleryPhoto.fromJson(item)).toList() ??
128 [];
129 }
130
131 Future<List<Gallery>> getTimeline({String? algorithm}) async {
132 final session = await auth.getValidSession();
133 final token = session?.token;
134 if (token == null) {
135 return [];
136 }
137 appLogger.i('Fetching timeline with algorithm: ${algorithm ?? 'default'}');
138 final uri = algorithm != null
139 ? Uri.parse('$_apiUrl/xrpc/social.grain.feed.getTimeline?algorithm=$algorithm')
140 : Uri.parse('$_apiUrl/xrpc/social.grain.feed.getTimeline');
141 final response = await http.get(
142 uri,
143 headers: {'Authorization': "Bearer $token", 'Content-Type': 'application/json'},
144 );
145 if (response.statusCode != 200) {
146 appLogger.w('Failed to fetch timeline: ${response.statusCode} ${response.body}');
147 return [];
148 }
149 final json = jsonDecode(response.body);
150 return (json['feed'] as List<dynamic>?)
151 ?.map((item) => Gallery.fromJson(item as Map<String, dynamic>))
152 .toList() ??
153 [];
154 }
155
156 Future<Gallery?> getGallery({required String uri}) async {
157 appLogger.i('Fetching gallery for uri: $uri');
158 final session = await auth.getValidSession();
159 final token = session?.token;
160 if (token == null) {
161 appLogger.w('No access token for getGallery');
162 return null;
163 }
164 final response = await http.get(
165 Uri.parse('$_apiUrl/xrpc/social.grain.gallery.getGallery?uri=$uri'),
166 headers: {'Authorization': "Bearer $token", 'Content-Type': 'application/json'},
167 );
168 if (response.statusCode != 200) {
169 appLogger.w('Failed to fetch gallery: ${response.statusCode} ${response.body}');
170 return null;
171 }
172 try {
173 final json = jsonDecode(response.body);
174 if (json is Map<String, dynamic>) {
175 return Gallery.fromJson(json);
176 } else {
177 appLogger.w('Unexpected response type for getGallery: ${response.body}');
178 return null;
179 }
180 } catch (e, st) {
181 appLogger.e('Error parsing getGallery response: $e', stackTrace: st);
182 return null;
183 }
184 }
185
186 Future<GalleryThread?> getGalleryThread({required String uri}) async {
187 appLogger.i('Fetching gallery thread for uri: $uri');
188 final session = await auth.getValidSession();
189 final token = session?.token;
190 if (token == null) {
191 appLogger.w('No access token for getGalleryThread');
192 return null;
193 }
194 final response = await http.get(
195 Uri.parse('$_apiUrl/xrpc/social.grain.gallery.getGalleryThread?uri=$uri'),
196 headers: {'Content-Type': 'application/json', 'Authorization': "Bearer $token"},
197 );
198 if (response.statusCode != 200) {
199 appLogger.w('Failed to fetch gallery thread: ${response.statusCode} ${response.body}');
200 return null;
201 }
202 final json = jsonDecode(response.body) as Map<String, dynamic>;
203 return GalleryThread.fromJson(json);
204 }
205
206 Future<List<grain.Notification>> getNotifications() async {
207 final session = await auth.getValidSession();
208 final token = session?.token;
209 if (token == null) {
210 appLogger.w('No access token for getNotifications');
211 return [];
212 }
213 appLogger.i('Fetching notifications');
214 final response = await http.get(
215 Uri.parse('$_apiUrl/xrpc/social.grain.notification.getNotifications'),
216 headers: {'Authorization': "Bearer $token", 'Content-Type': 'application/json'},
217 );
218 if (response.statusCode != 200) {
219 appLogger.w('Failed to fetch notifications: ${response.statusCode} ${response.body}');
220 return [];
221 }
222 final json = jsonDecode(response.body);
223 return (json['notifications'] as List<dynamic>?)
224 ?.map((item) {
225 final map = item as Map<String, dynamic>;
226 final reasonSubject = map['reasonSubject'];
227 if (reasonSubject != null && reasonSubject is Map<String, dynamic>) {
228 final type =
229 reasonSubject['\$type'] ?? reasonSubject[r'$type'] ?? reasonSubject['type'];
230 switch (type) {
231 case 'social.grain.gallery.defs#galleryView':
232 map['reasonSubjectGallery'] = map['reasonSubject'];
233 break;
234 case 'social.grain.actor.defs#profileView':
235 map['reasonSubjectProfile'] = map['reasonSubject'];
236 break;
237 case 'social.grain.comment.defs#commentView':
238 map['reasonSubjectComment'] = map['reasonSubject'];
239 break;
240 }
241 }
242 map.remove('reasonSubject');
243 try {
244 return grain.Notification.fromJson(map);
245 } catch (e, st) {
246 appLogger.e('Failed to deserialize notification: $e', stackTrace: st);
247 return null;
248 }
249 })
250 .whereType<grain.Notification>()
251 .toList() ??
252 [];
253 }
254
255 Future<List<Profile>> searchActors(String query) async {
256 final session = await auth.getValidSession();
257 final token = session?.token;
258 if (token == null) {
259 appLogger.w('No access token for searchActors');
260 return [];
261 }
262 appLogger.i('Searching actors with query: $query');
263 final response = await http.get(
264 Uri.parse('$_apiUrl/xrpc/social.grain.actor.searchActors?q=$query'),
265 headers: {'Authorization': "Bearer $token", 'Content-Type': 'application/json'},
266 );
267 if (response.statusCode != 200) {
268 appLogger.w('Failed to search actors: ${response.statusCode} ${response.body}');
269 return [];
270 }
271 final json = jsonDecode(response.body);
272 return (json['actors'] as List<dynamic>?)?.map((item) => Profile.fromJson(item)).toList() ?? [];
273 }
274
275 Future<List<Gallery>> getActorFavs({required String did}) async {
276 appLogger.i('Fetching actor favs for did: $did');
277 final response = await http.get(
278 Uri.parse('$_apiUrl/xrpc/social.grain.actor.getActorFavs?actor=$did'),
279 headers: {'Content-Type': 'application/json'},
280 );
281 if (response.statusCode != 200) {
282 appLogger.w('Failed to fetch actor favs: ${response.statusCode} ${response.body}');
283 return [];
284 }
285 final json = jsonDecode(response.body);
286 return (json['items'] as List<dynamic>?)?.map((item) => Gallery.fromJson(item)).toList() ?? [];
287 }
288
289 /// Fetch followers for a given actor DID
290 Future<FollowersResult> getFollowers({
291 required String actor,
292 String? cursor,
293 int limit = 50,
294 }) async {
295 final uri = Uri.parse(
296 '$_apiUrl/xrpc/social.grain.graph.getFollowers?actor=$actor&limit=$limit${cursor != null ? '&cursor=$cursor' : ''}',
297 );
298 final response = await http.get(uri, headers: {'Content-Type': 'application/json'});
299 if (response.statusCode != 200) {
300 throw Exception('Failed to fetch followers: \\${response.statusCode} \\${response.body}');
301 }
302 final json = jsonDecode(response.body);
303 return FollowersResult.fromJson(json);
304 }
305
306 /// Fetch follows for a given actor DID
307 Future<FollowsResult> getFollows({required String actor, String? cursor, int limit = 50}) async {
308 final uri = Uri.parse(
309 '$_apiUrl/xrpc/social.grain.graph.getFollows?actor=$actor&limit=$limit${cursor != null ? '&cursor=$cursor' : ''}',
310 );
311 final response = await http.get(uri, headers: {'Content-Type': 'application/json'});
312 if (response.statusCode != 200) {
313 throw Exception('Failed to fetch follows: \\${response.statusCode} \\${response.body}');
314 }
315 final json = jsonDecode(response.body);
316 return FollowsResult.fromJson(json);
317 }
318
319 // Procedures
320
321 Future<UpdateProfileResponse> updateProfile({required UpdateProfileRequest request}) async {
322 final session = await auth.getValidSession();
323 final token = session?.token;
324 if (token == null) {
325 throw Exception('No access token for updateProfile');
326 }
327 final uri = Uri.parse('$_apiUrl/xrpc/social.grain.actor.updateProfile');
328 final response = await http.post(
329 uri,
330 headers: {'Authorization': "Bearer $token", 'Content-Type': 'application/json'},
331 body: jsonEncode(request.toJson()),
332 );
333 if (response.statusCode != 200) {
334 throw Exception('Failed to update profile: ${response.statusCode} ${response.body}');
335 }
336 final json = jsonDecode(response.body);
337 return UpdateProfileResponse.fromJson(json);
338 }
339
340 Future<UpdateAvatarResponse> updateAvatar({required File avatarFile}) async {
341 final session = await auth.getValidSession();
342 final token = session?.token;
343 if (token == null) {
344 throw Exception('No access token for updateAvatar');
345 }
346 final uri = Uri.parse('$_apiUrl/xrpc/social.grain.actor.updateAvatar');
347 String? mimeType = lookupMimeType(avatarFile.path);
348 final contentType = mimeType ?? 'application/octet-stream';
349 final bytes = await avatarFile.readAsBytes();
350 final response = await http.post(
351 uri,
352 headers: {'Authorization': "Bearer $token", 'Content-Type': contentType},
353 body: bytes,
354 );
355 if (response.statusCode != 200) {
356 throw Exception('Failed to update avatar: ${response.statusCode} ${response.body}');
357 }
358 final json = jsonDecode(response.body);
359 return UpdateAvatarResponse.fromJson(json);
360 }
361
362 Future<ApplySortResponse> applySort({required ApplySortRequest request}) async {
363 final session = await auth.getValidSession();
364 final token = session?.token;
365 if (token == null) {
366 throw Exception('No access token for applySort');
367 }
368 final uri = Uri.parse('$_apiUrl/xrpc/social.grain.gallery.applySort');
369 final response = await http.post(
370 uri,
371 headers: {'Authorization': "Bearer $token", 'Content-Type': 'application/json'},
372 body: jsonEncode(request.toJson()),
373 );
374 if (response.statusCode != 200) {
375 throw Exception('Failed to apply sort: ${response.statusCode} ${response.body}');
376 }
377 final json = jsonDecode(response.body);
378 return ApplySortResponse.fromJson(json);
379 }
380
381 Future<ApplyAltsResponse> applyAlts({required ApplyAltsRequest request}) async {
382 final session = await auth.getValidSession();
383 final token = session?.token;
384 if (token == null) {
385 throw Exception('No access token for applyAlts');
386 }
387 final uri = Uri.parse('$_apiUrl/xrpc/social.grain.photo.applyAlts');
388 final response = await http.post(
389 uri,
390 headers: {'Authorization': "Bearer $token", 'Content-Type': 'application/json'},
391 body: jsonEncode(request.toJson()),
392 );
393 if (response.statusCode != 200) {
394 throw Exception('Failed to apply alts: ${response.statusCode} ${response.body}');
395 }
396 final json = jsonDecode(response.body);
397 return ApplyAltsResponse.fromJson(json);
398 }
399
400 Future<CreateExifResponse> createExif({required CreateExifRequest request}) async {
401 final session = await auth.getValidSession();
402 final token = session?.token;
403 if (token == null) {
404 throw Exception('No access token for createExif');
405 }
406 final uri = Uri.parse('$_apiUrl/xrpc/social.grain.photo.createExif');
407 final response = await http.post(
408 uri,
409 headers: {'Authorization': "Bearer $token", 'Content-Type': 'application/json'},
410 body: jsonEncode(request.toJson()),
411 );
412 if (response.statusCode != 200) {
413 throw Exception('Failed to create exif: ${response.statusCode} ${response.body}');
414 }
415 final json = jsonDecode(response.body);
416 return CreateExifResponse.fromJson(json);
417 }
418
419 Future<CreateFollowResponse> createFollow({required CreateFollowRequest request}) async {
420 final session = await auth.getValidSession();
421 final token = session?.token;
422 if (token == null) {
423 throw Exception('No access token for createFollow');
424 }
425 final uri = Uri.parse('$_apiUrl/xrpc/social.grain.graph.createFollow');
426 final response = await http.post(
427 uri,
428 headers: {'Authorization': "Bearer $token", 'Content-Type': 'application/json'},
429 body: jsonEncode(request.toJson()),
430 );
431 if (response.statusCode != 200) {
432 throw Exception('Failed to create follow: ${response.statusCode} ${response.body}');
433 }
434 final json = jsonDecode(response.body);
435 return CreateFollowResponse.fromJson(json);
436 }
437
438 Future<DeleteFollowResponse> deleteFollow({required DeleteFollowRequest request}) async {
439 final session = await auth.getValidSession();
440 final token = session?.token;
441 if (token == null) {
442 throw Exception('No access token for deleteFollow');
443 }
444 final uri = Uri.parse('$_apiUrl/xrpc/social.grain.graph.deleteFollow');
445 final response = await http.post(
446 uri,
447 headers: {'Authorization': "Bearer $token", 'Content-Type': 'application/json'},
448 body: jsonEncode(request.toJson()),
449 );
450 if (response.statusCode != 200) {
451 throw Exception('Failed to delete follow: {response.statusCode} {response.body}');
452 }
453 final json = jsonDecode(response.body);
454 return DeleteFollowResponse.fromJson(json);
455 }
456
457 Future<DeletePhotoResponse> deletePhoto({required DeletePhotoRequest request}) async {
458 final session = await auth.getValidSession();
459 final token = session?.token;
460 if (token == null) {
461 throw Exception('No access token for deletePhoto');
462 }
463 final uri = Uri.parse('$_apiUrl/xrpc/social.grain.photo.deletePhoto');
464 final response = await http.post(
465 uri,
466 headers: {'Authorization': "Bearer $token", 'Content-Type': 'application/json'},
467 body: jsonEncode(request.toJson()),
468 );
469 if (response.statusCode != 200) {
470 throw Exception('Failed to delete photo: ${response.statusCode} ${response.body}');
471 }
472 final json = jsonDecode(response.body);
473 return DeletePhotoResponse.fromJson(json);
474 }
475
476 Future<UploadPhotoResponse> uploadPhoto(File file) async {
477 final session = await auth.getValidSession();
478 if (session == null) {
479 appLogger.w('No valid session for uploadPhoto');
480 throw Exception('No valid session for uploadPhoto');
481 }
482 final token = session.token;
483 final uri = Uri.parse('${AppConfig.apiUrl}/xrpc/social.grain.photo.uploadPhoto');
484
485 // Detect MIME type, fallback to application/octet-stream if unknown
486 String? mimeType = lookupMimeType(file.path);
487 final contentType = mimeType ?? 'application/octet-stream';
488
489 appLogger.i('Uploading photo: ${file.path} (MIME: $mimeType)');
490 final bytes = await file.readAsBytes();
491
492 final response = await http.post(
493 uri,
494 headers: {'Authorization': 'Bearer $token', 'Content-Type': contentType},
495 body: bytes,
496 );
497
498 if (response.statusCode != 200 && response.statusCode != 201) {
499 appLogger.w(
500 'Failed to upload photo: ${response.statusCode} ${response.body} (File: ${file.path}, MIME: $mimeType)',
501 );
502 throw Exception('Failed to upload photo: ${response.statusCode} ${response.body}');
503 }
504
505 try {
506 final json = jsonDecode(response.body);
507 appLogger.i('Uploaded photo result: $json');
508 return UploadPhotoResponse.fromJson(json);
509 } catch (e, st) {
510 appLogger.e('Failed to parse createPhoto response: $e', stackTrace: st);
511 throw Exception('Failed to parse createPhoto response: $e');
512 }
513 }
514
515 Future<DeleteGalleryItemResponse> deleteGalleryItem({
516 required DeleteGalleryItemRequest request,
517 }) async {
518 final session = await auth.getValidSession();
519 final token = session?.token;
520 if (token == null) {
521 throw Exception('No access token for deleteGalleryItem');
522 }
523 final uri = Uri.parse('$_apiUrl/xrpc/social.grain.gallery.deleteItem');
524 final response = await http.post(
525 uri,
526 headers: {'Authorization': "Bearer $token", 'Content-Type': 'application/json'},
527 body: jsonEncode(request.toJson()),
528 );
529 if (response.statusCode != 200) {
530 throw Exception('Failed to delete gallery item: ${response.statusCode} ${response.body}');
531 }
532 final json = jsonDecode(response.body);
533 return DeleteGalleryItemResponse.fromJson(json);
534 }
535
536 Future<CreateGalleryItemResponse> createGalleryItem({
537 required CreateGalleryItemRequest request,
538 }) async {
539 final session = await auth.getValidSession();
540 final token = session?.token;
541 if (token == null) {
542 throw Exception('No access token for createGalleryItem');
543 }
544 final uri = Uri.parse('$_apiUrl/xrpc/social.grain.gallery.createItem');
545 final response = await http.post(
546 uri,
547 headers: {'Authorization': "Bearer $token", 'Content-Type': 'application/json'},
548 body: jsonEncode(request.toJson()),
549 );
550 if (response.statusCode != 200) {
551 throw Exception('Failed to create gallery item: ${response.statusCode} ${response.body}');
552 }
553 final json = jsonDecode(response.body);
554 return CreateGalleryItemResponse.fromJson(json);
555 }
556
557 Future<UpdateGalleryResponse> updateGallery({required UpdateGalleryRequest request}) async {
558 final session = await auth.getValidSession();
559 final token = session?.token;
560 if (token == null) {
561 throw Exception('No access token for updateGallery');
562 }
563 final uri = Uri.parse('$_apiUrl/xrpc/social.grain.gallery.updateGallery');
564 final response = await http.post(
565 uri,
566 headers: {'Authorization': "Bearer $token", 'Content-Type': 'application/json'},
567 body: jsonEncode(request.toJson()),
568 );
569 if (response.statusCode != 200) {
570 throw Exception('Failed to update gallery: ${response.statusCode} ${response.body}');
571 }
572 final json = jsonDecode(response.body);
573 return UpdateGalleryResponse.fromJson(json);
574 }
575
576 Future<DeleteGalleryResponse> deleteGallery({required DeleteGalleryRequest request}) async {
577 final session = await auth.getValidSession();
578 final token = session?.token;
579 if (token == null) {
580 throw Exception('No access token for deleteGallery');
581 }
582 final uri = Uri.parse('$_apiUrl/xrpc/social.grain.gallery.deleteGallery');
583 final response = await http.post(
584 uri,
585 headers: {'Authorization': "Bearer $token", 'Content-Type': 'application/json'},
586 body: jsonEncode(request.toJson()),
587 );
588 if (response.statusCode != 200) {
589 throw Exception('Failed to delete gallery: ${response.statusCode} ${response.body}');
590 }
591 final json = jsonDecode(response.body);
592 return DeleteGalleryResponse.fromJson(json);
593 }
594
595 Future<CreateGalleryResponse> createGallery({required CreateGalleryRequest request}) async {
596 final session = await auth.getValidSession();
597 final token = session?.token;
598 if (token == null) {
599 throw Exception('No access token for createGallery');
600 }
601 final uri = Uri.parse('$_apiUrl/xrpc/social.grain.gallery.createGallery');
602 final response = await http.post(
603 uri,
604 headers: {'Authorization': "Bearer $token", 'Content-Type': 'application/json'},
605 body: jsonEncode(request.toJson()),
606 );
607 if (response.statusCode != 200) {
608 throw Exception('Failed to create gallery: ${response.statusCode} ${response.body}');
609 }
610 final json = jsonDecode(response.body);
611 return CreateGalleryResponse.fromJson(json);
612 }
613
614 Future<DeleteFavoriteResponse> deleteFavorite({required DeleteFavoriteRequest request}) async {
615 final session = await auth.getValidSession();
616 final token = session?.token;
617 if (token == null) {
618 throw Exception('No access token for deleteFavorite');
619 }
620 final uri = Uri.parse('$_apiUrl/xrpc/social.grain.favorite.deleteFavorite');
621 final response = await http.post(
622 uri,
623 headers: {'Authorization': "Bearer $token", 'Content-Type': 'application/json'},
624 body: jsonEncode(request.toJson()),
625 );
626 if (response.statusCode != 200) {
627 throw Exception('Failed to delete favorite: ${response.statusCode} ${response.body}');
628 }
629 final json = jsonDecode(response.body);
630 return DeleteFavoriteResponse.fromJson(json);
631 }
632
633 Future<CreateFavoriteResponse> createFavorite({required CreateFavoriteRequest request}) async {
634 final session = await auth.getValidSession();
635 final token = session?.token;
636 if (token == null) {
637 throw Exception('No access token for createFavorite');
638 }
639 final uri = Uri.parse('$_apiUrl/xrpc/social.grain.favorite.createFavorite');
640 final response = await http.post(
641 uri,
642 headers: {'Authorization': "Bearer $token", 'Content-Type': 'application/json'},
643 body: jsonEncode(request.toJson()),
644 );
645 if (response.statusCode != 200) {
646 throw Exception('Failed to create favorite: ${response.statusCode} ${response.body}');
647 }
648 final json = jsonDecode(response.body);
649 return CreateFavoriteResponse.fromJson(json);
650 }
651
652 Future<DeleteCommentResponse> deleteComment({required DeleteCommentRequest request}) async {
653 final session = await auth.getValidSession();
654 final token = session?.token;
655 if (token == null) {
656 throw Exception('No access token for deleteComment');
657 }
658 final uri = Uri.parse('$_apiUrl/xrpc/social.grain.comment.deleteComment');
659 final response = await http.post(
660 uri,
661 headers: {'Authorization': "Bearer $token", 'Content-Type': 'application/json'},
662 body: jsonEncode(request.toJson()),
663 );
664 if (response.statusCode != 200) {
665 throw Exception('Failed to delete comment: ${response.statusCode} ${response.body}');
666 }
667 final json = jsonDecode(response.body);
668 return DeleteCommentResponse.fromJson(json);
669 }
670
671 Future<CreateCommentResponse> createComment({required CreateCommentRequest request}) async {
672 final session = await auth.getValidSession();
673 final token = session?.token;
674 if (token == null) {
675 throw Exception('No access token for createComment');
676 }
677 final uri = Uri.parse('$_apiUrl/xrpc/social.grain.comment.createComment');
678 final response = await http.post(
679 uri,
680 headers: {'Authorization': "Bearer $token", 'Content-Type': 'application/json'},
681 body: jsonEncode(request.toJson()),
682 );
683 if (response.statusCode != 200) {
684 throw Exception('Failed to create comment: ${response.statusCode} ${response.body}');
685 }
686 final json = jsonDecode(response.body);
687 return CreateCommentResponse.fromJson(json);
688 }
689
690 Future<bool> updateSeen() async {
691 final session = await auth.getValidSession();
692 final token = session?.token;
693 if (token == null) {
694 appLogger.w('No access token for updateSeen');
695 return false;
696 }
697 final url = Uri.parse('$_apiUrl/xrpc/social.grain.notification.updateSeen');
698 final seenAt = DateTime.now().toUtc().toIso8601String();
699 final body = jsonEncode({'seenAt': seenAt});
700 final headers = {'Authorization': 'Bearer $token', 'Content-Type': 'application/json'};
701 try {
702 final response = await http.post(url, headers: headers, body: body);
703 if (response.statusCode == 200) {
704 appLogger.i('Successfully updated seen notifications at $seenAt');
705 return true;
706 } else {
707 appLogger.w('Failed to update seen notifications: ${response.statusCode} ${response.body}');
708 return false;
709 }
710 } catch (e) {
711 appLogger.e('Error updating seen notifications: $e');
712 return false;
713 }
714 }
715}
716
717final apiService = ApiService();