diff --git a/bin/server.dart b/bin/server.dart index 77b7254..d174deb 100644 --- a/bin/server.dart +++ b/bin/server.dart @@ -5,10 +5,18 @@ import 'package:shelf/shelf.dart'; import 'package:shelf/shelf_io.dart'; import 'package:shelf_router/shelf_router.dart'; +import 'package:dio/dio.dart' as dio_lib; + +var dio = dio_lib.Dio(); + // Load confirmation token from env final confirmationToken = Platform.environment['CONFIRMATION_TOKEN']; // Load secret key from env final secretKey = Platform.environment['SECRET_KEY']; +// Load mastodon instance url from env +final instanceUrl = Platform.environment['INSTANCE_URL']; +// Load mastodon auth token from env +final authToken = Platform.environment['MASTODON_TOKEN']; // Configure routes. final _router = Router() @@ -38,6 +46,7 @@ Future _vkHandler(Request req) async { } else if (requestType == 'wall_post_new') { if (requestJson['secret'] == secretKey) { print(requestBody); + _postOnMastodon(requestJson); return Response.ok('ok'); } else { return Response.ok('invalid secret'); @@ -47,16 +56,76 @@ Future _vkHandler(Request req) async { } } -// Future _postOnMastodon(reqJson) async { -// // Called to repost the post on Mastodon on vk. -// // reqJson contains the 'object' field of the VK post. -// // This object has the 'text' field with the post text. -// // Also it contains the 'attachments' field with the attachments. -// // The attachments field is an array of objects. -// } +Future _postOnMastodon(reqJson) async { + // Called to repost the post on Mastodon on vk. + // reqJson contains the 'object' field of the VK post. + // This object has the 'text' field with the post text. + // Also it contains the 'attachments' field with the attachments. + // The attachments field is an array of objects. + // Each object has the 'type' field with the attachment type. + // We only want to post photos. + // The 'photo' type is 'photo'. + // The 'photo' type has the 'sizes' field with an array of objects. + // Each object has the 'url', 'height' and 'width' fields. + // We want to post the largest photo. + // The 'url' field is the url of the photo. + // The 'height' and 'width' fields are the height and width of the photo. + // We download all pictures with dio into a temporary directory. + + final postText = reqJson['object']['text'] ?? ''; + + final attachments = reqJson['object']['attachments']; + final photos = + attachments.where((attachment) => attachment['type'] == 'photo'); + + final photoUrls = photos.map((photo) { + final photoUrl = photo['photo']['sizes'].last['url']; + final photoPath = + '${Directory.systemTemp.path}/${photoUrl.split('/').last}'; + dio.download(photoUrl, photoPath); + return photoPath; + }); + + // Post the post on Mastodon. + // First we upload all photos with dio. + // Then we post the post with the uploaded photos and the text. + // We delete the photos after posting. + // Photos are uploaded to POST /api/v1/media in the 'file' form data field. + // We take the 'id' field of the response and add it to the post's 'media_ids' field. + // The 'media_ids' field is an array of media ids. + // We post the post with the media ids and the text in the 'status' field. + // Endpoint: POST /api/v1/statuses + // Headers: Authorization: Bearer + // We use auth on all requests. + + // if there are no photos, we don't fetch them + if (photos.isEmpty) { + await dio.post('$instanceUrl/api/v1/statuses', + data: {'status': postText}, + options: dio_lib.Options(headers: {'Authorization': 'Bearer $authToken'})); + } else { + final mediaResponses = await Future.wait( + photoUrls.map((photoUrl) => dio.post('$instanceUrl/api/v1/media', + data: dio_lib.FormData.fromMap({'file': File(photoUrl)}), + options: dio_lib.Options(headers: {'Authorization': 'Bearer $authToken'})))); + + final mediaIds = mediaResponses.map((mediaResponse) { + final mediaId = mediaResponse.data['id']; + return mediaId; + }).toList(); + + await dio.post('$instanceUrl/api/v1/statuses', + data: { + 'status': postText, + 'media_ids': mediaIds + }, + options: dio_lib.Options(headers: {'Authorization': 'Bearer $authToken'})); + + await Future.wait(photoUrls.map((photoUrl) => File(photoUrl).delete())); + } +} void main(List args) async { - // Use any available host or container IP (usually `0.0.0.0`). final ip = InternetAddress.anyIPv4;